Skip to content

Fix mixed currencies in paywall price variables (PW-133)#3119

Merged
facumenzella merged 12 commits into
mainfrom
facundo/pw-133-android-paywall-price-variables-show-mixed-currencies
Mar 31, 2026
Merged

Fix mixed currencies in paywall price variables (PW-133)#3119
facumenzella merged 12 commits into
mainfrom
facundo/pw-133-android-paywall-price-variables-show-mixed-currencies

Conversation

@facumenzella

@facumenzella facumenzella commented Feb 19, 2026

Copy link
Copy Markdown
Member

Summary

Fixes https://linear.app/revenuecat/issue/PW-133/android-paywall-price-variables-show-mixed-currencies

On Android, different paywall price variables showed mixed currencies/locales for the same user. The root cause was that product.offer_price and product.secondary_offer_price used the pre-formatted Price.formatted string — which was created with whatever locale the store SDK happened to use — instead of formatting with the correct currencyLocale.

Fix

Use NumberFormat.getCurrencyInstance(locale) with the correct locale, and let it do its thing.

  • Add Price.getFormatted(locale) that re-formats prices from raw amountMicros using the provided locale and BigDecimal arithmetic (to avoid floating-point precision issues)
  • Thread currencyLocale through VariableProcessorV2 and PricingPhaseExtensions so all offer price variables (product.offer_price, product.secondary_offer_price, per-period variants) use the same locale as product.currency_symbol
  • Update all price.formatted call sites in VariableDataProvider to use price.getFormatted(locale)

Root Cause

product.currency_symbol was correctly using currencyLocale:

Variable.PRODUCT_CURRENCY_SYMBOL -> Currency.getInstance(currencyCode).getSymbol(currencyLocale)

But product.offer_price was using the pre-formatted price:

// Before (buggy)
price.formatted  // Uses whatever locale the store SDK used when Price was created

// After (fixed)
price.getFormatted(locale)  // Always uses currencyLocale

The locale controls symbol position, decimal separator, thousands separator, and decimal places — same as how Google formats prices in the Play Store.

Test plan

  • Added PriceLocaleTests to verify Price.getFormatted() formats correctly with different locales
  • Added OfferPriceLocaleTests to verify offer price variables use currencyLocale
  • Updated existing test expectations to reflect locale-aware formatting
  • Manual testing with different storefront/device locale combinations

🤖 Generated with Claude Code


Note

Medium Risk
Changes how paywall price/offer variables are formatted across locales and currencies, which is user-visible and could affect displayed amounts/symbols if formatting differs from previous Price.formatted strings. Core purchase logic is untouched, but broad test expectation updates indicate wide surface area.

Overview
Fixes mixed-locale currency output in paywall variables by reformatting prices from amountMicros using the provided currencyLocale rather than relying on Price.formatted.

Adds Price.getFormatted(locale) (BigDecimal-based, currency-fraction-digit aware) and threads locale through PricingPhase offer-price helpers and VariableProcessorV2 so product.offer_price, product.secondary_offer_price, and per-period offer variants use consistent locale formatting.

Updates multiple tests and golden strings to match locale-correct currency symbol placement/separators, and adds focused coverage in PriceLocaleTests and OfferPriceLocaleTests.

Written by Cursor Bugbot for commit ce48d16. This will update automatically on new commits. Configure here.

@facumenzella facumenzella added the pr:fix A bug fix label Feb 19, 2026
@facumenzella facumenzella force-pushed the facundo/pw-133-android-paywall-price-variables-show-mixed-currencies branch from a55d650 to 8ecc985 Compare March 20, 2026 12:49
@emerge-tools

emerge-tools Bot commented Mar 20, 2026

Copy link
Copy Markdown

📸 Snapshot Test

137 modified, 445 unchanged

Name Added Removed Modified Renamed Unchanged Errored Approval
TestPurchasesUIAndroidCompatibility
com.revenuecat.testpurchasesuiandroidcompatibility
0 0 4 0 321 0 ⏳ Needs approval
TestPurchasesUIAndroidCompatibility Paparazzi
com.revenuecat.testpurchasesuiandroidcompatibility.paparazzi
0 0 133 0 124 0 ✅ Approved

🛸 Powered by Emerge Tools

@facumenzella facumenzella marked this pull request as ready for review March 20, 2026 14:28
@facumenzella facumenzella requested a review from a team as a code owner March 20, 2026 14:28
@codecov

codecov Bot commented Mar 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.47%. Comparing base (a485da9) to head (ce48d16).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3119   +/-   ##
=======================================
  Coverage   79.47%   79.47%           
=======================================
  Files         357      357           
  Lines       14351    14351           
  Branches     1960     1960           
=======================================
  Hits        11406    11406           
  Misses       2141     2141           
  Partials      804      804           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tonidero tonidero left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just a question on why we added decimals to round numbers in some currencies... Feels we should leave that as is if possible.

facumenzella and others added 6 commits March 30, 2026 14:12
On Android, different paywall price variables showed different currencies
for the same user. This was caused by `product.offer_price` and
`product.secondary_offer_price` using the pre-formatted `Price.formatted`
string (which may have been created with a different locale) instead of
formatting with the `currencyLocale` parameter.

Changes:
- Add `Price.getFormatted(locale)` function that formats prices
  consistently using the provided locale
