digetpay_plugin 0.4.1
digetpay_plugin: ^0.4.1 copied to clipboard
Pure-Dart DigetPay payment plugin: hosted (PCI-safe) checkout via WebView plus transaction management over the DigetPay REST API.
DigetPay for Flutter #
[DigetPay — pure-Dart hosted checkout for Flutter]
Accept card payments in your Flutter app in minutes. DigetPay defaults to a
hosted, PCI-safe WebView checkout — raw card data never passes through your
app, so you keep your PCI scope small — plus a full REST API for money
movement, recurring charges, and transaction history. Pure Dart, built on
http and webview_flutter; no native code to write or register.
await DigetPaySdk.cardPay()
.setOrder(DigetPaySaleOrder(id: orderId, description: 'Coffee', currency: 'SAR', amount: 15))
.setPayer(payer)
.onTransactionSuccess((res) => print('Paid ✓ ${res.transactionId}'))
.onTransactionFailure((res) => print('Failed: ${res.declineReason}'))
.onDismiss(() => print('User cancelled'))
.start(context);
Contents #
- Install
- Quick start
- What you can do
- Hosted checkout
- Direct (S2S) card sale
- Transaction history & lookup by id
- Listing checkout sessions
- Money movement
- Recurring charges & subscriptions
- Handling transaction data
- Debug logging
- Platform setup
- Security — mobile API key
- Not yet available
Install #
dependencies:
digetpay_plugin: ^0.4.1
flutter pub add digetpay_plugin
Quick start #
Initialize once at startup, then call the static API anywhere:
import 'package:digetpay_plugin/digetpay_plugin.dart';
DigetPaySdk.initialize(
apiKey: '<your-mobile-api-key>',
baseUrl: kDigetPayDefaultBaseUrl, // sandbox: https://fin-api.digetpay.com/v1
// production: https://api.digetpay.com/v1
);
Every call before
initialize()throwsDigetPaySdkIsNotInitializedException.
initialize()has no re-entrancy guard — call it again (e.g. after a user picks a different environment or updates their API key) to reconfigure the SDK at any time.
That's it — you're ready to open the checkout.
What you can do #
| ✅ Available now | Purpose |
|---|---|
cardPay() |
Hosted, PCI-safe card checkout (WebView) |
sale(request) |
Headless S2S card sale/authorization — ⚠️ widens PCI scope, see below |
getCheckoutStatus(sessionId) |
Confirm a checkout's outcome |
getCheckoutSessions([filter]) |
List your checkout sessions (paginated) |
getTransactionById(id) |
Full transaction details by gateway transaction id |
getTransactionHistory([filter]) |
List raw transaction records (paginated) |
capture · refund · voidd |
Move money on an existing transaction |
recurring(request) |
Charge a recurringToken from a prior cardPay() transaction |
getRecurringSubscriptions([filter]) |
List saved-card billing plans (paginated) |
chargeSubscription(...) |
Charge an existing subscription — no card data needed |
| 🚧 Not yet available | Behaviour |
|---|---|
applePay · externalPayment |
Throws UnsupportedError — no backend endpoint yet |
getTransactionByOrderId · getTransactionByRrn |
Throws UnsupportedError — no endpoint yet |
Hosted checkout (recommended card flow) #
await DigetPaySdk.cardPay()
.setOrder(DigetPaySaleOrder(
id: orderId, description: 'Coffee', currency: 'SAR', amount: 15))
.setPayer(DigetPayPayer(
firstName: 'Demo', lastName: 'User', address: 'Riyadh',
country: 'SA', city: 'Riyadh', zip: '00000',
email: 'demo@example.com', phone: '+966500000000'))
// .setResultUrls(...) is optional — omit to use the SDK defaults.
.onTransactionSuccess((res) { /* res.status, res.transactionId ... */ })
.onTransactionFailure((res) { /* res.declineReason, res.errorCode */ })
.onDismiss(() { /* user backed out */ })
.start(context);
start() calls POST /payment/checkout/intiate, opens the returned
redirectUrl in a WebView and — once the hosted page reaches a result page —
fetches GET /sdk/status?sessionId= and dispatches success or failure from the
transaction's paymentStatus. Exactly one callback fires.
Flutter app
│ DigetPaySdk.cardPay()…start(context)
▼
POST /payment/checkout/intiate ──▶ { id, redirectUrl }
│ push CheckoutPage → load redirectUrl
▼
WebView (hosted card form + 3-D Secure)
│
├─ reaches a result page ──▶ GET /sdk/status?sessionId=<id> (authoritative)
│ ├─ paymentStatus == APPROVED ─▶ onTransactionSuccess(res)
│ └─ otherwise ─▶ onTransactionFailure(res)
│
└─ user backs out (no result page) ──▶ onDismiss()
intiateis spelled that way on the backend — kept verbatim so the docs match the actual wire call (it is not a typo in your code).- The status response is authoritative — the SDK does not trust the result
URL alone, because the gateway can route to
/successbriefly before settling on/failure.
Direct (S2S) card sale #
⚠️ PCI scope.
cardPay()is the recommended default — raw card data never passes through your app.sale()is an explicit opt-in for a headless integration: your app collects the PAN/CVV directly, which widens your PCI-DSS scope. Only use it if you've independently assessed that tradeoff.
final response = await DigetPaySdk.sale(
SaleRequest(
orderId: orderId,
amount: 15,
currency: 'SAR',
auth: false, // true → authorization-only hold instead of a sale
customer: Customer(name: 'Demo User', email: 'demo@example.com'),
successUrl: 'https://example.com/success',
failureUrl: 'https://example.com/failure',
card: CardDto(
cardNumber: '4111111111111111',
cardHolder: 'Demo User',
cardExpiryMonth: '12',
cardExpiryYear: '2030',
cardCvv: '123',
),
),
);
sale() POSTs to /payment/s2s/sale.
🚧 Hash status (temporary). The collection's request body carries a
hashfield. We've ported the algorithm it implies (MD5 ofreverse(email) + apiKey + reverse(first6+last4 of the card number), uppercased) tocomputeSaleHash, but automatic injection is currently disabled pending confirmation of the exact algorithm/secret against this backend —sale()sends the request as-is. SetSaleRequest.hashyourself if your integration needs one in the meantime; this will switch back to auto-computed once confirmed (see theTODO(hash-decision)markers inlib/src/digetpay_sdk.dartandlib/src/models/requests/sale_request.dart).
Set auth: true to place an authorization-only hold instead of an immediate
sale — the response's data['action'] reads "SALE" or "AUTH" accordingly.
Transaction history & lookup by id #
getTransactionById(id) fetches full details for a single transaction by its
gateway transaction id (GET /payment/transactions/digetpay/{id}/details),
including its businessUnitHierarchy. getTransactionHistory([filter]) lists
raw transaction records the same way, paginated:
final txn = await DigetPaySdk.getTransactionById(gatewayTransactionId);
final history = await DigetPaySdk.getTransactionHistory(
const TransactionFilter(pageNumber: 0, pageSize: 30, status: 'SUCCESS'),
);
Which method do I use?
getCheckoutSessions()lists checkout sessions (PageDto<CheckoutSession>);getTransactionHistory()lists raw transactions (PageDto<Transaction>— purchases, recurring charges, and refunds all appear here). They are not interchangeable — pick based on which shape you need.
Listing checkout sessions #
getCheckoutSessions() returns a paginated PageDto<CheckoutSession> from
GET /payment/checkout/sessions:
final page = await DigetPaySdk.getCheckoutSessions(
const CheckoutSessionFilter(page: 1, limit: 20),
);
for (final session in page?.content ?? const <CheckoutSession>[]) {
print('${session.merchantOrderId} · ${session.amount} ${session.currency} · ${session.status}');
}
⚠️ Which id do I use?
A
CheckoutSessioncarries two ids. Forcapture/refund/voidd, always usesession.gatewayTransactionId— notsession.id(which identifies the checkout session, not the transaction).// ✅ correct await DigetPaySdk.refund(transactionId: session.gatewayTransactionId!, amount: session.amount!); // ❌ wrong — session.id is NOT a transaction id
PageDto exposes content, number (the backend's 1-based page), size,
totalElements, totalPages, first, and last.
Money movement #
Operate on an existing transaction by its gateway transaction id (no card data involved):
await DigetPaySdk.capture(transactionId: id, amount: 10); // POST /payment/s2s/capture
await DigetPaySdk.voidd(id); // POST /payment/s2s/void
await DigetPaySdk.refund(transactionId: id, amount: 10); // POST /payment/refund
These return a typed DigetPayResponse. HTTP errors and declines come back as an
unsuccessful response — they do not throw:
DigetPayResponse |
Meaning |
|---|---|
isSuccess |
2xx and not declined/failed |
status / result |
Gateway status text |
transactionId · orderId · rrn |
Identifiers |
message · errorCode · declineReason |
Failure detail |
data |
The raw decoded payload (for anything not typed above) |
Recurring charges & subscriptions #
recurring() charges a recurringToken obtained from a prior hosted
checkout transaction (see
Transaction.recurringToken) — no card data involved:
final response = await DigetPaySdk.recurring(
RecurringRequest(
transactionId: initialTransactionId,
orderId: 'ORD-2025-887899',
recurringToken: recurringToken,
amount: 100,
currency: 'SAR',
),
);
For saved-card billing plans (subscriptions), list them and charge one on demand — again, no card data needed, the subscription's saved token is charged server-side:
final page = await DigetPaySdk.getRecurringSubscriptions(
const RecurringSubscriptionFilter(page: 1, limit: 20),
);
for (final sub in page?.content ?? const <RecurringSubscription>[]) {
if (sub.isCompleted) continue;
final result = await DigetPaySdk.chargeSubscription(
subscriptionId: sub.id!,
amount: 50,
email: 'customer@example.com',
);
print(result?.isSuccess); // SubscriptionChargeResult — a flat, non-enveloped body
}
Handling transaction data #
getCheckoutStatus returns a typed Transaction? (a DECLINED transaction is
still returned — inspect paymentStatus). Some fields are sensitive; the SDK
delivers them to you but never logs them:
pan— already masked by the backend (4323 2** **** 0853); safe to show.cardHolderName— PII; display it, but don't log/persist broadly.recurringToken— sensitive: it can initiate future charges. Keep it server-side, never log it, and don't surface it in the UI.
Debug logging #
Pass an optional logger to initialize for a redacted, step-by-step trace
— HTTP method + path + status, checkout milestones, and the terminal outcome. It
never receives the API key, card data, request/response bodies, or query values.
import 'dart:developer' as developer;
DigetPaySdk.initialize(
apiKey: '<key>',
logger: (message) => developer.log(message, name: 'DigetPay'),
);
💳 checkout: creating session for order 6f… (15.0 SAR)
➡️ → POST /payment/checkout/intiate
⬅️ ← 201 OK POST /payment/checkout/intiate {code=201, session=ab02…, redirect=https://fin-admin.digetpay.com/pay/checkout}
🌐 webview → https://fin-admin.digetpay.com/pay/checkout/success
➡️ → GET /sdk/status?sessionId
⬅️ ← 200 OK GET /sdk/status {code=200, status=APPROVED, txn=070ae…, msg=Success}
✅ checkout: APPROVED → onTransactionSuccess
Leave logger unset (the default) for zero logging in production.
Platform setup #
Pure-Dart plugin — no native code to write. You only need standard network/WebView configuration.
Android #
-
minSdkVersion24 (Android 7.0) inandroid/app/build.gradle. -
INTERNET permission in
android/app/src/main/AndroidManifest.xml(the release manifest does not include it automatically):<uses-permission android:name="android.permission.INTERNET"/>
iOS #
- Deployment target iOS 13.0+ in
ios/Podfileand the Runner target. - DigetPay endpoints and the hosted page are served over HTTPS, so the
default App Transport Security policy works with no changes. Avoid a blanket
NSAllowsArbitraryLoadsin production.
Hosted checkout targets Android and iOS (via webview_flutter); Flutter web
and desktop are not supported in this version.
Security — mobile API key #
DigetPayConfig.apiKey is embedded in your app binary, and anything shipped in
a mobile app can be extracted. Always initialize with a publishable /
restricted mobile key scoped to client operations — never a full server
secret. Rotate the key if it is ever exposed, and keep privileged operations
behind your own server.
For card entry, prefer cardPay() —
raw card data never touches your app process, so your PCI scope stays small.
sale() is an explicit, opt-in exception to that: it
carries raw PAN/CVV through your app, which widens your PCI-DSS scope.
Only reach for it if you've independently assessed that tradeoff for your
integration.
Not yet available #
applePay, externalPayment, getTransactionByOrderId, and
getTransactionByRrn throw UnsupportedError until the corresponding backend
endpoints exist (see the TODO(endpoint) markers in the source).
getTransactionByOrderId — list sessions with
getCheckoutSessions() and match on
CheckoutSession.merchantOrderId, or filter
getTransactionHistory() instead.
MIT © DigetPay · Issues & source: https://github.com/DigetPay/digetpay_plugin