Skip to content
Open
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
27 changes: 24 additions & 3 deletions RevenueCatUI/Templates/V2/Variables/VariableHandlerV2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -742,11 +742,15 @@ extension VariablesV2 {
return localizations[VariableLocalizationKey.freePrice.rawValue] ?? ""
}

// Format the discount's displayed price string rather than reformatting the
// decimal price through `NumberFormatter`. The formatter can emit a different
// currency token than the displayed price (e.g. "6,99 USD" vs "$6.99") when its
// locale metadata doesn't match the product's currency, which would make the
// offer price inconsistent with `product.price` and `product.currency_symbol`.
return formatDiscountPrice(
discount.price,
discount.localizedPriceString,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can we add a TODO for the per-period ones? otherwise it looks like offer_price and offer_price_per_* still match

package: package,
showZeroDecimalPlacePrices: offerContext.showZeroDecimalPlacePrices,
fallback: discount.localizedPriceString
showZeroDecimalPlacePrices: offerContext.showZeroDecimalPlacePrices
)
}

Expand Down Expand Up @@ -1090,6 +1094,23 @@ private extension VariablesV2 {
) ?? fallback
}

/// Formats an already-displayed discount price string, preserving its currency token.
/// Mirrors the base price path (`Package.localizedPrice`) so the currency token stays
/// consistent across the paywall instead of being re-derived from `NumberFormatter`.
func formatDiscountPrice(
_ priceString: String,
package: Package,
showZeroDecimalPlacePrices: Bool
) -> String {
guard let formatter = package.storeProduct.priceFormatter else {
return priceString
}
return formatter.formattedPriceStrippingTrailingZerosIfNeeded(
from: priceString,
showZeroDecimalPlacePrices: showZeroDecimalPlacePrices
)
}

}

private extension String {
Expand Down
44 changes: 44 additions & 0 deletions Tests/RevenueCatUITests/PaywallsV2/VariableHandlerV2Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,50 @@ class VariableHandlerV2Test: TestCase {
expect(result).to(equal("$1.99"))
}

func testOfferPriceUsesDisplayedPriceCurrencyTokenWhenFormatterLocaleDoesNotMatch() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this runs with showZeroDecimalPlacePrices: false, so it just returns the string untouched and the formatter never runs. doesn't the mismatch come back with stripping on + a whole number? maybe worth a test there 🤔

// The displayed price uses "$", but the formatter locale (ro_RO) would render
// the currency differently. The offer price should match the displayed price
// token so it stays consistent with product.price and product.currency_symbol
// across the paywall (same divergence fixed for currency_symbol in #6572).
let package = Package(
identifier: PackageType.monthly.identifier,
packageType: .monthly,
storeProduct: TestStoreProduct(
localizedTitle: "Monthly",
price: 6.99,
currencyCode: "USD",
localizedPriceString: "$6.99",
productIdentifier: "com.revenuecat.product.offer_price_regression",
productType: .autoRenewableSubscription,
localizedDescription: "PRO monthly",
subscriptionGroupIdentifier: "group",
subscriptionPeriod: .init(value: 1, unit: .month),
introductoryDiscount: .init(
identifier: "intro",
price: 6.99,
localizedPriceString: "$6.99",
paymentMode: .payUpFront,
subscriptionPeriod: .init(value: 1, unit: .week),
numberOfPeriods: 1,
type: .introductory
),
locale: Locale(identifier: "ro_RO")
).toStoreProduct(),
offeringIdentifier: "offering",
webCheckoutUrl: nil
)

let result = variableHandler.processVariables(
in: "{{ product.offer_price }}",
with: package,
locale: locale,
localizations: localizations["en_US"]!,
isEligibleForIntroOffer: true
)

expect(result).to(equal("$6.99"))
}

func testProductPayUpFrontOfferPricePerDay() {
let result = variableHandler.processVariables(
in: "{{ product.offer_price_per_day }}",
Expand Down