- Update `productOfferPrice()` in VariableProcessorV2 to accept a locale
  parameter and use `price.getFormatted(locale)` instead of `price.formatted`
- This ensures `product.offer_price` and `product.secondary_offer_price`
  use the same locale as `product.currency_symbol` and other price variables

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extend the PW-133 fix to all code paths that used price.formatted instead
of getFormatted(locale): VariableDataProvider per-period helpers, intro
offer prices, PriceExtensions.localized(), and offer price per-period in
VariableProcessorV2. Update test expectations accordingly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rice functions

- Remove duplicate Price.getFormatted(locale) overload that caused a compile error
- Drop explicit currencySymbol override that produced full-width ¥ for JPY
- Add locale param to resolveOfferPrice, productOfferPrice, productOfferPricePerPeriod
  in PricingPhaseExtensions so offer prices use currencyLocale consistently
- Pass currencyLocale at both resolveOfferPrice call sites in VariableProcessorV2
- Update JPY test expectations (¥ → ¥) and NL storefront USD expectation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
USD $1,000 now formats as $1,000.00 with getFormatted(locale) instead
of using price.formatted (which omits trailing zeros).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Import BigDecimal and RoundingMode instead of using fully-qualified names
- Update getFormatted to preserve decimal precision from Price.formatted using
  regex detection, so "$1,000" stays "$1,000" rather than becoming "$1,000.00"
- Strengthen OfferPriceLocaleTests assertions from startsWith/contains to isEqualTo
- Update test expectations in VariableProcessorTest and PackageInfoForTest to
  match the preserved store precision for lifetime packages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the brittle regex-based decimal detection with a simpler approach:
set minimumFractionDigits=0 so round numbers (e.g. $1.00) display without
trailing zeros ($1), while non-round prices keep their cents ($1.99).

Also removes the redundant amountMicros==0L early-return for intro offer
prices — getFormatted already handles zero amounts correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@facumenzella facumenzella force-pushed the facundo/pw-133-android-paywall-price-variables-show-mixed-currencies branch from 6f98a25 to faced9b Compare March 30, 2026 12:17
PaywallDialog.kt and PaywallViewModel.kt had debug Logger.d calls and
handleCloseRequest changes that are unrelated to the PW-133 price
locale fix. Revert both to main's current state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
$1.30 should stay $1.30, not become $1.3. Only whole-unit prices
(amountMicros % 1_000_000 == 0) get minimumFractionDigits=0;
everything else uses the currency's default fraction digits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the whole-unit trailing zero detection (amountMicros % 1_000_000 == 0)
from getFormatted(). The fix for PW-133 is using the correct locale — let
NumberFormat decide how many decimal places to show, same as Google does.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@facumenzella facumenzella force-pushed the facundo/pw-133-android-paywall-price-variables-show-mixed-currencies branch from 223b9c9 to 30ac6ac Compare March 31, 2026 07:46

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

@facumenzella facumenzella requested review from tonidero and vegaro and removed request for vegaro March 31, 2026 14:53

@tonidero tonidero left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good! Thanks for bearing with all the iterations 🙏

@facumenzella facumenzella enabled auto-merge March 31, 2026 15:25
@facumenzella facumenzella added this pull request to the merge queue Mar 31, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Mar 31, 2026
@facumenzella facumenzella added this pull request to the merge queue Mar 31, 2026
Merged via the queue into main with commit 49359e6 Mar 31, 2026
36 checks passed
@facumenzella facumenzella deleted the facundo/pw-133-android-paywall-price-variables-show-mixed-currencies branch March 31, 2026 17:22
github-merge-queue Bot pushed a commit that referenced this pull request Apr 2, 2026
**This is an automatic release.**

## RevenueCat SDK
### ✨ New Features
* Use Amazon deep link for Amazon subscription management (#3291) via
Cesar de la Vega (@vegaro)
* Introduce purchases codegen package (#3163) via Jaewoong Eum
(@skydoves)
### 🐞 Bugfixes
* Fix Test Store Purchase dialog not cancelling purchase on outside tap
(#3289) via Antonio Pallares (@ajpallares)

## RevenueCatUI SDK
### Paywallv2
#### 🐞 Bugfixes
* Fix mixed currencies in paywall price variables (PW-133) (#3119) via
Facundo Menzella (@facumenzella)

### 🔄 Other Changes
* Add docs for the codegen plugin (#3288) via Jaewoong Eum (@skydoves)
* Run integration tests against all backend environments (#3278) via
Toni Rico (@tonidero)
* Use merge queue for release PR merging (#3281) via Antonio Pallares
(@ajpallares)
* ci: warn when pre-built material-icons are imported in
:ui:revenuecatui (#3282) via Facundo Menzella (@facumenzella)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: this is a version/release bookkeeping update plus docs/CI
deployment path changes, with no functional runtime logic changes beyond
the reported version string.
> 
> **Overview**
> Cuts the `9.29.0` release by switching all references from
`9.29.0-SNAPSHOT` to `9.29.0` (Gradle `VERSION_NAME`, internal
`frameworkVersion`, and sample/test app dependency versions).
> 
> Updates documentation publishing to point to the `9.29.0` docs
directory (CircleCI S3 sync path and `docs/index.html` redirect), and
rolls forward `CHANGELOG.md`/`CHANGELOG.latest.md` with the 9.29.0
release notes.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
bf217fd. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants