Uni Payments
Thirteen payment gateways. One Future<PaymentResult>. Zero glue code.
A unified Flutter API over Razorpay · Stripe · PayPal · Paystack · Flutterwave · Paytm · Cashfree · PhonePe · PayU · Square · Airwallex · Google Pay · Apple Pay.
final result = await UniPayments.payWithRazorpay(
keyId: 'YOUR_RAZORPAY_KEY_ID',
amount: 25.00,
businessName: 'Acme Inc',
customer: UniCustomer(name: 'Ada', email: 'ada@x.com', phone: '9999999999'),
);
switch (result) {
case PaymentSuccess(:final transactionId): /* verify on backend */
case PaymentFailure(:final errorCode, :final message): /* show error */
case PaymentCancelled(): /* user dismissed the sheet */
}
Swap
payWithRazorpayforpayWithStripe,payWithPaypal,payWithGooglePay, … every gateway still returns the samePaymentResult, while some hosted/native sheets also need aBuildContext.
Why this exists · Gateways · Install · Core types · Cookbook · Demo app · Security
Why this exists
Thirteen gateway SDKs ship thirteen call shapes, thirteen response objects, thirteen ways the user can cancel. Even when you wrap them, "user closed the sheet" usually disappears into a generic catch.
Uni Payments replaces all of that with a single sealed result type that the Dart compiler forces you to exhaust — every switch you write is checked at compile time, so you can't forget the cancel case again.
| Before | With Uni Payments | |
|---|---|---|
| API per gateway | Different class, different callbacks, different error shape | UniPayments.payWith*(...) everywhere |
| Cancellation | Hidden inside catch (e) or a stringy status |
PaymentCancelled — a real type |
| Verification | Dig through gateway-specific JSON | result.transactionId + result.rawResponse |
| Native setup | 13 different setup guides | Documented per gateway here |
Gateways
| Gateway | Region | SDK | Imperative call | Native button |
|---|---|---|---|---|
| India | razorpay_flutter |
payWithRazorpay |
— | |
| Global | flutter_stripe |
payWithStripe |
— | |
| Global | braintree_flutter_plus |
payWithPaypal |
— | |
| Africa | flutter_paystack_max |
payWithPaystack |
— | |
| Africa | flutterwave_standard |
payWithFlutterwave |
— | |
| India | paytmpayments_allinonesdk |
payWithPaytm |
— | |
| India | flutter_cashfree_pg_sdk |
payWithCashfree |
— | |
| India · UPI | phonepe_payment_sdk |
payWithPhonepe |
— | |
| India · LatAm · Turkey · CEE | payu_checkoutpro_flutter |
payWithPayu |
— | |
| US · UK · CA · AU | square_in_app_payments |
payWithSquare |
— | |
| Global · APAC-strong | airwallex_payment_flutter |
payWithAirwallex |
— | |
| Android | pay |
payWithGooglePay |
googlePayButton(...) |
|
| iOS | pay |
payWithApplePay |
applePayButton(...) |
Every imperative call returns Future<PaymentResult>. Wallet buttons exist because Google + Apple's brand guidelines require their own button design.
Install
dependencies:
uni_payments: ^0.0.9
flutter pub add uni_payments
import 'package:uni_payments/uni_payments.dart';
Platform requirements
| Platform | Minimum |
|---|---|
| Flutter | 3.44+ · Dart 3.12+ (required for square_in_app_payments 2.x) |
| Android | minSdkVersion 23 — modern Stripe / Razorpay / PhonePe builds need it. 28+ if you use Square. |
| iOS | iOS 16+ — required by braintree_flutter_plus 7.x and compatible with the other SDKs |
Required extra setup
Square needs a higher Android minSdkVersion
Square's In-App Payments SDK requires minSdkVersion 28 (Android 9). minSdkVersion is a single app-wide setting, so if you use payWithSquare, raise it in your app's android/app/build.gradle.kts:
android {
defaultConfig {
minSdk = 28
}
}
Skip this if you're not using Square — every other gateway in this package works down to minSdkVersion 23.
PhonePe needs a private Maven repo on Android
PhonePe's IntentSDK is hosted on PhonePe's CloudRepo, not on Maven Central. Add the repository to your app's android/build.gradle.kts:
allprojects {
repositories {
google()
mavenCentral()
maven { url = uri("https://phonepe.mycloudrepo.io/public/repositories/phonepe-intentsdk-android") }
}
}
Groovy DSL equivalent in android/build.gradle:
maven { url 'https://phonepe.mycloudrepo.io/public/repositories/phonepe-intentsdk-android' }
Core types
UniCustomer
Pass once, reuse everywhere a gateway prefills checkout fields.
const customer = UniCustomer(
name: 'Ada Lovelace',
email: 'ada@example.com',
phone: '9999999999', // optional
);
PaymentResult
sealed class PaymentResult {
String? gatewayName; // 'razorpay', 'stripe', 'apple_pay', …
String? message;
Map<String, dynamic>? rawResponse;
}
final class PaymentSuccess extends PaymentResult { String transactionId; }
final class PaymentFailure extends PaymentResult { String errorCode; String message; }
final class PaymentCancelled extends PaymentResult { }
rawResponse holds the untouched gateway payload — useful for audit logs and webhook reconciliation.
UniPayments.isWalletSupported
Probe before rendering a wallet button — the underlying pay package silently no-ops on unsupported platforms.
final canApplePay = await UniPayments.isWalletSupported(
WalletProvider.applePay,
configJson,
);
Per-gateway cookbook
Razorpay
final result = await UniPayments.payWithRazorpay(
keyId: 'YOUR_RAZORPAY_KEY_ID',
amount: 25.00, // major units (₹25.00)
businessName: 'Acme Inc', // merchant header inside the sheet
customer: customer,
description: 'Pro subscription',
themeColor: Colors.indigo, // Color, not '#RRGGBB'
currency: 'INR',
timeout: const Duration(minutes: 5), // optional — see below
);
By default this waits indefinitely for the checkout to complete. Pass
timeoutto get aPaymentFailureinstead of hanging forever if the SDK never calls back (e.g. the app was backgrounded and killed).
Stripe
final result = await UniPayments.payWithStripe(
publishableKey: 'YOUR_STRIPE_PUBLISHABLE_KEY',
clientSecret: 'YOUR_PAYMENT_INTENT_CLIENT_SECRET', // from your server
merchantDisplayName: 'Acme Inc',
merchantCountryCode: 'US',
// Optional — Apple Pay / Google Pay inside the PaymentSheet
applePayMerchantId: 'merchant.com.acme.app',
googlePayTestEnv: true,
// Optional — saved cards (ephemeral key from your server)
customerId: 'cus_xxx',
customerEphemeralKeySecret: 'ek_test_xxx',
);
PayPal (Braintree drop-in)
final result = await UniPayments.payWithPaypal(
context: context,
tokenizationKey: 'YOUR_BRAINTREE_TOKENIZATION_KEY',
amount: 25.00,
customer: customer,
currency: 'USD',
countryCode: 'US',
applePayMerchantId: 'merchant.com.acme', // optional
);
Paystack
final result = await UniPayments.payWithPaystack(
context: context,
secretKey: 'YOUR_PAYSTACK_SECRET_KEY',
amount: 25.00,
customer: customer,
reference: 'ref_${DateTime.now().millisecondsSinceEpoch}',
callbackUrl: 'https://acme.dev/paystack/callback',
currency: UniPaystackCurrency.usd,
);
Flutterwave
final result = await UniPayments.payWithFlutterwave(
context: context,
publicKey: 'YOUR_FLUTTERWAVE_PUBLIC_KEY',
currency: 'NGN',
amount: 25.00,
customer: customer,
txRef: 'tx_${DateTime.now().millisecondsSinceEpoch}',
redirectUrl: 'https://acme.dev/flutterwave/return',
testMode: true,
);
Standard checkout only needs your public key. Encryption happens on Flutterwave's hosted modal.
Paytm
// txnToken is issued by your backend via Paytm's initiateTransaction API.
final result = await UniPayments.payWithPaytm(
merchantId: 'YOUR_MERCHANT_ID',
orderId: 'order_${DateTime.now().millisecondsSinceEpoch}',
txnToken: 'YOUR_TXN_TOKEN',
amount: 25.00,
useStagingEnvironment: true,
);
Cashfree
// orderId + paymentSessionId come from your backend's call to the
// Cashfree Orders API.
final result = await UniPayments.payWithCashfree(
orderId: 'order_${DateTime.now().millisecondsSinceEpoch}',
paymentSessionId: 'session_xxx',
useStagingEnvironment: true,
timeout: const Duration(minutes: 5), // optional, see below
);
Only one Cashfree payment can be in flight at a time — the upstream SDK is a process-wide singleton. Calling this again before a prior call resolves fails fast with
PaymentFailure(errorCode: 'cashfree_already_in_progress')instead of corrupting the first call.timeoutworks the same way as Razorpay's above.
PhonePe
// requestBody is a base64-encoded JSON request your backend signs.
final result = await UniPayments.payWithPhonepe(
merchantId: 'YOUR_MERCHANT_ID',
flowId: 'flow_${DateTime.now().millisecondsSinceEpoch}',
requestBody: '<base64-encoded JSON from your backend>',
appSchema: 'unipaymentsdemo', // iOS URL scheme; '' on Android
useStagingEnvironment: true,
);
Also requires the Maven repo described in Required extra setup.
PayU
// Backend hands you a hash for every step PayU asks you to sign — see
// https://devguide.payu.in/flutter-sdk-integration/. Never compute this
// with the salt on-device.
Future<Map<dynamic, dynamic>> generateHash(Map<dynamic, dynamic> request) async {
final response = await yourBackend.post('/payu/hash', body: request);
return response.data; // e.g. { hashName: 'computedHashValue' }
}
final result = await UniPayments.payWithPayu(
merchantKey: 'YOUR_PAYU_MERCHANT_KEY',
amount: 25.00,
productInfo: 'Pro subscription',
customer: customer,
transactionId: 'txn_${DateTime.now().millisecondsSinceEpoch}',
successUrl: 'https://acme.dev/payu/success',
failureUrl: 'https://acme.dev/payu/failure',
generateHash: generateHash,
useStagingEnvironment: true,
);
Advanced CheckoutPro options (SI/subscriptions, split payments, EMI, custom notes, …) go through the optional
additionalPaymentParams/checkoutConfigmaps, using PayU's raw keys frompayu_checkoutpro_flutter'sPayUConstantKeys.
Square
final result = await UniPayments.payWithSquare(
applicationId: 'YOUR_SQUARE_APPLICATION_ID', // sandbox-sq0idb-... or sq0idp-...
);
This only tokenizes a card into a one-time-use nonce — Square's mobile SDK doesn't charge cards itself. Send
result.transactionId(the nonce) to your backend and charge it via Square's Payments API. Sandbox vs. production is decided entirely by whichapplicationIdyou pass. Also requiresminSdkVersion 28on Android — see Required extra setup.
Airwallex
final result = await UniPayments.payWithAirwallex(
clientSecret: 'YOUR_PAYMENT_INTENT_CLIENT_SECRET', // from your server
paymentIntentId: 'YOUR_PAYMENT_INTENT_ID',
amount: 25.00,
currency: 'USD',
countryCode: 'US',
useStagingEnvironment: true,
);
Presents Airwallex's full hosted payment sheet (cards, wallets, and local redirect methods, depending on what your account supports). A result of
errorCode: 'payment_in_progress'means the payment was submitted but its outcome isn't confirmed yet — the same situation as Razorpay's external-wallet case — verify via your backend before fulfilling.
Google Pay
// Imperative
final result = await UniPayments.payWithGooglePay(
paymentConfigurationJson: configJson,
lineItemLabel: 'Total',
amount: 25.00,
);
// Native button
UniPayments.googlePayButton(
paymentConfigurationJson: configJson,
lineItemLabel: 'Total',
amount: 25.00,
buttonType: UniGooglePayButtonType.pay,
onResult: (PaymentResult result) { /* … */ },
);
Apple Pay
final result = await UniPayments.payWithApplePay(
paymentConfigurationJson: configJson,
lineItemLabel: 'Total',
amount: 25.00,
);
UniPayments.applePayButton(
paymentConfigurationJson: configJson,
lineItemLabel: 'Total',
amount: 25.00,
type: UniApplePayButtonType.buy,
onResult: (PaymentResult result) { /* … */ },
);
Demo app
The repo ships a fully-styled demo with all thirteen gateways wired up — animated gradient background, glass-morphism tiles, error toasts, haptic feedback on each outcome.
git clone https://github.com/NehilKoshiya/uni_payments
cd uni_payments/example
flutter pub get
flutter run
Security notes
Secrets
- Never ship secret/private keys in your app binary.
secretKey(Paystack), the PayU merchant salt, and Stripe/AirwallexclientSecrets must be generated or held on your server — a decompiled APK/IPA hands over anything embedded in it. - PayU signs every checkout step with an HMAC computed from your salt. This package never computes that hash —
payWithPayu'sgenerateHashcallback exists specifically so the salt stays server-side; don't be tempted to inline the hash logic on-device "just for testing."
Verification
- A client-side result is a claim, not a fact — the device can lie, crash mid-flow, or lose network after the charge actually succeeded. Always reconcile server-side before fulfilling an order:
PaymentSuccess— re-check with the gateway (fetch the payment/order by id, or wait for its webhook) before shipping anything.PaymentFailure/PaymentCancelled— don't assume no money moved. Bank debits and UPI/wallet redirects can complete after the client already gave up; several gateways in this package surface that explicitly as a specificerrorCode(e.g.external_wallet_pendingfrom Razorpay,payment_in_progressfrom Airwallex,cashfree_already_in_progress) precisely so you don't silently treat "ambiguous" as "definitely unpaid."
- Where the gateway signs its response (Razorpay's
orderId/signatureinrawResponse, Paytm's checksum, PayU's hash), verify that signature server-side too —rawResponsecarries the untouched payload specifically so your backend has something to check against, not just log.
Reliability
- Use a genuinely unique
reference/transactionRef/orderId/transactionIdper attempt (not reused across retries) — every gateway that takes one uses it to detect duplicate charges on their end. payWithRazorpayandpayWithCashfreeaccept an optionaltimeout— without one, a native SDK that never calls back (killed app, OEM quirk) leaves theFuturepending forever with no way for your UI to recover.
Dependencies
- Every native SDK this package wraps is vetted for maintenance status before being added — see the CHANGELOG for two rejected candidates (a discontinued package, and one with a confirmed native-bridge bug) and the reasoning behind each. If you're auditing your own dependency tree, that's a good place to see the bar this package holds itself to.
- Keep
flutter_stripeand the other native SDKs current —dart pub outdatedshould show no available upgrades left on the table; payment SDKs receive security-relevant patches (fraud signals, TLS/crypto updates) more often than most packages.
Contributing
- Bug or feature request — open an issue.
- Found this useful? A ⭐ on GitHub goes a long way.
Local setup
git clone https://github.com/NehilKoshiya/uni_payments
cd uni_payments
flutter pub get
flutter test
cd example && flutter pub get
Before sending a PR
flutter analyzemust be clean. This package runs withstrict-casts,strict-inferenceandstrict-raw-typeson top offlutter_lints(see analysis_options.yaml) — code that's merely "lint-clean" elsewhere may still fail here.flutter testmust pass.- Update CHANGELOG.md under a new version heading describing what changed and why — not just what.
- If your PR touches the example app, run it once (
cd example && flutter run) and exercise the affected gateway's tile before opening the PR — analysis and tests don't catch a broken demo wiring.
Adding a new gateway
Every gateway in this package follows the same shape, so a new one is mostly mechanical:
lib/src/gateways/<name>_gateway.dart— a stateless<Name>Gatewayclass with apay(...)method that awaits/wraps the upstream SDK and maps its outcome ontoPaymentSuccess/PaymentFailure/PaymentCancelled. Give it aconst _gatewayName = '<name>';matching the public method name, and stash the untouched upstream response inrawResponsewherever you can.lib/src/uni_payments.dart— add apayWithXxx(...)static method that validates required fields via_validate(...)and delegates to the new gateway. Document any upstream quirks in the doc comment (seepayWithCashfreefor an example of calling out an upstream concurrency limitation).pubspec.yaml— add the dependency, and check itsenvironment:constraint before pinning — don't silently raise this package's Flutter/Dart floor for an optional gateway.example/— add a tile (lib/data/gateway.dart), a demo method (lib/services/payment_demos.dart), wire it into the dispatch map (lib/ui/home_screen.dart), and add a brand color/letter fallback (lib/ui/brand_icon.dart) if Simple Icons doesn't have the logo.README.md— add a row to the Gateways table and a cookbook entry under Per-gateway cookbook.CHANGELOG.md— new gateways get their own entry under "New gateways."
Before proposing a gateway, do the same vetting this package already applies: check pub.dev for maintenance status (points, likes, last published date, and whether it's marked discontinued), and skim the native bridge source for obvious gaps (does every outcome — including cancellation — actually reach Dart?). Two candidates were rejected for exactly these reasons; see the CHANGELOG.
Libraries
- uni_payments
- Uni Payments — a unified Flutter API over Razorpay, Stripe, Paystack, Flutterwave, Paytm, Google Pay, Apple Pay and PayPal/Braintree.