Agent-readable docs index: /llms.txt. Full docs in one file: /llms-full.txt. Download /docs.zip to grep all markdown files locally.

Bank Alfalah Payment

bank_alfalah_payment is a Flutter client SDK for accepting Bank Alfalah payments. It presents the bank-hosted checkout in a WebView, strictly validates the gateway's return redirect, and reports a single typed PaymentResult — while your backend owns credentials, session creation, and payment verification.
Never place Bank Alfalah merchant credentials inside your Flutter application. Merchant IDs, passwords, hashes, and encryption keys belong on your server. Anything shipped in an app binary can be extracted. This SDK is designed so it never needs them.
0.1.0-beta.x is a security-driven rewrite — every public API from 0.0.1 was replaced. Validate the flow against the current Bank Alfalah sandbox before going live. Upgrading? See Migration below.

Installation

dependencies: bank_alfalah_payment: ^0.1.0-beta.1
Platforms: Android, iOS, macOS (checkout renders via webview_flutter; only other dependency is meta). There is nothing to configure in the app — no merchant IDs, no keys, no gateway URLs. Your Bank Alfalah credentials (merchant ID, password, username, hash, store ID, hash keys, registered HTTPS return URL) live in your server's secret store.

How it works

The SDK follows one rule: the app displays checkout, the backend owns money.
Flutter app ────► Your backend ────► Bank Alfalah │ │ │ │ 1. create │ 2. gateway │ │ session │ transaction │ ◄─────────────────┘ │ │ 3. open bank-hosted checkout │ ├────────────────────────────────────► │ 4. redirect to return URL │ ◄────────────────────────────────────┤ │ 5. verify │ server-to- │ ├────────────────►│ server ─────►│ │ 6. PaymentCompleted (verified) │
Redirects are matched strictly against the session's return URI (scheme, host, port, path) — unknown URLs never affect payment state, and a redirect alone is never treated as proof of payment. Only a backend-verified transaction becomes PaymentCompleted.
Reliability guarantees: exactly-once completion (redirect, cancel, timeout, WebView error, and route dismissal race safely — one PaymentResult per checkout); the checkout screen owns navigation and pops itself exactly once; a cancellable 5-minute timeout (configurable); non-main-frame resource errors don't abort checkout.

