diff --git a/RevenueCatUI/Data/Localization.swift b/RevenueCatUI/Data/Localization.swift index 0f40bb9e19..973b460a10 100644 --- a/RevenueCatUI/Data/Localization.swift +++ b/RevenueCatUI/Data/Localization.swift @@ -12,16 +12,23 @@ enum Localization { /// - Returns: an appropriately short abbreviation for the given `unit`. static func abbreviatedUnitLocalizedString( - for unit: NSCalendar.Unit, + for unit: SubscriptionPeriod.Unit, locale: Locale = .current ) -> String { - let (full, abbreviated) = self.unitLocalizedString(for: unit, locale: locale) - - if full.count <= Self.unitAbbreviationMaximumLength { - return full - } else { - return abbreviated - } + let (full, brief, abbreviated) = self.unitLocalizedString(for: unit.calendarUnit, locale: locale) + + let options = [ + full, + brief, + abbreviated + ] + + // Return the first option that matches the preferred length + return self.unitAbbreviationLengthPriorities + .lazy + .compactMap { length in options.first { $0.count == length } } + .first + ?? options.last! } static func localizedDuration( @@ -112,7 +119,7 @@ private extension Localization { static func unitLocalizedString( for unit: NSCalendar.Unit, locale: Locale = .current - ) -> (full: String, abbreviated: String) { + ) -> (full: String, brief: String, abbreviated: String) { var calendar: Calendar = .current calendar.locale = locale @@ -122,7 +129,7 @@ private extension Localization { guard let sinceUnits = calendar.date(byAdding: component, value: value, - to: date) else { return ("", "") } + to: date) else { return ("", "", "") } let formatter = DateComponentsFormatter() formatter.calendar = calendar @@ -138,10 +145,12 @@ private extension Localization { } return (full: result(for: .full), + brief: result(for: .brief), abbreviated: result(for: .abbreviated)) } - static let unitAbbreviationMaximumLength = 3 + /// The order in which unit abbreviations are preferred. + static let unitAbbreviationLengthPriorities = [ 2, 3 ] /// For falling back in case language isn't localized. static let defaultLocale: Locale = .init(identifier: "en_US") diff --git a/RevenueCatUI/Data/TestData.swift b/RevenueCatUI/Data/TestData.swift index 1af5896ef3..80db3efeb0 100644 --- a/RevenueCatUI/Data/TestData.swift +++ b/RevenueCatUI/Data/TestData.swift @@ -56,7 +56,7 @@ internal enum TestData { localizedDescription: "PRO annual", subscriptionGroupIdentifier: "group", subscriptionPeriod: .init(value: 1, unit: .year), - introductoryDiscount: Self.intro(14, .day) + introductoryDiscount: Self.intro(14, .day, priceString: "$1.99") ) static let lifetimeProduct = TestStoreProduct( localizedTitle: "Lifetime", @@ -251,9 +251,10 @@ internal enum TestData { localization: .init( title: "How your **free** trial works", callToAction: "Start", - callToActionWithIntroOffer: "Start your {{ intro_duration }} free", - offerDetails: "Only {{ price }} per {{ period }}", - offerDetailsWithIntroOffer: "First {{ intro_duration }} free,\nthen {{ total_price_and_per_month }}", + callToActionWithIntroOffer: "Start your {{ sub_offer_duration }} free", + offerDetails: "Only {{ price }} per {{ sub_period }}", + offerDetailsWithIntroOffer: "First {{ sub_offer_duration }} free,\n" + + "then {{ total_price_and_per_month }}", features: [ .init(title: "Today", content: "Full access to 1000+ workouts plus _free_ meal plan worth {{ price }}.", @@ -303,8 +304,8 @@ internal enum TestData { title: "Get _unlimited_ access", callToAction: "Continue", offerDetails: "", - offerDetailsWithIntroOffer: "Includes {{ intro_duration }} **free** trial", - offerName: "{{ subscription_duration }}" + offerDetailsWithIntroOffer: "Includes {{ sub_offer_duration }} **free** trial", + offerName: "{{ sub_duration }}" ), assetBaseURL: Bundle.module.resourceURL ?? Bundle.module.bundleURL ), @@ -373,18 +374,19 @@ internal enum TestData { 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 {{ price_per_month }} per month", - offerDetails: "{{ price_per_month }} per month", - offerDetailsWithIntroOffer: "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month", + 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", features: [] ) static let localization2: PaywallData.LocalizedConfiguration = .init( title: "Call to action for _better_ conversion.", subtitle: "Lorem ipsum is simply dummy text of the ~printing and~ typesetting industry.", - callToAction: "Subscribe for {{ price_per_month }}/mo", + callToAction: "Subscribe for {{ sub_price_per_month }}/mo", offerDetails: "{{ total_price_and_per_month }}", - offerDetailsWithIntroOffer: "{{ total_price_and_per_month }} after {{ intro_duration }} trial", - offerName: "{{ period }}", + offerDetailsWithIntroOffer: "{{ total_price_and_per_month }} after {{ sub_offer_duration }} trial", + offerName: "{{ sub_period }}", features: [] ) static let paywallHeaderImageName = "9a17e0a7_1689854430..jpeg" @@ -398,11 +400,15 @@ internal enum TestData { private static let offeringIdentifier = "offering" - private static func intro(_ duration: Int, _ unit: SubscriptionPeriod.Unit) -> TestStoreProductDiscount { + private static func intro( + _ duration: Int, + _ unit: SubscriptionPeriod.Unit, + priceString: String = "$0.00" + ) -> TestStoreProductDiscount { return .init( identifier: "intro", price: 0, - localizedPriceString: "$0.00", + localizedPriceString: priceString, paymentMode: .freeTrial, subscriptionPeriod: .init(value: duration, unit: .day), numberOfPeriods: 1, diff --git a/RevenueCatUI/Data/Variables.swift b/RevenueCatUI/Data/Variables.swift index 326b5e9863..a443b587b7 100644 --- a/RevenueCatUI/Data/Variables.swift +++ b/RevenueCatUI/Data/Variables.swift @@ -16,17 +16,18 @@ protocol VariableDataProvider { var applicationName: String { get } - var isSubscription: Bool { get } - var isMonthly: Bool { get } - var localizedPrice: String { get } var localizedPricePerMonth: String { get } + var localizedIntroductoryOfferPrice: String? { get } var productName: String { get } func periodName(_ locale: Locale) -> String func subscriptionDuration(_ locale: Locale) -> String? func introductoryOfferDuration(_ locale: Locale) -> String? + func localizedPricePerPeriod(_ locale: Locale) -> String + func localizedPriceAndPerMonth(_ locale: Locale) -> String + } /// Processes strings, replacing `{{ variable }}` with their associated content. @@ -59,26 +60,19 @@ extension String { @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_month": return self.localizedPricePerMonth - case "total_price_and_per_month": - if !self.isSubscription || self.isMonthly { - return self.localizedPrice - } else { - let unit = Localization.abbreviatedUnitLocalizedString(for: .month, locale: locale) - return "\(self.localizedPrice) (\(self.localizedPricePerMonth)/\(unit))" - } - + 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 "period": - return self.periodName(locale) - case "subscription_duration": - return self.subscriptionDuration(locale) ?? "" - case "intro_duration": - return self.introductoryOfferDuration(locale) ?? "" + 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.could_not_find_content_for_variable(variableName: variableName)) diff --git a/RevenueCatUI/Helpers/Package+VariableDataProvider.swift b/RevenueCatUI/Helpers/Package+VariableDataProvider.swift index 40c648ca0b..b22eb76122 100644 --- a/RevenueCatUI/Helpers/Package+VariableDataProvider.swift +++ b/RevenueCatUI/Helpers/Package+VariableDataProvider.swift @@ -8,14 +8,6 @@ extension Package: VariableDataProvider { return Bundle.main.applicationDisplayName } - var isSubscription: Bool { - return self.storeProduct.productCategory == .subscription - } - - var isMonthly: Bool { - return self.packageType == .monthly - } - var localizedPrice: String { return self.storeProduct.localizedPriceString } @@ -24,6 +16,10 @@ extension Package: VariableDataProvider { return self.priceFormatter.string(from: self.pricePerMonth) ?? "" } + var localizedIntroductoryOfferPrice: String? { + return self.storeProduct.introductoryDiscount?.localizedPriceString + } + var productName: String { return self.storeProduct.localizedTitle } @@ -41,6 +37,24 @@ extension Package: VariableDataProvider { return self.introDuration(locale) } + func localizedPricePerPeriod(_ locale: Locale) -> String { + guard let period = self.storeProduct.subscriptionPeriod else { + return self.localizedPrice + } + + let unit = Localization.abbreviatedUnitLocalizedString(for: period.unit, locale: locale) + return "\(self.localizedPrice)/\(unit)" + } + + func localizedPriceAndPerMonth(_ locale: Locale) -> String { + if !self.isSubscription || self.isMonthly { + return self.localizedPrice + } else { + let unit = Localization.abbreviatedUnitLocalizedString(for: .month, locale: locale) + return "\(self.localizedPrice) (\(self.localizedPricePerMonth)/\(unit))" + } + } + } // MARK: - Private @@ -48,6 +62,14 @@ extension Package: VariableDataProvider { @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) private extension Package { + var isSubscription: Bool { + return self.storeProduct.productCategory == .subscription + } + + var isMonthly: Bool { + return self.packageType == .monthly + } + var pricePerMonth: NSDecimalNumber { guard let price = self.storeProduct.pricePerMonth else { Logger.warning(Strings.package_not_subscription(self)) diff --git a/RevenueCatUI/Helpers/PaywallData+Default.swift b/RevenueCatUI/Helpers/PaywallData+Default.swift index e0c06b60ad..e18a3e7388 100644 --- a/RevenueCatUI/Helpers/PaywallData+Default.swift +++ b/RevenueCatUI/Helpers/PaywallData+Default.swift @@ -64,7 +64,7 @@ private extension PaywallData { subtitle: "Unlock full access with these subscriptions:", callToAction: "Continue", offerDetails: "{{ total_price_and_per_month }}.", - offerDetailsWithIntroOffer: "Start your {{ intro_duration }} trial, then {{ total_price_and_per_month }}." + offerDetailsWithIntroOffer: "Start your {{ sub_offer_duration }} trial, then {{ total_price_and_per_month }}." ) static let backgroundImage = "background.jpg" diff --git a/Tests/BackendIntegrationTests/__Snapshots__/StoreKitIntegrationTests/testCanGetOfferings.1.json b/Tests/BackendIntegrationTests/__Snapshots__/StoreKitIntegrationTests/testCanGetOfferings.1.json index da1c367639..99a2399770 100644 --- a/Tests/BackendIntegrationTests/__Snapshots__/StoreKitIntegrationTests/testCanGetOfferings.1.json +++ b/Tests/BackendIntegrationTests/__Snapshots__/StoreKitIntegrationTests/testCanGetOfferings.1.json @@ -51,13 +51,13 @@ "default_locale" : "en_US", "localized_strings" : { "en_US" : { - "call_to_action" : "Purchase for {{ price_per_month }} per month", - "call_to_action_with_intro_offer" : "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month", + "call_to_action" : "Purchase for {{ sub_price_per_month }} per month", + "call_to_action_with_intro_offer" : "Start your {{ sub_offer_duration }} trial, then {{ sub_price_per_month }} per month", "features" : [ ], "offer_details" : "{{ total_price_and_per_month }}", - "offer_details_with_intro_offer" : "{{ total_price_and_per_month }} after {{ intro_duration }} trial", + "offer_details_with_intro_offer" : "{{ total_price_and_per_month }} after {{ sub_offer_duration }} trial", "offer_name" : "{{ period }}", "subtitle" : "Gert access to all our educational content trusted by thousands of parents", "title" : "Ignite your child's curiosity" diff --git a/Tests/RevenueCatUITests/Data/PackageVariablesTests.swift b/Tests/RevenueCatUITests/Data/PackageVariablesTests.swift new file mode 100644 index 0000000000..f9042634e7 --- /dev/null +++ b/Tests/RevenueCatUITests/Data/PackageVariablesTests.swift @@ -0,0 +1,112 @@ +// +// PackageVariablesTests.swift +// +// +// Created by Nacho Soto on 8/4/23. +// + +import Nimble +import RevenueCat +@testable import RevenueCatUI +import XCTest + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +class PackageVariablesTests: TestCase { + + func testAppplicationName() { + expect(TestData.monthlyPackage.applicationName) == "xctest" + } + + func testLocalizedPrice() { + expect(TestData.weeklyPackage.localizedPriceString) == "$1.99" + expect(TestData.monthlyPackage.localizedPriceString) == "$6.99" + expect(TestData.annualPackage.localizedPriceString) == "$53.99" + expect(TestData.lifetimePackage.localizedPriceString) == "$119.49" + } + + func testLocalizedPricePerMonth() { + expect(TestData.weeklyPackage.localizedPricePerMonth) == "$7.96" + expect(TestData.monthlyPackage.localizedPricePerMonth) == "$6.99" + expect(TestData.annualPackage.localizedPricePerMonth) == "$4.49" + expect(TestData.lifetimePackage.localizedPricePerMonth) == "$119.49" + } + + func testEnglishLocalizedPricePerPeriod() { + expect(TestData.weeklyPackage.localizedPricePerPeriod(Self.english)) == "$1.99/wk" + expect(TestData.monthlyPackage.localizedPricePerPeriod(Self.english)) == "$6.99/mo" + expect(TestData.annualPackage.localizedPricePerPeriod(Self.english)) == "$53.99/yr" + expect(TestData.lifetimePackage.localizedPricePerPeriod(Self.english)) == "$119.49" + } + + func testSpanishLocalizedPricePerPeriod() { + expect(TestData.weeklyPackage.localizedPricePerPeriod(Self.spanish)) == "$1.99/sem" + expect(TestData.monthlyPackage.localizedPricePerPeriod(Self.spanish)) == "$6.99/m." + expect(TestData.annualPackage.localizedPricePerPeriod(Self.spanish)) == "$53.99/año" + expect(TestData.lifetimePackage.localizedPricePerPeriod(Self.spanish)) == "$119.49" + } + + func testEnglishLocalizedPriceAndPerMonth() { + expect(TestData.weeklyPackage.localizedPriceAndPerMonth(Self.english)) == "$1.99 ($7.96/mo)" + expect(TestData.monthlyPackage.localizedPriceAndPerMonth(Self.english)) == "$6.99" + expect(TestData.annualPackage.localizedPriceAndPerMonth(Self.english)) == "$53.99 ($4.49/mo)" + expect(TestData.lifetimePackage.localizedPriceAndPerMonth(Self.english)) == "$119.49" + } + + func testSpanishLocalizedPriceAndPerMonth() { + expect(TestData.weeklyPackage.localizedPriceAndPerMonth(Self.spanish)) == "$1.99 ($7.96/m.)" + expect(TestData.monthlyPackage.localizedPriceAndPerMonth(Self.spanish)) == "$6.99" + expect(TestData.annualPackage.localizedPriceAndPerMonth(Self.spanish)) == "$53.99 ($4.49/m.)" + expect(TestData.lifetimePackage.localizedPriceAndPerMonth(Self.spanish)) == "$119.49" + } + + func testProductName() { + expect(TestData.weeklyPackage.productName) == "Weekly" + expect(TestData.monthlyPackage.productName) == "Monthly" + expect(TestData.annualPackage.productName) == "Annual" + expect(TestData.lifetimePackage.productName) == "Lifetime" + } + + func testEnglishPeriodName() { + expect(TestData.weeklyPackage.periodName(Self.english)) == "Weekly" + expect(TestData.monthlyPackage.periodName(Self.english)) == "Monthly" + expect(TestData.annualPackage.periodName(Self.english)) == "Annual" + expect(TestData.lifetimePackage.periodName(Self.english)) == "Lifetime" + } + + func testSpanishPeriodName() { + expect(TestData.weeklyPackage.periodName(Self.spanish)) == "Semanal" + expect(TestData.monthlyPackage.periodName(Self.spanish)) == "Mensual" + expect(TestData.annualPackage.periodName(Self.spanish)) == "Anual" + expect(TestData.lifetimePackage.periodName(Self.spanish)) == "Vitalicio" + } + + func testEnglishIntroductoryOfferDuration() { + expect(TestData.weeklyPackage.introductoryOfferDuration(Self.english)).to(beNil()) + expect(TestData.monthlyPackage.introductoryOfferDuration(Self.english)) == "7 days" + expect(TestData.annualPackage.introductoryOfferDuration(Self.english)) == "14 days" + expect(TestData.lifetimePackage.introductoryOfferDuration(Self.english)).to(beNil()) + } + + func testSpanishIntroductoryOfferDuration() { + expect(TestData.weeklyPackage.introductoryOfferDuration(Self.spanish)).to(beNil()) + expect(TestData.monthlyPackage.introductoryOfferDuration(Self.spanish)) == "7 días" + expect(TestData.annualPackage.introductoryOfferDuration(Self.spanish)) == "14 días" + expect(TestData.lifetimePackage.introductoryOfferDuration(Self.spanish)).to(beNil()) + } + + func testIntroductoryOfferPrice() { + expect(TestData.weeklyPackage.localizedIntroductoryOfferPrice).to(beNil()) + expect(TestData.monthlyPackage.localizedIntroductoryOfferPrice) == "$0.00" + expect(TestData.annualPackage.localizedIntroductoryOfferPrice) == "$1.99" + expect(TestData.lifetimePackage.localizedIntroductoryOfferPrice).to(beNil()) + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private extension PackageVariablesTests { + + static let english: Locale = .init(identifier: "en_US") + static let spanish: Locale = .init(identifier: "es_ES") + +} diff --git a/Tests/RevenueCatUITests/Data/TemplateViewConfigurationTests.swift b/Tests/RevenueCatUITests/Data/TemplateViewConfigurationTests.swift index 587bd74061..9f6eb86b72 100644 --- a/Tests/RevenueCatUITests/Data/TemplateViewConfigurationTests.swift +++ b/Tests/RevenueCatUITests/Data/TemplateViewConfigurationTests.swift @@ -286,8 +286,9 @@ private extension BaseTemplateViewConfigurationTests { subtitle: "Get access to all our educational content trusted by thousands of parents.", callToAction: "Purchase for {{ price }}", callToActionWithIntroOffer: nil, - offerDetails: "{{ price_per_month }} per month", - offerDetailsWithIntroOffer: "Start your {{ intro_duration }} trial, then {{ 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", features: [] ) diff --git a/Tests/RevenueCatUITests/Data/VariablesTests.swift b/Tests/RevenueCatUITests/Data/VariablesTests.swift index a954489b68..15c0a88a35 100644 --- a/Tests/RevenueCatUITests/Data/VariablesTests.swift +++ b/Tests/RevenueCatUITests/Data/VariablesTests.swift @@ -36,43 +36,19 @@ class VariablesTests: TestCase { expect(self.process("Purchase for {{ price }}")) == "Purchase for $10.99" } - func testPricePerMonth() { - self.provider.localizedPricePerMonth = "$3.99" - expect(self.process("{{ price_per_month }} per month")) == "$3.99 per month" + func testPricePerPeriod() { + self.provider.localizedPricePerPeriod = "$3.99/yr" + expect(self.process("{{ price_per_period }}")) == "$3.99/yr" } - func testTotalPriceAndPerMonthWithDifferentPrices() { - self.provider.localizedPrice = "$49.99" - self.provider.localizedPricePerMonth = "$4.16" - expect(self.process("{{ total_price_and_per_month }}")) == "$49.99 ($4.16/mo)" - } - - func testTotalPriceAndPerMonthForNonSubscriptions() { - self.provider.isSubscription = false - self.provider.isMonthly = false - self.provider.localizedPrice = "$49.99" - expect(self.process("{{ total_price_and_per_month }}")) == "$49.99" - } - - func testTotalPriceAndPerMonthWithDifferentPricesSpanish() { - self.provider.localizedPrice = "49,99€" - self.provider.localizedPricePerMonth = "4,16€" - expect(self.process("{{ total_price_and_per_month }}", - locale: .init(identifier: "es_ES"))) == "49,99€ (4,16€/mes)" - } - - func testTotalPriceAndPerMonthWithDifferentPricesFrench() { - self.provider.isMonthly = false - self.provider.localizedPrice = "49,99€" - self.provider.localizedPricePerMonth = "4,16€" - expect(self.process("{{ total_price_and_per_month }}", - locale: .init(identifier: "fr_FR"))) == "49,99€ (4,16€/m)" + func testPricePerMonth() { + self.provider.localizedPricePerMonth = "$3.99" + expect(self.process("{{ sub_price_per_month }} per month")) == "$3.99 per month" } - func testTotalPriceAndPerMonthWithSamePrice() { - self.provider.isMonthly = true - self.provider.localizedPrice = "$4.99" - expect(self.process("{{ total_price_and_per_month }}")) == "$4.99" + func testTotalPriceAndPerMonth() { + self.provider.localizedPriceAndPerMonth = "$49.99 ($4.16/mth)" + expect(self.process("{{ total_price_and_per_month }}")) == self.provider.localizedPriceAndPerMonth } func testProductName() { @@ -82,23 +58,28 @@ class VariablesTests: TestCase { func testPeriodName() { self.provider.periodName = "Monthly" - expect(self.process("{{ period }}")) == "Monthly" + expect(self.process("{{ sub_period }}")) == "Monthly" } func testSubscriptionDuration() { self.provider.subscriptionDuration = "1 month" - expect(self.process("{{ subscription_duration }}")) == "1 month" + expect(self.process("{{ sub_duration }}")) == "1 month" } func testIntroDurationName() { self.provider.introductoryOfferDuration = "1 week" - expect(self.process("Start {{ intro_duration }} trial")) == "Start 1 week trial" + expect(self.process("Start {{ sub_offer_duration }} trial")) == "Start 1 week trial" + } + + func testIntroPrice() { + self.provider.introductoryOfferPrice = "$4.99" + expect(self.process("{{ sub_offer_price }}")) == self.provider.localizedIntroductoryOfferPrice } func testMultipleVariables() { self.provider.productName = "Pro" self.provider.localizedPricePerMonth = "$1.99" - expect(self.process("Unlock {{ product_name }} for {{ price_per_month }}")) == "Unlock Pro for $1.99" + expect(self.process("Unlock {{ product_name }} for {{ sub_price_per_month }}")) == "Unlock Pro for $1.99" } func testHandlesUnknownVariablesGracefully() { @@ -109,19 +90,19 @@ class VariablesTests: TestCase { let configuration = PaywallData.LocalizedConfiguration( title: "Buy {{ product_name }} for {{ app_name }}", subtitle: "Price: {{ price }}", - callToAction: "Unlock {{ product_name }} for {{ price_per_month }}", - callToActionWithIntroOffer: "Start your {{ intro_duration }} free trial\n" + - "Then {{ price_per_month }} every month", - offerDetails: "Purchase for {{ price }} every {{ subscription_duration }}", - offerDetailsWithIntroOffer: "Start your {{ intro_duration }} free trial\n" + - "Then {{ price_per_month }} every month", - offerName: "{{ period }}", + callToAction: "Unlock {{ product_name }} for {{ sub_price_per_month }}", + callToActionWithIntroOffer: "Start your {{ sub_offer_duration }} free trial\n" + + "Then {{ sub_price_per_month }} every month", + offerDetails: "Purchase for {{ price }} every {{ sub_duration }}", + offerDetailsWithIntroOffer: "Start your {{ sub_offer_duration }} free trial\n" + + "Then {{ sub_price_per_month }} every month", + offerName: "{{ sub_period }}", features: [ .init(title: "Purchase {{ product_name }}", - content: "Trial lasts {{ intro_duration }}", + content: "Trial lasts {{ sub_offer_duration }}", iconID: nil), .init(title: "Only {{ price }}", - content: "{{ period }} subscription", + content: "{{ sub_period }} subscription", iconID: nil) ] ) @@ -148,7 +129,7 @@ class VariablesTests: TestCase { // and it's better than crashing. func testPricePerMonthForLifetimeProductsReturnsPrice() { let result = VariableHandler.processVariables( - in: "{{ price_per_month }}", + in: "{{ sub_price_per_month }}", with: TestData.lifetimePackage ) expect(result) == "$119.49" @@ -178,14 +159,15 @@ private extension VariablesTests { private struct MockVariableProvider: VariableDataProvider { var applicationName: String = "" - var isSubscription: Bool = true - var isMonthly: Bool = false var localizedPrice: String = "" var localizedPricePerMonth: String = "" + var localizedPriceAndPerMonth: String = "" + var localizedPricePerPeriod: String = "" var productName: String = "" var periodName: String = "" var subscriptionDuration: String? var introductoryOfferDuration: String? + var introductoryOfferPrice: String = "" func periodName(_ locale: Locale) -> String { return self.periodName @@ -199,4 +181,16 @@ private struct MockVariableProvider: VariableDataProvider { return self.introductoryOfferDuration } + func localizedPricePerPeriod(_ locale: Locale) -> String { + return self.localizedPricePerPeriod + } + + func localizedPriceAndPerMonth(_ locale: Locale) -> String { + return self.localizedPriceAndPerMonth + } + + var localizedIntroductoryOfferPrice: String? { + return self.introductoryOfferPrice + } + } diff --git a/Tests/RevenueCatUITests/LocalizationTests.swift b/Tests/RevenueCatUITests/LocalizationTests.swift index 8f748f7c21..11bb157a26 100644 --- a/Tests/RevenueCatUITests/LocalizationTests.swift +++ b/Tests/RevenueCatUITests/LocalizationTests.swift @@ -29,12 +29,16 @@ class AbbreviatedUnitEnglishLocalizationTests: BaseLocalizationTests { verify(.day, "day") } + func testWeek() { + verify(.week, "wk") + } + func testMonth() { verify(.month, "mo") } func testYear() { - verify(.year, "y") + verify(.year, "yr") } } @@ -48,8 +52,12 @@ class AbbreviatedUnitSpanishLocalizationTests: BaseLocalizationTests { verify(.day, "día") } + func testWeek() { + verify(.week, "sem") + } + func testMonth() { - verify(.month, "mes") + verify(.month, "m.") } func testYear() { @@ -243,7 +251,7 @@ private extension BaseLocalizationTests { } func verify( - _ unit: NSCalendar.Unit, + _ unit: SubscriptionPeriod.Unit, _ expected: String, file: StaticString = #file, line: UInt = #line diff --git a/Tests/RevenueCatUITests/Templates/PaywallViewLocalizationTests.swift b/Tests/RevenueCatUITests/Templates/PaywallViewLocalizationTests.swift index 33b29b78b7..b62621adea 100644 --- a/Tests/RevenueCatUITests/Templates/PaywallViewLocalizationTests.swift +++ b/Tests/RevenueCatUITests/Templates/PaywallViewLocalizationTests.swift @@ -79,8 +79,8 @@ private extension PaywallViewLocalizationTests { subtitle: "Accede a todo nuestro contenido educativo, confiado por miles de padres.", callToAction: "Comprar", offerDetails: "{{ total_price_and_per_month }}", - offerDetailsWithIntroOffer: "Comienza tu prueba de {{ intro_duration }}, " + - "después {{ price_per_month }} cada mes", + offerDetailsWithIntroOffer: "Comienza tu prueba de {{ sub_offer_duration }}, " + + "después {{ sub_price_per_month }} cada mes", features: [] ) diff --git a/Tests/RevenueCatUITests/Templates/__Snapshots__/PaywallViewLocalizationTests/iOS16-testSpanish.1.png b/Tests/RevenueCatUITests/Templates/__Snapshots__/PaywallViewLocalizationTests/iOS16-testSpanish.1.png index 9bf4bc6006..88a760b3c2 100644 Binary files a/Tests/RevenueCatUITests/Templates/__Snapshots__/PaywallViewLocalizationTests/iOS16-testSpanish.1.png and b/Tests/RevenueCatUITests/Templates/__Snapshots__/PaywallViewLocalizationTests/iOS16-testSpanish.1.png differ diff --git a/Tests/RevenueCatUITests/Templates/__Snapshots__/PaywallViewLocalizationTests/iOS17-testSpanish.1.png b/Tests/RevenueCatUITests/Templates/__Snapshots__/PaywallViewLocalizationTests/iOS17-testSpanish.1.png index d7ba03411d..e8b9abc7c6 100644 Binary files a/Tests/RevenueCatUITests/Templates/__Snapshots__/PaywallViewLocalizationTests/iOS17-testSpanish.1.png and b/Tests/RevenueCatUITests/Templates/__Snapshots__/PaywallViewLocalizationTests/iOS17-testSpanish.1.png differ diff --git a/Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift b/Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift index 5c328c2b8b..d1262a468e 100644 --- a/Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift +++ b/Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift @@ -204,9 +204,9 @@ private extension SamplePaywallLoader { 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 {{ price_per_month }} per month", - offerDetails: "{{ price_per_month }} per month", - offerDetailsWithIntroOffer: "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month" + 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 ) @@ -243,10 +243,10 @@ private extension SamplePaywallLoader { localization: .init( title: "Call to action for better conversion.", subtitle: "Lorem ipsum is simply dummy text of the printing and typesetting industry.", - callToAction: "Subscribe for {{ price_per_month }}/mo", + callToAction: "Subscribe for {{ sub_price_per_month }}/mo", offerDetails: "{{ total_price_and_per_month }}", - offerDetailsWithIntroOffer: "{{ total_price_and_per_month }} after {{ intro_duration }} trial", - offerName: "{{ period }}" + offerDetailsWithIntroOffer: "{{ total_price_and_per_month }} after {{ sub_offer_duration }} trial", + offerName: "{{ sub_period }}" ), assetBaseURL: Self.paywallAssetBaseURL ) @@ -273,9 +273,9 @@ private extension SamplePaywallLoader { localization: .init( title: "How your free trial works", callToAction: "Start", - callToActionWithIntroOffer: "Start your {{ intro_duration }} free", - offerDetails: "Only {{ price }} per {{ period }}", - offerDetailsWithIntroOffer: "First {{ intro_duration }} free, then\n{{ price }} per year ({{ price_per_month }} per month)", + callToActionWithIntroOffer: "Start your {{ sub_offer_duration }} free", + offerDetails: "Only {{ price_per_period }}", + offerDetailsWithIntroOffer: "First {{ sub_offer_duration }} free, then\n{{ price }} per year ({{ sub_price_per_month }} per month)", features: [ .init(title: "Today", content: "Full access to 1000+ workouts plus free meal plan worth $49.99.", @@ -315,8 +315,8 @@ private extension SamplePaywallLoader { title: "Get _unlimited_ access", callToAction: "Continue", offerDetails: nil, - offerDetailsWithIntroOffer: "Includes {{ intro_duration }} **free** trial", - offerName: "{{ subscription_duration }}" + offerDetailsWithIntroOffer: "Includes {{ sub_offer_duration }} **free** trial", + offerName: "{{ sub_duration }}" ), assetBaseURL: Self.paywallAssetBaseURL ) diff --git a/Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json b/Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json index 7b92cc28f4..4897c1e312 100644 --- a/Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json +++ b/Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json @@ -50,8 +50,8 @@ "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", + "offer_details": "{{ sub_price_per_month }} per month", + "offer_details_with_intro_offer": "Start your {{ sub_offer_duration }} trial, then {{ sub_price_per_month }} per month", "features": [ { "title": "Feature 1", @@ -64,8 +64,8 @@ "title": "Tienda", "call_to_action": "Comprar", "call_to_action_with_intro_offer": "Comprar", - "offer_details": "{{ price_per_month }} cada mes", - "offer_details_with_intro_offer": "Comienza tu prueba de {{ intro_duration }}, y después {{ price_per_month }} cada mes" + "offer_details": "{{ sub_price_per_month }} cada mes", + "offer_details_with_intro_offer": "Comienza tu prueba de {{ sub_offer_duration }}, y después {{ sub_price_per_month }} cada mes" } }, "default_locale": "en_US", diff --git a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-Sample1.json b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-Sample1.json index 9ab912d825..6b644865f2 100644 --- a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-Sample1.json +++ b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-Sample1.json @@ -6,8 +6,8 @@ "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", + "offer_details": "{{ sub_price_per_month }} per month", + "offer_details_with_intro_offer": "Start your {{ sub_offer_duration }} trial, then {{ sub_price_per_month }} per month", "offer_name": "{{ period }}", "features": [ { 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 74946fc5bf..ff1b80259f 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 @@ -6,8 +6,8 @@ "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" + "offer_details": "{{ sub_price_per_month }} per month", + "offer_details_with_intro_offer": "Start your {{ sub_offer_duration }} trial, then {{ sub_price_per_month }} per month" } }, "default_locale": "es_ES", 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 142863a554..2bc005de53 100644 --- a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_locale.json +++ b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_locale.json @@ -6,8 +6,8 @@ "subtitle": "Descripción", "call_to_action": "Comprar", "call_to_action_with_intro_offer": "Comprar", - "offer_details": "{{ price_per_month }} cada mes", - "offer_details_with_intro_offer": "Comienza tu prueba de {{ intro_duration }}, y después {{ price_per_month }} cada mes" + "offer_details": "{{ sub_price_per_month }} cada mes", + "offer_details_with_intro_offer": "Comienza tu prueba de {{ sub_offer_duration }}, y después {{ sub_price_per_month }} cada mes" } }, "default_locale": "es_ES", diff --git a/Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift b/Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift index a557f4ee55..e952f78deb 100644 --- a/Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift +++ b/Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift @@ -125,9 +125,9 @@ class OfferingsDecodingTests: BaseHTTPResponseTest { expect(enConfig.subtitle) == "Description" expect(enConfig.callToAction) == "Purchase now" expect(enConfig.callToActionWithIntroOffer) == "Purchase now" - expect(enConfig.offerDetails) == "{{ price_per_month }} per month" + expect(enConfig.offerDetails) == "{{ sub_price_per_month }} per month" expect(enConfig.offerDetailsWithIntroOffer) - == "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month" + == "Start your {{ sub_offer_duration }} trial, then {{ sub_price_per_month }} per month" expect(enConfig.offerName).to(beNil()) expect(enConfig.features) == [ .init(title: "Feature 1", content: "Content", iconID: "lock") @@ -138,9 +138,9 @@ class OfferingsDecodingTests: BaseHTTPResponseTest { expect(esConfig.subtitle).to(beNil()) expect(esConfig.callToAction) == "Comprar" expect(esConfig.callToActionWithIntroOffer) == "Comprar" - expect(esConfig.offerDetails) == "{{ price_per_month }} cada mes" + expect(esConfig.offerDetails) == "{{ sub_price_per_month }} cada mes" expect(esConfig.offerDetailsWithIntroOffer) - == "Comienza tu prueba de {{ intro_duration }}, y después {{ price_per_month }} cada mes" + == "Comienza tu prueba de {{ sub_offer_duration }}, y después {{ sub_price_per_month }} cada mes" expect(esConfig.offerName).to(beNil()) expect(esConfig.features).to(beEmpty()) diff --git a/Tests/UnitTests/Paywalls/PaywallDataTests.swift b/Tests/UnitTests/Paywalls/PaywallDataTests.swift index e6b78302e7..0b8ded0547 100644 --- a/Tests/UnitTests/Paywalls/PaywallDataTests.swift +++ b/Tests/UnitTests/Paywalls/PaywallDataTests.swift @@ -65,9 +65,9 @@ class PaywallDataTests: BaseHTTPResponseTest { expect(enConfig.subtitle) == "Description" expect(enConfig.callToAction) == "Purchase now" expect(enConfig.callToActionWithIntroOffer) == "Purchase now" - expect(enConfig.offerDetails) == "{{ price_per_month }} per month" + expect(enConfig.offerDetails) == "{{ sub_price_per_month }} per month" expect(enConfig.offerDetailsWithIntroOffer) - == "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month" + == "Start your {{ sub_offer_duration }} trial, then {{ sub_price_per_month }} per month" expect(enConfig.offerName) == "{{ period }}" expect(enConfig.features) == [ .init(title: "Feature 1", content: "Content 1", iconID: "lock"),