Skip to content

Commit

Permalink
Paywalls: added sample paywalls to SimpleApp (#2907)
Browse files Browse the repository at this point in the history
  • Loading branch information
NachoSoto committed Aug 31, 2023
1 parent 767b63c commit 62769e9
Show file tree
Hide file tree
Showing 4 changed files with 315 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -48,6 +50,8 @@
4FC046C32A572E3700A28BCF /* DebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugView.swift; sourceTree = "<group>"; };
4FC882E02A5870C6005BE85E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
4FDF11212A72714C004F3680 /* SamplePaywalls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplePaywalls.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -70,6 +74,7 @@
4F34FF622A60AD9A00AADF11 /* AppContentView.swift */,
4FC046C32A572E3700A28BCF /* DebugView.swift */,
4F6E5A4F2A660DD500C573C7 /* SampleChart.swift */,
4FDF111F2A7270F3004F3680 /* SamplePaywallsList.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -123,6 +128,7 @@
isa = PBXGroup;
children = (
4F34FF642A60ADBD00AADF11 /* Configuration.swift */,
4FDF11212A72714C004F3680 /* SamplePaywalls.swift */,
4FC882E02A5870C6005BE85E /* Info.plist */,
4FC046BF2A572E3700A28BCF /* Assets.xcassets */,
4FC046BD2A572E3700A28BCF /* SimpleApp.entitlements */,
Expand Down Expand Up @@ -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;
Expand Down
197 changes: 197 additions & 0 deletions Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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)})
Expand All @@ -56,6 +64,7 @@ struct AppContentView: View {
Spacer()
}
}
.padding(.horizontal)
.navigationTitle("Simple App")
.task {
for await info in self.customerInfoStream {
Expand Down
101 changes: 101 additions & 0 deletions Tests/TestingApps/SimpleApp/SimpleApp/Views/SamplePaywallsList.swift
Original file line number Diff line number Diff line change
@@ -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<SamplePaywallLoader, NSError>?

@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

0 comments on commit 62769e9

Please sign in to comment.