Skip to content

Commit

Permalink
Paywalls: new {{ sub_relative_discount }} variable
Browse files Browse the repository at this point in the history
  • Loading branch information
NachoSoto committed Aug 31, 2023
1 parent 1cd0f9f commit 5f356e2
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 54 deletions.
17 changes: 10 additions & 7 deletions RevenueCatUI/Data/ProcessedLocalizedConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,24 @@ struct ProcessedLocalizedConfiguration: PaywallLocalizedConfiguration {
init(
_ configuration: PaywallData.LocalizedConfiguration,
_ dataProvider: VariableDataProvider,
_ context: VariableHandler.Context,
_ locale: Locale
) {
self.init(
title: configuration.title.processed(with: dataProvider, locale: locale),
subtitle: configuration.subtitle?.processed(with: dataProvider, locale: locale),
callToAction: configuration.callToAction.processed(with: dataProvider, locale: locale),
title: configuration.title.processed(with: dataProvider, context: context, locale: locale),
subtitle: configuration.subtitle?.processed(with: dataProvider, context: context, locale: locale),
callToAction: configuration.callToAction.processed(with: dataProvider, context: context, locale: locale),
callToActionWithIntroOffer: configuration.callToActionWithIntroOffer?.processed(with: dataProvider,
context: context,
locale: locale),
offerDetails: configuration.offerDetails?.processed(with: dataProvider, locale: locale),
offerDetails: configuration.offerDetails?.processed(with: dataProvider, context: context, locale: locale),
offerDetailsWithIntroOffer: configuration.offerDetailsWithIntroOffer?.processed(with: dataProvider,
context: context,
locale: locale),
offerName: configuration.offerName?.processed(with: dataProvider, locale: locale),
offerName: configuration.offerName?.processed(with: dataProvider, context: context, locale: locale),
features: configuration.features.map {
.init(title: $0.title.processed(with: dataProvider, locale: locale),
content: $0.content?.processed(with: dataProvider, locale: locale),
.init(title: $0.title.processed(with: dataProvider, context: context, locale: locale),
content: $0.content?.processed(with: dataProvider, context: context, locale: locale),
iconID: $0.iconID)
}
)
Expand Down
20 changes: 13 additions & 7 deletions RevenueCatUI/Data/TemplateViewConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,18 +121,24 @@ extension TemplateViewConfiguration.PackageConfiguration {
let filtered = TemplateViewConfiguration.filter(packages: packages, with: filter)
let mostExpensivePricePerMonth = Self.mostExpensivePricePerMonth(in: filtered)

let filteredPackages = filtered
let filteredPackages: [TemplateViewConfiguration.Package] = filtered
.map { package in
TemplateViewConfiguration.Package(
let discount = Self.discount(
from: package.storeProduct.pricePerMonth?.doubleValue,
relativeTo: mostExpensivePricePerMonth
)

return .init(
content: package,
localization: localization.processVariables(with: package, locale: locale),
localization: localization.processVariables(
with: package,
context: .init(discountRelativeToMostExpensivePerMonth: discount),
locale: locale
),
currentlySubscribed: activelySubscribedProductIdentifiers.contains(
package.storeProduct.productIdentifier
),
discountRelativeToMostExpensivePerMonth: Self.discount(
from: package.storeProduct.pricePerMonth?.doubleValue,
relativeTo: mostExpensivePricePerMonth
)
discountRelativeToMostExpensivePerMonth: discount
)
}

Expand Down
79 changes: 58 additions & 21 deletions RevenueCatUI/Data/Variables.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ import RevenueCat
@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
extension PaywallData.LocalizedConfiguration {

func processVariables(with package: Package, locale: Locale = .current) -> ProcessedLocalizedConfiguration {
return .init(self, package, locale)
func processVariables(
with package: Package,
context: VariableHandler.Context,
locale: Locale = .current
) -> ProcessedLocalizedConfiguration {
return .init(self, package, context, locale)
}

}
Expand All @@ -38,22 +42,37 @@ protocol VariableDataProvider {

func localizedPricePerPeriod(_ locale: Locale) -> String
func localizedPriceAndPerMonth(_ locale: Locale) -> String
func localizedRelativeDiscount(_ discount: Double?, _ locale: Locale) -> String?

}

/// Processes strings, replacing `{{ variable }}` with their associated content.
@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
enum VariableHandler {

/// Information necessary for computing variables
struct Context {

var discountRelativeToMostExpensivePerMonth: Double?

}

static func processVariables(
in string: String,
with provider: VariableDataProvider,
context: Context,
locale: Locale = .current
) -> String {
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, *) {
return VariableHandlerIOS16.processVariables(in: string, with: provider, locale: locale)
return VariableHandlerIOS16.processVariables(in: string,
with: provider,
context: context,
locale: locale)
} else {
return VariableHandlerIOS15.processVariables(in: string, with: provider, locale: locale)
return VariableHandlerIOS15.processVariables(in: string,
with: provider,
context: context,
locale: locale)
}
}

Expand All @@ -65,21 +84,25 @@ enum VariableHandler {
)
}

fileprivate typealias ValueProvider = (VariableDataProvider, Locale) -> String?
fileprivate typealias ValueProvider = (VariableDataProvider,
VariableHandler.Context,
Locale) -> String?

// swiftlint:disable:next cyclomatic_complexity
fileprivate static func provider(for variableName: String) -> ValueProvider? {
switch variableName {
case "app_name": return { (provider, _) in provider.applicationName }
case "price": return { (provider, _) in provider.localizedPrice }
case "price_per_period": return { $0.localizedPricePerPeriod($1) }
case "total_price_and_per_month": return { $0.localizedPriceAndPerMonth($1) }
case "product_name": return { (provider, _) in provider.productName }
case "sub_period": return { $0.periodName($1) }
case "sub_price_per_month": return { (provider, _) in provider.localizedPricePerMonth }
case "sub_duration": return { $0.subscriptionDuration($1) }
case "sub_offer_duration": return { $0.introductoryOfferDuration($1) }
case "sub_offer_price": return { (provider, _) in provider.localizedIntroductoryOfferPrice }
case "app_name": return { (provider, _, _) in provider.applicationName }
case "price": return { (provider, _, _) in provider.localizedPrice }
case "price_per_period": return { (provider, _, locale) in provider.localizedPricePerPeriod(locale) }
case "total_price_and_per_month": return { (provider, _, locale) in provider.localizedPriceAndPerMonth(locale) }
case "product_name": return { (provider, _, _) in provider.productName }
case "sub_period": return { (provider, _, locale) in provider.periodName(locale) }
case "sub_price_per_month": return { (provider, _, _) in provider.localizedPricePerMonth }
case "sub_duration": return { (provider, _, locale) in provider.subscriptionDuration(locale) }
case "sub_offer_duration": return { (provider, _, locale) in provider.introductoryOfferDuration(locale) }
case "sub_offer_price": return { (provider, _, _) in provider.localizedIntroductoryOfferPrice }
case "sub_relative_discount": return { $0.localizedRelativeDiscount($1.discountRelativeToMostExpensivePerMonth,
$2) }

default:
Logger.warning(Strings.unrecognized_variable_name(variableName: variableName))
Expand All @@ -92,8 +115,12 @@ enum VariableHandler {
@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
extension String {

func processed(with provider: VariableDataProvider, locale: Locale) -> Self {
return VariableHandler.processVariables(in: self, with: provider, locale: locale)
func processed(
with provider: VariableDataProvider,
context: VariableHandler.Context,
locale: Locale
) -> Self {
return VariableHandler.processVariables(in: self, with: provider, context: context, locale: locale)
}

func unrecognizedVariables() -> Set<String> {
Expand All @@ -109,8 +136,12 @@ extension String {
@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
private extension VariableDataProvider {

func value(for variableName: String, locale: Locale) -> String {
VariableHandler.provider(for: variableName)?(self, locale) ?? ""
func value(
for variableName: String,
context: VariableHandler.Context,
locale: Locale
) -> String {
VariableHandler.provider(for: variableName)?(self, context, locale) ?? ""
}

}
Expand All @@ -123,13 +154,16 @@ private enum VariableHandlerIOS16 {
static func processVariables(
in string: String,
with provider: VariableDataProvider,
context: VariableHandler.Context,
locale: Locale = .current
) -> String {
let matches = Self.extractVariables(from: string)
var replacedString = string

for variableMatch in matches.reversed() {
let replacementValue = provider.value(for: variableMatch.variable, locale: locale)
let replacementValue = provider.value(for: variableMatch.variable,
context: context,
locale: locale)
replacedString = replacedString.replacingCharacters(in: variableMatch.range, with: replacementValue)
}

Expand Down Expand Up @@ -176,6 +210,7 @@ private enum VariableHandlerIOS15 {
static func processVariables(
in string: String,
with provider: VariableDataProvider,
context: VariableHandler.Context,
locale: Locale = .current
) -> String {
var replacedString = string
Expand All @@ -185,7 +220,9 @@ private enum VariableHandlerIOS15 {
let variableNameRange = match.range(at: 1)
if let variableNameRange = Range(variableNameRange, in: string) {
let variableName = String(string[variableNameRange])
let replacementValue = provider.value(for: variableName, locale: locale)
let replacementValue = provider.value(for: variableName,
context: context,
locale: locale)

let adjustedRange = NSRange(
location: variableNameRange.lowerBound.utf16Offset(in: string) - Self.pattern.count / 2,
Expand Down
6 changes: 6 additions & 0 deletions RevenueCatUI/Helpers/Package+VariableDataProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ extension Package: VariableDataProvider {
}
}

func localizedRelativeDiscount(_ discount: Double?, _ locale: Locale) -> String? {
guard let discount else { return nil }

return Localization.localized(discount: discount, locale: locale)
}

}

// MARK: - Private
Expand Down
1 change: 1 addition & 0 deletions RevenueCatUI/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
"Monthly" = "Monthly";
"Weekly" = "Weekly";
"Lifetime" = "Lifetime";
"%d%% off" = "%d%% off";
7 changes: 5 additions & 2 deletions RevenueCatUI/Views/PurchaseButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,11 @@ struct PurchaseButton_Previews: PreviewProvider {

private static let package: TemplateViewConfiguration.Package = .init(
content: TestData.packageWithIntroOffer,
localization: TestData.localization1.processVariables(with: TestData.packageWithIntroOffer,
locale: .current),
localization: TestData.localization1.processVariables(
with: TestData.packageWithIntroOffer,
context: .init(discountRelativeToMostExpensivePerMonth: nil),
locale: .current
),
currentlySubscribed: Bool.random(),
discountRelativeToMostExpensivePerMonth: nil
)
Expand Down
10 changes: 10 additions & 0 deletions Tests/RevenueCatUITests/Data/PackageVariablesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ class PackageVariablesTests: TestCase {
expect(TestData.lifetimePackage.localizedIntroductoryOfferPrice).to(beNil())
}

func testEnglishRelativeDiscount() {
expect(TestData.weeklyPackage.localizedRelativeDiscount(nil, Self.english)).to(beNil())
expect(TestData.weeklyPackage.localizedRelativeDiscount(0.372, Self.english)) == "37% off"
}

func testSpanishRelativeDiscount() {
expect(TestData.weeklyPackage.localizedRelativeDiscount(nil, Self.spanish)).to(beNil())
expect(TestData.weeklyPackage.localizedRelativeDiscount(0.372, Self.english)) == "ahorra 37%"
}

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
Expand Down
Loading

0 comments on commit 5f356e2

Please sign in to comment.