From a1a7c1c46175ea3b304370b19521911214c065bb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 22 Jun 2026 10:44:10 +0000 Subject: [PATCH 1/2] Align iOS paywall preview mocks with web/dashboard catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The validation-screenshot loader mocked every product at a flat $1.99 with no intro offers, and keyed products only by base identifier, so subscription packages (which carry billing plan identifiers in packages.json) resolved by compound 'product:plan' id could be dropped. Build the preview products with TestStoreProduct using the canonical web/dashboard prices and subscription periods (weekly $2.99 … annual $69.99, lifetime $119.99), attach a single $1.99 / 1-week introductory offer to each subscription so offers are intro-eligible and shown, and key the products map by both base and compound identifiers so packages resolve regardless of billing plan ids. Test-only loader; no SDK source changed. Co-authored-by: joop --- .../PaywallPreviewResourcesLoader.swift | 166 ++++++++++++++---- 1 file changed, 127 insertions(+), 39 deletions(-) diff --git a/Tests/RevenueCatUITests/PaywallsV2/PaywallPreviewResourcesLoader.swift b/Tests/RevenueCatUITests/PaywallsV2/PaywallPreviewResourcesLoader.swift index 0216c22638..da19d3ec36 100644 --- a/Tests/RevenueCatUITests/PaywallsV2/PaywallPreviewResourcesLoader.swift +++ b/Tests/RevenueCatUITests/PaywallsV2/PaywallPreviewResourcesLoader.swift @@ -133,45 +133,18 @@ class PaywallPreviewResourcesLoader { apiKey: "preview_api_key", preferredLocalesProvider: .init(preferredLocaleOverride: nil) ) - let offerings = OfferingsFactory(systemInfo: systemInfo).createOfferings(from: [ - "com.revenuecat.lifetime_product": .init(sk1Product: PreviewMock.Product( - price: 1.99, - unit: .week, - localizedTitle: "Lifeime" - )), - "com.revenuecat.annual_product": .init(sk1Product: PreviewMock.Product( - price: 1.99, - unit: .year, - localizedTitle: "Annual" - )), - "com.revenuecat.semester_product": .init(sk1Product: PreviewMock.Product( - price: 1.99, - unit: .month, - localizedTitle: "6 Month" - )), - "com.revenuecat.quarterly_product": .init(sk1Product: PreviewMock.Product( - price: 1.99, - unit: .week, - localizedTitle: "3 Month" - )), - "com.revenuecat.bimonthly_product": .init(sk1Product: PreviewMock.Product( - price: 1.99, - unit: .week, - localizedTitle: "2 Month" - )), - "com.revenuecat.monthly_product": .init(sk1Product: PreviewMock.Product( - price: 1.99, - unit: .month, - localizedTitle: "Monthly" - )), - "com.revenuecat.weekly_product": .init(sk1Product: PreviewMock.Product( - price: 1.99, - unit: .week, - localizedTitle: "Weekly" - )) - ], contents: Offerings.Contents(response: offeringsResponseWithPackages, - httpResponseOriginalSource: .mainServer), - loadedFromDiskCache: false) + // Mock products for the preview offerings. Prices, subscription periods, and + // the single introductory offer ($1.99 for 1 week) are kept in sync with the + // web/dashboard preview tables so the rendering-validation screenshots show the + // same products, prices, and offers across platforms. + let storeProductsByID = Self.previewStoreProductsByID() + + let offerings = OfferingsFactory(systemInfo: systemInfo).createOfferings( + from: storeProductsByID, + contents: Offerings.Contents(response: offeringsResponseWithPackages, + httpResponseOriginalSource: .mainServer), + loadedFromDiskCache: false + ) result.merge(offerings!.all) } @@ -179,4 +152,119 @@ class PaywallPreviewResourcesLoader { return result } + // MARK: - Preview products + + private static let previewLocale = Locale(identifier: "en_US") + + /// A single introductory offer ($1.99 for 1 week) attached to every subscription. + /// Matches the offer values in the web/dashboard preview variable tables and makes + /// the products intro-offer eligible so offer-gated UI renders. + private static func introductoryOffer() -> TestStoreProductDiscount { + return TestStoreProductDiscount( + identifier: "intro_offer", + price: 1.99, + localizedPriceString: "$1.99", + paymentMode: .payUpFront, + subscriptionPeriod: .init(value: 1, unit: .week), + numberOfPeriods: 1, + type: .introductory + ) + } + + private static func subscriptionProduct( + productIdentifier: String, + title: String, + price: Decimal, + localizedPriceString: String, + subscriptionPeriod: SubscriptionPeriod + ) -> StoreProduct { + return TestStoreProduct( + localizedTitle: title, + price: price, + currencyCode: "USD", + localizedPriceString: localizedPriceString, + productIdentifier: productIdentifier, + productType: .autoRenewableSubscription, + localizedDescription: title, + subscriptionPeriod: subscriptionPeriod, + introductoryDiscount: introductoryOffer(), + locale: previewLocale + ).toStoreProduct() + } + + /// Mock products keyed by both the base product identifier and the compound + /// "product:plan" identifier, so packages resolve whether or not packages.json + /// specifies a billing plan identifier. + private static func previewStoreProductsByID() -> [String: StoreProduct] { + let lifetime = TestStoreProduct( + localizedTitle: "Lifetime", + price: 119.99, + currencyCode: "USD", + localizedPriceString: "$119.99", + productIdentifier: "com.revenuecat.lifetime_product", + productType: .nonConsumable, + localizedDescription: "Lifetime", + locale: previewLocale + ).toStoreProduct() + + let weekly = subscriptionProduct( + productIdentifier: "com.revenuecat.weekly_product", + title: "Weekly", + price: 2.99, + localizedPriceString: "$2.99", + subscriptionPeriod: .init(value: 1, unit: .week) + ) + let monthly = subscriptionProduct( + productIdentifier: "com.revenuecat.monthly_product", + title: "Monthly", + price: 9.99, + localizedPriceString: "$9.99", + subscriptionPeriod: .init(value: 1, unit: .month) + ) + let bimonthly = subscriptionProduct( + productIdentifier: "com.revenuecat.bimonthly_product", + title: "2 Months", + price: 17.99, + localizedPriceString: "$17.99", + subscriptionPeriod: .init(value: 2, unit: .month) + ) + let quarterly = subscriptionProduct( + productIdentifier: "com.revenuecat.quarterly_product", + title: "3 Months", + price: 24.99, + localizedPriceString: "$24.99", + subscriptionPeriod: .init(value: 3, unit: .month) + ) + let semester = subscriptionProduct( + productIdentifier: "com.revenuecat.semester_product", + title: "6 Months", + price: 39.99, + localizedPriceString: "$39.99", + subscriptionPeriod: .init(value: 6, unit: .month) + ) + let annual = subscriptionProduct( + productIdentifier: "com.revenuecat.annual_product", + title: "Annual", + price: 69.99, + localizedPriceString: "$69.99", + subscriptionPeriod: .init(value: 1, unit: .year) + ) + + return [ + "com.revenuecat.lifetime_product": lifetime, + "com.revenuecat.weekly_product": weekly, + "com.revenuecat.weekly_product:p1w": weekly, + "com.revenuecat.monthly_product": monthly, + "com.revenuecat.monthly_product:p1m": monthly, + "com.revenuecat.bimonthly_product": bimonthly, + "com.revenuecat.bimonthly_product:p2m": bimonthly, + "com.revenuecat.quarterly_product": quarterly, + "com.revenuecat.quarterly_product:p3m": quarterly, + "com.revenuecat.semester_product": semester, + "com.revenuecat.semester_product:p6m": semester, + "com.revenuecat.annual_product": annual, + "com.revenuecat.annual_product:p1y": annual + ] + } + } From f552465290d9f3f80a87407ba0aaeb0ef9d578ed Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 22 Jun 2026 10:48:22 +0000 Subject: [PATCH 2/2] Use 'Pro Access' as preview store product name Match the product.store_product_name value used by the web/dashboard preview tables and the Android preview parser, so the rendering-validation screenshots show a consistent product name across platforms. Co-authored-by: joop --- .../PaywallPreviewResourcesLoader.swift | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Tests/RevenueCatUITests/PaywallsV2/PaywallPreviewResourcesLoader.swift b/Tests/RevenueCatUITests/PaywallsV2/PaywallPreviewResourcesLoader.swift index da19d3ec36..64483ca31a 100644 --- a/Tests/RevenueCatUITests/PaywallsV2/PaywallPreviewResourcesLoader.swift +++ b/Tests/RevenueCatUITests/PaywallsV2/PaywallPreviewResourcesLoader.swift @@ -173,19 +173,20 @@ class PaywallPreviewResourcesLoader { private static func subscriptionProduct( productIdentifier: String, - title: String, + description: String, price: Decimal, localizedPriceString: String, subscriptionPeriod: SubscriptionPeriod ) -> StoreProduct { return TestStoreProduct( - localizedTitle: title, + // Matches the web/dashboard `product.store_product_name` ("Pro Access"). + localizedTitle: "Pro Access", price: price, currencyCode: "USD", localizedPriceString: localizedPriceString, productIdentifier: productIdentifier, productType: .autoRenewableSubscription, - localizedDescription: title, + localizedDescription: description, subscriptionPeriod: subscriptionPeriod, introductoryDiscount: introductoryOffer(), locale: previewLocale @@ -197,7 +198,7 @@ class PaywallPreviewResourcesLoader { /// specifies a billing plan identifier. private static func previewStoreProductsByID() -> [String: StoreProduct] { let lifetime = TestStoreProduct( - localizedTitle: "Lifetime", + localizedTitle: "Pro Access", price: 119.99, currencyCode: "USD", localizedPriceString: "$119.99", @@ -209,42 +210,42 @@ class PaywallPreviewResourcesLoader { let weekly = subscriptionProduct( productIdentifier: "com.revenuecat.weekly_product", - title: "Weekly", + description: "Weekly", price: 2.99, localizedPriceString: "$2.99", subscriptionPeriod: .init(value: 1, unit: .week) ) let monthly = subscriptionProduct( productIdentifier: "com.revenuecat.monthly_product", - title: "Monthly", + description: "Monthly", price: 9.99, localizedPriceString: "$9.99", subscriptionPeriod: .init(value: 1, unit: .month) ) let bimonthly = subscriptionProduct( productIdentifier: "com.revenuecat.bimonthly_product", - title: "2 Months", + description: "2 Months", price: 17.99, localizedPriceString: "$17.99", subscriptionPeriod: .init(value: 2, unit: .month) ) let quarterly = subscriptionProduct( productIdentifier: "com.revenuecat.quarterly_product", - title: "3 Months", + description: "3 Months", price: 24.99, localizedPriceString: "$24.99", subscriptionPeriod: .init(value: 3, unit: .month) ) let semester = subscriptionProduct( productIdentifier: "com.revenuecat.semester_product", - title: "6 Months", + description: "6 Months", price: 39.99, localizedPriceString: "$39.99", subscriptionPeriod: .init(value: 6, unit: .month) ) let annual = subscriptionProduct( productIdentifier: "com.revenuecat.annual_product", - title: "Annual", + description: "Annual", price: 69.99, localizedPriceString: "$69.99", subscriptionPeriod: .init(value: 1, unit: .year)