Uni Payments
Ten payment gateways. One Future<PaymentResult>. Zero glue code.
A unified Flutter API over Razorpay · Stripe · PayPal · Paystack · Flutterwave · Paytm · Cashfree · PhonePe · 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, … the call shape is identical for every gateway.
Why this exists
Six gateway SDKs ship six call shapes, six response objects, six 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 | 6 different setup guides | Documented per gateway here |
Gateways
| Gateway | Region | SDK | Imperative call | Native button |
|---|---|---|---|---|
| Razorpay | India | razorpay_flutter |
payWithRazorpay |
— |
| Stripe | Global | flutter_stripe |
payWithStripe |
— |
| PayPal | Global | braintree_flutter_plus |
payWithPaypal |
— |
| Paystack | Africa | flutter_paystack_max |
payWithPaystack |
— |
| Flutterwave | Africa | flutterwave_standard |
payWithFlutterwave |
— |
| Paytm | India | paytmpayments_allinonesdk |
payWithPaytm |
— |
| Cashfree | India | flutter_cashfree_pg_sdk |
payWithCashfree |
— |
| PhonePe | India · UPI | phonepe_payment_sdk |
payWithPhonepe |
— |
| Google Pay | Android | pay |
payWithGooglePay |
googlePayButton(...) |
| Apple Pay | 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.6
flutter pub add uni_payments
import 'package:uni_payments/uni_payments.dart';
Platform requirements
| Platform | Minimum |
|---|---|
| Flutter | 3.32+ · Dart 3.8+ (sealed classes + switch expressions) |
| Android | minSdkVersion 23 — modern Stripe / Razorpay / PhonePe builds need it |
| iOS | iOS 15+ — driven by PhonePe's IntentSDK constraint |
Required extra setup
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',
);
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(
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,
);
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.
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 ten 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
- Never ship secret keys or
clientSecrets in your binary — generate them on your server and pass them in. - Always verify
PaymentSuccessserver-side before fulfilling. Client-side success only means the SDK said so. rawResponsecarries the untouched gateway payload — log it for audit + webhook reconciliation.
Contributing
- Bug or feature request — open an issue.
- Sending a PR —
flutter analyzemust be clean andflutter testmust pass. - Found this useful? A ⭐ on GitHub goes a long way.
Apache 2.0 · © Nehil Koshiya
Libraries
- uni_payments
- Uni Payments — a unified Flutter API over Razorpay, Stripe, Paystack, Flutterwave, Paytm, Google Pay, Apple Pay and PayPal/Braintree.