Handle outcomes
What open() reports, and which of it to trust.
Programmatic API
final outcome = await SuqoCheckout.open(
context,
sessionId: 'cks_9f2c41a8',
); // => Future<SuqoOutcome>
outcome.when(
success: (s) {}, // SuqoSuccess { sessionId, params, message? }
failure: (f) {}, // SuqoFailure { sessionId, status: failed | cancelled, params, message? }
closed: (c) {}, // SuqoClosed { sessionId }
);
Navigator.of(context, rootNavigator: true).pop(); // dismisses it as though the buyer closed itopen completes with exactly one of the three, never more than one per call. There are no callbacks: the awaited outcome is the result. Prefer when to an is-chain — Dart 2.18 has no sealed, so an is-chain that misses a case compiles silently, while when takes a required callback per case.
There is no close(). The screen is an ordinary route on the root navigator, so popping it there completes open as SuqoClosed — the same outcome a buyer gets from the back button or a hardware back press. Pass rootNavigator: true even if your app has one navigator: with a nested one (a go_router ShellRoute, or tabs that each own a Navigator), a plain Navigator.of(context).pop() pops your own screen underneath the checkout instead, and the checkout stays open.
SuqoClosed is not proof nothing was charged
Usually the buyer backed out before paying. But the outcome is delivered the moment the screen goes away, so a payment can still settle after it: the buyer approved in their bank app and then dismissed before the return page reported back, or your own code popped the screen mid-payment. A result that arrives after SuqoClosed is not delivered. Only pop the screen yourself while no payment can be in flight, and before releasing stock or offering the basket again, check the order against your webhook.
A session that cannot be paid at all — expired, already spent, created without a customer — also completes as SuqoClosed, after the buyer dismisses the page's own explanation. No payment was attempted, so it is not a failure. See Debug with events to find out why.
cancelled is not failed
SuqoFailure.status is SuqoFailureStatus.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 empty on a cancellation — there was no transaction to report.
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 (a Map<String, String>, exported as ReturnParams) keeps the gateway's keys exactly as sent, with no renaming, because you reconcile against whatever your own backend recorded. Every value arrives as a String, though: numbers and booleans are converted to text (an amount of 1000.0 arrives as "1000.0"), and null, nested objects and lists are dropped. Compare against the string form, and treat a missing key as possibly a non-text value the gateway sent.
message is what SUQO's backend said when it settled the payment — separate from params so SUQO's keys never mix into the gateway's. It is null when the backend said nothing; on a failure it is usually the only place the reason exists.
The outcome is a signal to navigate, not proof of fulfilment
SuqoSuccess arrives only 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 SuqoSuccess as "move the buyer on", and let your own backend decide an order is paid from its own record. See Receive webhook events.