Skip to main content
@zennopay/react-native is a native bridge: a thin JS API over the native Zennopay iOS and Android PaymentSheets. The camera scanner, slide-to-pay physics, and confirm/status UI all render natively — nothing is re-implemented in JS. One await presentSheet(...), one PaymentResult. It supports both the legacy bridge and the new architecture (Fabric/TurboModules) — the module is interop-compatible.
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

  • React Native 0.70+, React 17+
  • The native Zennopay SDKs must be linked (see below) — this package is the JS surface, not the implementation
  • A backend session endpoint that creates the intent and returns the Zennopay-minted session token

Install

If the native SDK isn’t linked, every presentSheet call rejects with the code native_sdk_unavailable. This is a build/link problem, not a payment failure — fix the steps below and rebuild. (Expo Go cannot load the native module; use a development build.)
iOS — the podspec declares a dependency on the native Zennopay pod, so installing pods pulls it in:
Then add the camera usage string to ios/YourApp/Info.plist:
Android — the module’s Gradle file depends on the native SDK (in.zennopay:sdk, on Maven Central). Make sure mavenCentral() is in your onboarding pack is declared in android/settings.gradle alongside google() and mavenCentral(). The CAMERA permission is merged from the native SDK’s manifest; the SDK requests it at runtime. Rebuild the app after installing (npx react-native run-ios / run-android) — a Metro-only reload will not pick up the native module.

Configure

Already shipped on zennopay.in? Nothing to do. Previously published SDK versions resolve the sandbox / production environments 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

Wrap your app in ZennopayProvider (sets the default environment), then call presentSheet from the useZennopay() hook:
There is also an imperative import if you don’t want the provider:

Handle the result

The promise rejects only for integration errors — no presentation context (no_presentation_context) or the native SDK not linked (native_sdk_unavailable). A payment failure resolves normally with { status: 'failed', error }, where error.code is a stable string from the shared taxonomy (invalid_jwt, intent_mismatch, jwt_expired, quote_expired, limit_exceeded, network_error, timed_out, …).
pending means status polling timed out — the payment may still settle; reconcile via your webhook or transaction history.

Customize appearance

Theming is a plain JS object, serialized to the native ZennopayAppearance:
Omit appearance for the default Zennopay look with system light/dark. Structural rules (radius cap, tabular figures, accent-as-state) are enforced natively and can’t be overridden.

How the bridge works

  • presentSheet calls the native module’s present(...), which wraps Zennopay.presentCheckout on each platform and resolves exactly once with the terminal result.
  • refreshSession is serviced over an event + reply: the native side emits ZennopaySessionExpired { intentId }, the bridge runs your async callback, and replies with the fresh token — the bridge never blocks.
  • The session token crosses the bridge once, into native memory; it is never placed in a URL. Confirm idempotency, retries, and relaunch recovery are owned by the native SDKs.

Test

  • Simulator/emulator: no usable camera — the native sheet falls back to paste-QR; paste any VietQR payload string.
  • Sandbox end-to-end: { environment: 'sandbox' } + a session from your sandbox backend drives the flow to a real captured on the sandbox payout rail.
  • Link check: if presentSheet rejects with native_sdk_unavailable, re-run pod install / a full Gradle build — the JS package is installed but the native SDK isn’t.
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.