Skip to content

Commit

Permalink
Paywalls: updated variable names (#2970)
Browse files Browse the repository at this point in the history
These match the new agreed upon variables.

- Improved the localized unit abbreviations
- Refactored `VariableDataProvider` so all the logic is now in `Package`
- Added new tests for `Package` variables
  • Loading branch information
NachoSoto committed Sep 15, 2023
1 parent 483e03c commit df7bd8b
Show file tree
Hide file tree
Showing 18 changed files with 284 additions and 138 deletions.
31 changes: 20 additions & 11 deletions RevenueCatUI/Data/Localization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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")
Expand Down
34 changes: 20 additions & 14 deletions RevenueCatUI/Data/TestData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 }}.",
Expand Down Expand Up @@ -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
),
Expand Down Expand Up @@ -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"
Expand All @@ -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,
Expand Down
30 changes: 12 additions & 18 deletions RevenueCatUI/Data/Variables.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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))
Expand Down
38 changes: 30 additions & 8 deletions RevenueCatUI/Helpers/Package+VariableDataProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -41,13 +37,39 @@ 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

@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))
Expand Down
2 changes: 1 addition & 1 deletion RevenueCatUI/Helpers/PaywallData+Default.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,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"
Expand Down
Loading

0 comments on commit df7bd8b

Please sign in to comment.