Skip to content

Commit

Permalink
Paywalls: initial configuration types (#2780)
Browse files Browse the repository at this point in the history
- Added `Offering.paywall`
- Decoding `PaywallData` in `OfferingsResponse`, using
`IgnoreDecodeErrors` (this will be better after #2778)
- New `PaywallData` `struct`
- Added new APIs to testers
- Testing paywall deserialization from `Offerings`
- Testing paywall deserialization separately to check edge cases
  • Loading branch information
NachoSoto committed Aug 31, 2023
1 parent 4c96735 commit e6cdad4
Show file tree
Hide file tree
Showing 19 changed files with 526 additions and 17 deletions.
20 changes: 20 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@
42F1DF385E3C1F9903A07FBF /* ProductsFetcherSK1.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFB3CBAA73855779FE828CE2 /* ProductsFetcherSK1.swift */; };
4F0201C42A13C85500091612 /* Assertions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0201C32A13C85500091612 /* Assertions.swift */; };
4F0404622A9CF64600009408 /* ProcessInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 578D798D2936ACF70042E434 /* ProcessInfo+Extensions.swift */; };
4F05876F2A5DE03F00E9A834 /* PaywallDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F05876E2A5DE03F00E9A834 /* PaywallDataTests.swift */; };
4F0BBA812A1D0524000E75AB /* DefaultDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0BBA802A1D0524000E75AB /* DefaultDecodable.swift */; };
4F0BBAAC2A1D253D000E75AB /* OfflineCustomerInfoCreatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0BBAAB2A1D253D000E75AB /* OfflineCustomerInfoCreatorTests.swift */; };
4F0CE2BD2A215CE600561895 /* TransactionPosterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0CE2BC2A215CE600561895 /* TransactionPosterTests.swift */; };
Expand Down Expand Up @@ -257,6 +258,8 @@
4F83F6BA2A5DB807003F90A5 /* CurrentTestCaseTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575A17AA2773A59300AA6F22 /* CurrentTestCaseTracker.swift */; };
4F83F6BB2A5DB80B003F90A5 /* OSVersionEquivalent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DE80AD28075D77008D6C6F /* OSVersionEquivalent.swift */; };
4F8452682A5756CC00084550 /* HTTPRequestBody+Signing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F8452672A5756CC00084550 /* HTTPRequestBody+Signing.swift */; };
4F87610F2A5C9E490006FA14 /* PaywallData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F87610E2A5C9E490006FA14 /* PaywallData.swift */; };
4F87612C2A5CAB980006FA14 /* PaywallTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F87612B2A5CAB980006FA14 /* PaywallTemplate.swift */; };
4F8929192A65EF3000A91EA2 /* EnsureNonEmptyCollectionDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F8929182A65EF3000A91EA2 /* EnsureNonEmptyCollectionDecodable.swift */; };
4F8A58172A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F8A58162A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift */; };
4F8A58182A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F8A58162A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift */; };
Expand Down Expand Up @@ -956,6 +959,7 @@
37E35F783903362B65FB7AF3 /* MockProductsRequestFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockProductsRequestFactory.swift; sourceTree = "<group>"; };
37E35FDA0A44EA03EA12DAA2 /* DateFormatter+ExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DateFormatter+ExtensionsTests.swift"; sourceTree = "<group>"; };
4F0201C32A13C85500091612 /* Assertions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assertions.swift; sourceTree = "<group>"; };
4F05876E2A5DE03F00E9A834 /* PaywallDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallDataTests.swift; sourceTree = "<group>"; };
4F0BBA802A1D0524000E75AB /* DefaultDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultDecodable.swift; sourceTree = "<group>"; };
4F0BBAAB2A1D253D000E75AB /* OfflineCustomerInfoCreatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineCustomerInfoCreatorTests.swift; sourceTree = "<group>"; };
4F0CE2BC2A215CE600561895 /* TransactionPosterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionPosterTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -985,6 +989,8 @@
4F7DBFBC2A1E986C00A2F511 /* StoreKit2TransactionFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2TransactionFetcher.swift; sourceTree = "<group>"; };
4F8038322A1EA7C300D21039 /* TransactionPoster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionPoster.swift; sourceTree = "<group>"; };
4F8452672A5756CC00084550 /* HTTPRequestBody+Signing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HTTPRequestBody+Signing.swift"; sourceTree = "<group>"; };
4F87610E2A5C9E490006FA14 /* PaywallData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallData.swift; sourceTree = "<group>"; };
4F87612B2A5CAB980006FA14 /* PaywallTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallTemplate.swift; sourceTree = "<group>"; };
4F8929182A65EF3000A91EA2 /* EnsureNonEmptyCollectionDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnsureNonEmptyCollectionDecodable.swift; sourceTree = "<group>"; };
4F8A58162A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOfflineCustomerInfoCreator.swift; sourceTree = "<group>"; };
4F90AFCA2A3915340047E63F /* TestMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMessage.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1635,6 +1641,7 @@
2DDA3E4524DB0B4500EDFE5B /* Misc */,
35D832CB262A5B3400E60AC5 /* Networking */,
57488B7D29CB70DA0000EE7E /* OfflineEntitlements */,
4F87610D2A5C9E330006FA14 /* Paywalls */,
354235D524C11138008C84EE /* Purchasing */,
57F3C0C929B7A04D0004FD7E /* Security */,
354895D0267AE32D001DC5B1 /* SubscriberAttributes */,
Expand Down Expand Up @@ -2229,6 +2236,15 @@
path = DebugUI;
sourceTree = "<group>";
};
4F87610D2A5C9E330006FA14 /* Paywalls */ = {
isa = PBXGroup;
children = (
4F87610E2A5C9E490006FA14 /* PaywallData.swift */,
4F87612B2A5CAB980006FA14 /* PaywallTemplate.swift */,
);
path = Paywalls;
sourceTree = "<group>";
};
4F90AFC92A39152A0047E63F /* Helpers */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -2376,6 +2392,7 @@
574A2F3E282D75E300150D40 /* OfferingsDecodingTests.swift */,
574A2F4E282D7B9E00150D40 /* PostOfferDecodingTests.swift */,
5766C61F282DA3D50067D886 /* GetIntroEligibilityDecodingTests.swift */,
4F05876E2A5DE03F00E9A834 /* PaywallDataTests.swift */,
57045B3729C514A8001A5417 /* ProductEntitlementMappingDecodingTests.swift */,
);
path = Responses;
Expand Down Expand Up @@ -3261,6 +3278,7 @@
57DC9F4627CC2E4900DA6AF9 /* HTTPRequest.swift in Sources */,
2DC5623024EC63730031F69B /* OperationDispatcher.swift in Sources */,
575642B62910116900719219 /* EligibilityStrings.swift in Sources */,
4F87610F2A5C9E490006FA14 /* PaywallData.swift in Sources */,
57A0FBF22749CF66009E2FC3 /* SynchronizedUserDefaults.swift in Sources */,
F5714EE526DC2F1D00635477 /* CodableStrings.swift in Sources */,
57488BE829CB7FB60000EE7E /* OfflineEntitlementsStrings.swift in Sources */,
Expand Down Expand Up @@ -3372,6 +3390,7 @@
2DDF41AD24F6F37C005BC22D /* ASN1ObjectIdentifier.swift in Sources */,
35549323269E298B005F9AE9 /* OfferingsFactory.swift in Sources */,
57536A28278522B400E2AE7F /* SK2StoreTransaction.swift in Sources */,
4F87612C2A5CAB980006FA14 /* PaywallTemplate.swift in Sources */,
2D9C7BB326D838FC006838BE /* UIApplication+RCExtensions.swift in Sources */,
F56E2E7727622B5E009FED5B /* TransactionsManager.swift in Sources */,
B34605CC279A6E380031CA74 /* LogInOperation.swift in Sources */,
Expand Down Expand Up @@ -3545,6 +3564,7 @@
B380D69B27726AB500984578 /* DNSCheckerTests.swift in Sources */,
5774F9C12805EA3000997128 /* BaseHTTPResponseTest.swift in Sources */,
351B51B526D450E800BD2BD7 /* ProductsFetcherSK1Tests.swift in Sources */,
4F05876F2A5DE03F00E9A834 /* PaywallDataTests.swift in Sources */,
2DDF41CC24F6F4C3005BC22D /* AppleReceiptBuilderTests.swift in Sources */,
575A8EE52922C9F300936709 /* MockStoreKit2TransactionListenerDelegate.swift in Sources */,
4F9BB63F2A7AFB72001C120D /* MockPayment.swift in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions Sources/Networking/Responses/OfferingsResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ struct OfferingsResponse {
let identifier: String
let description: String
let packages: [Package]
@IgnoreDecodeErrors<PaywallData?>
var paywall: PaywallData?
@DefaultDecodable.EmptyDictionary
var metadata: [String: AnyDecodable]

Expand Down
159 changes: 159 additions & 0 deletions Sources/Paywalls/PaywallData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// PaywallData.swift
//
// Created by Nacho Soto on 7/10/23.

import Foundation

/// The data necessary to display a paywall using the `RevenueCatUI` library.
/// They can be created and configured in the dashboard, then access from ``Offering/paywall``.
public struct PaywallData {

/// The type of template used to display this paywall.
public var template: PaywallTemplate

/// Generic configuration for any paywall.
public var config: Configuration

fileprivate var defaultLocaleIdentifier: String
fileprivate var localization: [String: LocalizedConfiguration]

}

extension PaywallData {

/// Configuration containing values for the necessary `Locale`s.
public struct LocalizedConfiguration {

/// The content of the main action button for purchasing a subscription.
public let callToAction: String
/// The title of the paywall screen.
public let title: String

/// swiftlint:disable:next missing_docs
public init(callToAction: String, title: String) {
self.callToAction = callToAction
self.title = title
}

}

/// - Returns: ``PaywallData/LocalizedConfiguration-swift.struct`` for the given `Locale`, if found.
public func config(for locale: Locale) -> LocalizedConfiguration? {
return self.localization[locale.identifier]
}

/// The default `Locale` used if `Locale.current` is not configured for this paywall.
public var defaultLocale: Locale {
return .init(identifier: self.defaultLocaleIdentifier)
}

/// - Returns: the ``PaywallData/LocalizedConfiguration-swift.struct`` associated to the current `Locale`
/// or the configuration associated to ``defaultLocale``.
public var localizedConfiguration: LocalizedConfiguration {
return self.config(for: Locale.current) ?? self.defaultLocalizedConfiguration
}

private var defaultLocalizedConfiguration: LocalizedConfiguration {
let defaultLocale = self.defaultLocale

guard let result = self.config(for: defaultLocale) else {
fatalError(
"Corrupted data. Expected to find locale \(defaultLocale.identifier) " +
"in locales: \(Set(self.localization.keys))"
)
}

return result
}

}

extension PaywallData {

/// Generic configuration for any paywall.
public struct Configuration {

// swiftlint:disable:next missing_docs
public init() {}

}

}

// MARK: - Constructors

extension PaywallData {

init(
template: PaywallTemplate,
config: Configuration,
defaultLocale: String,
localization: [String: LocalizedConfiguration]
) {
self.template = template
self.config = config
self.defaultLocaleIdentifier = defaultLocale
self.localization = localization
}

/// Creates a test ``PaywallData`` with one localization
public init(
template: PaywallTemplate,
config: Configuration,
localization: LocalizedConfiguration
) {
let locale = Locale.current.identifier

self.init(
template: template,
config: config,
defaultLocale: locale,
localization: [locale: localization]
)
}

}

// MARK: - Codable

extension PaywallData.LocalizedConfiguration: Codable {

private enum CodingKeys: String, CodingKey {
case callToAction = "cta"
case title
}

}
extension PaywallData.Configuration: Codable {}
extension PaywallData: Codable {

// Note: these are camel case but converted by the decoder
private enum CodingKeys: String, CodingKey {
case template = "templateName"
case defaultLocaleIdentifier = "defaultLocale"
case config
case localization = "localizedStrings"
}

}

// MARK: - Equatable

extension PaywallData.LocalizedConfiguration: Equatable {}
extension PaywallData.Configuration: Equatable {}
extension PaywallData: Equatable {}

// MARK: - Sendable

extension PaywallData.LocalizedConfiguration: Sendable {}
extension PaywallData.Configuration: Sendable {}
extension PaywallData: Sendable {}
27 changes: 27 additions & 0 deletions Sources/Paywalls/PaywallTemplate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// PaywallTemplate.swift
//
// Created by Nacho Soto on 7/10/23.

import Foundation

/// The type of template used to display a paywall.
public enum PaywallTemplate: String {

/// swiftlint:disable:next missing_docs
case example1 = "sample_1"

}

extension PaywallTemplate: Codable {}
extension PaywallTemplate: Sendable {}
extension PaywallTemplate: Equatable {}
extension PaywallTemplate: CaseIterable {}
25 changes: 24 additions & 1 deletion Sources/Purchasing/Offering.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ import Foundation
*/
@objc public var metadata: [String: Any] { self._metadata.data }

