-
Notifications
You must be signed in to change notification settings - Fork 404
Implement Price formatting ruleset for overriding currency symbol's per storefront and currency #5676
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
base: main
Are you sure you want to change the base?
Implement Price formatting ruleset for overriding currency symbol's per storefront and currency #5676
Conversation
| let uiConfig: UIConfig? | ||
| let config: Config? | ||
|
|
||
| public struct Config { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a Config 'envelope' here in order to future-proof ourselves for more configuration values. But we can also just call it priceFormattingRuleSets and only have this single purpose field here, happy to go either way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine either way too, but I can see us having this Config wrapper with a single key forever. I also wouldn't mind putting it in UIConfig.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. I think we may as well put it in UIConfig, as one could argue that this affects UI (how prices are presented in the UI).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good points, will move it to UIConfig
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved them to UIConfig, however on tvOS it's an empty structure. I think this should still be supported on tvOS so added it to that struct as well. LMK if you (dis)agree
1 build increased size
RevenueCat 1.0 (1)
|
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 53.7 kB |
| 📝 RevenueCat.CurrencySymbolOverridingPriceFormatter.CurrencySymbolO... | ⬆️ 5.7 kB |
| DYLD.Exports | ⬆️ 3.5 kB |
| Code Signature | ⬆️ 3.4 kB |
| 📝 RevenueCat.CurrencySymbolOverridingPriceFormatter.Objc Metadata | ⬆️ 1.3 kB |
🛸 Powered by Emerge Tools
Comment trigger: Size diff threshold of 100.00kB exceeded
| return Set(storeKitProducts.map { SK2StoreProduct(sk2Product: $0) }) | ||
| return Set(storeKitProducts.map { SK2StoreProduct( | ||
| sk2Product: $0, | ||
| priceFormatterProvider: .init(priceFormattingRuleSet: priceFormattingRuleSetProvider.priceFormattingRuleSet()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will hand the SKProduct the value type (PriceFormattingRuleSet) for the current storefront. However when the storefront changes the storekit cache is cleared and the storekit products are refetched, so I think this is fine
Sources/Purchasing/StoreKitAbstractions/SK1StoreProductDiscount.swift
Outdated
Show resolved
Hide resolved
| expect(priceFormatter.currencySymbol) == "RON" | ||
| XCTAssert(type(of: priceFormatter) == CurrencySymbolOverridingPriceFormatter.self) | ||
|
|
||
| XCTAssertEqual(priceFormatter.string(from: NSNumber(integerLiteral: 0)), "0,00 lei") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
real world example
|
@RCGitBot please test |
📸 Snapshot Test240 unchanged
🛸 Powered by Emerge Tools |
JayShortway
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some initial comments. Great job investigating this and adding support for this! 💪
| /// Cardinal plural selection per CLDR/ICU baseline: | ||
| /// - Non-integers → .other | ||
| /// - Integers: 0 → .zero, 1 → .one, 2 → .two, else → .other | ||
| /// This function is intentionally locale-agnostic; apply your locale-specific rules upstream. | ||
| /// Spec reference: Unicode TR35 (Plural Rules). | ||
| private func rule(for value: NSNumber) -> PriceFormattingRuleSet.CurrencySymbolOverride.PluralRule { | ||
| let n = value.doubleValue | ||
|
|
||
| // Guard weird numerics | ||
| if n.isNaN || n.isInfinite { return .other } | ||
|
|
||
| guard let intValue = Int64(exactly: n) else { | ||
| return .other | ||
| } | ||
|
|
||
| // Check if value has any fractional part | ||
| let isInteger = n == Double(intValue) | ||
|
|
||
| // Per CLDR/ICU, decimals are "other" unless a locale defines explicit fraction rules. | ||
| guard isInteger else { return .other } | ||
|
|
||
| // Integer-only mapping; locale-specific categories like "few"/"many" are handled elsewhere. | ||
| switch intValue { | ||
| case 0: return .zero | ||
| case 1: return .one | ||
| case 2: return .two | ||
| default: return .other | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should follow the same rules as we use for VariableLocalizationKeys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be matching now :)
| let uiConfig: UIConfig? | ||
| let config: Config? | ||
|
|
||
| public struct Config { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine either way too, but I can see us having this Config wrapper with a single key forever. I also wouldn't mind putting it in UIConfig.
ajpallares
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank for tackling this!
On a quick initial review, I'm seeing some APIs made public, but, if I'm not wrong, I don't think any of these APIs should be public, as it's server-driven. Please correct me if I'm wrong 🙏
| /// A `NumberFormatter` provider class for prices. | ||
| /// This provider caches the formatter to improve the performance. | ||
| final class PriceFormatterProvider: Sendable { | ||
| public final class PriceFormatterProvider: Sendable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder, why should this API be public? Its init is not public.
It seems to me like it's only public to allow StoreProduct's new init be public
convenience init(
sk2Product: SK2Product,
priceFormatterProvider: PriceFormatterProvider
)But I think that one shouldn't be public either?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ajpallares indeed that's the reason, but this was already public. There also seems to be a public API tester for so I guess it is used by developers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand. It seems like that init was made public in #1341. But I don't see why the new init that receives a priceFormatterProvider needs to be public. Initially, I think we should not make it public unless we find a good reason. I would keep it internal so that PriceFormatterProvider can also be internal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah you're right, my bad! I've made it internal again :) Thanks for clarifying
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I would make this PriceFormatterProvider internal (probably we need it to be @_spi(Internal) public). I see that it's needed in the UIConfig's init that is already public (but that init should actually be @_spi(Internal) public instead).
I would add the @_spi(Internal) marker to that init in UIConfig to allow making this @_spi as well.
There's precedent: in #5208 we added the @_spi(Internal) marker to some existing public subtypes of UIConfig, with no complain from anyone as these APIs are not for public use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(unresolving to make sure this is addressed)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point and agree, will make that change :)
| let uiConfig: UIConfig? | ||
| let config: Config? | ||
|
|
||
| public struct Config { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. I think we may as well put it in UIConfig, as one could argue that this affects UI (how prices are presented in the UI).
b020ff5 to
b4bdbf3
Compare
| /// A `NumberFormatter` provider class for prices. | ||
| /// This provider caches the formatter to improve the performance. | ||
| final class PriceFormatterProvider: Sendable { | ||
| public final class PriceFormatterProvider: Sendable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I would make this PriceFormatterProvider internal (probably we need it to be @_spi(Internal) public). I see that it's needed in the UIConfig's init that is already public (but that init should actually be @_spi(Internal) public instead).
I would add the @_spi(Internal) marker to that init in UIConfig to allow making this @_spi as well.
There's precedent: in #5208 we added the @_spi(Internal) marker to some existing public subtypes of UIConfig, with no complain from anyone as these APIs are not for public use.
| @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) | ||
| public convenience init(sk2Product: SK2Product) { | ||
| self.init(SK2StoreProduct(sk2Product: sk2Product)) | ||
| public convenience init( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to be sure: this one should be internal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be done :)
|
@RCGitBot please test |
1 similar comment
|
@RCGitBot please test |
c52ff21 to
c112d18
Compare
|
@RCGitBot please test |
5d82758 to
b316ec3
Compare
|
@RCGitBot please test |
1 similar comment
|
@RCGitBot please test |
805ab88 to
21dfe3c
Compare
|
@RCGitBot please test |
2 similar comments
|
@RCGitBot please test |
|
@RCGitBot please test |
4eab3f3 to
6fddc7c
Compare
bbdfc29 to
3308827
Compare
…urrent storefront
… to improve testability
… product price formatting using the priceFormattingRuleSets
…o VariableHandlerV2's one
…ence type and update it's rule set whenever the offerings are received
…se because it also needs to work without paywalls
3308827 to
a2b6935
Compare
|
@RCGitBot please test |

Based on this PoC PR.
Checklist
purchases-androidand hybridsMotivation
Description