Skip to main content

esaalat 0.2.0 copy "esaalat: ^0.2.0" to clipboard
esaalat: ^0.2.0 copied to clipboard

Esaalat hosted checkout for Flutter — create a checkout, let the buyer pay in a WebView, and receive a verified result.

esaalat #

Flutter checkout for Esaalat. One branded button starts a checkout, shows the hosted portal in a WebView, and reports the outcome to the callback you care about — same option names, same status strings and same error codes as esaalat-react on the web.

EsaalatCheckoutButton(
  publishableKey: 'pk_live_...', // Dashboard → API Keys
  amount: '5000.00',
  externalOrderId: order.id,
  onVerified: (result) => markOrderPaid(order.id, result.intentCode),
  onCancelled: (_) => setStatus('cancelled'),
  onWrongAmount: (result) => setStatus('paid ${result.paidAmount}'),
)

That is the whole integration: the button creates the session, pushes the full-screen checkout route, locks itself while the checkout is open, and routes the terminal result to exactly one of onVerified, onCancelled, onWrongAmount, onExpired or onFailed. Only a checkout that cannot start goes to onError.

Install #

flutter pub add esaalat

Until the package is on pub.dev, use a path dependency:

dependencies:
  esaalat:
    path: ../sdk/flutter

Android needs the android.permission.INTERNET permission (any app that talks to an API already declares it). The package uses webview_flutter; see its platform requirements (Android 5.0+, iOS 12+).

The button #

EsaalatCheckoutButton renders the Esaalat lockup on the brand's black pill — "Pay with Esaalat" / «ادفع عبر إيصالات» — and needs no client or route management in your app.

EsaalatCheckoutButton(
  publishableKey: 'pk_live_...',
  amount: '5000.00',
  externalOrderId: order.id,
  onVerified: (result) => setState(() => _result = result),
)

Props #

Prop Type Notes
publishableKey String Required to create a session; omit it when sessionToken is used. Safe to embed.
amount String? Order total. Required unless items or sessionToken is used.
items List<CheckoutItem> Order lines; when present they determine the amount.
currency String? Defaults to YER server-side.
externalOrderId String? Your order reference, echoed back in the result.
notes String? Internal merchant notes.
wallet String? Optional preselected receiving wallet id.
locale EsaalatLocale Portal language. Defaults to Arabic.
sessionToken / portalUrl String? Open a session created by your server. Takes precedence over amount / items.
idempotencyKey String? Stable retry key; generated per checkout when omitted.
simulateResult CheckoutSimulation? Demo-only outcome for keys that allow simulation.
apiBaseUrl String Override for sandbox/local.
portalBaseUrl String Portal origin when the API returns no portal_url.
label String? Replaces the branded lockup with your own text.
enabled bool Lock the button. It is also locked automatically while its checkout is open.

Callbacks #

Exactly one result callback runs per outcome — the status of the checkout result decides which, so there is nothing to switch on:

Callback Fires when
onVerified A wallet alert matched the payment.
onCancelled The buyer dismissed the checkout, or the intent was cancelled.
onWrongAmount A payment matched for a different amount; read result.paidAmount.
onExpired The buyer ran out of time.
onFailed The portal could not load a valid session.
onError The checkout could not start (EsaalatCheckoutError with a code).

Every result callback receives a CheckoutResult; the status table is below.

A result is not a licence to fulfil. Confirm the order with the signed payment.verified webhook, or GET /api/v1/merchant/intents/{id}/, on your server before shipping anything.

Lock the amount on your server #

The publishable key can start a session with any amount because the amount lives in the app. If it must be authoritative, create the session from your server with the client JWT and hand only its token to the app:

POST /api/v1/checkout/merchant/sessions/
Authorization: Bearer <access_token>
Content-Type: application/json

{"amount": "5000.00", "external_order_id": "order-123"}

Then let the button open it — no key is needed for that path:

EsaalatCheckoutButton(
  sessionToken: session.sessionToken,
  portalUrl: session.portalUrl, // optional; the SDK builds it if omitted
  onVerified: onVerified,
)

The token is a capability: do not log it, and give it only to the buyer who is paying that order.

Imperative client #

When the button's layout or lifecycle does not fit (a custom row, a checkout started from a deep link, a bottom-sheet trigger), use the client directly. It is the same checkout the button drives; only the wiring is yours.

final checkout = EsaalatCheckout(publishableKey: 'pk_live_...');

final result = await checkout.open(
  context, // used to push the checkout route
  amount: '5000.00',
  currency: 'YER',
  externalOrderId: 'order-123',
);

if (result.isVerified) {
  // Show the buyer a success state. Confirm the order server-side first.
}

