Store all prices in USD cents in Firestore to avoid conversion inconsistencies. Use a Cloud Function to fetch daily exchange rates and write them to a Firestore 'currencies' collection. Build a formatPrice Custom Function using Dart's NumberFormat.currency to display the correct symbol and formatting per locale. Wire the user's selected currency to App State so every price widget updates automatically.
One Source of Truth for Every Price
Multi-currency support sounds simple — multiply a price by an exchange rate. But doing this wrong causes prices to drift as rates change, Stripe charges that do not match what the user saw, and rounding errors that add up at scale. The correct architecture stores every price in a single baseline currency (USD cents, an integer with no floating-point issues), fetches rates once per day from an exchange rate API, and converts only when rendering the price on screen. This guide wires that pattern into FlutterFlow from Firestore through to Stripe.
Prerequisites
- FlutterFlow Pro plan (Custom Functions required)
- Firebase project with Firestore enabled
- Exchange rate API key (exchangerate-api.com free tier works for development)
- Basic understanding of Firestore collections and App State
Step-by-step guide
Set Up the Currencies Collection in Firestore
Set Up the Currencies Collection in Firestore
Create a Firestore collection called 'currencies' where each document ID is a currency code (USD, EUR, GBP, JPY, etc.). Each document needs four fields: code (String), symbol (String), rate_to_usd (Double — how many USD equal one unit of this currency, inverted for display), and updated_at (Timestamp). Seed it with a few initial rates so your app has something to display while the Cloud Function runs for the first time. Also create a 'currency_config' document in a 'app_config' collection with a list of supported_currencies and a base_currency field set to 'USD'. In FlutterFlow, add a Firestore collection schema for 'currencies' in your project settings.
Expected result: Firestore shows a 'currencies' collection with documents like 'EUR' containing code: EUR, symbol: €, rate_to_usd: 0.92, updated_at: now.
Create a Cloud Function to Refresh Exchange Rates Daily
Create a Cloud Function to Refresh Exchange Rates Daily
In Firebase Console, create a scheduled Cloud Function (via Cloud Scheduler) that runs once per day. The function fetches the latest rates from your exchange rate API and updates each currency document in Firestore. Using a server-side function instead of a client-side API call keeps your API key secret and ensures all users see the same rates refreshed at the same time. The function should update only the rate_to_usd and updated_at fields to avoid overwriting any manual configuration on other fields.
1const functions = require('firebase-functions');2const admin = require('firebase-admin');3const axios = require('axios');45exports.refreshExchangeRates = functions.pubsub6 .schedule('0 6 * * *')7 .timeZone('UTC')8 .onRun(async () => {9 const apiKey = process.env.EXCHANGE_RATE_API_KEY;10 const base = 'USD';11 const url = `https://v6.exchangerate-api.com/v6/${apiKey}/latest/${base}`;1213 const response = await axios.get(url);14 const rates = response.data.conversion_rates;1516 const db = admin.firestore();17 const batch = db.batch();1819 const supportedCurrencies = ['USD', 'EUR', 'GBP', 'JPY', 'CAD', 'AUD', 'INR', 'BRL'];2021 for (const code of supportedCurrencies) {22 if (rates[code]) {23 const ref = db.collection('currencies').doc(code);24 batch.update(ref, {25 rate_to_usd: rates[code],26 updated_at: admin.firestore.FieldValue.serverTimestamp(),27 });28 }29 }3031 await batch.commit();32 return null;33 });Expected result: Cloud Function logs show successful rate refresh each morning and Firestore currency documents show today's date in updated_at.
Build the formatPrice Custom Function
Build the formatPrice Custom Function
In FlutterFlow, open Custom Code > Custom Functions and create a new function called formatPrice. It takes three parameters: priceInUsdCents (integer), currencyCode (String), and rateToUsd (double). The function converts from cents to the display currency and formats it using Dart's intl package NumberFormat.currency with the correct locale and symbol. Returning a formatted string like '€12.50' or '¥1,350' keeps all price rendering logic in one reusable place across every widget in your app.
1import 'package:intl/intl.dart';23String formatPrice(4 int priceInUsdCents,5 String currencyCode,6 double rateToUsd,7) {8 // Convert cents to dollars, then to target currency9 final double usdAmount = priceInUsdCents / 100.0;10 final double displayAmount = usdAmount * rateToUsd;1112 // Map currency codes to locales for correct formatting13 const Map<String, String> currencyLocales = {14 'USD': 'en_US',15 'EUR': 'de_DE',16 'GBP': 'en_GB',17 'JPY': 'ja_JP',18 'CAD': 'en_CA',19 'AUD': 'en_AU',20 'INR': 'hi_IN',21 'BRL': 'pt_BR',22 };2324 final String locale = currencyLocales[currencyCode] ?? 'en_US';2526 // JPY has no decimal places27 final int decimalDigits = currencyCode == 'JPY' ? 0 : 2;2829 final formatter = NumberFormat.currency(30 locale: locale,31 symbol: '',32 decimalDigits: decimalDigits,33 );3435 final symbols = {36 'USD': r'$', 'EUR': '€', 'GBP': '£',37 'JPY': '¥', 'CAD': r'CA$', 'AUD': r'A$',38 'INR': '₹', 'BRL': r'R$',39 };4041 final symbol = symbols[currencyCode] ?? currencyCode + ' ';42 return '$symbol${formatter.format(displayAmount)}';43}Expected result: Calling formatPrice(1999, 'EUR', 0.92) returns '€18.39' with correct European decimal formatting.
Wire Currency Selection to App State
Wire Currency Selection to App State
Add three App State variables: selectedCurrencyCode (String, default: USD), selectedCurrencySymbol (String, default: $), and selectedCurrencyRate (double, default: 1.0). On your profile or settings page, add a DropdownButton widget populated from your Firestore currencies collection. When the user picks a currency, update all three App State variables in the On Change action. On every price Text widget, replace the static value with the formatPrice Custom Function, passing the product price field, AppState.selectedCurrencyCode, and AppState.selectedCurrencyRate. The prices will update across the whole app instantly whenever the user changes currency.
Expected result: Switching from USD to EUR in the settings dropdown immediately updates all product prices across every page to show euro amounts with the correct symbol.
Pass the Selected Currency to Stripe Checkout
Pass the Selected Currency to Stripe Checkout
When creating a Stripe PaymentIntent or Checkout Session in your Cloud Function, pass the user's selected currency code and calculate the amount in the target currency. Stripe accepts amounts as the smallest currency unit (cents for USD/EUR, yen for JPY), so multiply the display amount accordingly. Important: always re-fetch the current rate from Firestore in the Cloud Function at checkout time rather than trusting the client-sent rate, which could be tampered with. This ensures the charge matches what the user saw within the tolerance of the day's exchange rate.
1// Cloud Function — create Stripe Checkout Session2exports.createCheckoutSession = functions.https.onCall(async (data, context) => {3 const { priceInUsdCents, currencyCode, successUrl, cancelUrl } = data;4 const userId = context.auth?.uid;5 if (!userId) throw new functions.https.HttpsError('unauthenticated', 'Login required');67 // Fetch current rate from Firestore (never trust client-sent rate)8 const currencyDoc = await admin.firestore()9 .collection('currencies').doc(currencyCode).get();10 const rateToUsd = currencyDoc.data()?.rate_to_usd ?? 1.0;1112 const usdAmount = priceInUsdCents / 100;13 const displayAmount = usdAmount * rateToUsd;1415 // Stripe uses smallest unit — JPY has no cents16 const stripeAmount = currencyCode === 'JPY'17 ? Math.round(displayAmount)18 : Math.round(displayAmount * 100);1920 const session = await stripe.checkout.sessions.create({21 payment_method_types: ['card'],22 line_items: [{23 price_data: {24 currency: currencyCode.toLowerCase(),25 unit_amount: stripeAmount,26 product_data: { name: data.productName },27 },28 quantity: 1,29 }],30 mode: 'payment',31 success_url: successUrl,32 cancel_url: cancelUrl,33 });3435 return { sessionId: session.id, url: session.url };36});Expected result: Stripe Checkout opens in the user's selected currency and shows the correctly converted amount that matches the in-app price they saw.
Complete working example
1// FlutterFlow Custom Function: formatPrice2// Parameters:3// priceInUsdCents — int (e.g. 1999 = $19.99 USD)4// currencyCode — String (e.g. 'EUR')5// rateToUsd — double (e.g. 0.92 means 1 USD = 0.92 EUR)6// Returns: String formatted price with symbol78import 'package:intl/intl.dart';910String formatPrice(11 int priceInUsdCents,12 String currencyCode,13 double rateToUsd,14) {15 if (priceInUsdCents <= 0) return 'Free';16 if (rateToUsd <= 0) rateToUsd = 1.0;1718 final double usdAmount = priceInUsdCents / 100.0;19 final double displayAmount = usdAmount * rateToUsd;2021 const Map<String, String> currencyLocales = {22 'USD': 'en_US',23 'EUR': 'de_DE',24 'GBP': 'en_GB',25 'JPY': 'ja_JP',26 'CAD': 'en_CA',27 'AUD': 'en_AU',28 'INR': 'hi_IN',29 'BRL': 'pt_BR',30 'CHF': 'de_CH',31 'SGD': 'en_SG',32 };3334 const Map<String, String> symbols = {35 'USD': r'$',36 'EUR': '€',37 'GBP': '£',38 'JPY': '¥',39 'CAD': r'CA$',40 'AUD': r'A$',41 'INR': '₹',42 'BRL': r'R$',43 'CHF': 'CHF ',44 'SGD': r'S$',45 };4647 // Currencies with no decimal places48 const Set<String> zeroCentCurrencies = {'JPY', 'KRW', 'VND', 'IDR'};49 final int decimalDigits = zeroCentCurrencies.contains(currencyCode) ? 0 : 2;5051 final String locale = currencyLocales[currencyCode] ?? 'en_US';5253 final formatter = NumberFormat.currency(54 locale: locale,55 symbol: '',56 decimalDigits: decimalDigits,57 );5859 final String symbol = symbols[currencyCode] ?? '$currencyCode ';60 return '$symbol${formatter.format(displayAmount)}';61}6263// ─── Usage Examples ────────────────────────────────────────────────────────64// formatPrice(1999, 'USD', 1.0) → '$19.99'65// formatPrice(1999, 'EUR', 0.92) → '€18.39'66// formatPrice(1999, 'JPY', 150) → '¥2,999'67// formatPrice(0, 'USD', 1.0) → 'Free'Common mistakes when implementing Multi-Currency Support in FlutterFlow
Why it's a problem: Converting prices on the server and storing the converted amounts in Firestore
How to avoid: Store all prices as USD cents in Firestore. Convert only at display time using the current rate from your currencies collection. The single source of truth never changes.
Why it's a problem: Using floating-point doubles to store monetary amounts
How to avoid: Store prices as integers representing the smallest currency unit (cents for USD). Only convert to a double when displaying or passing to Stripe, and apply Math.round() before creating a charge.
Why it's a problem: Trusting the currency rate sent from the client app when creating a Stripe charge
How to avoid: Always re-fetch the current exchange rate from Firestore inside your Cloud Function when creating a Stripe PaymentIntent. Never trust any financial calculation that originates on the client.
Best practices
- Store all prices as integer USD cents in Firestore — this is the single source of truth that never changes with exchange rate fluctuations.
- Refresh exchange rates server-side via Cloud Function once per day — more frequent updates are unnecessary for retail use cases and cost extra API calls.
- Display the last_updated date of exchange rates somewhere in your checkout flow so users understand prices may vary slightly with currency fluctuations.
- Handle zero-decimal currencies like JPY and KRW explicitly — Stripe and NumberFormat both need different treatment for these.
- Cache the selected currency and rate in App State so you are not reading Firestore on every price widget render.
- Add a 'default_currency' field to your user document so returning users see their preferred currency automatically without having to select it again.
- Test your Stripe integration in test mode with at least 3 different currencies before going live, including a zero-decimal currency if you support any.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a FlutterFlow app that sells products globally. Explain the correct architecture for multi-currency support: where to store prices, how to fetch and cache exchange rates, how to format currency with the correct locale and symbol in Dart using NumberFormat.currency, and how to pass the correct currency amount to Stripe Checkout without trusting client-sent data.
In my FlutterFlow app, create a Custom Function called formatPrice that takes priceInUsdCents (int), currencyCode (String), and rateToUsd (double) and returns a formatted currency string with the correct symbol. Use Dart's NumberFormat.currency and handle JPY and other zero-decimal currencies. Also create App State variables selectedCurrencyCode, selectedCurrencySymbol, and selectedCurrencyRate.
Frequently asked questions
Which exchange rate API works best for FlutterFlow apps?
ExchangeRate-API (exchangerate-api.com) has a generous free tier (1,500 requests/month), simple JSON response format, and reliable uptime. For production apps with higher volume, Open Exchange Rates or Fixer.io offer more history and reliability. Call the API from a Cloud Function, not from your FlutterFlow app directly, to protect your API key.
Does Stripe support all the currencies I want to offer?
Stripe supports 135+ currencies. Check stripe.com/docs/currencies for the full list. Some currencies are presentment-only (you can display them but are actually charged in a different currency). Review the minimum charge amounts per currency — some have higher minimums than USD.
How do I handle currency selection for guest (unauthenticated) users?
Store the selected currency in FlutterFlow's App State (persisted to local storage) rather than the Firestore user document. On app start, read the stored currency from local storage and initialize the App State variables. When the user signs in, sync the local selection to their Firestore document.
What happens if the exchange rate API is down when my Cloud Function runs?
Add error handling in your Cloud Function that catches API failures and skips the Firestore update rather than overwriting rates with zeros. Your app will continue displaying the previous day's rates, which is acceptable for a 24-hour outage. Log the failure to a Firestore error document so you can monitor it.
Should I show prices inclusive or exclusive of VAT in different countries?
This is a legal and business decision, not purely technical. EU consumers expect VAT-inclusive prices; US consumers expect pre-tax prices. Consider adding a vat_rate field per currency/region and a showPriceWithVat boolean in your app config. This is separate from currency conversion and should be handled after you have the base currency conversion working correctly.
Why do my prices look different in the Stripe Dashboard versus what users see in my app?
Stripe Dashboard shows the charge amount in the currency you passed to the PaymentIntent. If your app display uses a slightly different exchange rate than the one used at checkout time (e.g., App State cached an old rate), you will see rounding differences. Always re-fetch the rate from Firestore in your Cloud Function at checkout time to ensure the displayed price and the charge are calculated from the same rate.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation