Skip to main content

suqo_checkout 0.1.1 copy "suqo_checkout: ^0.1.1" to clipboard
suqo_checkout: ^0.1.1 copied to clipboard

PlatformAndroid

Collect a SUQO payment on a full-screen checkout screen. No gateway integration, no payment UI to build.

suqo_checkout #

Collect a SUQO payment on a full-screen checkout screen, from Flutter. No gateway integration, no payment UI to build.

doc/usage.md covers the same ground as this README with more detail — every onEvent shape, the full options table, and troubleshooting. The wire protocol this package implements against the SUQO web app is js-checkout/docs/protocol.md in the js-checkout repo.

flutter pub add suqo_checkout

What it does, and what it does not #

Your backend already creates the checkout session through SUQO's API and holds its id. This package takes that id, opens SUQO's own checkout page for it, and tells you how the payment went.

It makes no SUQO API calls of its own and holds no credential. The gateway list, the payment call, the hand-off to eSewa / Khalti / ConnectIPS / NPS and the verification all belong to the page inside the WebView. The session id is the only thing this package knows — and it is single-use, short-lived, and carries no buyer details of its own, so nothing sensitive and nothing long-lived ever sits in your app bundle.

Create the session with a customer already attached. There is no field anywhere on open for one. A session created without one shows the page's own "can't be paid" explanation and reports SuqoUnavailableReason.noCustomer on onEvent rather than ever becoming payable.

Usage #

final outcome = await SuqoCheckout.open(context, sessionId: 'cks_9f2c41a8');

if (outcome is SuqoSuccess) {
  // move the buyer on — see "the outcome is a signal to navigate" below
}

That is the whole integration. open pushes a full-screen route and completes with exactly one SuqoOutcome once the buyer is done with it.

Prefer exhaustive handling? when takes a callback per case, so it cannot be written incompletely:

final message = outcome.when(
  success: (s) => 'Paid — ${s.params}',
  failure: (f) => f.status == SuqoFailureStatus.cancelled
      ? 'Buyer backed out'
      : 'Payment rejected: ${f.message}',
  closed: (_) => 'Dismissed before anything settled',
);

Defaults, once #

SuqoProvider is optional and holds configuration only — there is no screen for it to own, because Navigator already gives one that returns a result. Mount it to avoid repeating yourself:

SuqoProvider(
  mode: SuqoMode.sandbox,
  debug: true,
  child: MaterialApp(home: MyApp()),
)

Anything it sets can still be passed per call, and a per-call value always wins.

Options #

Argument Default Notes
sessionId — Required. The checkout session to pay, e.g. cks_9f2c41a8.
mode SuqoMode.sandbox live → app.suqo.ai, sandbox → test.suqo.ai.
baseUrl — Where SUQO's checkout is served from. Overrides mode.
checkoutPath /c/:id The route, with :id for the session id.
title Secure payment Screen header. The amount and seller are rendered by the page.
loadingLabel Loading secure checkout The line under the spinner. Pass '' for a spinner alone.
debug false Turns on this package's redacted logging.
onEvent — A debug stream for your own logs. Not a payment signal.

Sandbox is the default on purpose: an app that names neither mode nor baseUrl cannot take a live payment by accident.

Outcomes #

open completes with one of three, and never more than one per call:

  • SuqoSuccess — the page verified the payment with SUQO's backend. Carries params and an optional message.
  • SuqoFailure — carries a status of failed or cancelled.
  • SuqoClosed — the screen was dismissed before anything settled. Nothing was charged.

Dart 2.18 has no sealed, so an is-chain over these is not checked for exhaustiveness by the compiler. Use when where that matters.

cancelled is not failed #

SuqoFailure.status is cancelled when the buyer backed out at the gateway, and failed when a payment was attempted and rejected. Check it: a buyer who changed their mind wants the basket offered again, while a rejected payment is worth looking into. Nothing is charged in either case.

params is the gateway's, verbatim #

