billing_country 1.0.1
billing_country: ^1.0.1 copied to clipboard
Cross-platform Flutter plugin to get the App Store / Google Play billing country and gate third-party payments.
billing_country #
Cross-platform Flutter plugin that reports the billing storefront country for the current device and helps you gate third-party / external payment flows for digital goods.
- iOS → App Store storefront country via StoreKit
(
Storefront.current/SKPaymentQueue.storefront). - Android → Google Play billing country via
BillingClient.getBillingConfigAsync(), falling back to the SIM / device locale country when Play billing is unavailable.
All country codes are normalized to ISO 3166-1 alpha-2 (e.g. US, IN,
RU) regardless of platform. (iOS reports alpha-3 internally; the plugin
converts it.)
This plugin only tells you where the user is billed. It does not decide whether third-party payments are actually allowed — Apple and Google decide that per program, app category, and date. The built-in country lists are illustrative defaults, not legal advice. Configure them yourself.
Why #
Apple and Google both restrict selling digital goods through third-party payment systems, but both allow it in certain regions/programs (e.g. the US external purchase link entitlement on iOS; India, South Korea, and Russia among others on Google Play). Deciding which payment UI to show usually hinges on the user's billing region, not their device locale. This plugin gives you that region on both platforms behind one API.
Install #
dependencies:
billing_country: ^1.0.0
- iOS: minimum iOS 13 (StoreKit
Storefront.currentis used on iOS 15+, with anSKPaymentQueuefallback below that). - Android: bundles Google Play Billing Library (
7.1.1by default; adjust inandroid/build.gradle.ktsif your app needs another version).minSdk 24.
Usage #
import 'package:billing_country/billing_country.dart';
// Which store are we on?
final BillingStore store = BillingCountry.store; // appStore / playStore / unknown
// ISO 3166-1 alpha-2 billing country, or null if unavailable. Never throws.
final String? country = await BillingCountry.getCountryCode(); // e.g. "IN"
// Convenience.
final bool isUS = await BillingCountry.isUnitedStates();
// Gate third-party payment for the *current* device in one call.
if (await BillingCountry.isExternalPaymentSupportedHere()) {
// show your own / external payment flow
} else {
// fall back to StoreKit / Google Play Billing
}
Configuring supported countries #
The defaults are only a starting point. Set your own lists per store — driven by your legal/compliance decision, ideally from a remote config so you can change them without shipping an update:
BillingCountry.setSupportedCountries(
appStore: {'US'},
playStore: {'IN', 'RU', 'KR', 'BR'},
);
// Check an arbitrary code against the current store's list.
final bool ok = BillingCountry.isExternalPaymentSupported('IN');
// Inspect / reset.
final Set<String> current = BillingCountry.supportedCountriesForCurrentStore();
BillingCountry.resetSupportedCountries();
Defaults:
| Store | Default supported countries |
|---|---|
| App Store | US |
| Play Store | IN, RU, KR |
Driving the lists from remote config #
The package is transport-agnostic and adds no networking / Firebase dependency. Feed it a JSON payload of this shape from any source:
{ "appStore": ["US"], "playStore": ["IN", "RU", "KR"] }
Apply a map you already have:
BillingCountry.updateFromJson(remoteMap);
Or hand it a fetcher that returns the raw JSON string — updateFromRemote
decodes, applies, and never throws (returns false and leaves the config
untouched on any failure):
// Firebase Remote Config
import 'package:firebase_remote_config/firebase_remote_config.dart';
final rc = FirebaseRemoteConfig.instance;
await rc.fetchAndActivate();
await BillingCountry.updateFromRemote(
() async => rc.getString('billing_country_support'),
);
// Plain HTTP endpoint
import 'package:http/http.dart' as http;
await BillingCountry.updateFromRemote(() async {
final res = await http.get(Uri.parse('https://example.com/billing.json'));
return res.statusCode == 200 ? res.body : null;
});
Rules:
- Only keys present in the payload are applied; an omitted store keeps its current list.
- Codes are uppercased; non-string / blank entries are dropped.
- An explicit empty array (
"playStore": []) clears that store.
Call updateFromRemote early (e.g. on app start, before your first payment
gate) and re-run it whenever remote config refreshes.
API #
| Member | Description |
|---|---|
BillingCountry.store |
BillingStore.appStore / playStore / unknown for the current platform. |
getCountryCode() |
Future<String?> — ISO alpha-2 billing country, or null. Never throws. |
isUnitedStates() |
Future<bool> — true when the billing country is US. |
isExternalPaymentSupported(code) |
bool — is code (alpha-2) in the current store's configured list. |
isExternalPaymentSupportedHere() |
Future<bool> — resolves the current country and checks it. |
setSupportedCountries({appStore, playStore}) |
Override the per-store lists. |
updateFromJson(map) |
Apply a {appStore:[], playStore:[]} config map. Returns bool. |
updateFromRemote(fetch) |
Future<bool> — fetch a raw JSON string and apply it. Never throws. |
supportedCountriesForCurrentStore() |
The configured list for the current store. |
appStoreSupportedCountries / playStoreSupportedCountries |
The configured list per store (any platform). |
resetSupportedCountries() |
Restore library defaults. |
normalizeToAlpha2(raw) |
Utility: convert an alpha-3 or alpha-2 code to alpha-2 (exported). |
Notes & caveats #
- Per Google's docs, the value from
getBillingConfigAsync()is meant for one-time use and may change; do not persist it or use it for profiling / tracking / advertising. - On Android, if the Play Store is missing or the billing connection fails, the
plugin falls back to SIM country, then network country, then device locale —
which is an approximation of the billing region, not the billing region
itself. If you need Play-only accuracy, treat a
null/fallback result accordingly in your own logic. - On unsupported platforms (desktop, web)
storeisunknownandgetCountryCode()returnsnull.