open() needs a mounted BuildContext and pushes a full-screen route. The future completes when the route closes; if the buyer closes it, the result is cancelled / dismissed. Call checkout.dispose() from the owning widget's dispose().

Constructor #

Option Default Notes
publishableKey '' Required to create a session; optional when every open() passes sessionToken. Safe to embed: it can only create sessions.
locale EsaalatLocale.ar Portal language.
apiBaseUrl https://api.esaalat.com/api/v1 Override for sandbox/local.
portalBaseUrl https://esaalat.com Fallback only; the API's portal_url wins.
httpClient http.Client() Custom transport (tests, proxies).
dispose() — Releases the SDK's HTTP client.

open() options #

Option Notes
amount Order total (decimal string). Required unless items or sessionToken is used.
items List<CheckoutItem>; determines the amount.
currency Defaults to YER server-side.
externalOrderId Your order reference, echoed back in the result.
notes Internal merchant notes.
wallet Optional preselected receiving wallet id.
sessionToken Open a server-created session instead of creating one.
portalUrl Portal URL for that session (optional; the SDK builds it).
idempotencyKey Stable retry key; generated per call when omitted.
locale Overrides the client locale for this checkout.
simulateResult Demo-only outcome; refused unless the key allows simulation.
pollInterval Fallback polling cadence. Defaults to 4 seconds.

Session creation, timeout and API failures throw EsaalatCheckoutError with a code of configuration, network, api, session, unavailable or aborted; a checkout that starts but ends badly is a result, not an exception.

Result #

status Meaning
CheckoutResultStatus.verified A wallet alert matched the order and the amount was verified.
CheckoutResultStatus.wrongAmount A payment matched but the amount differs; paidAmount says what arrived.
CheckoutResultStatus.expired The buyer did not finish before the payment window closed.
CheckoutResultStatus.cancelled The buyer closed the checkout, or the intent was cancelled.
CheckoutResultStatus.failed The portal could not load a valid session.

reason carries the web SDK's reasons (dismissed, intent_cancelled, session_expired, session_not_found, ...).

How the WebView is resolved #

The portal is loaded top-level, exactly as a standalone visitor sees it. A WebView has no window.parent / window.opener, so the web SDK's iframe handshake does not apply — and a wrapper iframe or an injected shim is not reliable (iOS gives loadHtmlString an opaque origin, and webview_flutter has no document-start injection). The SDK therefore uses the documented native path: the portal posts its terminal result to the EsaalatNativeBridge JS channel this SDK registers, so the route closes as soon as the payment is matched; it also polls GET /checkout/public/sessions/{token} with the portal's own result mapping as the fallback, for a page reload or a missed message. See CONTRACT.md §7 for the full decision and its consequences (the contract itself is owned by the checkout portal).

The route closes after a verified payment (1.8 s, so the buyer sees the success screen) or immediately for other terminal states. Network hiccups while polling are ignored; a session the backend does not know settles as failed / session_not_found.

Local development #

# backend — builds checkout links from FRONTEND_URL
FRONTEND_URL=http://localhost:3000

# web app
cd fe/esaalat && npm run dev
EsaalatCheckoutButton(
  publishableKey: 'pk_...',
  amount: '5000.00',
  apiBaseUrl: 'http://10.0.2.2:8000/api/v1', // Android emulator
  portalBaseUrl: 'http://10.0.2.2:3000',
  onVerified: onVerified,
)

Cleartext HTTP is blocked by default on Android 9+; enable it for the debug build (android:usesCleartextTraffic="true") or run the API behind HTTPS.

Demo mode #

simulateResult asks the backend to resolve the session with a chosen outcome (verified, underpaid, overpaid, cancelled, expired) as soon as a wallet is selected. It only works for publishable keys whose client has allow_checkout_simulation enabled — a normal production key cannot create a simulated session. It is an open() option; the imperative client is where it belongs.

Example #

See example/ for a complete app with an amount field and a result card built on the button. Generate its platform folders once with flutter create . inside example/, then run it with your publishable key:

cd example
flutter create . --platforms=android,ios
flutter run --dart-define=ESAALAT_PUBLISHABLE_KEY=pk_...

License #

MIT © Esaalat

0
likes
0
points
735
downloads

Publisher

verified publisheresaalat.com

Weekly Downloads

Esaalat hosted checkout for Flutter — create a checkout, let the buyer pay in a WebView, and receive a verified result.

Homepage
View/report issues

Topics

#payments #checkout #esaalat #yemen

License

unknown (license)

Dependencies

flutter, http, webview_flutter

More

Packages that depend on esaalat