Skip to main content
zennopay_flutter brings the PaymentSheet to Flutter with the same shape as Stripe’s Flutter SDK: one await Zennopay.presentSheet(...) and the terminal outcome is the resolved value. The sheet renders the full pay experience — QR scan → amount + FX quote → slide-to-pay → result — and your app never leaves the foreground.
Animated flow: scan a QR code, review the amount and USD quote, slide to pay, and see the success screen.

The PaymentSheet, end to end, presented from the sample wallet app.

Requirements

  • Flutter 3.19+ / Dart 3.4+
  • iOS and Android targets (the sheet uses the device camera via mobile_scanner)
  • A backend session endpoint that creates the intent and returns the Zennopay-minted session token

Install

Platform setup

The scanner uses the camera; add the platform declarations: iOSios/Runner/Info.plist:
Android — the plugin’s manifest merges in the CAMERA permission; nothing to add. On denial (or no camera), the sheet falls back to a paste-QR field on both platforms.

Configure

The environment is a config value, not a code path:
Already shipped on zennopay.in? Nothing to do. Previously published SDK versions resolve sandbox / production to the api.zennopay.in hosts, and those hosts continue to be served — same API, same credentials. You do not need to ship an app update to stay working. See Environments.
Your API keys never live in the app: per payment, your backend creates the intent and Zennopay returns a session token. Your backend passes it to your app, and the SDK holds only that short-lived token.

Present the PaymentSheet

With a checkout session from your backend in hand, your app awaits one call:
To present without a BuildContext (e.g. from a service layer), install the SDK’s navigator key on your MaterialAppMaterialApp(navigatorKey: Zennopay.navigatorKey, ...) — and omit context:.

Handle the result

Flutter promotes pending to a first-class case, and Completed/Pending carry an optional Receipt:
  • Pending means status polling timed out before a terminal state — the payment may still settle. Show a soft state and reconcile via your webhook or transaction history.
  • Failed carries a typed ZennopayError with a stable ZennopayErrorCode (invalidJwt, intentMismatch, jwtExpired, quoteExpired, limitExceeded, networkError, …).

Customize appearance

ZennopayAppearance themes the sheet — colors (with per-mode dark variants), corner radii, typography, primary button, and an optional header logo:
Pass nothing (const ZennopayAppearance.automatic()) for the default Zennopay look with system light/dark. Structural rules are not overridable — radii are clamped to ≤ 12, amounts render in tabular figures, and the accent is reserved for state.

Analytics hook

presentSheet accepts an optional onEvent callback with a small, privacy-safe event stream (sheet_presented, scan_validated, slide_committed, confirm_result, …) you can forward to your own analytics:

Test

  • Simulator/emulator: no usable camera — use the sheet’s paste-QR fallback with any VietQR payload string; the backend does the authoritative parse.
  • Sandbox end-to-end: ZennopayConfig.sandbox + a session from your sandbox backend drives the flow to a real captured on the sandbox payout rail.
  • Fail-fast checks: a blank or mismatched session token resolves immediately with Failed (invalidJwt / intentMismatch) before any UI is shown.
The full sandbox checklist — including limit-blocked payments and pending reconciliation — is in Test your integration.

Next steps

Build your session endpoint

The one backend route the sheet needs.

Test your integration

The sandbox loops to run before release.

Webhooks

Reconcile terminal states server-side.

Per-user limits

The corridor limits Zennopay enforces for you.