Skip to content

Conversation

@rickvdl
Copy link
Contributor

@rickvdl rickvdl commented Oct 15, 2025

We're having an issue related to currency formatting behavior in Romania (and possibly a few other currencies), internal discussion can be found here.

But some context: when using the Romanian Storefront, and a Romanian locale (ro_RO) prices are formatted as 1,23 RON, which is correct, but the more conventional way to write this in Romanian (according to reports) is 1,23 lei.

underlyingSK2Product.displayPrice uses the more common lei rather than RON, however the priceFormatter we create in SK2StoreProduct.swift through priceFormatterProvider.priceFormatterForSK2 based on the currency code and locale given to us by StoreKit creates strings using RON instead. This is causing discrepancies between localizedPriceString and the price strings we generate using the formatter (for example pricePerMonth) which looks confusing on the paywall especially.

After some research and discussions it looks like we can't get the NumberFormatter API to mimic the behavior of StoreKit, and thus we've been looking into an alternative approach.

We came up with a backend driven approach where we (given a ruleset) provide the option to override the currencySymbol for specific currency codes, given a storekit storefront (similar to what we already do for paywall localizations).

This PR is just a PoC outlining the rough workings of this approach. Happy to hear any feedback

@RevenueCat-Danger-Bot
Copy link

1 Error
🚫 Label the PR using one of the change type labels. If you are not sure which label to use, choose pr:other.
Label Description
pr:feat A new feature. Use along with pr:breaking to force a major release.
pr:fix A bug fix. Use along with pr:force_minor to force a minor release.
pr:other Other changes. Catch-all for anything that doesn't fit the above categories. Releases that only contain this label will not be released. Use along with pr:force_patch, or pr:force_minor to force a patch or minor release.
pr:RevenueCatUI Use along any other tag to mark a PR that only contains RevenueCatUI changes
pr:next_release Preparing a new release
pr:dependencies Updating a dependency
pr:phc_dependencies Updating purchases-hybrid-common dependency
pr:changelog_ignore The PR will not be included in the changelog. This label doesn't determine the type of bump of the version and must be combined with pr:feat, pr:fix or pr:other.

Generated by 🚫 Danger

@emerge-tools
Copy link

emerge-tools bot commented Oct 15, 2025

4 builds increased size

Name Version Download Change Install Change Approval
RevenueCat
com.revenuecat.PaywallsTester
1.0 (1) 14.9 MB ⬆️ 33.8 kB (0.23%) 51.8 MB ⬆️ 135.2 kB (0.26%) N/A
RevenueCat
com.revenuecat.PaywallsTester.mac-catalyst-scaled-to-match-ipad
1.0 (1) 12.1 MB ⬆️ 29.3 kB (0.24%) 44.5 MB ⬆️ 126.7 kB (0.29%) N/A
RevenueCat
com.revenuecat.PaywallsTester.mac-catalyst-optimized-for-mac
1.0 (1) 12.1 MB ⬆️ 29.3 kB (0.24%) 44.5 MB ⬆️ 126.7 kB (0.29%) N/A
RevenueCat
com.revenuecat.PaywallsTester.mac-native
1.0 (1) 10.6 MB ⬆️ 24.5 kB (0.23%) 38.9 MB ⬆️ 126.9 kB (0.33%) N/A

RevenueCat 1.0 (1)
com.revenuecat.PaywallsTester

⚖️ Compare build
⏱️ Analyze build performance

Total install size change: ⬆️ 135.2 kB (0.26%)
Total download size change: ⬆️ 33.8 kB (0.23%)

Largest size changes

Item Install Size Change
DYLD.String Table ⬆️ 50.9 kB
📝 RevenueCat.CurrencySymbolOverridingPriceFormatter.CurrencySymbolO... ⬆️ 4.8 kB
Code Signature ⬆️ 3.7 kB
DYLD.Exports ⬆️ 3.4 kB
📝 RevenueCat.CurrencySymbolOverridingPriceFormatter.Objc Metadata ⬆️ 1.3 kB
View Treemap

Image of diff

RevenueCat 1.0 (1)
com.revenuecat.PaywallsTester.mac-catalyst-scaled-to-match-ipad

⚖️ Compare build
⏱️ Analyze build performance

Total install size change: ⬆️ 126.7 kB (0.29%)
Total download size change: ⬆️ 29.3 kB (0.24%)

Largest size changes

Item Install Size Change
DYLD.String Table ⬆️ 45.3 kB
📝 RevenueCat.CurrencySymbolOverridingPriceFormatter.CurrencySymbolO... ⬆️ 4.6 kB
DYLD.Exports ⬆️ 3.4 kB
Code Signature ⬆️ 2.8 kB
📝 RevenueCat.CurrencySymbolOverridingPriceFormatter.Objc Metadata ⬆️ 1.2 kB
View Treemap