Quick start

  1. Implement the session provider
    The bridge to your server — it never sees credentials, only session data:
    class MyBackendSessionProvider implements BankAlfalahSessionProvider { @override Future<CheckoutSession> createSession(CheckoutRequest request) async => CheckoutSession.fromJson( await api.post('/payments/create', request.toJson()), ); @override Future<PaymentVerification> verifyPayment(String transactionId) async => PaymentVerification.fromJson( await api.get('/payments/$transactionId/verify'), ); }
  2. Start a checkout
    Amounts are typed with Money.pkr() — validated, minor-units based:
    final bankAlfalah = BankAlfalahPayment( environment: BankAlfalahEnvironment.sandbox, sessionProvider: MyBackendSessionProvider(), ); final result = await bankAlfalah.startCheckout( context: context, request: CheckoutRequest( amount: Money.pkr(2500), orderId: 'ORDER-123', customer: Customer(email: 'user@example.com', phone: '03001234567'), ), );
  3. Handle the sealed result
    switch (result) { case PaymentCompleted(): // verified paid — fulfill the order case PaymentPending(): // wait for backend/webhook confirmation case PaymentFailed(): // gateway declined case PaymentCancelled(): // user closed checkout case PaymentTimedOut(): // checkout took too long case PaymentVerificationFailed(): // redirect said OK, backend said no case PaymentError(): // network / WebView / backend error }

Backend contract

Implement two endpoints behind BankAlfalahSessionProvider:
POST /payments/create — receives CheckoutRequest.toJson(); creates the Bank Alfalah transaction (hashing, credentials, gateway calls all happen here) and responds:
{ "transactionId": "TX-123", "checkoutUrl": "https://payments.bankalfalah.com/...", "returnUrl": "https://yourserver.example/payments/return", "formFields": { "optional": "POST form hand-off fields" } }
GET /payments/{transactionId}/verify — checks the transaction server-to-server and responds:
{ "status": "verified | pending | failed", "transactionId": "TX-123", "orderId": "ORDER-123", "gatewayReference": "...", "responseCode": "00", "message": "..." }
CheckoutSession.fromJson / PaymentVerification.fromJson parse these shapes directly. status maps to PaymentCompleted / PaymentPending / PaymentVerificationFailed. When formFields is present the SDK performs a POST-form hand-off with every name and value HTML-escaped. Gateway endpoints are chosen by your backend, never hardcoded in the app.

Results & errors

ResultMeaningWhat to do
PaymentCompletedRedirect + backend verification confirmedFulfill the order
PaymentPendingVerification returned pendingPoll backend / await webhook
PaymentFailedGateway declinedOffer retry
PaymentCancelledUser closed checkoutReturn to cart
PaymentTimedOutExceeded timeout (default 5 min)Offer retry
PaymentVerificationFailedRedirect OK, backend said noDo not fulfill; investigate
PaymentErrorNetwork / WebView / backend errorLog the typed exception
All failures carry a BankAlfalahException subtype: ConfigurationException, NetworkException, GatewayException, RedirectException, VerificationException, TimeoutException (each prefixed BankAlfalah). Exceptions thrown by your provider surface as PaymentErrorstartCheckout always resolves with exactly one result.
Optional observability (identifiers only, never payloads) via PaymentLifecycleObserver(onPaymentStarted:, onCheckoutOpened:, onRedirectReceived:, onVerificationStarted:, onCompleted:). The PaymentLogger is off by default and redacts credential-like keys.

Sandbox & testing

Set environment: BankAlfalahEnvironment.sandbox and point your backend at the Bank Alfalah sandbox gateway — switching to production is a backend config change, not an app release. The bundled example app includes a MockSessionProvider so you can exercise every UI state with no credentials at all.

Production checklist

  • No Bank Alfalah credentials anywhere in the Flutter app or repo (including git history); rotate anything that ever shipped in a binary.
  • createSession and verifyPayment implemented on your backend.
  • Orders fulfilled only on PaymentCompleted (or a server-side webhook), never on a redirect alone.
  • Return URL uses HTTPS and is registered with Bank Alfalah.
  • Flow manually verified against the current Bank Alfalah sandbox.
  • PaymentLogger.enabled is false in release builds.
  • PaymentPending handled (poll your backend / webhook).

Migrating from 0.0.1

0.0.1 required merchant credentials in the app and decided success from a URL substring — both security defects, so the API was deliberately broken:
0.0.10.1.0
BankAlfalahConfig with credentials in the appGone — credentials on your backend
Client-side gateway request + hashBackend session via createSession
Success = URL contains RC-00Strict return-URI match + mandatory verifyPayment
PaymentRequest(amount: "100") (string, unused)CheckoutRequest(amount: Money.pkr(100), ...)
Status enum + nullable fieldsSealed PaymentResult, seven states
Double-fire callbacks / double popsExactly-once completion
Full-payload print() loggingOff-by-default redacting PaymentLogger
Steps: build the two backend endpoints → delete BankAlfalahConfig and every credential string (and rotate anything that shipped) → implement the provider → replace initiatePayment with startCheckout → fulfill only on PaymentCompleted. Full guide: MIGRATION.md.

For AI agents

Copy this block into Cursor, Claude Code, or any coding agent to integrate the package correctly:
# bank_alfalah_payment — agent integration instructions Add Bank Alfalah payments to this Flutter app using bank_alfalah_payment ^0.1.0-beta.1 (pub.dev). Follow these rules exactly: 1. NEVER put Bank Alfalah merchant credentials (merchant ID, password, username, hash, store ID, keys) in Flutter code, config, or env files shipped with the app. They belong on the backend only. 2. Add to pubspec.yaml: bank_alfalah_payment: ^0.1.0-beta.1 3. Implement `BankAlfalahSessionProvider` with two backend calls: - createSession(CheckoutRequest) -> POST /payments/create, parse with CheckoutSession.fromJson. Backend returns {transactionId, checkoutUrl, returnUrl, formFields?}. - verifyPayment(transactionId) -> GET /payments/{id}/verify, parse with PaymentVerification.fromJson. Backend returns {status: "verified|pending|failed", transactionId, orderId, ...}. 4. Create the entry point once: BankAlfalahPayment(environment: BankAlfalahEnvironment.sandbox, sessionProvider: MyBackendSessionProvider()) 5. Start checkout with typed money: startCheckout(context: context, request: CheckoutRequest( amount: Money.pkr(2500), orderId: 'ORDER-123', customer: Customer(email: ..., phone: ...))) 6. Handle the sealed PaymentResult exhaustively: PaymentCompleted, PaymentPending, PaymentFailed, PaymentCancelled, PaymentTimedOut, PaymentVerificationFailed, PaymentError. 7. Fulfill orders ONLY on PaymentCompleted (or a server-side webhook), never on a redirect alone. Handle PaymentPending by polling the backend. 8. If the backend endpoints don't exist yet, scaffold them server-side (all hashing/credentials/gateway calls there) before wiring the app. 9. Keep PaymentLogger.enabled = false in release builds. Docs: https://docs.aliarain.com/bank-alfalah-payment
Agents can also read this page as plain markdown — every page on this site is available to LLMs in raw form via the docs' llms.txt index.