Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Paywalls: updated variable names #2970

Merged
merged 1 commit into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New variable

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 ?? ""
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New variable


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 @@ -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"
Expand Down
Loading