Image of diff

RevenueCat 1.0 (1)
com.revenuecat.PaywallsTester.mac-catalyst-optimized-for-mac

⚖️ Compare build
⏱️ Analyze build performance

Total install size change: ⬆️ 126.7 kB (0.29%)
Total download size change: ⬆️ 29.3 kB (0.24%)

Largest size changes

Item Install Size Change
DYLD.String Table ⬆️ 45.3 kB
📝 RevenueCat.CurrencySymbolOverridingPriceFormatter.CurrencySymbolO... ⬆️ 4.6 kB
DYLD.Exports ⬆️ 3.4 kB
Code Signature ⬆️ 2.8 kB
📝 RevenueCat.CurrencySymbolOverridingPriceFormatter.Objc Metadata ⬆️ 1.2 kB
View Treemap

Image of diff

RevenueCat 1.0 (1)
com.revenuecat.PaywallsTester.mac-native

⚖️ Compare build
⏱️ Analyze build performance

Total install size change: ⬆️ 126.9 kB (0.33%)
Total download size change: ⬆️ 24.5 kB (0.23%)

Largest size changes

Item Install Size Change
DYLD.String Table ⬆️ 45.3 kB
📝 RevenueCat.CurrencySymbolOverridingPriceFormatter.CurrencySymbolO... ⬆️ 4.6 kB
DYLD.Exports ⬆️ 3.4 kB
Code Signature ⬆️ 3.1 kB
📝 RevenueCat.CurrencySymbolOverridingPriceFormatter.Objc Metadata ⬆️ 1.2 kB
View Treemap

Image of diff


🛸 Powered by Emerge Tools

Comment trigger: Size diff threshold of 100.00kB exceeded

@emerge-tools
Copy link

emerge-tools bot commented Oct 15, 2025

📸 Snapshot Test

7 modified, 868 unchanged

Name Added Removed Modified Renamed Unchanged Errored Approval
RevenueCat
com.revenuecat.PaywallsTester.mac-native
0 0 7 0 160 0 ⏳ Needs approval
RevenueCat
com.revenuecat.PaywallsTester
0 0 0 0 236 0 N/A
RevenueCat
com.revenuecat.PaywallsTester.mac-catalyst-scaled-to-match-ipad
0 0 0 0 236 0 N/A
RevenueCat
com.revenuecat.PaywallsTester.mac-catalyst-optimized-for-mac
0 0 0 0 236 0 N/A

🛸 Powered by Emerge Tools

/// A `NumberFormatter` provider class for prices.
/// This provider caches the formatter to improve the performance.
final class PriceFormatterProvider: Sendable {
public final class PriceFormatterProvider: Sendable {
Copy link
Contributor

Choose a reason for hiding this comment

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

This probably doesn't need to be public public if it's just RevenueCatUI using this, I'd probably add @_spi(Internal) so it's only used by RevenueCatUI

Suggested change
public final class PriceFormatterProvider: Sendable {
@_spi(Internal) public final class PriceFormatterProvider: Sendable {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately I don't think this will work, the SK2 product initializer in StoreProduct is public and has this type in the function signature. Will make sure to bring this up in the actual PR

return formatter
}

if let formatter = formatter, formatter.currencyCode == currencyCode, formatter.locale == locale {
Copy link
Contributor

Choose a reason for hiding this comment

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

In the logic here:

if let formatter = formatter as? CurrencySymbolOverridingPriceFormatter, formatter.currencyCode == currencyCode, formatter.locale == locale, formatter.currencySymbolOverride == currencySymbolOverride {
                return formatter
            }
            
            if let formatter = formatter, formatter.currencyCode == currencyCode, formatter.locale == locale {
                return formatter
            }

The first if checks if the formatter is a CurrencySymbolOverridingPriceFormatter and that all of the parameters match. If it fails, the second if still returns the formatter as long as the formatter's currency code & locale match.

Could this lead to a bug where if the formatter is a CurrencySymbolOverridingPriceFormatter, the overrides don't match, but the currency code & locale do, we return a CurrencySymbolOverridingPriceFormatter that doesn't match the current context?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, I think you're right. Addressed this :)

}

private func formatter(for rule: PriceFormattingRuleSet.CurrencySymbolOverride.PluralRule) -> NumberFormatter {
if let formatter = numberFormatterCache[rule] {
Copy link
Contributor

Choose a reason for hiding this comment

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

Love this 👏👏

let placements: Placements?
let targeting: Targeting?
let uiConfig: UIConfig?
let config: Config?
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: we should probably name this something more specific, like priceFormattingConfig, in case we decide to add other configs in the future

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good one, I figured we might want to use this as a 'config envelope' to also hold future configuration values, in order to avoid having to update all initializers and mocks again. But I will make sure to bring this up in my PR and ask for the team's opinion on this as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants