Storefront.current to confirm it is StoreKit, not your app code.If your app shows USD 5.99 in TestFlight but EUR 6.99 in a development build on the same iPhone — while Apple’s purchase sheet shows EUR 6.99 — your code is almost certainly correct. This is a StoreKit metadata localization difference between development-signed builds and TestFlight builds.
It is confusing because everything else looks right: you are not hardcoding prices, you are using the localized price from StoreKit, and App Store Connect has the right tiers for Bulgaria and the US. Here is how to prove it and what to tell testers.
Why this happens
Development builds and TestFlight builds do not resolve the storefront the same way.
A development-signed build running from Xcode uses the Sandbox Apple Account signed in on the device under Settings > App Store > Sandbox Account. If that sandbox tester is set to Bulgaria, Product.price and priceFormatStyle come back localized for Bulgaria. That is why both a bare StoreKit 2 probe and the full Flutter app show EUR 6.99 when sideloaded.
A TestFlight build runs in the sandbox purchase environment, but it is tied to the tester’s real Apple ID, not the separate Sandbox Account. StoreKit product metadata in TestFlight can fall back to USD / US storefront data even when the device, App Store region, and purchase sheet are Bulgarian. The purchase itself still goes through at the correct local price, which is why tapping Buy shows EUR 6.99 on Apple’s native confirmation sheet.
In short: your Flutter in_app_purchase display layer is just rendering what StoreKit gave it. When TestFlight gives it USD metadata, the app shows USD. When the system purchase sheet appears, iOS re-resolves the correct storefront and shows EUR.
How to fix it
There is no code fix that forces TestFlight to return the Bulgarian price, because the wrong value is coming from StoreKit before your app ever formats it. The goal is to verify that, rule out your app, and avoid shipping a workaround that breaks production.
1. Prove it is StoreKit, not Flutter, with a TestFlight probe
The most important diagnostic is to get the same logging build into TestFlight. Developers often test only a local StoreKit probe via Xcode and conclude the Flutter plugin is broken when TestFlight differs. Upload that probe to TestFlight too.
Create a minimal native probe or add temporary logging to your app and run it from TestFlight on the same Bulgarian iPhone:
import StoreKit
func logStoreKitLocalization(for productID: String) async {
do {
let storefront = try await Storefront.current
print("Storefront countryCode: \(storefront.countryCode)")
print("Storefront id: \(storefront.id)")
let products = try await Product.products(for: [productID])
guard let product = products.first else {
print("Product not found: \(productID)")
return
}
print("displayPrice: \(product.displayPrice)")
print("price: \(product.price)")
print("currencyCode: \(product.priceFormatStyle.currencyCode)")
} catch {
print("StoreKit lookup failed: \(error)")
}
}
Steps:
- Replace
productIDwith your non-consumable product ID, for examplecom.example.app.pro. - Archive and upload build 23 or later to App Store Connect and release it to your internal TestFlight group.
- Install that TestFlight build on the Bulgarian iPhone, launch it, and collect the console output via Xcode > Window > Devices and Simulators > View Device Logs, or send the strings to your own debug screen.
- Compare to the same build installed development-signed from Xcode.
If the TestFlight log shows countryCode: USA and currencyCode: USD while the dev-signed log shows countryCode: BGR and currencyCode: EUR, you have confirmed TestFlight storefront metadata behavior. Do not change your price formatting code.
2. Check that you are displaying StoreKit prices correctly
Make sure you are not accidentally caching an old product or converting currencies yourself. This will not fix TestFlight returning USD, but it rules out a second bug.
For StoreKit 2, display the price exactly as provided:
import StoreKit
import SwiftUI
struct PriceView: View {
let product: Product
var body: some View {
Text(product.displayPrice)
}
}
For Flutter in_app_purchase, use the price string from ProductDetails directly. Do not do this:
- Do not store
pricein SharedPreferences / UserDefaults and reuse it across builds. - Do not parse the numeric amount and reformat with
NumberFormatterset to another locale. - Do not map
productIDto a hardcoded"$5.99"fallback shown when lookup is slow.
Also call queryProductDetails fresh on each TestFlight launch during testing. If you query once on first launch in the US and cache it, later Bulgarian testers will keep seeing USD.
Verify in App Store Connect:
- Go to Apps > your app > Monetization > In-App Purchases > your non-consumable.
- Open Pricing and confirm Bulgaria is
EUR 6.99and United States isUSD 5.99. - Confirm the product status is Approved or Ready to Submit and attached to the TestFlight build’s version.
If App Store Connect is correct and Apple’s purchase sheet shows EUR 6.99, your catalog is correct.
3. Validate the real charge and document it for QA
Since you cannot change TestFlight metadata localization, treat the purchase sheet as source of truth during testing.
- In the TestFlight build, note the in-app list price (likely
USD 5.99). - Tap Buy and screenshot Apple’s system sheet (should be
EUR 6.99). - Complete the sandbox purchase with a sandbox tester and confirm no error.
- Add a TestFlight tester note: “Known TestFlight issue: list price may show USD, checkout charges correct local price.”
Do not ship a workaround like forcing EUR based on Locale.current or device region. In production, Product.displayPrice is correct for the customer’s actual App Store storefront. Overriding it with device locale will break travelers, users with a US account living in Bulgaria, and price-increase consent flows.
If you need automated testing, test pricing logic with Xcode StoreKit Configuration files (.storekit) where you control the storefront, and test real localization in production with a phased release or promo codes rather than relying on TestFlight screenshots.
How to prevent it
- Always render
displayPrice/ProductDetails.priceverbatim — never reformat or hardcode currency symbols. - Never cache
Productresponses across installs; query on launch and afterAppStore.sync()if needed. - Keep a native StoreKit 2 logging build you can push to TestFlight to check
Storefront.current.countryCodewhen testers report pricing bugs. - Document for QA that TestFlight list prices can show USD while the Apple purchase sheet shows the correct local total.
- Verify final pricing in App Store Connect and with the production purchase sheet, not with TestFlight list UI screenshots.
FAQ
Does this mean customers will be charged USD 5.99 instead of EUR 6.99?
No. The charge follows the storefront resolved at purchase time, which is what Apple’s system sheet shows. If the sheet says EUR 6.99, that is the correct sandbox and production behavior for that Bulgarian account.
Why does the same iPhone show EUR in dev builds and USD in TestFlight?
Because dev builds use the Sandbox Account region set in Settings, while TestFlight uses the real Apple ID in a sandbox purchase context. That account linkage can cause StoreKit to return US metadata for the product list in TestFlight only.
Should I file a bug or change my Flutter plugin version?
Updating the plugin will not help if a native StoreKit 2 Product.products(for:) call also returns USD in TestFlight. Log Storefront.current and priceFormatStyle.currencyCode from TestFlight first. If native StoreKit shows the same USD result, it is platform behavior to work around with tester communication, not app code.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
