Skip to content

Commit

Permalink
Paywalls: added ability to calculate and localize subscription disc…
Browse files Browse the repository at this point in the history
…ounts (#2943)

![Screenshot 2023-08-02 at 08 17
45](https://github.com/RevenueCat/purchases-ios/assets/685609/0c3ee7b8-1b37-4e6d-81d0-4b2d18086d82)

Useful for template 4, but also for others.
  • Loading branch information
NachoSoto committed Sep 8, 2023
1 parent bcbe046 commit 36dff3b
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 6 deletions.
14 changes: 14 additions & 0 deletions RevenueCatUI/Data/Localization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,20 @@ enum Localization {
return path.flatMap(Bundle.init(path:)) ?? containerBundle
}

/// - Returns: localized string for a discount. Example: "37% off"
static func localized(
discount: Double,
locale: Locale
) -> String {
assert(discount >= 0, "Invalid discount: \(discount)")

let number = Int((discount * 100).rounded(.toNearestOrAwayFromZero))
let format = self.localizedBundle(locale)
.localizedString(forKey: "%d%% off", value: nil, table: nil)

return String(format: format, number)
}

}

// MARK: - Private
Expand Down
44 changes: 38 additions & 6 deletions RevenueCatUI/Data/TemplateViewConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ extension TemplateViewConfiguration {

let content: RevenueCat.Package
let localization: ProcessedLocalizedConfiguration
let discountRelativeToMostExpensivePerMonth: Double?

}

Expand Down Expand Up @@ -107,33 +108,64 @@ extension TemplateViewConfiguration.PackageConfiguration {
guard !packages.isEmpty else { throw TemplateError.noPackages }
guard !filter.isEmpty else { throw TemplateError.emptyPackageList }

let filtered = TemplateViewConfiguration
.filter(packages: packages, with: filter)
let filtered = TemplateViewConfiguration.filter(packages: packages, with: filter)
let mostExpensivePricePerMonth = Self.mostExpensivePricePerMonth(in: filtered)

let filteredPackages = filtered
.map { package in
TemplateViewConfiguration.Package(
content: package,
localization: localization.processVariables(with: package, locale: locale)
localization: localization.processVariables(with: package, locale: locale),
discountRelativeToMostExpensivePerMonth: Self.discount(
from: package.storeProduct.pricePerMonth?.doubleValue,
relativeTo: mostExpensivePricePerMonth
)
)
}

guard let firstPackage = filtered.first else {
guard let firstPackage = filteredPackages.first else {
throw TemplateError.couldNotFindAnyPackages(expectedTypes: filter)
}

switch setting {
case .single:
return .single(firstPackage)
case .multiple:
let defaultPackage = filtered
let defaultPackage = filteredPackages
.first { $0.content.packageType == `default` }
?? firstPackage

return .multiple(first: firstPackage,
default: defaultPackage,
all: filtered)
all: filteredPackages)
}
}

private static func mostExpensivePricePerMonth(in packages: [Package]) -> Double? {
return packages
.lazy
.map(\.storeProduct)
.compactMap { product in
product.pricePerMonth.map {
return (
product: product,
pricePerMonth: $0
)
}
}
.max { productA, productB in
return productA.pricePerMonth.doubleValue < productB.pricePerMonth.doubleValue
}
.map(\.pricePerMonth.doubleValue)
}

private static func discount(from pricePerMonth: Double?, relativeTo mostExpensive: Double?) -> Double? {
guard let pricePerMonth, let mostExpensive else { return nil }
guard pricePerMonth < mostExpensive else { return nil }

return (mostExpensive - pricePerMonth) / mostExpensive
}

}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
Expand Down
2 changes: 2 additions & 0 deletions RevenueCatUI/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
"PackageType.twoMonth" = "2 month";
"PackageType.monthly" = "Monthly";
"PackageType.weekly" = "Weekly";

"%d%% off" = "%d%% off";
2 changes: 2 additions & 0 deletions RevenueCatUI/Resources/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
"PackageType.twoMonth" = "2 meses";
"PackageType.monthly" = "Mensual";
"PackageType.weekly" = "Semanal";

"%d%% off" = "Ahorra %d%%";
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class TemplateViewConfigurationCreationTests: BaseTemplateViewConfigurationTests
switch result {
case let .single(package):
expect(package.content) === TestData.monthlyPackage
expect(package.discountRelativeToMostExpensivePerMonth).to(beNil())
Self.verifyLocalizationWasProcessed(package.localization, for: TestData.monthlyPackage)
case .multiple:
fail("Invalid result: \(result)")
Expand All @@ -80,10 +81,13 @@ class TemplateViewConfigurationCreationTests: BaseTemplateViewConfigurationTests

let annual = packages[0]
expect(annual.content) === TestData.annualPackage
expect(annual.discountRelativeToMostExpensivePerMonth)
.to(beCloseTo(0.55, within: 0.01))
Self.verifyLocalizationWasProcessed(annual.localization, for: TestData.annualPackage)

let monthly = packages[1]
expect(monthly.content) === TestData.monthlyPackage
expect(monthly.discountRelativeToMostExpensivePerMonth).to(beNil())
Self.verifyLocalizationWasProcessed(monthly.localization, for: TestData.monthlyPackage)
}
}
Expand Down
56 changes: 56 additions & 0 deletions Tests/RevenueCatUITests/LocalizationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,51 @@ class PackageTypeOtherLanguageLocalizationTests: BaseLocalizationTests {
}
}

// MARK: - Discount

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
class DiscountEnglishLocalizationTests: BaseLocalizationTests {

override var locale: Locale { return .init(identifier: "en_US") }

func testLocalization() {
verify(0, "0% off")
verify(0.1, "10% off")
verify(0.331, "33% off")
verify(0.5, "50% off")
verify(1, "100% off")
verify(1.1, "110% off")
}

}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
class DiscountSpanishLocalizationTests: BaseLocalizationTests {

override var locale: Locale { return .init(identifier: "es_ES") }

func testLocalization() {
verify(0, "Ahorra 0%")
verify(0.1, "Ahorra 10%")
verify(0.331, "Ahorra 33%")
verify(0.5, "Ahorra 50%")
verify(1, "Ahorra 100%")
verify(1.1, "Ahorra 110%")
}

}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
class DiscountOtherLanguageLocalizationTests: BaseLocalizationTests {

override var locale: Locale { return .init(identifier: "fr") }

func testLocalizationDefaultsToEnglish() {
verify(1, "100% off")
}

}

// MARK: - Private

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
Expand Down Expand Up @@ -219,4 +264,15 @@ private extension BaseLocalizationTests {
expect(file: file, line: line, result) == expected
}

func verify(
_ discount: Double,
_ expected: String,
file: StaticString = #file,
line: UInt = #line
) {
let result = Localization.localized(discount: discount,
locale: self.locale)
expect(file: file, line: line, result) == expected
}

}

0 comments on commit 36dff3b

Please sign in to comment.