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: added packages to configuration #2798

Merged
merged 1 commit into from
Jul 13, 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
18 changes: 18 additions & 0 deletions RevenueCatUI/Helpers/Logger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Logger.swift
//
//
// Created by Nacho Soto on 7/12/23.
//

import RevenueCat

enum Logger {

static func warning(_ text: String) {
// Note: this isn't ideal.
// Once we can use the `package` keyword it can use the internal `Logger`.
Purchases.logHandler(.warn, text)
}

}
10 changes: 2 additions & 8 deletions RevenueCatUI/Helpers/Variables.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private extension VariableDataProvider {
case "product_name": return self.productName
case "intro_duration":
guard let introDuration = self.introductoryOfferDuration else {
Self.logWarning(
Logger.warning(
"Unexpectedly tried to look for intro duration when there is none, this is a logic error."
)
return ""
Expand All @@ -83,15 +83,9 @@ private extension VariableDataProvider {
return introDuration

default:
Self.logWarning("Couldn't find content for variable '\(variableName)'")
Logger.warning("Couldn't find content for variable '\(variableName)'")
return ""
}
}

private static func logWarning(_ text: String) {
// Note: this isn't ideal.
// Once we can use the `package` keyword it can use the internal `Logger`.
Purchases.logHandler(.warn, text)
}

}
3 changes: 2 additions & 1 deletion RevenueCatUI/PaywallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public struct PaywallView: View {
struct PaywallView_Previews: PreviewProvider {

static var previews: some View {
PaywallView(offering: TestData.offering, paywall: TestData.paywall)
let offering = TestData.offeringWithIntroOffer
PaywallView(offering: offering, paywall: offering.paywall!)
Comment on lines +29 to +30
Copy link
Member

Choose a reason for hiding this comment

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

might as well do an if let here and have a text explaining that there's no offering so the preview doesn't ugly-crash?

I.e.:

        let offering = TestData.offeringWithIntroOffer
        if let paywall = offering.paywall {
            PaywallView(offering: offering, paywall: paywall)
        } else {
            Text("Preview not correctly set up! The offering's paywall need to be non-nil")
        }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍🏻 I guess debugging crashes in previews is annoying, so yeah.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in the next PR.

}

}
Expand Down
103 changes: 84 additions & 19 deletions RevenueCatUI/Templates/Example1Template.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,62 @@ import SwiftUI
@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
struct Example1Template: TemplateViewType {

private let package: Package
private let localization: ProcessedLocalizedConfiguration
private let configuration: PaywallData.Configuration
private var data: Result<Example1TemplateContent.Data, Example1TemplateContent.Error>

init(
packages: [Package],
localization: PaywallData.LocalizedConfiguration,
configuration: PaywallData.Configuration
) {
// The RC SDK ensures this when constructing Offerings
precondition(!packages.isEmpty)
// Fix-me: move this logic out to be used by all templates
if packages.isEmpty {
self.data = .failure(.noPackages)
} else {
let packages = Self.filter(packages: packages, with: configuration.packages)

if let package = packages.first {
self.data = .success(.init(
package: package,
localization: localization.processVariables(with: package),
configuration: configuration
))
} else {
self.data = .failure(.couldNotFindAnyPackages(expectedTypes: configuration.packages))
}
}
}

self.package = packages[0]
self.localization = localization.processVariables(with: self.package)
self.configuration = configuration
// Fix-me: this can be extracted to be used by all templates
var body: some View {
switch self.data {
case let .success(data):
Example1TemplateContent(data: data)
case let .failure(error):
#if DEBUG
// Fix-me: implement a proper production error screen
EmptyView()
.onAppear {
Logger.warning("Couldn't load paywall: \(error.description)")
}
#else
Text(error.description)
.background(
Color.red
.edgesIgnoringSafeArea(.all)
)
#endif
}
}

precondition(
self.package.storeProduct.productCategory == .subscription,
"Unexpected product type for this template: \(self.package.storeProduct.productType)"
)
}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
private struct Example1TemplateContent: View {

private var data: Data

init(data: Data) {
self.data = data
}

var body: some View {
Expand All @@ -50,12 +86,12 @@ struct Example1Template: TemplateViewType {
Spacer()

VStack {
Text(verbatim: self.localization.title)
Text(verbatim: self.data.localization.title)
.font(.largeTitle)
.fontWeight(.heavy)
.padding(.bottom)

Text(verbatim: self.localization.subtitle)
Text(verbatim: self.data.localization.subtitle)
.font(.subheadline)
.padding(.horizontal)

Expand All @@ -73,9 +109,9 @@ struct Example1Template: TemplateViewType {
@ViewBuilder
private var offerDetails: some View {
// Fix-me: this needs to handle other types of intro discounts
let text = self.package.storeProduct.introductoryDiscount == nil
? self.localization.offerDetails
: self.localization.offerDetailsWithIntroOffer
let text = self.data.package.storeProduct.introductoryDiscount == nil
? self.data.localization.offerDetails
: self.data.localization.offerDetailsWithIntroOffer

Text(verbatim: text)
.font(.callout)
Expand All @@ -86,7 +122,7 @@ struct Example1Template: TemplateViewType {
Button {

} label: {
Text(self.localization.callToAction)
Text(self.data.localization.callToAction)
.frame(maxWidth: .infinity)
}
.font(.title2)
Expand All @@ -99,8 +135,23 @@ struct Example1Template: TemplateViewType {

}

// MARK: -

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
private extension Example1Template {
private extension Example1TemplateContent {

struct Data {
let package: Package
let localization: ProcessedLocalizedConfiguration
let configuration: PaywallData.Configuration
}

enum Error: Swift.Error {

case noPackages
case couldNotFindAnyPackages(expectedTypes: [PackageType])

}

private func label(for package: Package) -> some View {
HStack {
Expand All @@ -120,3 +171,17 @@ private extension Example1Template {
}

}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
extension Example1TemplateContent.Error {

var description: String {
switch self {
case .noPackages:
return "Attempted to display paywall with no packages."
case let .couldNotFindAnyPackages(expectedTypes):
return "Couldn't find any requested packages: \(expectedTypes)"
}
}

}
29 changes: 29 additions & 0 deletions RevenueCatUI/Templates/TemplateViewType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,32 @@ extension PaywallData {
}
}
}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
extension TemplateViewType {

static func filter(packages: [Package], with list: [PackageType]) -> [Package] {
// Only subscriptions are supported at the moment
let subscriptions = packages.filter { $0.storeProduct.productCategory == .subscription }
let map = Dictionary(grouping: subscriptions) { $0.packageType }

return list.compactMap { type in
if let packages = map[type] {
switch packages.count {
case 0:
// This isn't actually possible because of `Dictionary(grouping:by:)
return nil
case 1:
return packages.first
default:
Logger.warning("Found multiple \(type) packages. Will use the first one.")
return packages.first
}
} else {
Logger.warning("Couldn't find '\(type)'")
return nil
}
}
}

}
56 changes: 32 additions & 24 deletions RevenueCatUI/TestData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,41 +57,49 @@ internal enum TestData {
offeringIdentifier: Self.offeringIdentifier
)

static let paywall = PaywallData(
static let packages = [
Self.packageWithIntroOffer,
Self.packageWithNoIntroOffer
]

static let paywallWithIntroOffer = PaywallData(
template: .example1,
config: .init(),
localization: .init(
title: "Ignite your child's curiosity",
subtitle: "Get access to all our educational content trusted by thousands of parents.",
callToAction: "Continue",
callToActionWithIntroOffer: "Continue",
offerDetails: "{{ price_per_month }} per month",
offerDetailsWithIntroOffer: "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month"
)
config: .init(
packages: [.monthly]
),
localization: Self.localization
)
static let paywallWithNoIntroOffer = PaywallData(
template: .example1,
config: .init(
packages: [.annual]
),
localization: Self.localization
)

// Fix-me: remove this when we can filter by package type

static let offering = Offering(
static let offeringWithIntroOffer = Offering(
identifier: Self.offeringIdentifier,
serverDescription: "Main offering",
metadata: [:],
paywall: Self.paywall,
availablePackages: [
packageWithNoIntroOffer,
packageWithIntroOffer
]
paywall: Self.paywallWithIntroOffer,
availablePackages: Self.packages
)

static let offeringWithIntroOffer = Offering(
static let offeringWithNoIntroOffer = Offering(
identifier: Self.offeringIdentifier,
serverDescription: "Main offering",
metadata: [:],
paywall: Self.paywall,
availablePackages: [
packageWithIntroOffer,
packageWithNoIntroOffer
]
paywall: Self.paywallWithNoIntroOffer,
availablePackages: Self.packages
)

private static let localization: PaywallData.LocalizedConfiguration = .init(
title: "Ignite your child's curiosity",
subtitle: "Get access to all our educational content trusted by thousands of parents.",
callToAction: "Continue",
callToActionWithIntroOffer: "Continue",
offerDetails: "{{ price_per_month }} per month",
offerDetailsWithIntroOffer: "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month"
)

private static let offeringIdentifier = "offering"
Expand Down
7 changes: 6 additions & 1 deletion Sources/Paywalls/PaywallData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,13 @@ extension PaywallData {
/// Generic configuration for any paywall.
public struct Configuration {

/// The list of package types this paywall will display
public var packages: [PackageType]

// swiftlint:disable:next missing_docs
public init() {}
public init(packages: [PackageType]) {
self.packages = packages
}

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ func checkPaywallData(_ data: PaywallData) {
}

func checkPaywallConfiguration(_ config: PaywallData.Configuration) {
let _: PaywallData.Configuration = .init()
let _: PaywallData.Configuration = .init(packages: [.monthly, .annual])
let _: [PackageType] = config.packages
}

func checkPaywallLocalizedConfig(_ config: PaywallData.LocalizedConfiguration) {
Expand Down
14 changes: 10 additions & 4 deletions Tests/RevenueCatUITests/PaywallViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,23 @@ class PaywallViewTests: TestCase {
}

func testSamplePaywall() {
let view = PaywallView(offering: TestData.offering, paywall: TestData.paywall)
.frame(width: 460, height: 950)
let offering = TestData.offeringWithNoIntroOffer

let view = PaywallView(offering: offering, paywall: offering.paywall!)
.frame(width: Self.size.width, height: Self.size.height)

expect(view).to(haveValidSnapshot(as: .image))
}

func testSamplePaywallWithIntroOffer() {
let view = PaywallView(offering: TestData.offeringWithIntroOffer, paywall: TestData.paywall)
.frame(width: 460, height: 950)
let offering = TestData.offeringWithIntroOffer

let view = PaywallView(offering: offering, paywall: offering.paywall!)
.frame(width: Self.size.width, height: Self.size.height)

expect(view).to(haveValidSnapshot(as: .image))
}

private static let size: CGSize = .init(width: 460, height: 950)

}
4 changes: 3 additions & 1 deletion Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@
}
},
"default_locale": "en_US",
"config": {}
"config": {
"packages": ["$rc_monthly", "$rc_annual"]
}
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@
}
},
"default_locale": "en_US",
"config": {}
"config": {
"packages": ["$rc_monthly", "$rc_annual"]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@
}
},
"default_locale": "es_ES",
"config": {}
"config": {
"packages": ["$rc_monthly", "$rc_annual"]
}
}
Loading