/**
Paywall configuration defined in RevenueCat dashboard.
*/
public let paywall: PaywallData?

/**
Array of ``Package`` objects available for purchase.
*/
Expand Down Expand Up @@ -118,17 +123,35 @@ import Foundation

// swiftlint:disable cyclomatic_complexity

/// Initialize an ``Offering`` given a list of ``Package``s.
@objc
public convenience init(
identifier: String,
serverDescription: String,
metadata: [String: Any] = [:],
availablePackages: [Package]
) {
self.init(
identifier: identifier,
serverDescription: serverDescription,
metadata: metadata,
paywall: nil,
availablePackages: availablePackages
)
}
/// Initialize an ``Offering`` given a list of ``Package``s.
public init(
identifier: String,
serverDescription: String,
metadata: [String: Any],
metadata: [String: Any] = [:],
paywall: PaywallData? = nil,
availablePackages: [Package]
) {
self.identifier = identifier
self.serverDescription = serverDescription
self.availablePackages = availablePackages
self._metadata = Metadata(data: metadata)
self.paywall = paywall

var foundPackages: [PackageType: Package] = [:]

Expand Down
1 change: 1 addition & 0 deletions Sources/Purchasing/OfferingsFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class OfferingsFactory {
return Offering(identifier: offering.identifier,
serverDescription: offering.description,
metadata: offering.metadata.mapValues(\.asAny),
paywall: offering.paywall,
availablePackages: availablePackages)
}

Expand Down
7 changes: 6 additions & 1 deletion Tests/APITesters/ObjCAPITester/ObjCAPITester/RCOfferingAPI.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
@implementation RCOfferingAPI

+ (void)checkAPI {
RCOffering *o = nil; // No public initializer.
RCOffering *o = nil;
NSString *i = o.identifier;
NSString *sd = o.serverDescription;
NSArray<RCPackage *> *a = o.availablePackages;
Expand All @@ -34,6 +34,11 @@ + (void)checkAPI {
RCPackage *ok = [o objectForKeyedSubscript:@""];
NSDictionary<NSString *, id> *md = o.metadata;

o = [[RCOffering alloc] initWithIdentifier:@""
serverDescription:@""
metadata:@{}
availablePackages:a];

NSLog(o, i, sd, a, l, an, s, t, tm, m, w, p, ok, md);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
4F1428A22A4A11D7006CD196 /* TestStoreProductAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F1428A12A4A11D7006CD196 /* TestStoreProductAPI.swift */; };
4F1428A72A4A16C0006CD196 /* TestStoreProductDiscountAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F1428A62A4A16C0006CD196 /* TestStoreProductDiscountAPI.swift */; };
4F6BEE752A27C77C00CD9322 /* OtherAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6BEE742A27C77C00CD9322 /* OtherAPI.swift */; };
4FF58AD22A5DDA5B00451B28 /* PaywallAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF58AD12A5DDA5B00451B28 /* PaywallAPI.swift */; };
570FAF562864EE1D00D3C769 /* NonSubscriptionTransactionAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570FAF552864EE1D00D3C769 /* NonSubscriptionTransactionAPI.swift */; };
5738F40C27866DD00096D623 /* StoreProductDiscountAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5738F40B27866DD00096D623 /* StoreProductDiscountAPI.swift */; };
5738F42127866F8F0096D623 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C626EBE7EA007DDB75 /* main.swift */; };
Expand Down Expand Up @@ -60,6 +61,7 @@
4F1428A12A4A11D7006CD196 /* TestStoreProductAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestStoreProductAPI.swift; sourceTree = "<group>"; };
4F1428A62A4A16C0006CD196 /* TestStoreProductDiscountAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestStoreProductDiscountAPI.swift; sourceTree = "<group>"; };
4F6BEE742A27C77C00CD9322 /* OtherAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherAPI.swift; sourceTree = "<group>"; };
4FF58AD12A5DDA5B00451B28 /* PaywallAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallAPI.swift; sourceTree = "<group>"; };
570FAF552864EE1D00D3C769 /* NonSubscriptionTransactionAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonSubscriptionTransactionAPI.swift; sourceTree = "<group>"; };
5738F40B27866DD00096D623 /* StoreProductDiscountAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreProductDiscountAPI.swift; sourceTree = "<group>"; };
5738F429278673A80096D623 /* SubscriptionPeriodAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPeriodAPI.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -146,6 +148,7 @@
A5D614C326EBE7EA007DDB75 /* OfferingsAPI.swift */,
4F6BEE742A27C77C00CD9322 /* OtherAPI.swift */,
A5D614C926EBE7EA007DDB75 /* PackageAPI.swift */,
4FF58AD12A5DDA5B00451B28 /* PaywallAPI.swift */,
B3A4C833280DE72600D4AE17 /* PromotionalOfferAPI.swift */,
A5D614CE26EBE7EA007DDB75 /* PurchasesAPI.swift */,
A513AD33272B4C0100E0C1BA /* RefundRequestStatusAPI.swift */,
Expand Down Expand Up @@ -249,6 +252,7 @@
2DD778E6270E23460079CBD4 /* PurchasesAPI.swift in Sources */,
2DD778EF270E23460079CBD4 /* EntitlementInfoAPI.swift in Sources */,
4F1428A72A4A16C0006CD196 /* TestStoreProductDiscountAPI.swift in Sources */,
4FF58AD22A5DDA5B00451B28 /* PaywallAPI.swift in Sources */,
2DD778E9270E23460079CBD4 /* CustomerInfoAPI.swift in Sources */,
5758EE4F2786493400B3B703 /* StoreProductAPI.swift in Sources */,
5753ED9D294A6F3F00CBAB54 /* PurchasesReceiptParserAPI.swift in Sources */,
Expand Down
Loading

0 comments on commit e6cdad4

Please sign in to comment.