Multi-Currency Display UX
Global checkout fails when currency display lies. Users must know what card will be charged before 3DS — surprises at statement time generate chargebacks labeled "unrecognized charge."
Shopper currency vs settlement currency
Display prices in shopper-selected currency when FX available; label settlement currency at pay button: "You will be charged $45.12 USD." DCC at terminal is separate from browse-time estimate — don't conflate.
Currency selector UX
Geo-IP default with explicit override. Show USD code beside symbol — $ is ambiguous. Persist selection in session across navigation.
Intl.NumberFormat everywhere
const fmt = new Intl.NumberFormat(locale, { style: 'currency', currency });
fmt.format(minorUnits / 10 ** exp);
Concatenating $ + amount breaks de-DE formatting (comma decimals).
FX rounding and minor units
JPY: zero decimals. BHD: three. Round display from authoritative minor units in charge currency — display is derived, never source of truth.
Dual display pattern
€42,00 (estimate)
Charged: $45.12 USD
Refresh estimate daily or hide when rate stale >24h — stale FX worse than no estimate.
Regulatory markup disclosure
Some markets require conversion fee disclosure — "Includes 2.5% conversion fee" near total. Legal review per acquirer matrix.
Crypto separation
If accepting stablecoins, never format USDC with fiat formatter — separate section "Pay with USDC" with blockchain amount.
Measuring conversion impact
Baseline checkout completion and step latency in RUM before UX changes. Ship payment UX behind flags; roll out on low-traffic weekday after issuer test card validation passes in staging.
Hedging and enterprise contracts
B2B contracts may fix FX rate quarterly — display differs from spot rate on consumer site. Enterprise portal shows contract rate; consumer uses daily spot.
PayPal and local methods
Show PayPal in local currency even when card checkout in USD — mixed currency UI per payment method is OK if labeled.
Rounding on discounts
Percentage discounts apply before FX conversion or after — document order in FAQ. Users audit math on cross-border carts meticulously.
Tax-inclusive display regions
EU requires VAT-inclusive display to consumers — US excludes tax until checkout. Geo gate display logic, not one global template.
Stripe adaptive pricing
If using adaptive pricing, label "Price adjusted for your region" — transparency reduces trust issues when VPN users see unexpected currency.
Historical order currency
Order history shows currency at purchase time — never retroactively convert to today's user preference.
Small currency units
ISK and HUF have no minor units in display — formatter minimumFractionDigits: 0.
FX fee transparency in B2B
SME buyers compare invoice to XE.com rate — explain spread in tooltip if you markup FX.
BNPL currency display
Klarna/Afterpay show local currency installments — card checkout USD while BNPL shows EUR installments requires clear section headers per method.
Refund currency
Refund in original charge currency even if browse currency changed — receipt states "Refund $45.12 USD to card ending 4242".
Pen testing currency manipulation
Tampering currency query param must not change charge currency without server validation — display currency is cosmetic until server confirms.
Accounting export
Finance CSV uses settlement currency only — UX display currency not exported to ERP without conversion metadata columns.
Travel and dynamic FX
Airline holds fare in booking currency — show "Price guaranteed in EUR until timer expires" with countdown.
Minimum amount formatting
Stripe minimum charge amounts differ by currency — validate before show misleading $0.50 equivalent in zero-decimal currency.
Right-to-left currency
Arabic locale RTL layout — currency symbol position follows CLDR, not string concat.
A/B test currency presentation
Test showing approximate vs exact FX disclaimer — legal approves exact wording before experiment.
Checkout copy patterns that reduce disputes
Pair every approximate FX display with the sentence users remember at statement time: "Your card will be charged in [settlement currency]." Hiding settlement currency until the processor redirect is how "unrecognized charge" disputes start — the shopper approved an estimated € price but sees a slightly different USD amount after bank FX.
For mixed carts (subscription in USD + one-time in EUR), split line items by settlement currency in the review step. A single grand total in browse currency when two charges settle differently creates support tickets that take three emails to unravel.
QA matrix before launch
Test display formatting across: zero-decimal (JPY), three-decimal (BHD), EUR comma decimals, and RTL Arabic. Snapshot tests on Intl.NumberFormat output per locale — string concat regressions slip in via copy-paste from English-only Figma.
Finance reconciliation note
Treasury expects settlement reports in acquirer currency. UX browse currency is marketing state; ERP import uses settlement only. Document the handoff in your payments runbook so product does not promise finance a unified currency column that never existed server-side.
Edge cases in promotions and refunds
Percentage-off coupons apply to presentment currency subtotal or settlement base depending on tax law — show which in discount line microcopy. Refunds must quote original settlement amount; displaying refunded browse currency after FX moved confuses users comparing to card statement. For partial refunds on multi-currency carts, itemize which currency bucket was credited.
API contract between pricing and UI
Expose presentment_currency, settlement_currency, fx_rate, fx_timestamp, and is_estimate on checkout session objects. Frontend must not infer FX from cached marketing prices. Contract tests fail CI when API removes is_estimate flag — that flag prevented a production incident where VPN users saw wrong yen prices.
Launch checklist
Before entering a new country: legal approves price display rules, finance confirms settlement currency, support receives issuer decline cheat sheet for that currency, and QA runs device matrix on formatting locales. Currency UX bugs become regulatory bugs faster than almost any other checkout surface.
Keep a living spreadsheet of acquirer settlement currencies versus browse currencies per country — product managers update at launch, finance audits quarterly.
Rounding disputes on carts with many line items
Rounding per line versus per invoice total diverges on large B2B carts — pick one rule, document on checkout, and mirror in ERP export. Users manually summing line items will email support if off by one cent; enterprise AP rejects entire invoice if totals disagree with PO.
Travel and multi-leg pricing
Show each leg currency when legs settle differently (domestic + international segments). Bundled "from $299" marketing must clarify "charged in airline settlement currency" before card capture — advertising regulators and card schemes both care about bait-and-switch perception.
Crypto display guardrails
If experimenting with stablecoin display, never use fiat formatter — separate tab with blockchain amount and network fee line. Mixed crypto/fiat checkout without section headers caused 40% abandonment in one pilot — users feared wrong asset debit.
Historical orders currency lock
Order detail always renders in charge_currency from payment record — never re-convert with today's FX for past orders. Finance disputes and user memory both reference charged amount, not repriced nostalgia.
Stale rate indicators
When FX estimate older than 24h, show "Rate updated at checkout" and refresh on payment step mount — stale crypto-style volatility fears apply to emerging-market FX too.
Product analytics should track currency override rate — users changing geo default currency signal pricing confusion or VPN-heavy traffic worth segmenting.
Run quarterly audit comparing displayed subtotal to server settlement amount on random sample of 100 orders — catches formatter bugs before finance month-close.
Document who owns FX rate source API outages — UX should degrade to single-currency mode with banner, not wrong prices.
Resources
- Stripe — 3DS2 guide
- Adyen — Authentication documentation
- EMVCo 3-D Secure specification
- WCAG 2.2 — form accessibility
- web.dev — payment request API
Frequently asked questions
Should prices show shopper currency or merchant currency?
Shopper currency for display when you have FX rates — always label which currency will be charged. DCC at checkout is separate; don't surprise users at 3DS step.
How do you handle FX rounding?
Round display to minor units per ISO 4217 — JPY zero decimals, BHD three. Store authoritative amount in charge currency; display amounts are derived, never source of truth.
What about crypto-style precision?
Fiat UX uses 2 decimal places max unless zero-decimal currency. Trailing zeros optional by locale (€10 vs €10,00).
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →