Skip to content

Fix SK2 computed period prices showing incorrectly rounded values in production#460

Open
yusuftor wants to merge 4 commits intomasterfrom
fix/sk2-price-format-rounding
Open

Fix SK2 computed period prices showing incorrectly rounded values in production#460
yusuftor wants to merge 4 commits intomasterfrom
fix/sk2-price-format-rounding

Conversation

@yusuftor
Copy link
Copy Markdown
Collaborator

@yusuftor yusuftor commented Apr 9, 2026

Summary

  • Bug: On StoreKit 2 in production, computed period prices (weeklyPrice, dailyPrice, monthlyPrice, yearlyPrice) display incorrectly rounded values. For example, a £4.99/week product shows as £5.00/week on the UK App Store.
  • Root cause: Product.priceFormatStyle carries storefront-specific rounding rules in production that don't exist in StoreKit config file testing. When we feed a computed Decimal back through priceFormatStyle, it re-applies rounding that corrupts the value.
  • Fix: Switch computed price formatting from priceFormatStyle to NumberFormatter via the existing PriceFormatterProvider.priceFormatterForSK2(), matching the SK1 code path. localizedPrice still uses priceFormatStyle since it formats the product's own original price (not a computed value).

Evidence from production (app 31689, UK App Store, GBP)

Variable Expected Actual (before fix)
$product_price £4.99 £4.99 ✓ (direct from StoreKit)
$product_weekly_price £4.99 £5.00 ✗ (via priceFormatStyle)
$product_monthly_price ~£21.62 £21.68
$product_yearly_price ~£259.48 £260.19

This bug is not reproducible with StoreKit config files in Xcode because local test products create a simpler priceFormatStyle without storefront rounding rules.

Test plan

  • Verify existing SubscriptionPeriodPriceTests still pass (math layer unchanged)
  • Build and run demo project on iOS
  • Test with a StoreKit config file to confirm no regression
  • Deploy to TestFlight and verify on a live UK App Store product that £4.99 weekly displays correctly

  • All unit tests pass.
  • All UI tests pass.
  • Demo project builds and runs on iOS.
  • Demo project builds and runs on Mac Catalyst.
  • Demo project builds and runs on visionOS.
  • I added/updated tests or detailed why my change isn't tested.
  • I added an entry to the CHANGELOG.md for any breaking changes, enhancements, or bug fixes.
  • I have run swiftlint in the main directory and fixed any issues.
  • I have updated the SDK documentation as well as the online docs.
  • I have reviewed the contributing guide

🤖 Generated with Claude Code

Greptile Summary

This PR fixes incorrectly rounded SK2 computed period prices (weeklyPrice, dailyPrice, monthlyPrice, yearlyPrice) in production by replacing Product.priceFormatStyle — which carries storefront-specific rounding rules — with a NumberFormatter via the existing PriceFormatterProvider.priceFormatterForSK2(), matching the SK1 code path. All four computed price properties and the trial price helpers are updated; localizedPrice intentionally retains priceFormatStyle since it formats the product's raw price directly.

Confidence Score: 5/5

Safe to merge — the core fix is correct and well-targeted; remaining findings are P2 style issues only.

The fix correctly addresses the root cause (storefront-specific rounding from priceFormatStyle) by routing computed prices through NumberFormatter, matching the SK1 precedent. The two P2 comments cover a hardcoded "$0.00" fallback (practically unreachable) and a minor design inconsistency with localizedTrialPeriodPrice. Neither affects correctness in normal operation.

No files require special attention.

Vulnerabilities

No security concerns identified. The change only affects how computed prices are formatted for display; no new data sources, network calls, or sensitive inputs are introduced.

Important Files Changed

Filename Overview
Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift Replaces priceFormatStyle with NumberFormatter via PriceFormatterProvider for computed period prices to avoid storefront-specific rounding; two minor P2 issues: hardcoded "$0.00" fallback and inconsistency between localizedTrialPeriodPrice and localizedPrice formatting paths.
CHANGELOG.md Adds v4.14.3 entry describing the SK2 computed period price rounding fix; entry is clear and customer-focused.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[SK2StoreProduct computed price properties] --> B[Compute Decimal via division + roundedPrice]
    B --> C{Before fix}
    B --> D{After fix}
    C --> E["Decimal.formatted(priceFormatStyle)\n⚠ applies storefront rounding\ne.g. £4.99 → £5.00"]
    D --> F["priceFormatterProvider.priceFormatterForSK2()\n✅ standard NumberFormatter\ne.g. £4.99 → £4.99"]
    G[SK2StoreProduct.localizedPrice] --> H["Decimal.formatted(priceFormatStyle)\n✅ unchanged – raw SK2 price, no computed division"]
    style E fill:#ffcccc
    style F fill:#ccffcc
    style H fill:#ccffcc
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift
Line: 611-627

Comment:
**Hardcoded `"$0.00"` fallback leaks USD symbol**

`NumberFormatter.string(from:)` can theoretically return `nil`, and both `trialPeriodPricePerUnit` and `localizedTrialPeriodPrice` now fall back to the literal `"$0.00"`. For GBP or any non-USD locale this would display the wrong currency symbol. Since the `priceFormatter` is already configured with the correct `currencyCode` and `locale`, a safer fallback would match the `"n/a"` sentinel used elsewhere for error cases.

```suggestion
  func trialPeriodPricePerUnit(_ unit: SubscriptionPeriod.Unit) -> String {
    guard let introductoryDiscount = introductoryDiscount else {
      return priceFormatter.string(from: 0.00) ?? "n/a"
    }
    if introductoryDiscount.price == 0.00 {
      return priceFormatter.string(from: 0.00) ?? "n/a"
    }

    let introMonthlyPrice = introductoryDiscount.pricePerUnit(unit)

    return priceFormatter.string(from: NSDecimalNumber(decimal: introMonthlyPrice)) ?? "n/a"
  }

  var localizedTrialPeriodPrice: String {
    guard let price = underlyingSK2Product.subscription?.introductoryOffer?.price else {
      return priceFormatter.string(from: 0.00) ?? "n/a"
    }
    return priceFormatter.string(from: NSDecimalNumber(decimal: price)) ?? "n/a"
  }
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift
Line: 622-627

Comment:
**`localizedTrialPeriodPrice` inconsistency with PR motivation**

The PR description states "`localizedPrice` still uses `priceFormatStyle` since it formats the product's own original price (not a computed value)." `localizedTrialPeriodPrice` is in the same category — it reads `introductoryOffer?.price` directly from StoreKit, not a computed division result. Switching it to `priceFormatter` is harmless but creates an inconsistency with `localizedPrice`. Consider leaving it on `priceFormatStyle` to match the stated design rule, or document why introductory prices also need the `NumberFormatter` path.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Fix SK2 computed prices showing incorrec..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Product.priceFormatStyle carries storefront-specific rounding rules in
production that differ from StoreKit config file testing. This caused
computed period prices (weeklyPrice, dailyPrice, monthlyPrice,
yearlyPrice) to display incorrectly — e.g. a £4.99/week product showing
as £5.00/week on the UK App Store.

Switch from priceFormatStyle to NumberFormatter (via the existing
PriceFormatterProvider) for formatting computed prices, matching how the
SK1 path works. localizedPrice still uses priceFormatStyle since it
formats the product's own original price.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
claude and others added 3 commits April 9, 2026 11:32
This property reads the intro offer price directly from StoreKit (not a
computed value), so it should use priceFormatStyle like localizedPrice.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Unit

Avoids leaking a USD currency symbol in non-USD locales when
NumberFormatter.string(from:) returns nil.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

2 participants