Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Paywalls: multi-package template #2840

Merged
merged 3 commits into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,6 @@ let package = Package(
.product(name: "SnapshotTesting", package: "swift-snapshot-testing")
],
exclude: ["__Snapshots__"],
resources: [.copy("Resources/image.png")])
resources: [.copy("Resources/image_1.jpg"), .copy("Resources/image_2.jpg")])
NachoSoto marked this conversation as resolved.
Show resolved Hide resolved
]
)
3 changes: 3 additions & 0 deletions RevenueCatUI/Data/ProcessedLocalizedConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ struct ProcessedLocalizedConfiguration: PaywallLocalizedConfiguration {
}

}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
extension ProcessedLocalizedConfiguration: Equatable {}
21 changes: 8 additions & 13 deletions RevenueCatUI/Data/TemplateViewConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ struct TemplateViewConfiguration {
extension TemplateViewConfiguration {

/// A `Package` with its processed localized strings.
struct Package {
struct Package: Equatable {

let content: RevenueCat.Package
let localization: ProcessedLocalizedConfiguration

}

/// Whether a template displays 1 or multiple packages.
enum PackageSetting {
enum PackageSetting: Equatable {

case single
case multiple
Expand All @@ -43,10 +43,10 @@ extension TemplateViewConfiguration {

/// Describes the possible displayed packages in a paywall.
/// See `create(with:filter:setting:)` for how to create these.
enum PackageConfiguration {
enum PackageConfiguration: Equatable {

case single(Package)
case multiple([Package])
case multiple(first: Package, all: [Package])

}

Expand All @@ -62,13 +62,8 @@ extension TemplateViewConfiguration.PackageConfiguration {
switch self {
case let .single(package):
return package
case let .multiple(packages):
guard let package = packages.first else {
// `create()` makes this impossible.
fatalError("Unexpectedly found no packages in `PackageConfiguration.multiple`")
}

return package
case let .multiple(first, _):
Comment on lines -71 to +65
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks to this new definition we don't need a fatalError, we know at compile time that it's not empty 😎

return first
}
}

Expand All @@ -77,7 +72,7 @@ extension TemplateViewConfiguration.PackageConfiguration {
switch self {
case let .single(package):
return [package]
case let .multiple(packages):
case let .multiple(_, packages):
return packages
}
}
Expand Down Expand Up @@ -116,7 +111,7 @@ extension TemplateViewConfiguration.PackageConfiguration {
case .single:
return .single(firstPackage)
case .multiple:
return .multiple(filtered)
return .multiple(first: firstPackage, all: filtered)
}
}

Expand Down
121 changes: 112 additions & 9 deletions RevenueCatUI/Data/TestData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,43 @@ import RevenueCat
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
internal enum TestData {

static let weeklyProduct = TestStoreProduct(
localizedTitle: "Weekly",
price: 1.99,
localizedPriceString: "$1.99",
productIdentifier: "com.revenuecat.product_1",
productType: .autoRenewableSubscription,
localizedDescription: "PRO weekly",
subscriptionGroupIdentifier: "group",
subscriptionPeriod: .init(value: 1, unit: .week)
)
static let monthlyProduct = TestStoreProduct(
localizedTitle: "Monthly",
price: 12.99,
localizedPriceString: "$12.99",
productIdentifier: "com.revenuecat.product_2",
productType: .autoRenewableSubscription,
localizedDescription: "PRO monthly",
subscriptionGroupIdentifier: "group",
subscriptionPeriod: .init(value: 1, unit: .month),
introductoryDiscount: Self.intro(7, .day)
)
static let annualProduct = TestStoreProduct(
localizedTitle: "Annual",
price: 69.49,
localizedPriceString: "$69.49",
productIdentifier: "com.revenuecat.product_3",
productType: .autoRenewableSubscription,
localizedDescription: "PRO annual",
subscriptionGroupIdentifier: "group",
subscriptionPeriod: .init(value: 1, unit: .year),
introductoryDiscount: Self.intro(14, .day)
)
static let productWithIntroOffer = TestStoreProduct(
localizedTitle: "PRO monthly",
price: 3.99,
localizedPriceString: "$3.99",
productIdentifier: "com.revenuecat.product",
productIdentifier: "com.revenuecat.product_4",
productType: .autoRenewableSubscription,
localizedDescription: "PRO monthly",
subscriptionGroupIdentifier: "group",
Expand All @@ -37,14 +69,33 @@ internal enum TestData {
localizedTitle: "PRO annual",
price: 34.99,
localizedPriceString: "$34.99",
productIdentifier: "com.revenuecat.product",
productIdentifier: "com.revenuecat.product_3",
productType: .autoRenewableSubscription,
localizedDescription: "PRO annual",
subscriptionGroupIdentifier: "group",
subscriptionPeriod: .init(value: 1, unit: .year),
introductoryDiscount: nil,
discounts: []
)
static let weeklyPackage = Package(
identifier: "weekly",
packageType: .weekly,
storeProduct: Self.weeklyProduct.toStoreProduct(),
offeringIdentifier: Self.offeringIdentifier
)
static let monthlyPackage = Package(
identifier: "monthly",
packageType: .monthly,
storeProduct: Self.monthlyProduct.toStoreProduct(),
offeringIdentifier: Self.offeringIdentifier
)
static let annualPackage = Package(
identifier: "annual",
packageType: .annual,
storeProduct: Self.annualProduct.toStoreProduct(),
offeringIdentifier: Self.offeringIdentifier
)

static let packageWithIntroOffer = Package(
identifier: "monthly",
packageType: .monthly,
Expand All @@ -64,23 +115,23 @@ internal enum TestData {
]

static let paywallWithIntroOffer = PaywallData(
template: .example1,
template: .singlePackage,
config: .init(
packages: [.monthly],
imageNames: [Self.paywallHeaderImageName],
colors: .init(light: Self.lightColors, dark: Self.darkColors)
),
localization: Self.localization,
localization: Self.localization1,
assetBaseURL: Self.paywallAssetBaseURL
)
static let paywallWithNoIntroOffer = PaywallData(
template: .example1,
template: .singlePackage,
config: .init(
packages: [.annual],
imageNames: [Self.paywallHeaderImageName],
colors: .init(light: Self.lightColors, dark: Self.darkColors)
),
localization: Self.localization,
localization: Self.localization1,
assetBaseURL: Self.paywallAssetBaseURL
)

Expand All @@ -100,6 +151,39 @@ internal enum TestData {
availablePackages: Self.packages
)

static let offeringWithMultiPackagePaywall = Offering(
identifier: Self.offeringIdentifier,
serverDescription: "Offering",
metadata: [:],
paywall: .init(
template: .multiPackage,
config: .init(
packages: [.annual, .monthly],
imageNames: [Self.paywallBackgroundImageName,
Self.paywallHeaderImageName],
colors: .init(
light: .init(
background: "#FFFFFF",
foreground: "#000000",
callToActionBackground: "#EC807C",
callToActionForeground: "#FFFFFF"
),
dark: .init(
background: "#000000",
foreground: "#FFFFFF",
callToActionBackground: "#ACD27A",
callToActionForeground: "#000000"
)
)
),
localization: Self.localization2,
assetBaseURL: Self.paywallAssetBaseURL
),
availablePackages: [Self.weeklyPackage,
Self.monthlyPackage,
Self.annualPackage]
)

static let lightColors: PaywallData.Configuration.Colors = .init(
background: "#FFFFFF",
foreground: "#000000",
Expand Down Expand Up @@ -150,19 +234,38 @@ internal enum TestData {
return try! decoder.decode(CustomerInfo.self, from: Data(json.utf8))
}()

static let localization: PaywallData.LocalizedConfiguration = .init(
static let localization1: PaywallData.LocalizedConfiguration = .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"
)
static let localization2: PaywallData.LocalizedConfiguration = .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"
)
static let paywallHeaderImageName = "9a17e0a7_1689854430..jpeg"
static let paywallBackgroundImageName = "9a17e0a7_1689854342..jpg"
static let paywallAssetBaseURL = URL(string: "https://d35rwhxn1vk1te.cloudfront.net")!

private static let offeringIdentifier = "offering"
private static let paywallHeaderImageName = "cd84ac55_paywl0884b9ceb4_header_1689214657.jpg"
private static let paywallAssetBaseURL = URL(string: "https://d2ban7feka8lu3.cloudfront.net")!

private static func intro(_ duration: Int, _ unit: SubscriptionPeriod.Unit) -> TestStoreProductDiscount {
return .init(
identifier: "intro",
price: 0,
localizedPriceString: "$0.00",
paymentMode: .freeTrial,
subscriptionPeriod: .init(value: duration, unit: .day),
numberOfPeriods: 1,
type: .introductory
)
}
}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
Expand Down
31 changes: 22 additions & 9 deletions RevenueCatUI/PaywallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,29 +170,42 @@ private extension PaywallViewMode {
struct PaywallView_Previews: PreviewProvider {

static var previews: some View {
let offering = TestData.offeringWithNoIntroOffer

if let paywall = offering.paywall {
ForEach(PaywallViewMode.allCases, id: \.self) { mode in
ForEach(Self.offerings, id: \.self) { offering in
ForEach(Self.modes, id: \.self) { mode in
Comment on lines +173 to +174
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All The Permutations 😎

PaywallView(
offering: offering,
paywall: paywall,
paywall: offering.paywall!,
mode: mode,
introEligibility: Self.introEligibility,
purchaseHandler: Self.purchaseHandler
)
.previewLayout(mode.layout)
.previewDisplayName("\(offering.identifier)-\(mode)")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This provides a nice title to the previews:
Screenshot 2023-07-20 at 18 30 14

}
} else {
Text("Preview not correctly setup, offering has no paywall!")
}
}

private static let introEligibility: TrialOrIntroEligibilityChecker = .producing(eligibility: .eligible)
private static let introEligibility: TrialOrIntroEligibilityChecker =
.producing(eligibility: .eligible)
.with(delay: .seconds(1))
private static let purchaseHandler: PurchaseHandler = .mock()
private static let purchaseHandler: PurchaseHandler =
.mock()
.with(delay: .seconds(1))

private static let offerings: [Offering] = [
TestData.offeringWithIntroOffer,
TestData.offeringWithMultiPackagePaywall
]

private static let modes: [PaywallViewMode] = [
.fullScreen
]

private static let colors: PaywallData.Configuration.ColorInformation = .init(
light: TestData.lightColors,
dark: TestData.darkColors
)

Comment on lines +195 to +208
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this TestData going to be renamed at somepoint? 😇

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just internal and DEBUG only. But is it because you don't think it's a good name? 😇
Would you name it SampleData maybe?

}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
Expand Down
Loading