From 7e09c3df66c73aab683924bc91463c6988e25eac Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Fri, 28 Jul 2023 00:28:45 +0200 Subject: [PATCH] `Paywalls`: added sample paywalls to `SimpleApp` (#2907) ![image](https://github.com/RevenueCat/purchases-ios/assets/685609/33f2adaf-3b4a-49a0-b60b-4fafbda20153) --- .../SimpleApp.xcodeproj/project.pbxproj | 8 + .../SimpleApp/SimpleApp/SamplePaywalls.swift | 197 ++++++++++++++++++ .../SimpleApp/Views/AppContentView.swift | 9 + .../SimpleApp/Views/SamplePaywallsList.swift | 101 +++++++++ 4 files changed, 315 insertions(+) create mode 100644 Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift create mode 100644 Tests/TestingApps/SimpleApp/SimpleApp/Views/SamplePaywallsList.swift diff --git a/Tests/TestingApps/SimpleApp/SimpleApp.xcodeproj/project.pbxproj b/Tests/TestingApps/SimpleApp/SimpleApp.xcodeproj/project.pbxproj index 888bf9f68f..b01489b1a2 100644 --- a/Tests/TestingApps/SimpleApp/SimpleApp.xcodeproj/project.pbxproj +++ b/Tests/TestingApps/SimpleApp/SimpleApp.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ 4F6E5A502A660DD500C573C7 /* SampleChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6E5A4F2A660DD500C573C7 /* SampleChart.swift */; }; 4FC80B202A5DE8E300E95DB0 /* RevenueCatUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4FC80B1F2A5DE8E300E95DB0 /* RevenueCatUI */; }; 4FCA01FB2A3A1CBD00B262C0 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4FCA01FA2A3A1CBD00B262C0 /* StoreKit.framework */; }; + 4FDF11202A7270F3004F3680 /* SamplePaywallsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDF111F2A7270F3004F3680 /* SamplePaywallsList.swift */; }; + 4FDF11222A72714C004F3680 /* SamplePaywalls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDF11212A72714C004F3680 /* SamplePaywalls.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -48,6 +50,8 @@ 4FC046C32A572E3700A28BCF /* DebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugView.swift; sourceTree = ""; }; 4FC882E02A5870C6005BE85E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 4FCA01FA2A3A1CBD00B262C0 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + 4FDF111F2A7270F3004F3680 /* SamplePaywallsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplePaywallsList.swift; sourceTree = ""; }; + 4FDF11212A72714C004F3680 /* SamplePaywalls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplePaywalls.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -70,6 +74,7 @@ 4F34FF622A60AD9A00AADF11 /* AppContentView.swift */, 4FC046C32A572E3700A28BCF /* DebugView.swift */, 4F6E5A4F2A660DD500C573C7 /* SampleChart.swift */, + 4FDF111F2A7270F3004F3680 /* SamplePaywallsList.swift */, ); path = Views; sourceTree = ""; @@ -123,6 +128,7 @@ isa = PBXGroup; children = ( 4F34FF642A60ADBD00AADF11 /* Configuration.swift */, + 4FDF11212A72714C004F3680 /* SamplePaywalls.swift */, 4FC882E02A5870C6005BE85E /* Info.plist */, 4FC046BF2A572E3700A28BCF /* Assets.xcassets */, 4FC046BD2A572E3700A28BCF /* SimpleApp.entitlements */, @@ -211,7 +217,9 @@ 4F4EE7CD2A572F5400D7EAE1 /* SimpleApp.swift in Sources */, 4F4EE7CF2A572F5D00D7EAE1 /* DebugView.swift in Sources */, 4F6E5A502A660DD500C573C7 /* SampleChart.swift in Sources */, + 4FDF11222A72714C004F3680 /* SamplePaywalls.swift in Sources */, 4F34FF652A60ADBD00AADF11 /* Configuration.swift in Sources */, + 4FDF11202A7270F3004F3680 /* SamplePaywallsList.swift in Sources */, 4F34FF632A60AD9A00AADF11 /* AppContentView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift b/Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift new file mode 100644 index 0000000000..6594d8d1be --- /dev/null +++ b/Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift @@ -0,0 +1,197 @@ +// +// SamplePaywalls.swift +// SimpleApp +// +// Created by Nacho Soto on 7/27/23. +// + +import Foundation +import RevenueCat + +final class SamplePaywallLoader { + + private let packages: [Package] + + init(packages: [Package]) { + self.packages = packages + } + + static func create() async throws -> Self { + struct PurchasesNotConfigured: Error {} + + guard Purchases.isConfigured else { + throw PurchasesNotConfigured() + } + + let packages = try await Purchases.shared.offerings().current?.availablePackages + + return .init(packages: packages ?? []) + } + + func offering(for template: PaywallTemplate) -> Offering { + return .init( + identifier: Self.offeringIdentifier, + serverDescription: Self.offeringIdentifier, + metadata: [:], + paywall: self.paywall(for: template), + availablePackages: self.packages + ) + } + + private func paywall(for template: PaywallTemplate) -> PaywallData { + switch template { + case .onePackageStandard: + return Self.onePackageStandardTemplate() + case .multiPackageBold: + return Self.multiPackageBoldTemplate() + case .onePackageWithFeatures: + return Self.onePackageWithFeaturesTemplate() + } + } + +} + +private extension SamplePaywallLoader { + + static func onePackageStandardTemplate() -> PaywallData { + return .init( + template: .onePackageStandard, + config: .init( + packages: [.monthly], + images: Self.images, + colors: .init( + light: .init( + background: "#FFFFFF", + foreground: "#000000", + callToActionBackground: "#5CD27A", + callToActionForeground: "#FFFFFF", + accent1: "#BC66FF" + ), + dark: .init( + background: "#000000", + foreground: "#FFFFFF", + callToActionBackground: "#ACD27A", + callToActionForeground: "#000000", + accent1: "#B022BB" + ) + ), + termsOfServiceURL: Self.tosURL + ), + localization: .init( + title: "Ignite your child's curiosity", + subtitle: "Get access to all our educational content trusted by thousands of parents.", + callToAction: "Purchase for {{ price }}", + callToActionWithIntroOffer: "Purchase for {{ price_per_month }} per month", + offerDetails: "{{ price_per_month }} per month", + offerDetailsWithIntroOffer: "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month" + ), + assetBaseURL: Self.paywallAssetBaseURL + ) + } + + static func multiPackageBoldTemplate() -> PaywallData { + return .init( + template: .multiPackageBold, + config: .init( + packages: [.weekly, .monthly, .annual], + images: Self.images, + colors: .init( + light: .init( + background: "#FFFFFF", + foreground: "#000000", + callToActionBackground: "#EC807C", + callToActionForeground: "#FFFFFF", + accent1: "#BC66FF" + ), + dark: .init( + background: "#000000", + foreground: "#FFFFFF", + callToActionBackground: "#ACD27A", + callToActionForeground: "#000000", + accent1: "#B022BB" + ) + ), + blurredBackgroundImage: true, + termsOfServiceURL: Self.tosURL + ), + localization: .init( + title: "Call to action for better conversion.", + subtitle: "Lorem ipsum is simply dummy text of the printing and typesetting industry.", + callToAction: "Subscribe for {{ price_per_month }}/mo", + offerDetails: "{{ total_price_and_per_month }}", + offerDetailsWithIntroOffer: "{{ total_price_and_per_month }} after {{ intro_duration }} trial", + offerName: "{{ period }}" + ), + assetBaseURL: Self.paywallAssetBaseURL + ) + } + + static func onePackageWithFeaturesTemplate() -> PaywallData { + return .init( + template: .onePackageWithFeatures, + config: .init( + packages: [.monthly], + images: Self.images, + colors: .init( + light: .init( + background: "#272727", + foreground: "#FFFFFF", + callToActionBackground: "#FFFFFF", + callToActionForeground: "#000000", + accent1: "#F4E971", + accent2: "#B7B7B7" + ) + ), + termsOfServiceURL: Self.tosURL + ), + localization: .init( + title: "How your free trial works", + callToAction: "Start", + callToActionWithIntroOffer: "Start your {{ intro_duration }} free", + offerDetails: "Only {{ price }} per {{ period }}", + offerDetailsWithIntroOffer: "First {{ intro_duration }} free, then\n{{ price }} per {{ period }} ({{ price_per_month }} per month)", + features: [ + .init(title: "Today", + content: "Full access to 1000+ workouts plus free meal plan worth $49.99.", + iconID: "tick"), + .init(title: "Day 7", + content: "Get a reminder about when your trial is about to end.", + iconID: "notifications"), + .init(title: "Day 14", + content: "You'll automatically get subscribed. Cancel anytime before if you didn't love our app.", + iconID: "attachment") + ]), + assetBaseURL: Self.paywallAssetBaseURL + ) + } + +} + +private extension SamplePaywallLoader { + + static let images: PaywallData.Configuration.Images = .init( + header: "9a17e0a7_1689854430..jpeg", + background: "9a17e0a7_1689854342..jpg", + icon: "9a17e0a7_1689854430..jpeg" + ) + + static let offeringIdentifier = "offering" + static let paywallAssetBaseURL = URL(string: "https://d35rwhxn1vk1te.cloudfront.net")! + static let tosURL = URL(string: "https://revenuecat.com/tos")! + +} + +// This is provided by RevenueCatUI only for debug builds +// But we want to be able to use it in release builds too. +#if !DEBUG + +extension PaywallColor: ExpressibleByStringLiteral { + + public init(stringLiteral value: StringLiteralType) { + // swiftlint:disable:next force_try + try! self.init(stringRepresentation: value) + } + +} + +#endif diff --git a/Tests/TestingApps/SimpleApp/SimpleApp/Views/AppContentView.swift b/Tests/TestingApps/SimpleApp/SimpleApp/Views/AppContentView.swift index da5b2cff82..ceb7ea23f7 100644 --- a/Tests/TestingApps/SimpleApp/SimpleApp/Views/AppContentView.swift +++ b/Tests/TestingApps/SimpleApp/SimpleApp/Views/AppContentView.swift @@ -43,6 +43,14 @@ struct AppContentView: View { Text(verbatim: "You're signed in: \(info.originalAppUserId)") .font(.callout) + NavigationLink { + SamplePaywallsList() + } label: { + Text("Sample paywalls") + } + .buttonStyle(.borderedProminent) + .tint(.mint) + Spacer() BarChartView(data: (0..<10).map { _ in Double.random(in: 0..<100)}) @@ -56,6 +64,7 @@ struct AppContentView: View { Spacer() } } + .padding(.horizontal) .navigationTitle("Simple App") .task { for await info in self.customerInfoStream { diff --git a/Tests/TestingApps/SimpleApp/SimpleApp/Views/SamplePaywallsList.swift b/Tests/TestingApps/SimpleApp/SimpleApp/Views/SamplePaywallsList.swift new file mode 100644 index 0000000000..03e2cf22f2 --- /dev/null +++ b/Tests/TestingApps/SimpleApp/SimpleApp/Views/SamplePaywallsList.swift @@ -0,0 +1,101 @@ +// +// SamplePaywallsList.swift +// SimpleApp +// +// Created by Nacho Soto on 7/27/23. +// + +import RevenueCat +import RevenueCatUI +import SwiftUI + +struct SamplePaywallsList: View { + + @State + private var loader: Result? + + @State + private var selectedTemplate: PaywallTemplate? + + var body: some View { + self.content + .navigationTitle("Paywalls") + .task { + do { + self.loader = .success(try await .create()) + } catch let error as NSError { + self.loader = .failure(error) + } + } + } + + @ViewBuilder + private var content: some View { + switch self.loader { + case let .success(loader): + self.list(with: loader) + .sheet(item: self.$selectedTemplate) { template in + PaywallView(offering: loader.offering(for: template)) + } + + case let .failure(error): + Text(error.description) + + case .none: + ProgressView() + } + } + + private func list(with loader: SamplePaywallLoader) -> some View { + List { + Section("Templates") { + ForEach(PaywallTemplate.allCases, id: \.rawValue) { template in + Button { + self.selectedTemplate = template + } label: { + Text(template.name) + } + .buttonStyle(.plain) + } + } + } + } + +} + +// MARK: - + +extension PaywallTemplate: Identifiable { + + public var id: String { + return self.rawValue + } + +} + +private extension PaywallTemplate { + + var name: String { + switch self { + case .onePackageStandard: + return "One package standard" + case .multiPackageBold: + return "Multi package bold" + case .onePackageWithFeatures: + return "One package with features" + } + } + +} + +#if DEBUG + +struct SamplePaywallsList_Previews: PreviewProvider { + static var previews: some View { + NavigationStack { + SamplePaywallsList() + } + } +} + +#endif