Every gateway names its transaction differently — eSewa a signed data blob, Khalti a pidx, ConnectIPS a TXNID, NPS a MerchantTxnId. params is passed through with no renaming or filtering, because you reconcile against whatever your own backend recorded, and normalising would throw away the only field you can match on.

message is what SUQO's backend said when it settled the payment. It is separate from params so our keys never mix into the gateway's, and it is null when the backend said nothing — so it is never copy this SDK invented. On a failure it is usually the only place the reason exists.

A session that can't be paid at all is not a failure #

There is no onUnavailable. When the session is notFound, expired, spent, noCustomer, noMethods or loadFailed, the page shows its own explanation and the buyer dismisses, which completes as SuqoClosed — routing it through a failure would claim a payment was attempted and rejected when none was. Pass onEvent and watch for an unavailable event if you want to know why in your own logs.

The outcome is a signal to navigate, not proof of fulfilment #

SuqoSuccess only arrives after the checkout page verified the payment with SUQO's backend, so it is a real signal rather than a redirect's claim. Even so, the webhook is authoritative. Treat it as "move the buyer on", and let your own backend decide that an order is paid from its own record.

Android: declare the schemes your gateways use #

When a bank or wallet needs its own app, this package hands the deeplink to the OS. On Android 11+ (API 30) the system will not resolve an intent for a scheme the app has not declared, and a package cannot add manifest entries on your behalf — so add one <intent> block per scheme to your own AndroidManifest.xml:

<queries>
    <intent>
        <action android:name="android.intent.action.VIEW"/>
        <data android:scheme="fonepayApp"/>
    </intent>
</queries>

Without it the launch fails silently and you will see a handoff_failed event. See example/android/app/src/main/AndroidManifest.xml.

Requirements #

Minimum
Flutter 3.3.0
Dart 2.18.0
webview_flutter 4.1.0
url_launcher 6.1.11

Flutter 3.3.0 rather than 3.0.0 because setAcceptThirdPartyCookies arrives in webview_flutter_android 3.7.0, which is the first release requiring it — and the gateway journey crosses sites, so that setting is not optional.

Requirements on the SUQO web app #

This package is one half of a protocol; js-checkout/docs/protocol.md is the contract. It needs nothing SUQO does not already do: the page recognises any native host that exposes window.SuqoWebView.postMessage(string), which is exactly what this package's JavaScript channel is named. The page then delivers suqo:result over that channel, treats the host as a gateway window so it does not follow the seller's return_url inside the screen, and hands off to gateways in the same window rather than opening a popup.

Known difference from @suqo/react-native #

The debug forwarder cannot run before the page's own scripts. React Native injects it with injectedJavaScriptBeforeContentLoaded; webview_flutter has no document-start injection (flutter#105202), so the earliest hook available is onPageStarted, which races the page's first evaluation. A throw during the page's very first moments can be missed. Debug-only, and it does not affect payments.

Development #

flutter analyze
flutter test
dart format lib test

cd example && flutter run

The example runs against the React Native example's mock server unchanged — it is a plain Node script speaking the same protocol. Run npm run mock in suqo-react-native/example and point the example's base URL at the address it prints.

The example's default base URL, http://10.0.2.2:8787, is the Android emulator's route to the host machine, and the example's Android manifest sets usesCleartextTraffic="true" so plain HTTP reaches it. Neither applies on iOS: use http://localhost:8787 on the simulator, and add an App Transport Security exception (NSAppTransportSecurity → NSAllowsLocalNetworking) to example/ios/Runner/Info.plist, or the load is blocked and the sheet shows the generic "Couldn't reach the payment page" panel 12 seconds later with no hint why. Only the example needs this; a real integration talks to app.suqo.ai over HTTPS and needs no exception.

License #

MIT

0
likes
150
points
30
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Collect a SUQO payment on a full-screen checkout screen. No gateway integration, no payment UI to build.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, meta, url_launcher, webview_flutter, webview_flutter_android

More

Packages that depend on suqo_checkout