diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 8a0d246bfc..ed8ca14997 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -199,6 +199,7 @@ 37E35C8515C5E2D01B0AF5C1 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E3507939634ED5A9280544 /* Strings.swift */; }; 42F1DF385E3C1F9903A07FBF /* ProductsFetcherSK1.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFB3CBAA73855779FE828CE2 /* ProductsFetcherSK1.swift */; }; 4F0201C42A13C85500091612 /* Assertions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0201C32A13C85500091612 /* Assertions.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 */; }; @@ -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 */; }; @@ -943,6 +946,7 @@ 37E35F783903362B65FB7AF3 /* MockProductsRequestFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockProductsRequestFactory.swift; sourceTree = ""; }; 37E35FDA0A44EA03EA12DAA2 /* DateFormatter+ExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DateFormatter+ExtensionsTests.swift"; sourceTree = ""; }; 4F0201C32A13C85500091612 /* Assertions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assertions.swift; sourceTree = ""; }; + 4F05876E2A5DE03F00E9A834 /* PaywallDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallDataTests.swift; sourceTree = ""; }; 4F0BBA802A1D0524000E75AB /* DefaultDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultDecodable.swift; sourceTree = ""; }; 4F0BBAAB2A1D253D000E75AB /* OfflineCustomerInfoCreatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineCustomerInfoCreatorTests.swift; sourceTree = ""; }; 4F0CE2BC2A215CE600561895 /* TransactionPosterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionPosterTests.swift; sourceTree = ""; }; @@ -973,6 +977,8 @@ 4F7DBFBC2A1E986C00A2F511 /* StoreKit2TransactionFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2TransactionFetcher.swift; sourceTree = ""; }; 4F8038322A1EA7C300D21039 /* TransactionPoster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionPoster.swift; sourceTree = ""; }; 4F8452672A5756CC00084550 /* HTTPRequestBody+Signing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HTTPRequestBody+Signing.swift"; sourceTree = ""; }; + 4F87610E2A5C9E490006FA14 /* PaywallData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallData.swift; sourceTree = ""; }; + 4F87612B2A5CAB980006FA14 /* PaywallTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallTemplate.swift; sourceTree = ""; }; 4F8929182A65EF3000A91EA2 /* EnsureNonEmptyCollectionDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnsureNonEmptyCollectionDecodable.swift; sourceTree = ""; }; 4F8A58162A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOfflineCustomerInfoCreator.swift; sourceTree = ""; }; 4F8DDB682AAA9189000188F2 /* OperationDispatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationDispatcherTests.swift; sourceTree = ""; }; @@ -1616,6 +1622,7 @@ 2DDA3E4524DB0B4500EDFE5B /* Misc */, 35D832CB262A5B3400E60AC5 /* Networking */, 57488B7D29CB70DA0000EE7E /* OfflineEntitlements */, + 4F87610D2A5C9E330006FA14 /* Paywalls */, 354235D524C11138008C84EE /* Purchasing */, 57F3C0C929B7A04D0004FD7E /* Security */, 354895D0267AE32D001DC5B1 /* SubscriberAttributes */, @@ -2211,6 +2218,15 @@ path = DebugUI; sourceTree = ""; }; + 4F87610D2A5C9E330006FA14 /* Paywalls */ = { + isa = PBXGroup; + children = ( + 4F87610E2A5C9E490006FA14 /* PaywallData.swift */, + 4F87612B2A5CAB980006FA14 /* PaywallTemplate.swift */, + ); + path = Paywalls; + sourceTree = ""; + }; 4F90AFC92A39152A0047E63F /* Helpers */ = { isa = PBXGroup; children = ( @@ -2358,6 +2374,7 @@ 574A2F3E282D75E300150D40 /* OfferingsDecodingTests.swift */, 574A2F4E282D7B9E00150D40 /* PostOfferDecodingTests.swift */, 5766C61F282DA3D50067D886 /* GetIntroEligibilityDecodingTests.swift */, + 4F05876E2A5DE03F00E9A834 /* PaywallDataTests.swift */, 57045B3729C514A8001A5417 /* ProductEntitlementMappingDecodingTests.swift */, ); path = Responses; @@ -3225,6 +3242,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 */, @@ -3337,6 +3355,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 */, @@ -3510,6 +3529,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 */, diff --git a/Sources/Networking/Responses/OfferingsResponse.swift b/Sources/Networking/Responses/OfferingsResponse.swift index d513b2d5a4..5e425d82e6 100644 --- a/Sources/Networking/Responses/OfferingsResponse.swift +++ b/Sources/Networking/Responses/OfferingsResponse.swift @@ -29,6 +29,8 @@ struct OfferingsResponse { let identifier: String let description: String let packages: [Package] + @IgnoreDecodeErrors + var paywall: PaywallData? @DefaultDecodable.EmptyDictionary var metadata: [String: AnyDecodable] diff --git a/Sources/Paywalls/PaywallData.swift b/Sources/Paywalls/PaywallData.swift new file mode 100644 index 0000000000..7ba3738558 --- /dev/null +++ b/Sources/Paywalls/PaywallData.swift @@ -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 {} diff --git a/Sources/Paywalls/PaywallTemplate.swift b/Sources/Paywalls/PaywallTemplate.swift new file mode 100644 index 0000000000..837fad6e15 --- /dev/null +++ b/Sources/Paywalls/PaywallTemplate.swift @@ -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 {} diff --git a/Sources/Purchasing/Offering.swift b/Sources/Purchasing/Offering.swift index 466746edf7..b6eb310cdb 100644 --- a/Sources/Purchasing/Offering.swift +++ b/Sources/Purchasing/Offering.swift @@ -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. */ @@ -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] = [:] diff --git a/Sources/Purchasing/OfferingsFactory.swift b/Sources/Purchasing/OfferingsFactory.swift index f929ec744c..542ca674d4 100644 --- a/Sources/Purchasing/OfferingsFactory.swift +++ b/Sources/Purchasing/OfferingsFactory.swift @@ -50,6 +50,7 @@ class OfferingsFactory { return Offering(identifier: offering.identifier, serverDescription: offering.description, metadata: offering.metadata.mapValues(\.asAny), + paywall: offering.paywall, availablePackages: availablePackages) } diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCOfferingAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCOfferingAPI.m index 96d63fe9e2..bd891e0692 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCOfferingAPI.m +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCOfferingAPI.m @@ -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 *a = o.availablePackages; @@ -34,6 +34,11 @@ + (void)checkAPI { RCPackage *ok = [o objectForKeyedSubscript:@""]; NSDictionary *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); } diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj b/Tests/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj index 22265c2544..d531336446 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester.xcodeproj/project.pbxproj @@ -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 */; }; @@ -60,6 +61,7 @@ 4F1428A12A4A11D7006CD196 /* TestStoreProductAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestStoreProductAPI.swift; sourceTree = ""; }; 4F1428A62A4A16C0006CD196 /* TestStoreProductDiscountAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestStoreProductDiscountAPI.swift; sourceTree = ""; }; 4F6BEE742A27C77C00CD9322 /* OtherAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherAPI.swift; sourceTree = ""; }; + 4FF58AD12A5DDA5B00451B28 /* PaywallAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallAPI.swift; sourceTree = ""; }; 570FAF552864EE1D00D3C769 /* NonSubscriptionTransactionAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonSubscriptionTransactionAPI.swift; sourceTree = ""; }; 5738F40B27866DD00096D623 /* StoreProductDiscountAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreProductDiscountAPI.swift; sourceTree = ""; }; 5738F429278673A80096D623 /* SubscriptionPeriodAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPeriodAPI.swift; sourceTree = ""; }; @@ -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 */, @@ -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 */, diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/OfferingAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/OfferingAPI.swift index 2d549aead4..dae1ca4e34 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/OfferingAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/OfferingAPI.swift @@ -33,15 +33,35 @@ func checkOfferingAPI() { let metadataString: String = off.getMetadataValue(for: "", default: "") let metadataInt: Int = off.getMetadataValue(for: "", default: 0) let metadataOptionalInt: Int? = off.getMetadataValue(for: "", default: nil) + let _: PaywallData? = off.paywall print(off!, ident, sDesc, aPacks, lPack!, annPack!, smPack!, thmPack!, twmPack!, mPack!, wPack!, pPack!, package!, metadata, metadataString, metadataInt, metadataOptionalInt!) } private func checkCreateOfferingAPI(package: Package) { - _ = Offering(identifier: "", - serverDescription: "", - metadata: [String: Any](), - availablePackages: [package] + _ = Offering( + identifier: "", + serverDescription: "", + availablePackages: [package] + ) + _ = Offering( + identifier: "", + serverDescription: "", + metadata: [String: Any](), + availablePackages: [package] + ) + _ = Offering( + identifier: "", + serverDescription: "", + paywall: Optional.none, + availablePackages: [package] + ) + _ = Offering( + identifier: "", + serverDescription: "", + metadata: [String: Any](), + paywall: Optional.none, + availablePackages: [package] ) } diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PaywallAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PaywallAPI.swift new file mode 100644 index 0000000000..bc9d2c69af --- /dev/null +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PaywallAPI.swift @@ -0,0 +1,39 @@ +// +// PaywallAPI.swift +// SwiftAPITester +// +// Created by Nacho Soto on 7/11/23. +// + +import Foundation +import RevenueCat + +func checkPaywallData(_ data: PaywallData) { + let template: PaywallTemplate = data.template + let config: PaywallData.Configuration = data.config + let locale: Locale = data.defaultLocale + let _: PaywallData.LocalizedConfiguration? = data.config(for: locale) + let localization: PaywallData.LocalizedConfiguration = data.localizedConfiguration + + let _: PaywallData = .init(template: template, + config: config, + localization: localization) +} + +func checkPaywallConfiguration(_ config: PaywallData.Configuration) { + let _: PaywallData.Configuration = .init() +} + +func checkPaywallLocalizedConfig(_ config: PaywallData.LocalizedConfiguration) { + let _: String = config.callToAction + let _: String = config.title +} + +func checkPaywallTemplate(_ template: PaywallTemplate) { + switch template { + case .example1: + break + @unknown default: + break + } +} diff --git a/Tests/BackendIntegrationTests/__Snapshots__/LoadShedderIntegrationTests/testCanGetOfferings.1.json b/Tests/BackendIntegrationTests/__Snapshots__/LoadShedderIntegrationTests/testCanGetOfferings.1.json index 676bc5319f..2b66e0d1ce 100644 --- a/Tests/BackendIntegrationTests/__Snapshots__/LoadShedderIntegrationTests/testCanGetOfferings.1.json +++ b/Tests/BackendIntegrationTests/__Snapshots__/LoadShedderIntegrationTests/testCanGetOfferings.1.json @@ -13,7 +13,8 @@ "identifier" : "$rc_monthly", "platform_product_identifier" : "com.revenuecat.loadShedder.monthly" } - ] + ], + "paywall" : null }, { "description" : "an alternative offering", @@ -26,7 +27,8 @@ "identifier" : "$rc_monthly", "platform_product_identifier" : "com.revenuecat.loadShedder.monthly" } - ] + ], + "paywall" : null } ] } \ No newline at end of file diff --git a/Tests/BackendIntegrationTests/__Snapshots__/StoreKitIntegrationTests/testCanGetOfferings.1.json b/Tests/BackendIntegrationTests/__Snapshots__/StoreKitIntegrationTests/testCanGetOfferings.1.json index 2ab1b2f771..98f32e8379 100644 --- a/Tests/BackendIntegrationTests/__Snapshots__/StoreKitIntegrationTests/testCanGetOfferings.1.json +++ b/Tests/BackendIntegrationTests/__Snapshots__/StoreKitIntegrationTests/testCanGetOfferings.1.json @@ -28,7 +28,8 @@ "identifier" : "non_renewing", "platform_product_identifier" : "non_renewing_subscription" } - ] + ], + "paywall" : null }, { "description" : "Coins", @@ -41,7 +42,8 @@ "identifier" : "10.coins", "platform_product_identifier" : "consumable.10_coins" } - ] + ], + "paywall" : null } ] } \ No newline at end of file diff --git a/Tests/UnitTests/Networking/Responses/BaseHTTPResponseTest.swift b/Tests/UnitTests/Networking/Responses/BaseHTTPResponseTest.swift index 3311951e2d..53b4a968f4 100644 --- a/Tests/UnitTests/Networking/Responses/BaseHTTPResponseTest.swift +++ b/Tests/UnitTests/Networking/Responses/BaseHTTPResponseTest.swift @@ -21,16 +21,31 @@ class BaseHTTPResponseTest: TestCase { file: StaticString = #filePath, line: UInt = #line ) throws -> T { + return try T.create(with: self.data(for: name, file: file, line: line)) + } + + @_disfavoredOverload + func decodeFixture( + _ name: String, + file: StaticString = #filePath, + line: UInt = #line + ) throws -> T { + return try T.create(with: self.data(for: name, file: file, line: line)) + } + + private func data( + for fileName: String, + file: StaticString, + line: UInt + ) throws -> Data { let url = try XCTUnwrap( - Bundle(for: BundleToken.self).url(forResource: name, + Bundle(for: BundleToken.self).url(forResource: fileName, withExtension: "json", subdirectory: "Fixtures"), "Could not find file with name: '\(name).json'", file: file, line: line ) - let data = try XCTUnwrap(Data(contentsOf: url), file: file, line: line) - - return try T.create(with: data) + return try XCTUnwrap(Data(contentsOf: url), file: file, line: line) } } diff --git a/Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json b/Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json index 9b84d11293..d4ec346835 100644 --- a/Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json +++ b/Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json @@ -25,6 +25,35 @@ } ] }, + { + "description": "Offering with paywall", + "identifier": "paywall", + "packages": [ + { + "identifier": "$rc_monthly", + "platform_product_identifier": "com.revenuecat.monthly_4.99.1_week_intro" + }, + { + "identifier": "$rc_annual", + "platform_product_identifier": "com.revenuecat.yearly_10.99.2_week_intro" + } + ], + "paywall": { + "template_name": "sample_1", + "localized_strings": { + "en_US": { + "cta": "Purchase now", + "title": "Paywall" + }, + "es_ES": { + "cta": "Comprar", + "title": "Tienda" + } + }, + "default_locale": "en_US", + "config": {} + } + }, { "description": "offering with metadata", "identifier": "metadata", @@ -55,6 +84,19 @@ "platform_product_identifier": "com.revenuecat.other_product" } ] + }, + { + "description": "offering with invalid paywall", + "identifier": "invalid_paywall", + "packages": [ + { + "identifier": "$rc_lifetime", + "platform_product_identifier": "com.revenuecat.other_product" + } + ], + "paywall": { + "Missing": "data" + } } ] } diff --git a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-Sample1.json b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-Sample1.json new file mode 100644 index 0000000000..c16f8d6592 --- /dev/null +++ b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-Sample1.json @@ -0,0 +1,15 @@ +{ + "template_name": "sample_1", + "localized_strings": { + "en_US": { + "cta": "Purchase now", + "title": "Paywall" + }, + "es_ES": { + "cta": "Comprar", + "title": "Tienda" + } + }, + "default_locale": "en_US", + "config": {} +} diff --git a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_and_default_locale.json b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_and_default_locale.json new file mode 100644 index 0000000000..265fe0789e --- /dev/null +++ b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_and_default_locale.json @@ -0,0 +1,11 @@ +{ + "template_name": "sample_1", + "localized_strings": { + "it_IT": { + "cta": "Purchase now", + "title": "Paywall" + } + }, + "default_locale": "es_ES", + "config": {} +} diff --git a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_locale.json b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_locale.json new file mode 100644 index 0000000000..8b7c670ebc --- /dev/null +++ b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_locale.json @@ -0,0 +1,11 @@ +{ + "template_name": "sample_1", + "localized_strings": { + "es_ES": { + "cta": "Purchase now", + "title": "Paywall" + } + }, + "default_locale": "es_ES", + "config": {} +} diff --git a/Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift b/Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift index 0dcb17263d..ce2c94b4e2 100644 --- a/Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift +++ b/Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift @@ -27,7 +27,7 @@ class OfferingsDecodingTests: BaseHTTPResponseTest { func testDecodesAllOfferings() throws { expect(self.response.currentOfferingId) == "default" - expect(self.response.offerings).to(haveCount(4)) + expect(self.response.offerings).to(haveCount(6)) } func testDecodesFirstOffering() throws { @@ -63,7 +63,7 @@ class OfferingsDecodingTests: BaseHTTPResponseTest { } func testDecodesMetadataOffering() throws { - let offering = try XCTUnwrap(self.response.offerings[safe: 2]) + let offering = try XCTUnwrap(self.response.offerings[safe: 3]) expect(offering.identifier) == "metadata" expect(offering.description) == "offering with metadata" @@ -86,7 +86,7 @@ class OfferingsDecodingTests: BaseHTTPResponseTest { } func testDecodesNullMetadataOffering() throws { - let offering = try XCTUnwrap(self.response.offerings[safe: 3]) + let offering = try XCTUnwrap(self.response.offerings[safe: 4]) expect(offering.identifier) == "nullmetadata" expect(offering.description) == "offering with null metadata" @@ -99,6 +99,41 @@ class OfferingsDecodingTests: BaseHTTPResponseTest { expect(package.platformProductIdentifier) == "com.revenuecat.other_product" } + func testDecodesPaywallData() throws { + let offering = try XCTUnwrap(self.response.offerings[safe: 2]) + + expect(offering.identifier) == "paywall" + expect(offering.description) == "Offering with paywall" + expect(offering.metadata) == [:] + expect(offering.packages).to(haveCount(2)) + + let paywall = try XCTUnwrap(offering.paywall) + expect(paywall.template) == .example1 + expect(paywall.defaultLocale) == Locale(identifier: "en_US") + + let enConfig = try XCTUnwrap(paywall.config(for: Locale(identifier: "en_US"))) + expect(enConfig.callToAction) == "Purchase now" + expect(enConfig.title) == "Paywall" + + let esConfig = try XCTUnwrap(paywall.config(for: Locale(identifier: "es_ES"))) + expect(esConfig.callToAction) == "Comprar" + expect(esConfig.title) == "Tienda" + + // This test relies on this + expect(Locale.current.identifier) == "en_US" + expect(paywall.localizedConfiguration) == paywall.config(for: Locale.current) + + expect(paywall.config(for: Locale(identifier: "gl_ES"))).to(beNil()) + } + + func testIgnoresInvalidPaywallData() throws { + let offering = try XCTUnwrap(self.response.offerings[safe: 5]) + + expect(offering.identifier) == "invalid_paywall" + expect(offering.packages).to(haveCount(1)) + expect(offering.paywall).to(beNil()) + } + func testEncoding() throws { expect(try self.response.encodeAndDecode()) == self.response } diff --git a/Tests/UnitTests/Networking/Responses/PaywallDataTests.swift b/Tests/UnitTests/Networking/Responses/PaywallDataTests.swift new file mode 100644 index 0000000000..46e95008f0 --- /dev/null +++ b/Tests/UnitTests/Networking/Responses/PaywallDataTests.swift @@ -0,0 +1,76 @@ +// +// 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 +// +// PaywallDataTests.swift +// +// Created by Nacho Soto on 7/11/23. + +import Nimble +@testable import RevenueCat +import XCTest + +class PaywallDataTests: BaseHTTPResponseTest { + + override func setUp() { + super.setUp() + + expect(Locale.current.identifier).to( + equal(Self.defaultLocale), + description: "Tests require this" + ) + } + + func testSample1() throws { + let paywall: PaywallData = try self.decodeFixture("PaywallData-Sample1") + + expect(paywall.template) == .example1 + expect(paywall.defaultLocale) == Locale(identifier: Self.defaultLocale) + + let enConfig = try XCTUnwrap(paywall.config(for: Locale(identifier: "en_US"))) + expect(enConfig.callToAction) == "Purchase now" + expect(enConfig.title) == "Paywall" + + let esConfig = try XCTUnwrap(paywall.config(for: Locale(identifier: "es_ES"))) + expect(esConfig.callToAction) == "Comprar" + expect(esConfig.title) == "Tienda" + + expect(paywall.localizedConfiguration) == paywall.config(for: Locale.current) + + expect(paywall.config(for: Locale(identifier: "gl_ES"))).to(beNil()) + } + + func testMissingCurrentLocaleLoadsDefault() throws { + let paywall: PaywallData = try self.decodeFixture("PaywallData-missing_current_locale") + + expect(paywall.defaultLocale.identifier) == "es_ES" + + let localization = paywall.localizedConfiguration + expect(localization.callToAction) == "Purchase now" + expect(localization.title) == "Paywall" + } + + #if !os(watchOS) + func testMissingCurrentAndDefaultFails() throws { + let paywall: PaywallData = try self.decodeFixture("PaywallData-missing_current_and_default_locale") + + expect(paywall.defaultLocale.identifier) == "es_ES" + + expect { + let _: PaywallData.LocalizedConfiguration = paywall.localizedConfiguration + }.to(throwAssertion()) + } + #endif + +} + +private extension PaywallDataTests { + + static let defaultLocale = "en_US" + +}