diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 265fe711b4..74a7e3795a 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -265,7 +265,6 @@ 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 */; }; 4F89A55D2A6ABADF008A411E /* PaywallViewMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F89A55C2A6ABADF008A411E /* PaywallViewMode.swift */; }; 4F8A58172A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F8A58162A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift */; }; @@ -1005,7 +1004,6 @@ 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 = ""; }; 4F89A55C2A6ABADF008A411E /* PaywallViewMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallViewMode.swift; sourceTree = ""; }; 4F8A58162A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOfflineCustomerInfoCreator.swift; sourceTree = ""; }; @@ -2263,7 +2261,6 @@ 4FBBD4E52A620573001CBA21 /* PaywallColor.swift */, 4F87610E2A5C9E490006FA14 /* PaywallData.swift */, 4F062D312A85A11600A8A613 /* PaywallData+Localization.swift */, - 4F87612B2A5CAB980006FA14 /* PaywallTemplate.swift */, 4F6ABC772A81595900250E63 /* PaywallCacheWarming.swift */, 4F89A55C2A6ABADF008A411E /* PaywallViewMode.swift */, ); @@ -3427,7 +3424,6 @@ 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 */, diff --git a/RevenueCatUI/Data/PaywallData+Validation.swift b/RevenueCatUI/Data/PaywallData+Validation.swift new file mode 100644 index 0000000000..644b45bad5 --- /dev/null +++ b/RevenueCatUI/Data/PaywallData+Validation.swift @@ -0,0 +1,179 @@ +// +// PaywallData+Validation.swift +// +// +// Created by Nacho Soto on 8/15/23. +// + +import RevenueCat + +// MARK: - Errors + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension Offering { + + typealias ValidationResult = (displayablePaywall: PaywallData, + template: PaywallTemplate, + error: Offering.PaywallValidationError?) + + enum PaywallValidationError: Swift.Error, Equatable { + + case missingPaywall + case invalidTemplate(String) + case invalidVariables(Set) + case invalidIcons(Set) + + } + +} + +// MARK: - Offering validation + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension Offering { + + /// - Returns: a validated paywall suitable to be displayed, and any associated error. + func validatedPaywall() -> ValidationResult { + if let paywall = self.paywall { + switch paywall.validate() { + case let .success(template): + return (paywall, template, nil) + + case let .failure(error): + // If there are any errors, create a default paywall + // with only the configured packages. + return (.createDefault(with: paywall.config.packages), + PaywallData.defaultTemplate, + error) + } + } else { + // If `Offering` has no paywall, create a default one with all available packages. + return (displayablePaywall: .createDefault(with: self.availablePackages), + PaywallData.defaultTemplate, + error: .missingPaywall) + } + } + +} + +// MARK: - PaywallData validation + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private extension PaywallData { + + typealias Error = Offering.PaywallValidationError + + /// - Returns: `nil` if there are no validation errors. + func validate() -> Result { + if let error = Self.validateLocalization(self.localizedConfiguration) { + return .failure(error) + } + + guard let template = PaywallTemplate(rawValue: self.templateName) else { + return .failure(.invalidTemplate(self.templateName)) + } + + let invalidIcons = self.localizedConfiguration.validateIcons() + guard invalidIcons.isEmpty else { + return .failure(.invalidIcons(invalidIcons)) + } + + return .success(template) + } + + /// Validates that all strings inside of `LocalizedConfiguration` contain no unrecognized variables. + private static func validateLocalization(_ localization: LocalizedConfiguration) -> Error? { + let unrecognizedVariables = Set( + localization + .allValues + .lazy + .compactMap { $0.unrecognizedVariables() } + .joined() + ) + + return unrecognizedVariables.isEmpty + ? nil + : .invalidVariables(unrecognizedVariables) + } + +} + +private extension PaywallData.LocalizedConfiguration { + + /// - Returns: the set of invalid icons + func validateIcons() -> Set { + return Set(self.features.compactMap { $0.validateIcon() }) + } + +} + +private extension PaywallData.LocalizedConfiguration.Feature { + + /// - Returns: the icon ID if it's not recognized + func validateIcon() -> String? { + guard let iconID = self.iconID else { return nil } + + return PaywallIcon(rawValue: iconID) == nil + ? iconID + : nil + } + +} + +// MARK: - Errors + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension Offering.PaywallValidationError: CustomStringConvertible { + + var description: String { + switch self { + case .missingPaywall: + return "Offering has no configured paywall." + + case let .invalidTemplate(name): + return "Template not recognized: \(name)." + + case let .invalidVariables(names): + return "Found unrecognized variables: \(names.joined(separator: ", "))." + + case let .invalidIcons(names): + return "Found unrecognized icons: \(names.joined(separator: ", "))." + } + } + +} + +// MARK: - PaywallLocalizedConfiguration + +private extension PaywallLocalizedConfiguration { + + /// The set of properties inside a `PaywallLocalizedConfiguration`. + static var allProperties: Set> { + return [ + \.optionalTitle, + \.subtitle, + \.optionalCallToAction, + \.callToActionWithIntroOffer, + \.offerDetails, + \.offerDetailsWithIntroOffer, + \.offerName + ] + } + + var allValues: [String] { + return Self + .allProperties + .compactMap { self[keyPath: $0] } + + self.features.flatMap { + [$0.title, $0.content].compactMap { $0 } + } + } + +} + +private extension PaywallLocalizedConfiguration { + + var optionalTitle: String? { return self.title } + var optionalCallToAction: String? { self.callToAction } + +} diff --git a/Sources/Paywalls/PaywallTemplate.swift b/RevenueCatUI/Data/PaywallTemplate.swift similarity index 75% rename from Sources/Paywalls/PaywallTemplate.swift rename to RevenueCatUI/Data/PaywallTemplate.swift index d374a4a529..8a98bc5c23 100644 --- a/Sources/Paywalls/PaywallTemplate.swift +++ b/RevenueCatUI/Data/PaywallTemplate.swift @@ -14,19 +14,14 @@ import Foundation /// The type of template used to display a paywall. -public enum PaywallTemplate: String { +internal enum PaywallTemplate: String { - // swiftlint:disable missing_docs case template1 = "1" case template2 = "2" case template3 = "3" case template4 = "4" - // swiftlint:enable missing_docs - } -extension PaywallTemplate: Codable {} -extension PaywallTemplate: Sendable {} extension PaywallTemplate: Equatable {} extension PaywallTemplate: CaseIterable {} diff --git a/RevenueCatUI/Data/TestData.swift b/RevenueCatUI/Data/TestData.swift index 7341193c1a..f2b79506c7 100644 --- a/RevenueCatUI/Data/TestData.swift +++ b/RevenueCatUI/Data/TestData.swift @@ -150,7 +150,7 @@ internal enum TestData { ] static let paywallWithIntroOffer = PaywallData( - template: .template1, + templateName: PaywallTemplate.template1.rawValue, config: .init( packages: [PackageType.monthly.identifier], images: Self.images, @@ -162,7 +162,7 @@ internal enum TestData { assetBaseURL: Self.paywallAssetBaseURL ) static let paywallWithNoIntroOffer = PaywallData( - template: .template1, + templateName: PaywallTemplate.template1.rawValue, config: .init( packages: [PackageType.annual.identifier], images: Self.images, @@ -193,7 +193,7 @@ internal enum TestData { serverDescription: "Offering", metadata: [:], paywall: .init( - template: .template2, + templateName: PaywallTemplate.template2.rawValue, config: .init( packages: [PackageType.annual.identifier, PackageType.monthly.identifier], images: Self.images, @@ -231,7 +231,7 @@ internal enum TestData { serverDescription: "Offering", metadata: [:], paywall: .init( - template: .template3, + templateName: PaywallTemplate.template3.rawValue, config: .init( packages: [PackageType.annual.identifier], images: Self.images, @@ -288,7 +288,7 @@ internal enum TestData { serverDescription: "Offering", metadata: [:], paywall: .init( - template: .template4, + templateName: PaywallTemplate.template4.rawValue, config: .init( packages: [PackageType.monthly.identifier, PackageType.sixMonth.identifier, @@ -323,6 +323,14 @@ internal enum TestData { TestData.annualPackage] ) + static let offeringWithNoPaywall = Offering( + identifier: Self.offeringIdentifier, + serverDescription: "Offering", + metadata: [:], + paywall: nil, + availablePackages: Self.packages + ) + static let lightColors: PaywallData.Configuration.Colors = .init( background: "#FFFFFF", text1: "#000000", diff --git a/RevenueCatUI/Data/Variables.swift b/RevenueCatUI/Data/Variables.swift index 8d6e49fd9e..0df2158b3f 100644 --- a/RevenueCatUI/Data/Variables.swift +++ b/RevenueCatUI/Data/Variables.swift @@ -46,6 +46,36 @@ enum VariableHandler { } } + fileprivate static func unrecognizedVariables(in set: Set) -> Set { + return Set( + set + .lazy + .filter { VariableHandler.provider(for: $0) == nil } + ) + } + + fileprivate typealias ValueProvider = (VariableDataProvider, Locale) -> String? + + // swiftlint:disable:next cyclomatic_complexity + fileprivate static func provider(for variableName: String) -> ValueProvider? { + switch variableName { + case "app_name": return { (provider, _) in provider.applicationName } + case "price": return { (provider, _) in provider.localizedPrice } + case "price_per_period": return { $0.localizedPricePerPeriod($1) } + case "total_price_and_per_month": return { $0.localizedPriceAndPerMonth($1) } + case "product_name": return { (provider, _) in provider.productName } + case "sub_period": return { $0.periodName($1) } + case "sub_price_per_month": return { (provider, _) in provider.localizedPricePerMonth } + case "sub_duration": return { $0.subscriptionDuration($1) } + case "sub_offer_duration": return { $0.introductoryOfferDuration($1) } + case "sub_offer_price": return { (provider, _) in provider.localizedIntroductoryOfferPrice } + + default: + Logger.warning(Strings.unrecognized_variable_name(variableName: variableName)) + return nil + } + } + } @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) @@ -55,29 +85,21 @@ extension String { return VariableHandler.processVariables(in: self, with: provider, locale: locale) } + func unrecognizedVariables() -> Set { + if #available(iOS 16.0, macOS 13.0, tvOS 16.0, *) { + return VariableHandlerIOS16.unrecognizedVariables(in: self) + } else { + return VariableHandlerIOS15.unrecognizedVariables(in: self) + } + } + } @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) private extension VariableDataProvider { - // swiftlint:disable:next cyclomatic_complexity func value(for variableName: String, locale: Locale) -> String { - switch variableName { - case "app_name": return self.applicationName - case "price": return self.localizedPrice - case "price_per_period": return self.localizedPricePerPeriod(locale) - case "total_price_and_per_month": return self.localizedPriceAndPerMonth(locale) - case "product_name": return self.productName - case "sub_period": return self.periodName(locale) - case "sub_price_per_month": return self.localizedPricePerMonth - case "sub_duration": return self.subscriptionDuration(locale) ?? "" - case "sub_offer_duration": return self.introductoryOfferDuration(locale) ?? "" - case "sub_offer_price": return self.localizedIntroductoryOfferPrice ?? "" - - default: - Logger.warning(Strings.unrecognized_variable_name(variableName: variableName)) - return "" - } + VariableHandler.provider(for: variableName)?(self, locale) ?? "" } } @@ -103,6 +125,12 @@ private enum VariableHandlerIOS16 { return replacedString } + static func unrecognizedVariables(in string: String) -> Set { + return VariableHandler.unrecognizedVariables( + in: Set(Self.extractVariables(from: string).map(\.variable)) + ) + } + private static func extractVariables(from expression: String) -> [VariableMatch] { return expression.matches(of: Self.regex).map { match in let (_, variable) = match.output @@ -140,8 +168,7 @@ private enum VariableHandlerIOS15 { locale: Locale = .current ) -> String { var replacedString = string - let range = NSRange(string.startIndex..., in: string) - let matches = Self.regex.matches(in: string, options: [], range: range) + let matches = Self.regex.matches(in: string, options: [], range: string.range) for match in matches.reversed() { let variableNameRange = match.range(at: 1) @@ -162,6 +189,19 @@ private enum VariableHandlerIOS15 { return replacedString } + static func unrecognizedVariables(in string: String) -> Set { + let matches = Self.regex.matches(in: string, options: [], range: string.range) + + var variables: Set = [] + for match in matches { + if let variableNameRange = Range(match.range(at: 1), in: string) { + variables.insert(String(string[variableNameRange])) + } + } + + return VariableHandler.unrecognizedVariables(in: variables) + } + private static let pattern = "{{ }}" // Fix-me: this can be implemented using the new Regex from Swift. // This regex is known at compile time and tested: @@ -169,3 +209,9 @@ private enum VariableHandlerIOS15 { private static let regex = try! NSRegularExpression(pattern: "\\{\\{ (\\w+) \\}\\}", options: []) } + +private extension String { + + var range: NSRange { .init(self.startIndex..., in: self) } + +} diff --git a/RevenueCatUI/Helpers/PaywallData+Default.swift b/RevenueCatUI/Helpers/PaywallData+Default.swift index aed46285e2..28a8202450 100644 --- a/RevenueCatUI/Helpers/PaywallData+Default.swift +++ b/RevenueCatUI/Helpers/PaywallData+Default.swift @@ -22,7 +22,7 @@ extension PaywallData { static func createDefault(with packageIdentifiers: [String]) -> Self { return .init( - template: .template2, + templateName: Self.defaultTemplate.rawValue, config: .init( packages: packageIdentifiers, images: .init( @@ -38,6 +38,8 @@ extension PaywallData { ) } + static let defaultTemplate: PaywallTemplate = .template2 + static let appIconPlaceholder = "revenuecatui_default_paywall_app_icon" } diff --git a/RevenueCatUI/Helpers/PreviewHelpers.swift b/RevenueCatUI/Helpers/PreviewHelpers.swift index 5cf783ca3d..6b21fdc358 100644 --- a/RevenueCatUI/Helpers/PreviewHelpers.swift +++ b/RevenueCatUI/Helpers/PreviewHelpers.swift @@ -57,8 +57,11 @@ struct PreviewableTemplate: View { presentInSheet: Bool = false, creator: @escaping Creator ) { - self.configuration = offering.paywall!.configuration( + let paywall = offering.paywall! + + self.configuration = paywall.configuration( for: offering, + template: PaywallTemplate(rawValue: paywall.templateName)!, mode: mode, fonts: DefaultPaywallFontProvider(), locale: .current diff --git a/RevenueCatUI/PaywallView.swift b/RevenueCatUI/PaywallView.swift index f88873b968..4ba3c6fdc3 100644 --- a/RevenueCatUI/PaywallView.swift +++ b/RevenueCatUI/PaywallView.swift @@ -121,34 +121,28 @@ public struct PaywallView: View { checker: TrialOrIntroEligibilityChecker, purchaseHandler: PurchaseHandler ) -> some View { - if let paywall = offering.paywall { - LoadedOfferingPaywallView( - offering: offering, - paywall: paywall, - mode: self.mode, - fonts: fonts, - introEligibility: checker, - purchaseHandler: purchaseHandler - ) - } else { + let (paywall, template, error) = offering.validatedPaywall() + + let paywallView = LoadedOfferingPaywallView( + offering: offering, + paywall: paywall, + template: template, + mode: self.mode, + fonts: fonts, + introEligibility: checker, + purchaseHandler: purchaseHandler + ) + + if let error { DebugErrorView( - "Offering '\(offering.identifier)' has no configured paywall, or it has invalid data.\n" + + "\(error.description)\n" + "You can fix this by editing the paywall in the RevenueCat dashboard.\n" + "The displayed paywall contains default configuration.\n" + "This error will be hidden in production.", - releaseBehavior: .replacement( - AnyView( - LoadedOfferingPaywallView( - offering: offering, - paywall: .createDefault(with: offering.availablePackages), - mode: self.mode, - fonts: fonts, - introEligibility: checker, - purchaseHandler: purchaseHandler - ) - ) - ) + releaseBehavior: .replacement(AnyView(paywallView)) ) + } else { + paywallView } } @@ -162,6 +156,7 @@ struct LoadedOfferingPaywallView: View { private let offering: Offering private let paywall: PaywallData + private let template: PaywallTemplate private let mode: PaywallViewMode private let fonts: PaywallFontProvider @@ -176,6 +171,7 @@ struct LoadedOfferingPaywallView: View { init( offering: Offering, paywall: PaywallData, + template: PaywallTemplate, mode: PaywallViewMode, fonts: PaywallFontProvider, introEligibility: TrialOrIntroEligibilityChecker, @@ -183,6 +179,7 @@ struct LoadedOfferingPaywallView: View { ) { self.offering = offering self.paywall = paywall + self.template = template self.mode = mode self.fonts = fonts self._introEligibility = .init( @@ -194,6 +191,7 @@ struct LoadedOfferingPaywallView: View { var body: some View { let view = self.paywall .createView(for: self.offering, + template: self.template, mode: self.mode, fonts: self.fonts, introEligibility: self.introEligibility, @@ -238,7 +236,7 @@ struct PaywallView_Previews: PreviewProvider { purchaseHandler: PreviewHelpers.purchaseHandler ) .previewLayout(mode.layout) - .previewDisplayName("\(offering.paywall?.template.name ?? "")-\(mode)") + .previewDisplayName("\(offering.paywall?.templateName ?? "")-\(mode)") } } } diff --git a/RevenueCatUI/Templates/TemplateViewType.swift b/RevenueCatUI/Templates/TemplateViewType.swift index b4e3813b5d..90ab5bd474 100644 --- a/RevenueCatUI/Templates/TemplateViewType.swift +++ b/RevenueCatUI/Templates/TemplateViewType.swift @@ -40,14 +40,20 @@ private extension PaywallTemplate { extension PaywallData { @ViewBuilder + // swiftlint:disable:next function_parameter_count func createView(for offering: Offering, + template: PaywallTemplate, mode: PaywallViewMode, fonts: PaywallFontProvider, introEligibility: IntroEligibilityViewModel, locale: Locale) -> some View { - switch self.configuration(for: offering, mode: mode, fonts: fonts, locale: locale) { + switch self.configuration(for: offering, + template: template, + mode: mode, + fonts: fonts, + locale: locale) { case let .success(configuration): - Self.createView(template: self.template, configuration: configuration) + Self.createView(template: template, configuration: configuration) .task(id: offering) { await introEligibility.computeEligibility(for: configuration.packages) } @@ -60,6 +66,7 @@ extension PaywallData { func configuration( for offering: Offering, + template: PaywallTemplate, mode: PaywallViewMode, fonts: PaywallFontProvider, locale: Locale @@ -71,7 +78,7 @@ extension PaywallData { filter: self.config.packages, default: self.config.defaultPackage, localization: self.localizedConfiguration, - setting: self.template.packageSetting, + setting: template.packageSetting, locale: locale), configuration: self.config, colors: self.config.colors.multiScheme, diff --git a/RevenueCatUI/Views/LoadingPaywallView.swift b/RevenueCatUI/Views/LoadingPaywallView.swift index 11fbd9135f..2e8e7dd735 100644 --- a/RevenueCatUI/Views/LoadingPaywallView.swift +++ b/RevenueCatUI/Views/LoadingPaywallView.swift @@ -27,6 +27,7 @@ struct LoadingPaywallView: View { availablePackages: Self.packages ), paywall: Self.defaultPaywall, + template: Self.template, mode: self.mode, fonts: DefaultPaywallFontProvider(), introEligibility: Self.introEligibility, @@ -36,6 +37,7 @@ struct LoadingPaywallView: View { .redacted(reason: .placeholder) } + private static let template: PaywallTemplate = PaywallData.defaultTemplate private static let defaultPaywall: PaywallData = .createDefault(with: Self.packages) private static let packages: [Package] = [ diff --git a/Sources/Paywalls/PaywallData.swift b/Sources/Paywalls/PaywallData.swift index 389e0cc60b..9af11999f0 100644 --- a/Sources/Paywalls/PaywallData.swift +++ b/Sources/Paywalls/PaywallData.swift @@ -23,7 +23,7 @@ import Foundation public struct PaywallData { /// The type of template used to display this paywall. - public var template: PaywallTemplate + public var templateName: String /// Generic configuration for any paywall. public var config: Configuration @@ -32,7 +32,7 @@ public struct PaywallData { public var assetBaseURL: URL @EnsureNonEmptyCollectionDecodable - internal var localization: [String: LocalizedConfiguration] + internal private(set) var localization: [String: LocalizedConfiguration] } @@ -55,6 +55,8 @@ public protocol PaywallLocalizedConfiguration { var offerDetailsWithIntroOffer: String? { get } /// The name representing each of the packages, most commonly a variable. var offerName: String? { get } + /// An optional list of features that describe this paywall. + var features: [PaywallData.LocalizedConfiguration.Feature] { get } } @@ -327,12 +329,12 @@ extension PaywallData.Configuration { extension PaywallData { init( - template: PaywallTemplate, + templateName: String, config: Configuration, localization: [String: LocalizedConfiguration], assetBaseURL: URL ) { - self.template = template + self.templateName = templateName self.config = config self.localization = localization self.assetBaseURL = assetBaseURL @@ -340,7 +342,7 @@ extension PaywallData { /// Creates a test ``PaywallData`` with one localization public init( - template: PaywallTemplate, + templateName: String, config: Configuration, localization: LocalizedConfiguration, assetBaseURL: URL @@ -348,7 +350,7 @@ extension PaywallData { let locale = Locale.current.identifier self.init( - template: template, + templateName: templateName, config: config, localization: [locale: localization], assetBaseURL: assetBaseURL @@ -407,7 +409,7 @@ extension PaywallData: Codable { // Note: these are camel case but converted by the decoder private enum CodingKeys: String, CodingKey { - case template = "templateName" + case templateName case config case localization = "localizedStrings" case assetBaseURL = "assetBaseUrl" diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PaywallAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PaywallAPI.swift index d05f5836f6..8784625972 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PaywallAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PaywallAPI.swift @@ -13,13 +13,13 @@ import SwiftUI #endif func checkPaywallData(_ data: PaywallData) { - let template: PaywallTemplate = data.template + let templateName: String = data.templateName let config: PaywallData.Configuration = data.config let _: PaywallData.LocalizedConfiguration? = data.config(for: Locale.current) let localization: PaywallData.LocalizedConfiguration = data.localizedConfiguration let assetBaseURL: URL = data.assetBaseURL - let _: PaywallData = .init(template: template, + let _: PaywallData = .init(templateName: templateName, config: config, localization: localization, assetBaseURL: assetBaseURL) @@ -143,21 +143,6 @@ func checkPaywallColor(_ color: PaywallColor) throws { #endif } -func checkPaywallTemplate(_ template: PaywallTemplate) { - switch template { - case .template1: - break - case .template2: - break - case .template3: - break - case .template4: - break - @unknown default: - break - } -} - func checkPaywallViewMode(_ mode: PaywallViewMode) { switch mode { case .fullScreen: diff --git a/Tests/RevenueCatUITests/Data/PaywallDataValidationTests.swift b/Tests/RevenueCatUITests/Data/PaywallDataValidationTests.swift new file mode 100644 index 0000000000..e7e6bf3ffe --- /dev/null +++ b/Tests/RevenueCatUITests/Data/PaywallDataValidationTests.swift @@ -0,0 +1,153 @@ +// +// PaywallDataValidationTests.swift +// +// +// Created by Nacho Soto on 8/15/23. +// + +import Nimble +import RevenueCat +@testable import RevenueCatUI +import SnapshotTesting +import XCTest + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +class PaywallDataValidationTests: TestCase { + + func testValidateMissingPaywall() { + let offering = TestData.offeringWithNoPaywall + let result = TestData.offeringWithNoPaywall.validatedPaywall() + + Self.verifyPackages(in: result.displayablePaywall, match: offering.availablePackages) + Self.snapshot(result.displayablePaywall) + + expect(result.error) == .missingPaywall + } + + func testValidateValidPaywall() { + let offering = TestData.offeringWithSinglePackageFeaturesPaywall + let result = offering.validatedPaywall() + + expect(result.displayablePaywall) == offering.paywall + expect(result.error).to(beNil()) + } + + func testUnrecognizedTemplateNameGeneratesDefaultPaywall() { + let templateName = "unrecognized_template" + + let originalOffering = TestData.offeringWithMultiPackagePaywall + let offering = originalOffering.with(templateName: templateName) + let result = offering.validatedPaywall() + + Self.verifyPackages(in: result.displayablePaywall, match: originalOffering.paywall) + Self.snapshot(result.displayablePaywall) + + expect(result.error) == .invalidTemplate(templateName) + } + + func testUnrecognizedVariableGeneratesDefaultPaywall() { + let originalOffering = TestData.offeringWithMultiPackagePaywall + let offering = originalOffering + .with(localization: .init( + title: "Title with {{ unrecognized_variable }}", + callToAction: "{{ future_variable }}", + offerDetails: nil + )) + let result = offering.validatedPaywall() + + Self.verifyPackages(in: result.displayablePaywall, match: originalOffering.paywall) + Self.snapshot(result.displayablePaywall) + + expect(result.error) == .invalidVariables(["unrecognized_variable", "future_variable"]) + } + + func testUnrecognizedVariableInFeaturesGeneratesDefaultPaywall() throws { + let originalOffering = TestData.offeringWithMultiPackagePaywall + var localization = try XCTUnwrap(originalOffering.paywall?.localizedConfiguration) + localization.features = [ + .init(title: "{{ future_variable }}", content: "{{ new_variable }}"), + .init(title: "{{ another_one }}") + ] + + let offering = originalOffering.with(localization: localization) + let result = offering.validatedPaywall() + + Self.verifyPackages(in: result.displayablePaywall, match: originalOffering.paywall) + Self.snapshot(result.displayablePaywall) + + expect(result.error) == .invalidVariables(["future_variable", "new_variable", "another_one"]) + } + + func testUnrecognizedIconsGeneratesDefaultPaywall() throws { + let originalOffering = TestData.offeringWithMultiPackagePaywall + var localization = try XCTUnwrap(originalOffering.paywall?.localizedConfiguration) + localization.features = [ + .init(title: "Title 1", content: "Content 1", iconID: "unrecognized_icon_1"), + .init(title: "Title 2", content: "Content 2", iconID: "unrecognized_icon_2") + ] + + let offering = originalOffering.with(localization: localization) + let result = offering.validatedPaywall() + + Self.verifyPackages(in: result.displayablePaywall, match: originalOffering.paywall) + Self.snapshot(result.displayablePaywall) + + expect(result.error) == .invalidIcons(["unrecognized_icon_1", "unrecognized_icon_2"]) + } + +} + +// MARK: - + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private extension PaywallDataValidationTests { + + static func verifyPackages( + in paywall: PaywallData, + match other: PaywallData?, + file: StaticString = #file, + line: UInt = #line + ) { + expect( + file: file, line: line, + paywall.config.packages + ) == other?.config.packages + } + + static func verifyPackages( + in paywall: PaywallData, + match packages: [Package], + file: StaticString = #file, + line: UInt = #line + ) { + expect( + file: file, line: line, + paywall.config.packages + ) == packages.map(\.identifier) + } + + static func snapshot( + _ paywall: PaywallData, + file: StaticString = #file, + testName: String = #function, + line: UInt = #line + ) { + assertSnapshot( + matching: paywall.withTestAssetBaseURL, + as: .formattedJson, + file: file, + testName: testName, + line: line + ) + } + + static func offering(with paywall: PaywallData?) -> Offering { + return .init( + identifier: "offering", + serverDescription: "Offering", + paywall: paywall, + availablePackages: TestData.packages + ) + } + +} diff --git a/Tests/RevenueCatUITests/Data/VariablesTests.swift b/Tests/RevenueCatUITests/Data/VariablesTests.swift index e08d99d797..82bfaa97c8 100644 --- a/Tests/RevenueCatUITests/Data/VariablesTests.swift +++ b/Tests/RevenueCatUITests/Data/VariablesTests.swift @@ -169,6 +169,25 @@ class VariablesTests: TestCase { expect(result) == "$53.99 ($4.49/mo)" } + // MARK: - validation + + func testNoUnrecognizedVariables() { + let allVariables = "{{ app_name }} {{ price }} {{ price_per_period }} " + + "{{ total_price_and_per_month }} {{ product_name }} {{ sub_period }} " + + "{{ sub_price_per_month }} {{ sub_duration }} {{ sub_offer_duration }} " + + "{{ sub_offer_price }}" + + expect("".unrecognizedVariables()).to(beEmpty()) + expect(allVariables.unrecognizedVariables()).to(beEmpty()) + } + + func testUnrecognizedVariable() { + expect("This contains {{ multiple }} unrecognized {{ variables }}".unrecognizedVariables()) == [ + "multiple", + "variables" + ] + } + } // MARK: - Private diff --git a/Tests/RevenueCatUITests/Data/__Snapshots__/PaywallDataValidationTests/testUnrecognizedIconsGeneratesDefaultPaywall.1.json b/Tests/RevenueCatUITests/Data/__Snapshots__/PaywallDataValidationTests/testUnrecognizedIconsGeneratesDefaultPaywall.1.json new file mode 100644 index 0000000000..c0614761df --- /dev/null +++ b/Tests/RevenueCatUITests/Data/__Snapshots__/PaywallDataValidationTests/testUnrecognizedIconsGeneratesDefaultPaywall.1.json @@ -0,0 +1,40 @@ +{ + "asset_base_url" : "https://assets.pawwalls.com", + "config" : { + "blurred_background_image" : true, + "colors" : { + "light" : { + "accent1" : "#FFFFFF", + "accent2" : "#FFFFFF", + "background" : "#FFFFFF", + "call_to_action_background" : "#FFFFFF", + "call_to_action_foreground" : "#FFFFFF", + "text1" : "#FFFFFF" + } + }, + "display_restore_purchases" : true, + "images" : { + "background" : "background.jpg", + "icon" : "revenuecatui_default_paywall_app_icon" + }, + "packages" : [ + "$rc_annual", + "$rc_monthly" + ], + "privacy_url" : null, + "tos_url" : null + }, + "localized_strings" : { + "en_US" : { + "call_to_action" : "Continue", + "call_to_action_with_intro_offer" : null, + "features" : [ + + ], + "offer_details" : "{{ total_price_and_per_month }}", + "offer_details_with_intro_offer" : "Start your {{ sub_offer_duration }} trial, then {{ total_price_and_per_month }}.", + "title" : "{{ app_name }}" + } + }, + "template_name" : "2" +} \ No newline at end of file diff --git a/Tests/RevenueCatUITests/Data/__Snapshots__/PaywallDataValidationTests/testUnrecognizedTemplateNameGeneratesDefaultPaywall.1.json b/Tests/RevenueCatUITests/Data/__Snapshots__/PaywallDataValidationTests/testUnrecognizedTemplateNameGeneratesDefaultPaywall.1.json new file mode 100644 index 0000000000..c0614761df --- /dev/null +++ b/Tests/RevenueCatUITests/Data/__Snapshots__/PaywallDataValidationTests/testUnrecognizedTemplateNameGeneratesDefaultPaywall.1.json @@ -0,0 +1,40 @@ +{ + "asset_base_url" : "https://assets.pawwalls.com", + "config" : { + "blurred_background_image" : true, + "colors" : { + "light" : { + "accent1" : "#FFFFFF", + "accent2" : "#FFFFFF", + "background" : "#FFFFFF", + "call_to_action_background" : "#FFFFFF", + "call_to_action_foreground" : "#FFFFFF", + "text1" : "#FFFFFF" + } + }, + "display_restore_purchases" : true, + "images" : { + "background" : "background.jpg", + "icon" : "revenuecatui_default_paywall_app_icon" + }, + "packages" : [ + "$rc_annual", + "$rc_monthly" + ], + "privacy_url" : null, + "tos_url" : null + }, + "localized_strings" : { + "en_US" : { + "call_to_action" : "Continue", + "call_to_action_with_intro_offer" : null, + "features" : [ + + ], + "offer_details" : "{{ total_price_and_per_month }}", + "offer_details_with_intro_offer" : "Start your {{ sub_offer_duration }} trial, then {{ total_price_and_per_month }}.", + "title" : "{{ app_name }}" + } + }, + "template_name" : "2" +} \ No newline at end of file diff --git a/Tests/RevenueCatUITests/Data/__Snapshots__/PaywallDataValidationTests/testUnrecognizedVariableGeneratesDefaultPaywall.1.json b/Tests/RevenueCatUITests/Data/__Snapshots__/PaywallDataValidationTests/testUnrecognizedVariableGeneratesDefaultPaywall.1.json new file mode 100644 index 0000000000..c0614761df --- /dev/null +++ b/Tests/RevenueCatUITests/Data/__Snapshots__/PaywallDataValidationTests/testUnrecognizedVariableGeneratesDefaultPaywall.1.json @@ -0,0 +1,40 @@ +{ + "asset_base_url" : "https://assets.pawwalls.com", + "config" : { + "blurred_background_image" : true, + "colors" : { + "light" : { + "accent1" : "#FFFFFF", + "accent2" : "#FFFFFF", + "background" : "#FFFFFF", + "call_to_action_background" : "#FFFFFF", + "call_to_action_foreground" : "#FFFFFF", + "text1" : "#FFFFFF" + } + }, + "display_restore_purchases" : true, + "images" : { + "background" : "background.jpg", + "icon" : "revenuecatui_default_paywall_app_icon" + }, + "packages" : [ + "$rc_annual", + "$rc_monthly" + ], + "privacy_url" : null, + "tos_url" : null + }, + "localized_strings" : { + "en_US" : { + "call_to_action" : "Continue", + "call_to_action_with_intro_offer" : null, + "features" : [ + + ], + "offer_details" : "{{ total_price_and_per_month }}", + "offer_details_with_intro_offer" : "Start your {{ sub_offer_duration }} trial, then {{ total_price_and_per_month }}.", + "title" : "{{ app_name }}" + } + }, + "template_name" : "2" +} \ No newline at end of file diff --git a/Tests/RevenueCatUITests/Data/__Snapshots__/PaywallDataValidationTests/testUnrecognizedVariableInFeaturesGeneratesDefaultPaywall.1.json b/Tests/RevenueCatUITests/Data/__Snapshots__/PaywallDataValidationTests/testUnrecognizedVariableInFeaturesGeneratesDefaultPaywall.1.json new file mode 100644 index 0000000000..c0614761df --- /dev/null +++ b/Tests/RevenueCatUITests/Data/__Snapshots__/PaywallDataValidationTests/testUnrecognizedVariableInFeaturesGeneratesDefaultPaywall.1.json @@ -0,0 +1,40 @@ +{ + "asset_base_url" : "https://assets.pawwalls.com", + "config" : { + "blurred_background_image" : true, + "colors" : { + "light" : { + "accent1" : "#FFFFFF", + "accent2" : "#FFFFFF", + "background" : "#FFFFFF", + "call_to_action_background" : "#FFFFFF", + "call_to_action_foreground" : "#FFFFFF", + "text1" : "#FFFFFF" + } + }, + "display_restore_purchases" : true, + "images" : { + "background" : "background.jpg", + "icon" : "revenuecatui_default_paywall_app_icon" + }, + "packages" : [ + "$rc_annual", + "$rc_monthly" + ], + "privacy_url" : null, + "tos_url" : null + }, + "localized_strings" : { + "en_US" : { + "call_to_action" : "Continue", + "call_to_action_with_intro_offer" : null, + "features" : [ + + ], + "offer_details" : "{{ total_price_and_per_month }}", + "offer_details_with_intro_offer" : "Start your {{ sub_offer_duration }} trial, then {{ total_price_and_per_month }}.", + "title" : "{{ app_name }}" + } + }, + "template_name" : "2" +} \ No newline at end of file diff --git a/Tests/RevenueCatUITests/Data/__Snapshots__/PaywallDataValidationTests/testValidateMissingPaywall.1.json b/Tests/RevenueCatUITests/Data/__Snapshots__/PaywallDataValidationTests/testValidateMissingPaywall.1.json new file mode 100644 index 0000000000..087d20323d --- /dev/null +++ b/Tests/RevenueCatUITests/Data/__Snapshots__/PaywallDataValidationTests/testValidateMissingPaywall.1.json @@ -0,0 +1,40 @@ +{ + "asset_base_url" : "https://assets.pawwalls.com", + "config" : { + "blurred_background_image" : true, + "colors" : { + "light" : { + "accent1" : "#FFFFFF", + "accent2" : "#FFFFFF", + "background" : "#FFFFFF", + "call_to_action_background" : "#FFFFFF", + "call_to_action_foreground" : "#FFFFFF", + "text1" : "#FFFFFF" + } + }, + "display_restore_purchases" : true, + "images" : { + "background" : "background.jpg", + "icon" : "revenuecatui_default_paywall_app_icon" + }, + "packages" : [ + "$rc_monthly", + "$rc_annual" + ], + "privacy_url" : null, + "tos_url" : null + }, + "localized_strings" : { + "en_US" : { + "call_to_action" : "Continue", + "call_to_action_with_intro_offer" : null, + "features" : [ + + ], + "offer_details" : "{{ total_price_and_per_month }}", + "offer_details_with_intro_offer" : "Start your {{ sub_offer_duration }} trial, then {{ total_price_and_per_month }}.", + "title" : "{{ app_name }}" + } + }, + "template_name" : "2" +} \ No newline at end of file diff --git a/Tests/RevenueCatUITests/Helpers/DataExtensions.swift b/Tests/RevenueCatUITests/Helpers/DataExtensions.swift index e75757cc47..5dac5dc35b 100644 --- a/Tests/RevenueCatUITests/Helpers/DataExtensions.swift +++ b/Tests/RevenueCatUITests/Helpers/DataExtensions.swift @@ -7,18 +7,32 @@ import Foundation import RevenueCat -import RevenueCatUI +@testable import RevenueCatUI // MARK: - Extensions extension Offering { var withLocalImages: Offering { + return self.map { $0?.withLocalImages } + } + + /// Creates a copy of the offering's paywall with a single localization + func with(localization: PaywallData.LocalizedConfiguration) -> Self { + return self.map { $0?.with(localization: localization) } + } + + /// Creates a copy of the offering's paywall with a new template name + func with(templateName: String) -> Self { + return self.map { $0?.with(templateName: templateName) } + } + + private func map(_ modifier: (PaywallData?) -> PaywallData?) -> Self { return .init( identifier: self.identifier, serverDescription: self.serverDescription, metadata: self.metadata, - paywall: self.paywall?.withLocalImages, + paywall: modifier(self.paywall), availablePackages: self.availablePackages ) } @@ -37,4 +51,29 @@ extension PaywallData { return copy } + /// For snapshot tests to be able to produce a consistent `assetBaseURL` + @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) + var withTestAssetBaseURL: Self { + var copy = self + copy.assetBaseURL = TestData.paywallAssetBaseURL + + return copy + } + + /// Creates a copy of the paywall with a single localization + func with(localization: LocalizedConfiguration) -> Self { + return .init(templateName: self.templateName, + config: self.config, + localization: localization, + assetBaseURL: self.assetBaseURL) + } + + /// Creates a copy of the paywall with a new template name + func with(templateName: String) -> Self { + return .init(templateName: templateName, + config: self.config, + localization: self.localizedConfiguration, + assetBaseURL: self.assetBaseURL) + } + } diff --git a/Tests/RevenueCatUITests/Templates/OtherPaywallViewTests.swift b/Tests/RevenueCatUITests/Templates/OtherPaywallViewTests.swift index 9708005993..950d1f87f1 100644 --- a/Tests/RevenueCatUITests/Templates/OtherPaywallViewTests.swift +++ b/Tests/RevenueCatUITests/Templates/OtherPaywallViewTests.swift @@ -16,7 +16,7 @@ import SnapshotTesting class OtherPaywallViewTests: BaseSnapshotTest { func testDefaultPaywall() { - let view = PaywallView(offering: Self.offeringWithNoPaywall, + let view = PaywallView(offering: TestData.offeringWithNoPaywall, introEligibility: Self.eligibleChecker, purchaseHandler: Self.purchaseHandler) @@ -24,7 +24,7 @@ class OtherPaywallViewTests: BaseSnapshotTest { } func testDefaultDarkModePaywall() { - let view = PaywallView(offering: Self.offeringWithNoPaywall, + let view = PaywallView(offering: TestData.offeringWithNoPaywall, introEligibility: Self.eligibleChecker, purchaseHandler: Self.purchaseHandler) .environment(\.colorScheme, .dark) @@ -47,14 +47,6 @@ class OtherPaywallViewTests: BaseSnapshotTest { .snapshot(size: Self.cardSize) } - private static let offeringWithNoPaywall = Offering( - identifier: "offering", - serverDescription: "Main offering", - metadata: [:], - paywall: nil, - availablePackages: TestData.packages - ) - } #endif diff --git a/Tests/RevenueCatUITests/Templates/PaywallViewLocalizationTests.swift b/Tests/RevenueCatUITests/Templates/PaywallViewLocalizationTests.swift index 5651471eff..54b4510a6c 100644 --- a/Tests/RevenueCatUITests/Templates/PaywallViewLocalizationTests.swift +++ b/Tests/RevenueCatUITests/Templates/PaywallViewLocalizationTests.swift @@ -53,7 +53,7 @@ private extension PaywallViewLocalizationTests { serverDescription: "Offering", metadata: [:], paywall: .init( - template: .template2, + templateName: PaywallTemplate.template2.rawValue, config: .init( packages: [PackageType.weekly.identifier, PackageType.annual.identifier, diff --git a/Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift b/Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift index ffd0d445ae..71f152f62e 100644 --- a/Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift +++ b/Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift @@ -7,6 +7,7 @@ import Foundation import RevenueCat +@testable import RevenueCatUI final class SamplePaywallLoader { @@ -42,6 +43,16 @@ final class SamplePaywallLoader { ) } + func offeringWithUnrecognizedPaywall() -> Offering { + return .init( + identifier: Self.offeringIdentifier, + serverDescription: Self.offeringIdentifier, + metadata: [:], + paywall: Self.unrecognizedTemplate(), + availablePackages: self.packages + ) + } + private func paywall(for template: PaywallTemplate) -> PaywallData { switch template { case .template1: @@ -178,7 +189,7 @@ private extension SamplePaywallLoader { static func template1() -> PaywallData { return .init( - template: .template1, + templateName: PaywallTemplate.template1.rawValue, config: .init( packages: [Package.string(from: PackageType.monthly)!], images: Self.images, @@ -214,7 +225,7 @@ private extension SamplePaywallLoader { static func template2() -> PaywallData { return .init( - template: .template2, + templateName: PaywallTemplate.template2.rawValue, config: .init( packages: Array([.weekly, .monthly, .annual, .lifetime]) .map { Package.string(from: $0)! }, @@ -254,7 +265,7 @@ private extension SamplePaywallLoader { static func template3() -> PaywallData { return .init( - template: .template3, + templateName: PaywallTemplate.template3.rawValue, config: .init( packages: [Package.string(from: .annual)!], images: Self.images, @@ -303,7 +314,7 @@ private extension SamplePaywallLoader { static func template4() -> PaywallData { return .init( - template: .template4, + templateName: PaywallTemplate.template4.rawValue, config: .init( packages: Array([.monthly, .sixMonth, .annual]) .map { Package.string(from: $0)! }, @@ -332,6 +343,35 @@ private extension SamplePaywallLoader { ) } + static func unrecognizedTemplate() -> PaywallData { + return .init( + templateName: "unrecognized_template_name", + config: .init( + packages: [Package.string(from: PackageType.monthly)!], + images: Self.images, + colors: .init( + light: .init( + background: "#FFFFFF", + text1: "#000000", + callToActionBackground: "#5CD27A", + callToActionForeground: "#FFFFFF", + accent1: "#BC66FF" + ) + ), + 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 {{ sub_price_per_month }} per month", + offerDetails: "{{ sub_price_per_month }} per month", + offerDetailsWithIntroOffer: "Start your {{ sub_offer_duration }} trial, then {{ sub_price_per_month }} per month" + ), + assetBaseURL: Self.paywallAssetBaseURL + ) + } + } private extension SamplePaywallLoader { diff --git a/Tests/TestingApps/SimpleApp/SimpleApp/Views/OfferingsList.swift b/Tests/TestingApps/SimpleApp/SimpleApp/Views/OfferingsList.swift index b284a23aad..0e24343f23 100644 --- a/Tests/TestingApps/SimpleApp/SimpleApp/Views/OfferingsList.swift +++ b/Tests/TestingApps/SimpleApp/SimpleApp/Views/OfferingsList.swift @@ -65,7 +65,7 @@ struct OfferingsList: View { } label: { VStack(alignment: .leading) { Text(offering.serverDescription) - Text(verbatim: "Template: \(paywall.template.name)") + Text(verbatim: "Template: \(paywall.templateName)") } } .buttonStyle(.plain) diff --git a/Tests/TestingApps/SimpleApp/SimpleApp/Views/SamplePaywallsList.swift b/Tests/TestingApps/SimpleApp/SimpleApp/Views/SamplePaywallsList.swift index 759def0754..34aaec7b99 100644 --- a/Tests/TestingApps/SimpleApp/SimpleApp/Views/SamplePaywallsList.swift +++ b/Tests/TestingApps/SimpleApp/SimpleApp/Views/SamplePaywallsList.swift @@ -46,10 +46,15 @@ struct SamplePaywallsList: View { case let .customPaywall(mode): CustomPaywall(mode: mode) - case .defaultTemplate: + case .missingPaywall: PaywallView(offering: Self.loader.offeringWithDefaultPaywall(), introEligibility: Self.introEligibility, purchaseHandler: .default()) + + case .unrecognizedPaywall: + PaywallView(offering: Self.loader.offeringWithUnrecognizedPaywall(), + introEligibility: Self.introEligibility, + purchaseHandler: .default()) } } .navigationTitle("Paywalls") @@ -92,9 +97,15 @@ struct SamplePaywallsList: View { } Button { - self.display = .defaultTemplate + self.display = .missingPaywall + } label: { + TemplateLabel(name: "Offering with no paywall", icon: "exclamationmark.triangle") + } + + Button { + self.display = .unrecognizedPaywall } label: { - TemplateLabel(name: "Default template", icon: "exclamationmark.triangle") + TemplateLabel(name: "Unrecognized paywall", icon: "exclamationmark.triangle") } } } @@ -141,7 +152,8 @@ private extension SamplePaywallsList { case template(PaywallTemplate, PaywallViewMode) case customFont(PaywallTemplate) case customPaywall(PaywallViewMode) - case defaultTemplate + case missingPaywall + case unrecognizedPaywall } @@ -160,8 +172,11 @@ extension SamplePaywallsList.Display: Identifiable { case .customPaywall: return "custom-paywall" - case .defaultTemplate: - return "default" + case .missingPaywall: + return "missing" + + case .unrecognizedPaywall: + return "unrecognized" } } diff --git a/Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift b/Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift index 70e9e82405..0f10c2b2c0 100644 --- a/Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift +++ b/Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift @@ -110,7 +110,7 @@ class OfferingsDecodingTests: BaseHTTPResponseTest { expect(offering.packages).to(haveCount(2)) let paywall = try XCTUnwrap(offering.paywall) - expect(paywall.template) == .template1 + expect(paywall.templateName) == "1" try expect(paywall.assetBaseURL) == XCTUnwrap(URL(string: "https://rc-paywalls.s3.amazonaws.com")) expect(paywall.config.packages) == ["$rc_monthly", "$rc_annual", "custom_package"] diff --git a/Tests/UnitTests/Paywalls/PaywallDataTests.swift b/Tests/UnitTests/Paywalls/PaywallDataTests.swift index 8ba116b712..260f9afe3d 100644 --- a/Tests/UnitTests/Paywalls/PaywallDataTests.swift +++ b/Tests/UnitTests/Paywalls/PaywallDataTests.swift @@ -20,7 +20,7 @@ class PaywallDataTests: BaseHTTPResponseTest { func testSample1() throws { let paywall: PaywallData = try self.decodeFixture("PaywallData-Sample1") - expect(paywall.template) == .template1 + expect(paywall.templateName) == "1" expect(paywall.assetBaseURL) == URL(string: "https://rc-paywalls.s3.amazonaws.com")! expect(paywall.config.packages) == ["$rc_monthly", "$rc_annual", "custom_package"] expect(paywall.config.defaultPackage) == "$rc_annual"