Flutter SDK
Present the Qasimha hosted checkout from a Flutter app with the qasimha_checkout plugin and handle the result.
qasimha_checkout presents the Qasimha hosted checkout from a Flutter app in the system browser sheet and tells you how it ended. Your server creates the session and receives a paymentUrl; your app hands it to the plugin. The plugin holds no secret and makes no API call with a secret key.
Requirements
| Item | Requirement |
|---|---|
| Flutter | 3.38 or newer (Dart SDK 3.10) |
| Android | API 24; presents with Custom Tabs (androidx.browser) |
| iOS | 15.0; presents with SFSafariViewController, or ASWebAuthenticationSession on request |
| Server | A Qasimha server SDK that creates the checkout session. See Checkout flows. |
Install
flutter pub add qasimha_checkoutSet up the return into your app
Use your own HTTPS pages (Android App Links, iOS Universal Links) when you can: they are verified against your domain. A custom scheme (myshop://qasimha/return) is simpler but any app can claim it. Both work with the same merchantUrl.
- Declare the return-scheme placeholder in
android/app/build.gradle.kts. It is required even if you only use App Links (any unused value is fine then):
android {
defaultConfig {
manifestPlaceholders["qasimhaReturnScheme"] = "myshop"
}
}The plugin's QasimhaReturnActivity receives myshop://… links, hands them to the pending checkout and brings your activity back on top, which closes the Custom Tab.
- For App Links (
httpsreturn URLs), add the intent filter to your activity, host/.well-known/assetlinks.jsonon the return host, and setandroid:launchMode="singleTask"so the link reaches the running instance:
<activity android:name=".MainActivity" android:launchMode="singleTask">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="shop.example" android:pathPrefix="/qasimha/return" />
</intent-filter>
</activity>- If the activity that receives the link is not a
FlutterActivity/FlutterFragmentActivity, forward it:
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
intent.data?.let { QasimhaCheckoutPlugin.handleReturn(it) }
}- Custom scheme: register it in
ios/Runner/Info.plist.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key><string>com.example.shop</string>
<key>CFBundleURLSchemes</key><array><string>myshop</string></array>
</dict>
</array>-
Universal Links: add the Associated Domains capability (
applinks:shop.example) and serve/.well-known/apple-app-site-associationon the return host. -
With
FlutterAppDelegateandFlutterSceneDelegatenothing else is needed: the plugin registers for the open-URL and continue-user-activity callbacks itself. If your delegates do not derive from Flutter's, callQasimhaCheckoutPlugin.handleReturn(url)from them.
If your app intercepts links itself, call QasimhaCheckout.handleReturn(uri) from Dart. It returns true when the URI completed the pending checkout and false when it is not a Qasimha return, so your own router can take it.
Start a checkout
Get paymentUrl from your server and pass it with the same three return URLs your server sent when creating the session.
import 'package:qasimha_checkout/qasimha_checkout.dart';
final result = await QasimhaCheckout.start(
CheckoutOptions(
paymentUrl: Uri.parse(session.paymentUrl),
merchantUrl: MerchantUrl(
success: Uri.parse('https://shop.example/qasimha/return?order=1001'),
failure: Uri.parse('https://shop.example/qasimha/return?order=1001'),
cancel: Uri.parse('https://shop.example/cart'),
),
presentation: Presentation.systemBrowser,
language: 'ar',
),
);
switch (result) {
case CheckoutSuccess(:final orderId):
final order = await myServer.confirmQasimhaOrder(orderId);
if (order.isPaid) showThankYou();
case CheckoutFailure(:final status):
showNotCompleted(status);
case CheckoutCancelled():
backToCart();
}CheckoutOptions
CheckoutOptions| Field | Rules |
|---|---|
paymentUrl | Absolute https URL from the API. http only for localhost / 127.0.0.1. |
merchantUrl | The URLs the session was created with. Each absolute, at most 1024 characters, no fragment; https, http on localhost, or a custom app scheme of at least 3 characters (never javascript, data, file, blob, vbscript). cancel defaults to failure. |
presentation | systemBrowser (default) or webAuthenticationSession (iOS; the same as systemBrowser on Android). |
language | ar (default) or en. Appended to paymentUrl as lang unless it already has one. |
start throws ArgumentError for invalid options, StateError while another checkout is in progress, and CheckoutException (no_activity, no_browser, no_view_controller, unsupported_presentation, unsupported_platform) when the platform cannot present the checkout.
On iOS, webAuthenticationSession shows the system consent sheet and needs custom-scheme return URLs below iOS 17.4 (https callbacks work from 17.4). The three URLs must share a scheme, and for https a host and path.
This plugin has no embedded WebView mode. The checkout redirects to the payment gateway and the Nafath app switch, which the system browser handles with the platform's TLS, autofill and cookie isolation. If you must keep the checkout in-app, the native Android and iOS SDKs offer a hardened WebView.
Handle the result and confirm on your server
| Result | When | Fields |
|---|---|---|
CheckoutSuccess | status=confirmed and an orderId | orderId, orderReferenceId? |
CheckoutFailure | declined, expired or failed; unknown when the URL had no recognised status; malformed when it said confirmed without an order id | status, orderId?, orderReferenceId? |
CheckoutCancelled | status=cancelled, the cancel URL, or the shopper dismissed the sheet | orderId?, orderReferenceId? |
Treat CheckoutSuccess as "the shopper came back on the success URL". Only your server, calling GET /api/v1/merchant/orders/{orderId} with your secret key, tells you the order is approved. Your server also receives the webhook. A cancelled order stays awaiting checkout; cancel it through the API if you wish.
Promo widget
QasimhaPromo shows "Split into 4 monthly payments of [riyal symbol] 300" with your publishable key.
QasimhaPromo(amount: 1200, publicKey: 'pk_test_...', lang: 'ar')- Calls
GET /api/v1/widget/planswith theX-Public-Keyheader and a 5-second timeout.pk_test_keys go to UAT,pk_live_keys to production.apiBaseUrloverrides the host (https, orhttpon localhost). - Fails closed: a zero-size box, never a computed number, when the key or amount is invalid, the request fails, or the amount is outside the plan range.
RiyalAmount,RiyalAmount.spanandRiyalSymbolare exported for your own screens that show Qasimha amounts.- Sends only the amount and the publishable key. No cookies, no storage.
Release builds
- Android: consumer R8 rules are shipped; the plugin uses no reflection. You do not need extra ProGuard rules.
- iOS: symbols are stripped with your release build. The plugin ships a privacy manifest declaring no tracking and no collected data.
Security
- No secret on the device. The session is created on your server. The only credential the plugin holds is the
pk_key for the promo widget. - TLS validation is the platform's. No certificate pinning and no option to disable validation.
- No analytics, fingerprinting or storage.
- PCI DSS: the plugin never sees card data. You stay in SAQ-A scope.
- App hardening: root and jailbreak detection, anti-tamper and runtime protection are recommended for apps that handle payments. They are your app's responsibility.
Troubleshooting
Android build fails on qasimhaReturnScheme
Add manifestPlaceholders["qasimhaReturnScheme"] to defaultConfig in android/app/build.gradle.kts, even if you only use App Links.
A second app instance opens after payment on Android
Set android:launchMode="singleTask" on the activity with the App Links intent filter. The link then reaches the running instance and clears the Custom Tab.
StateError when calling start
Another checkout is still in progress. Only one can be open at a time.
CheckoutException no_browser
Without a Custom Tabs provider the plugin uses the default browser. With no browser at all, it throws no_browser.
The return is not detected on iOS
If your app delegate or scene delegate does not derive from FlutterAppDelegate / FlutterSceneDelegate, call QasimhaCheckoutPlugin.handleReturn(url) from it. For webAuthenticationSession, use a custom scheme below iOS 17.4.
The promo widget is empty
It fails closed. Check the key starts with pk_test_ or pk_live_, the amount is positive, and the amount is within your plan range.
Related
Updated 3 days ago
