From 8cbd12918602d0ed97ba9d782b86ecab9ad65aee Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Tue, 18 Jul 2023 08:26:29 -0700 Subject: [PATCH] `Paywalls`: add support for multiple images in template configuration (#2832) Changed `PaywallData.Configuration.headerImageName` to `imageNames`. This more flexible definition allows future templates to display a carrousel of images instead of a single image. Depends on #2831. --- .../Data/TemplateViewConfiguration.swift | 2 +- RevenueCatUI/Data/TestData.swift | 4 +- RevenueCatUI/Templates/Example1Template.swift | 57 +++++++++++-------- RevenueCatUI/Templates/TemplateViewType.swift | 2 +- Sources/Paywalls/PaywallData.swift | 22 ++++--- .../SwiftAPITester/PaywallAPI.swift | 4 +- .../Helpers/DataExtensions.swift | 2 +- .../Responses/Fixtures/Offerings.json | 2 +- .../Fixtures/PaywallData-Sample1.json | 2 +- .../Fixtures/PaywallData-empty_images.json | 28 +++++++++ ...ta-missing_current_and_default_locale.json | 2 +- .../PaywallData-missing_current_locale.json | 2 +- .../Responses/OfferingsDecodingTests.swift | 2 +- .../UnitTests/Paywalls/PaywallDataTests.swift | 12 +++- 14 files changed, 97 insertions(+), 46 deletions(-) create mode 100644 Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-empty_images.json diff --git a/RevenueCatUI/Data/TemplateViewConfiguration.swift b/RevenueCatUI/Data/TemplateViewConfiguration.swift index e109f56502..cc5a77d5ff 100644 --- a/RevenueCatUI/Data/TemplateViewConfiguration.swift +++ b/RevenueCatUI/Data/TemplateViewConfiguration.swift @@ -16,7 +16,7 @@ struct TemplateViewConfiguration { let packages: PackageConfiguration let configuration: PaywallData.Configuration let colors: PaywallData.Configuration.Colors - let headerImageURL: URL + let imageURLs: [URL] } diff --git a/RevenueCatUI/Data/TestData.swift b/RevenueCatUI/Data/TestData.swift index a1f9317330..c4ec4e6fce 100644 --- a/RevenueCatUI/Data/TestData.swift +++ b/RevenueCatUI/Data/TestData.swift @@ -66,7 +66,7 @@ internal enum TestData { template: .example1, config: .init( packages: [.monthly], - headerImageName: Self.paywallHeaderImageName, + imageNames: [Self.paywallHeaderImageName], colors: .init(light: Self.lightColors, dark: Self.darkColors) ), localization: Self.localization, @@ -76,7 +76,7 @@ internal enum TestData { template: .example1, config: .init( packages: [.annual], - headerImageName: Self.paywallHeaderImageName, + imageNames: [Self.paywallHeaderImageName], colors: .init(light: Self.lightColors, dark: Self.darkColors) ), localization: Self.localization, diff --git a/RevenueCatUI/Templates/Example1Template.swift b/RevenueCatUI/Templates/Example1Template.swift index d32b6a5c1c..0d62cd6dd4 100644 --- a/RevenueCatUI/Templates/Example1Template.swift +++ b/RevenueCatUI/Templates/Example1Template.swift @@ -52,31 +52,8 @@ private struct Example1TemplateContent: View { VStack { ScrollView(.vertical) { VStack { - AsyncImage( - url: self.configuration.headerImageURL, - transaction: .init(animation: Constants.defaultAnimation) - ) { phase in - if let image = phase.image { - image - .fitToAspect(Self.imageAspectRatio, contentMode: .fill) - } else if let error = phase.error { - DebugErrorView("Error loading image from '\(self.configuration.headerImageURL)': \(error)", - releaseBehavior: .emptyView) - } else { - Rectangle() - .hidden() - } - } - .frame(maxWidth: .infinity) - .aspectRatio(Self.imageAspectRatio, contentMode: .fit) - .clipShape( - Circle() - .offset(y: -140) - .scale(3.0) - ) - .padding(.bottom) - - Spacer() + self.headerImage + .padding(.bottom) Group { Text(verbatim: self.localization.title) @@ -106,6 +83,36 @@ private struct Example1TemplateContent: View { .background(self.configuration.colors.backgroundColor) } + @ViewBuilder + private var headerImage: some View { + if let headerImage = self.configuration.imageURLs.first { + AsyncImage( + url: headerImage, + transaction: .init(animation: Constants.defaultAnimation) + ) { phase in + if let image = phase.image { + image + .fitToAspect(Self.imageAspectRatio, contentMode: .fill) + } else if let error = phase.error { + DebugErrorView("Error loading image from '\(headerImage)': \(error)", + releaseBehavior: .emptyView) + } else { + Rectangle() + .hidden() + } + } + .frame(maxWidth: .infinity) + .aspectRatio(Self.imageAspectRatio, contentMode: .fit) + .clipShape( + Circle() + .offset(y: -140) + .scale(3.0) + ) + + Spacer() + } + } + private var offerDetails: some View { let detailsWithIntroOffer = self.localization.offerDetailsWithIntroOffer diff --git a/RevenueCatUI/Templates/TemplateViewType.swift b/RevenueCatUI/Templates/TemplateViewType.swift index 2764ed7d2e..dd49202958 100644 --- a/RevenueCatUI/Templates/TemplateViewType.swift +++ b/RevenueCatUI/Templates/TemplateViewType.swift @@ -47,7 +47,7 @@ extension PaywallData { setting: self.template.packageSetting), configuration: self.config, colors: self.config.colors.multiScheme, - headerImageURL: self.headerImageURL + imageURLs: self.imageURLs ) } } diff --git a/Sources/Paywalls/PaywallData.swift b/Sources/Paywalls/PaywallData.swift index cc149ab48e..c23a2e6b63 100644 --- a/Sources/Paywalls/PaywallData.swift +++ b/Sources/Paywalls/PaywallData.swift @@ -137,19 +137,27 @@ extension PaywallData { /// The list of package types this paywall will display public var packages: [PackageType] - /// The name for the header image asset. - public var headerImageName: String + /// The names for image assets. + public var imageNames: [String] { + get { self._imageNames } + set { self._imageNames = newValue } + } /// The set of colors used public var colors: ColorInformation // swiftlint:disable:next missing_docs - public init(packages: [PackageType], headerImageName: String, colors: ColorInformation) { + public init(packages: [PackageType], imageNames: [String], colors: ColorInformation) { + assert(!imageNames.isEmpty) + self.packages = packages - self.headerImageName = headerImageName + self._imageNames = imageNames self.colors = colors } + @EnsureNonEmptyArrayDecodable + var _imageNames: [String] + } } @@ -208,8 +216,8 @@ extension PaywallData.Configuration { public extension PaywallData { /// The remote URL to load the header image asset. - var headerImageURL: URL { - return self.assetBaseURL.appendingPathComponent(self.config.headerImageName) + var imageURLs: [URL] { + self.config.imageNames.map { self.assetBaseURL.appendingPathComponent($0) } } } @@ -274,7 +282,7 @@ extension PaywallData.Configuration: Codable { private enum CodingKeys: String, CodingKey { case packages - case headerImageName = "headerImage" + case _imageNames = "images" case colors } diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PaywallAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PaywallAPI.swift index 084f9189ba..4c87928ffb 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PaywallAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PaywallAPI.swift @@ -28,9 +28,9 @@ func checkPaywallData(_ data: PaywallData) { func checkPaywallConfiguration(_ config: PaywallData.Configuration, _ colors: PaywallData.Configuration.ColorInformation) { - let _: PaywallData.Configuration = .init(packages: [.monthly, .annual], headerImageName: "", colors: colors) + let _: PaywallData.Configuration = .init(packages: [.monthly, .annual], imageNames: [""], colors: colors) let _: [PackageType] = config.packages - let _: String = config.headerImageName + let _: [String] = config.imageNames } func checkPaywallLocalizedConfig(_ config: PaywallData.LocalizedConfiguration) { diff --git a/Tests/RevenueCatUITests/Helpers/DataExtensions.swift b/Tests/RevenueCatUITests/Helpers/DataExtensions.swift index 81ea217f28..cec1e6b18e 100644 --- a/Tests/RevenueCatUITests/Helpers/DataExtensions.swift +++ b/Tests/RevenueCatUITests/Helpers/DataExtensions.swift @@ -24,7 +24,7 @@ extension PaywallData { var withLocalImage: Self { var copy = self copy.assetBaseURL = URL(fileURLWithPath: Bundle.module.bundlePath) - copy.config.headerImageName = "image.png" + copy.config.imageNames = ["image.png"] return copy } diff --git a/Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json b/Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json index 05366f6ff1..37bb7beffc 100644 --- a/Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json +++ b/Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json @@ -61,7 +61,7 @@ "default_locale": "en_US", "config": { "packages": ["$rc_monthly", "$rc_annual"], - "header_image": "asset_name", + "images": ["asset_1", "asset_2"], "colors": { "light": { "background": "#FF00AA", diff --git a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-Sample1.json b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-Sample1.json index 3b79df35ca..c916a19e31 100644 --- a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-Sample1.json +++ b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-Sample1.json @@ -20,7 +20,7 @@ "default_locale": "en_US", "config": { "packages": ["$rc_monthly", "$rc_annual"], - "header_image": "asset_name.png", + "images": ["asset_name.png"], "colors": { "light": { "background": "#FF00AA", diff --git a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-empty_images.json b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-empty_images.json new file mode 100644 index 0000000000..43b0e21782 --- /dev/null +++ b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-empty_images.json @@ -0,0 +1,28 @@ +{ + "template_name": "sample_1", + "localized_strings": { + "en_US": { + "title": "Paywall", + "subtitle": "Description", + "call_to_action": "Purchase now", + "call_to_action_with_intro_offer": "Purchase now", + "offer_details": "{{ price_per_month }} per month", + "offer_details_with_intro_offer": "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month" + } + }, + "default_locale": "en_US", + "config": { + "packages": ["$rc_monthly"], + "images": [], + "colors": { + "light": { + "background": "#FF00AA", + "foreground": "#FF00AA22", + "call_to_action_background": "#FF00AACC", + "call_to_action_foreground": "#FF00AA" + }, + "dark": null + } + }, + "asset_base_url": "https://rc-paywalls.s3.amazonaws.com" +} 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 index a94257672c..479685c856 100644 --- 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 @@ -13,7 +13,7 @@ "default_locale": "es_ES", "config": { "packages": ["$rc_monthly", "$rc_annual"], - "header_image": "asset_name", + "images": ["asset_name"], "colors": { "light": { "background": "#FFFFFF", diff --git a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_locale.json b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_locale.json index 893ab70547..a939c209b5 100644 --- a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_locale.json +++ b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_locale.json @@ -13,7 +13,7 @@ "default_locale": "es_ES", "config": { "packages": ["$rc_monthly", "$rc_annual"], - "header_image": "asset_name", + "images": ["asset_name"], "colors": { "light": { "background": "#FFFFFF", diff --git a/Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift b/Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift index 2fdc868382..c161315cd0 100644 --- a/Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift +++ b/Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift @@ -113,7 +113,7 @@ class OfferingsDecodingTests: BaseHTTPResponseTest { try expect(paywall.assetBaseURL) == XCTUnwrap(URL(string: "https://rc-paywalls.s3.amazonaws.com")) expect(paywall.config.packages) == [.monthly, .annual] - expect(paywall.config.headerImageName) == "asset_name" + expect(paywall.config.imageNames) == ["asset_1", "asset_2"] let enConfig = try XCTUnwrap(paywall.config(for: Locale(identifier: "en_US"))) expect(enConfig.title) == "Paywall" diff --git a/Tests/UnitTests/Paywalls/PaywallDataTests.swift b/Tests/UnitTests/Paywalls/PaywallDataTests.swift index c65e750fad..ee380ca512 100644 --- a/Tests/UnitTests/Paywalls/PaywallDataTests.swift +++ b/Tests/UnitTests/Paywalls/PaywallDataTests.swift @@ -33,7 +33,7 @@ class PaywallDataTests: BaseHTTPResponseTest { expect(paywall.defaultLocale) == Locale(identifier: Self.defaultLocale) expect(paywall.assetBaseURL) == URL(string: "https://rc-paywalls.s3.amazonaws.com")! expect(paywall.config.packages) == [.monthly, .annual] - expect(paywall.config.headerImageName) == "asset_name.png" + expect(paywall.config.imageNames) == ["asset_name.png"] expect(paywall.config.colors.light.background.stringRepresentation) == "#FF00AA" expect(paywall.config.colors.light.foreground.stringRepresentation) == "#FF00AA22" @@ -45,7 +45,9 @@ class PaywallDataTests: BaseHTTPResponseTest { expect(paywall.config.colors.dark?.callToActionBackground.stringRepresentation) == "#112233AA" expect(paywall.config.colors.dark?.callToActionForeground.stringRepresentation) == "#AABBCC" - expect(paywall.headerImageURL) == URL(string: "https://rc-paywalls.s3.amazonaws.com/asset_name.png")! + expect(paywall.imageURLs) == [ + URL(string: "https://rc-paywalls.s3.amazonaws.com/asset_name.png")! + ] let enConfig = try XCTUnwrap(paywall.config(for: Locale(identifier: "en_US"))) expect(enConfig.title) == "Paywall" @@ -79,6 +81,12 @@ class PaywallDataTests: BaseHTTPResponseTest { expect(localization.title) == "Tienda" } + func testFailsToDecodeWithNoImages() throws { + expect { + let _: PaywallData = try self.decodeFixture("PaywallData-empty_images") + }.to(throwError(EnsureNonEmptyArrayDecodable.Error())) + } + #if !os(watchOS) func testMissingCurrentAndDefaultFails() throws { let paywall: PaywallData = try self.decodeFixture("PaywallData-missing_current_and_default_locale")