Uni Payments 是一個 Flutter/Dart 套件,可簡化與多個支付網關的整合,讓應用程式內能快速且安全地完成交易。專為尋求統一解決方案的開發人員設計,它抽象了處理不同 API 的複雜性,實現跨平台的無縫處理。該套件透過一致的介面支援各種付款方式,減少開發時間並提升可擴展性。特別適用於電商、訂閱式服務及應用程式內購買。原始碼開源,可於 GitHub 上取得,貢獻者可依需求擴充或自訂功能。
適用情境
電子商務結帳
應用內購買
訂閱計費
多閘道支援
行動支付整合
主要特色
統一支付 API
對多個網關的支援
安全交易處理
輕鬆整合
開源程式碼
適合對象
Flutter 開發者
支付整合應用
新創企業需要快速設置
需要多提供者支援的應用程式
開發者避免使用閘道器特定的程式碼
使用建議
每個閘道器都需要個別設定
列出的平台支援有限
依賴外部閘道器的可用性
未提供官方文件
每種支付方式都需進行測試
原始 README
以下為關聯原始碼儲存庫的 README 原文快照,最新內容請造訪原始碼儲存庫。
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 = awaitUniPayments.payWithRazorpay(
keyId: 'YOUR_RAZORPAY_KEY_ID',
amount: 25.00,
businessName: 'Acme Inc',
customer: UniCustomer(name: 'Ada', email: 'ada@x.com', phone: '9999999999'),
);
switch (result) {
casePaymentSuccess(:final transactionId): /* verify on backend */casePaymentFailure(:final errorCode, :final message): /* show error */casePaymentCancelled(): /* user dismissed the sheet */
}
Swap payWithRazorpay for payWithStripe, payWithPaypal, payWithGooglePay, … every gateway still returns the same PaymentResult, while some hosted/native sheets also need a BuildContext.
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.41+ · Dart 3.8+ (required for flutter_stripe 14.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:
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 = awaitUniPayments.isWalletSupported(
WalletProvider.applePay,
configJson,
);
Per-gateway cookbook
Razorpay
final result = awaitUniPayments.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: constDuration(minutes: 5), // optional — see below
);
By default this waits indefinitely for the checkout to complete. Pass
timeout to get a PaymentFailure instead of hanging forever if the
SDK never calls back (e.g. the app was backgrounded and killed).
Stripe
final result = awaitUniPayments.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',
);
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 = awaitUniPayments.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 = awaitUniPayments.payWithCashfree(
orderId: 'order_${DateTime.now().millisecondsSinceEpoch}',
paymentSessionId: 'session_xxx',
useStagingEnvironment: true,
timeout: constDuration(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.
timeout works the same way as Razorpay's above.
PhonePe
// requestBody is a base64-encoded JSON request your backend signs.final result = awaitUniPayments.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,
);
// 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 = awaitUniPayments.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 / checkoutConfig maps, using PayU's raw keys from payu_checkoutpro_flutter's PayUConstantKeys.
Square
final result = awaitUniPayments.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 which applicationId you pass. Also requires minSdkVersion 28 on Android — see Required extra setup.
Airwallex
final result = awaitUniPayments.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.
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/Airwallex clientSecrets 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's generateHash callback 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 specific errorCode (e.g. external_wallet_pending from Razorpay, payment_in_progress from 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/signature in rawResponse, Paytm's checksum, PayU's hash), verify that signature server-side too — rawResponse carries the untouched payload specifically so your backend has something to check against, not just log.
Reliability
Use a genuinely unique reference / transactionRef / orderId / transactionId per attempt (not reused across retries) — every gateway that takes one uses it to detect duplicate charges on their end.
payWithRazorpay and payWithCashfree accept an optional timeout — without one, a native SDK that never calls back (killed app, OEM quirk) leaves the Future pending 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_stripe and the other native SDKs current — dart pub outdated should show no available upgrades left on the table; payment SDKs receive security-relevant patches (fraud signals, TLS/crypto updates) more often than most packages.
git clone https://github.com/NehilKoshiya/uni_payments
cd uni_payments
flutter pub get
flutter testcd example && flutter pub get
Before sending a PR
flutter analyze must be clean. This package runs with strict-casts, strict-inference and strict-raw-types on top of flutter_lints (see analysis_options.yaml) — code that's merely "lint-clean" elsewhere may still fail here.
flutter test must 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>Gateway class with a pay(...) method that awaits/wraps the upstream SDK and maps its outcome onto PaymentSuccess / PaymentFailure / PaymentCancelled. Give it a const _gatewayName = '<name>'; matching the public method name, and stash the untouched upstream response in rawResponse wherever you can.
lib/src/uni_payments.dart — add a payWithXxx(...) static method that validates required fields via _validate(...) and delegates to the new gateway. Document any upstream quirks in the doc comment (see payWithCashfree for an example of calling out an upstream concurrency limitation).
pubspec.yaml — add the dependency, and check its environment: constraint before pinning — don't silently raise this package's Flutter/Dart floor for an optional gateway (see the square_in_app_payments comment for the pattern).
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.
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.