Android SDK
Present the Qasimha hosted checkout from an Android app with com.qasimha:checkout-android and handle the result.
com.qasimha:checkout-android opens a Qasimha hosted checkout from your Android app and reports how it ended. Your server creates the session and hands the app a paymentUrl; the SDK opens it in a Custom Tab (default) or a hardened WebView, watches for the return to your app and gives you a typed result. It holds no secret key, stores nothing and tracks nothing.
Requirements
| Item | Requirement |
|---|---|
| Language | Kotlin (Java callers supported) |
| Android | minSdk 24, compileSdk 35 |
| Dependencies | AndroidX only: androidx.browser, androidx.appcompat, androidx.core |
| Permissions | android.permission.INTERNET only |
| Server | A Qasimha server SDK that creates the checkout session. See Checkout flows. |
Install
dependencies {
implementation("com.qasimha:checkout-android:0.1.0")
}Add the return-scheme manifest placeholder to your app module. The library manifest needs it to build, even if you only use App Links:
android {
defaultConfig {
manifestPlaceholders["qasimhaReturnScheme"] = "myshop"
}
}If you only use App Links, set it to something unique, for example your application id.
Set up the return into your app
Only Presentation.CUSTOM_TAB needs this: the browser has to bring the shopper back into your app. In WEB_VIEW mode the SDK intercepts the return URLs inside the WebView, so no manifest work is needed.
Your return URLs are https pages on your own domain, verified as App Links into your app. Publish assetlinks.json for the domain as usual.
Either let the library's receiver handle the link by adding an intent filter to it in your manifest (the manifest merger merges by activity name):
<activity android:name="com.qasimha.checkout.QasimhaReturnActivity">
<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>or keep the link on your own activity and forward it:
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val data = intent.data ?: return
if (!QasimhaCheckout.handleReturn(data)) handleMyOwnDeepLink(data)
}handleReturn returns false and does nothing when no checkout is open or the URL is not one of its return URLs, so your other deep links keep working.
Set the placeholder and use <scheme>://qasimha/... return URLs:
manifestPlaceholders["qasimhaReturnScheme"] = "myshop"MerchantUrl(
success = "myshop://qasimha/return",
failure = "myshop://qasimha/return",
cancel = "myshop://qasimha/cancel",
)The library's QasimhaReturnActivity is registered for myshop://qasimha only, so the rest of your scheme is untouched.
Custom schemes are not verified by the OS. Another app can register the same scheme and receive the return URL. Use App Links in production.
Start a checkout
Get paymentUrl from your server, then launch the checkout with the same three return URLs your server sent when creating the session. Use the Activity Result contract: it survives process death.
class CheckoutActivity : AppCompatActivity() {
private val checkout = registerForActivityResult(QasimhaCheckoutContract()) { result ->
when (result) {
is CheckoutResult.Success -> confirmWithMyServer(result.orderId)
is CheckoutResult.Failure -> showFailure(result.status)
CheckoutResult.Cancelled -> showCart()
}
}
fun pay(paymentUrl: String) {
checkout.launch(
CheckoutOptions(
paymentUrl = paymentUrl,
merchantUrl = MerchantUrl(
success = "https://shop.example/qasimha/return?order=1001",
failure = "https://shop.example/qasimha/return?order=1001",
cancel = "https://shop.example/cart",
),
presentation = Presentation.CUSTOM_TAB,
language = "ar",
),
)
}
}A callback API also exists: QasimhaCheckout.start(activity, options) { result -> ... } (from Java: QasimhaCheckout.start(this, options, result -> { ...; return Unit.INSTANCE; })). The callback lives in memory: if Android kills the process while the checkout is open, it is lost. Prefer the contract.
CheckoutOptions
CheckoutOptions| Parameter | Rule |
|---|---|
paymentUrl | Absolute https URL from your server. The SDK appends lang= from language. |
merchantUrl.success / .failure / .cancel | Absolute, no fragment, at most 1024 characters. https with a host, or a custom app scheme of at least 3 characters. http, javascript, data, file, blob, vbscript, intent, content, android-app and about are refused. |
presentation | CUSTOM_TAB (default) or WEB_VIEW. |
language | "ar" (default) or "en". |
webViewAllowedHosts | WEB_VIEW only. Extra hosts the page may visit (payment provider, Nafath). Exact host names, or *.example.com for every subdomain (never the apex). Default empty. |
An invalid value throws IllegalArgumentException from the constructor.
Handle the result and confirm on your server
| Result | Meaning |
|---|---|
Success(orderId, orderReferenceId) | The return URL said status=confirmed and carried an orderId. |
Failure(status, orderId, orderReferenceId) | declined, expired or failed from the checkout, or an SDK status: ssl_error, network_error (WebView), no_browser (no browser for the Custom Tab), malformed (confirmed without an orderId), unknown (no status). Other statuses pass through lowercased. |
Cancelled | status=cancelled, the cancel URL without a status, the shopper closed the Custom Tab, or pressed back out of the WebView. |
Do not show "paid" on any result. On Success, send orderId to your server. Your server calls GET /api/v1/merchant/orders/{orderId} (orders.get in the server SDKs) with its secret key and fulfils only if the order is paid. Your server also receives the webhook for the same order.
If the process was killed while the Custom Tab was open, there is no pending checkout. The App Link then arrives at your activity as a normal deep link with orderId, orderReferenceId and status in the query, and your server can still confirm the order.
Custom Tab or WebView
CUSTOM_TAB(default). The system browser sheet. The shopper gets the platform's TLS, autofill and cookie isolation, and the journey's third-party pages (payment provider, Nafath app switch, App Links into the Qasimha app) work as in a browser. Use this unless you have a reason not to.WEB_VIEW(opt-in).QasimhaCheckoutActivityhosts a hardenedWebView: no file or content access, no mixed content, noaddJavascriptInterface, Safe Browsing on, third-party cookies off, permission prompts denied. Onlyhttpshosts on the allow-list load (thepaymentUrlhost, your return hosts andwebViewAllowedHosts); everything else, includingintent://and app-switch schemes, is dropped. A Nafath app hand-off does not work here; the Nafath web flow does when its host is allow-listed. A TLS error ends withFailure("ssl_error").
Promo view
QasimhaPromoView shows "Split into 4 monthly payments of [riyal symbol] 300" on product and cart screens. It uses your publishable key only.
<com.qasimha.checkout.QasimhaPromoView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:qasimhaPublicKey="pk_test_..."
app:qasimhaAmount="1200"
app:qasimhaLang="ar" />promo.publicKey = "pk_test_..."
promo.amount = "300.50"
promo.lang = "en"
promo.textView.textSize = 16f
promo.refresh()- Calls
GET /api/v1/widget/planswith theX-Public-Keyheader.pk_test_keys go to UAT,pk_live_keys to production. - Fails closed: the view is
GONEwhen the key or amount is invalid, the request fails, or the amount is outside the plan range. It never invents a number. - Nothing is cached; every attribute change re-fetches. Style it through
textView, and callrefresh()after changing the text size.
Release builds
- R8 / ProGuard: the AAR ships
consumer-rules.pro. It keeps the public API (com.qasimha.checkout.*and theCheckoutResultsubclasses) and lets R8 shrink and obfuscate the internals with your app. You do not need to add rules. - The SDK uses no reflection. No mapping file or crash report leaves your app through the SDK.
Security
- No secret on the device. Keep
sk_test_.../sk_live_...on your server. The app only ever seespaymentUrland, for the promo view, apk_key. - No storage, analytics, advertising id or fingerprinting. No certificate pinning; TLS validation is the platform's.
- WebView cookies: the WebView shares your app's WebView cookie jar, as any WebView does. The SDK does not read or persist it.
- PCI DSS: no card data passes through the SDK or your app. You stay in SAQ-A scope.
- App hardening: root detection, anti-tamper, anti-hooking and RASP are recommended for apps that take payments. The SDK does not perform them.
Troubleshooting
Build fails: manifest placeholder qasimhaReturnScheme is missing
Add manifestPlaceholders["qasimhaReturnScheme"] to defaultConfig. It is required even when you only use App Links.
The checkout ends as Cancelled after the shopper paid
With App Links, an unverified link opens in the browser instead of your app, and the checkout ends as Cancelled when the shopper closes the sheet. Check that assetlinks.json is published and that the intent filter has android:autoVerify="true". Then confirm the order on your server anyway: the payment may have succeeded.
IllegalArgumentException when building CheckoutOptions
A return URL breaks a rule: http, a fragment, more than 1024 characters, or a refused scheme such as intent. Use https or a custom scheme of at least 3 characters.
Failure with status no_browser
The device has no browser to open the Custom Tab. Ask the shopper to install or enable one.
The WebView journey stops at the payment provider or Nafath
The WebView only loads allow-listed https hosts. Add the hosts Qasimha confirms for your journey to webViewAllowedHosts, or switch to CUSTOM_TAB. Nafath app hand-off never works in the WebView.
Failure with status ssl_error in WebView mode
The WebView hit a TLS error, cancelled the request and ended the checkout. This is deliberate: there is no option to ignore TLS errors. Confirm the order on your server before retrying with a new session.
The promo view is not shown
It fails closed. Check that the key matches pk_test_ or pk_live_, the amount is a positive number with at most two decimals, and the amount is within your plan's minimum and maximum.
Related
Updated 3 days ago
