@qasimha/react-native-sdk
v0.1.1
Published
React Native checkout module for Qasimha: presents a hosted checkout session in the system browser sheet, reports the outcome, and offers an instalment promo component.
Maintainers
Readme
@qasimha/react-native-sdk presents the Qasimha hosted checkout in the system browser sheet, detects the return to your app and resolves a typed result. Your server creates the session and hands the app a paymentUrl. The module also ships QasimhaPromo, an instalment summary for product, cart and checkout screens. No secret key lives on the device.
Requirements
| Item | Requirement |
| :--- | :--- |
| React Native | 0.72 or later |
| React | 18 or 19 |
| Android | minSdk 24, AndroidX, Kotlin 1.9+, a browser with Custom Tabs (falls back to the default browser) |
| iOS | 13.4 or later, Swift 5.9. ASWebAuthenticationSession with an https return URL needs iOS 17.4. |
| Optional | react-native-svg 13+ for the riyal symbol and logo in QasimhaPromo |
| Server | A Qasimha server SDK that creates the checkout session. See Checkout flows. |
Install
npm install @qasimha/react-native-sdk
cd ios && pod installAutolinking registers the native module on both platforms. Under the New Architecture it runs through the interop layer (enabled by default from React Native 0.74).
Set up the return into your app
App Links and Universal Links (https) are the secure choice. Custom schemes are simpler but can be claimed by other apps.
- Declare the return scheme in
android/app/build.gradle. The build fails without it; set any unused value if you only use App Links.
android {
defaultConfig {
manifestPlaceholders = [qasimhaReturnScheme: "myshop"]
}
}myshop://qasimha/return then reaches your app.
- For App Links, add a verified intent filter to your main activity and publish
/.well-known/assetlinks.jsonon that host:
<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>- Keep
android:launchMode="singleTask"on the main activity (the React Native template default). The return arrives throughonNewIntent, which also closes the Custom Tab.
- Custom scheme: add it to
Info.plist.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array><string>myshop</string></array>
</dict>
</array>Universal Links: add the Associated Domains capability (
applinks:shop.example) and publish/.well-known/apple-app-site-associationon that host.Forward links to React Native's
LinkinginAppDelegate:
import React
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
return RCTLinkingManager.application(app, open: url, options: options)
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
return RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
}SFSafariViewController opens a Universal Link into your app only from a user tap. The hosted checkout shows a "Return to store" button on every final screen, so the shopper's tap brings the result back. Custom-scheme URLs and presentation: 'webAuthenticationSession' return without a tap.
Then pass every incoming link to handleReturn:
import { useEffect } from 'react';
import { Linking } from 'react-native';
import { handleReturn } from '@qasimha/react-native-sdk';
function useQasimhaReturn() {
useEffect(() => {
const subscription = Linking.addEventListener('url', ({ url }) => handleReturn(url));
Linking.getInitialURL().then((url) => url && handleReturn(url));
return () => subscription.remove();
}, []);
}handleReturn(url) returns true when the URL matched the pending checkout (the checkout resolves and the sheet closes), and false otherwise. Keep your own deep-link handling for the false case.
Start a checkout
Get paymentUrl from your server and pass the same three return URLs your server sent to POST /api/v1/pay/session/create.
import { startCheckout } from '@qasimha/react-native-sdk';
async function pay(paymentUrl: string) {
const result = await startCheckout({
paymentUrl,
merchantUrl: {
success: 'https://shop.example/qasimha/return?order=1001',
failure: 'https://shop.example/qasimha/return?order=1001',
cancel: 'https://shop.example/cart',
},
language: 'ar',
});
switch (result.type) {
case 'success':
await confirmOnServer(result.orderId);
break;
case 'failure':
showDeclined(result.status);
break;
case 'cancelled':
break;
}
}Options
| Option | Notes |
| :--- | :--- |
| paymentUrl | Required. The paymentUrl from session creation; https only. |
| merchantUrl | Required: success, failure, cancel. Each absolute, at most 1024 characters, no fragment; https, http on localhost / 127.0.0.1, or a custom app scheme of at least 3 characters (javascript, data, file, blob, vbscript refused). |
| presentation | systemBrowser (default: Android Custom Tabs, iOS SFSafariViewController) or webAuthenticationSession (iOS only; ephemeral, no shared cookies, needs no Linking wiring; the three URLs must share one custom scheme, or one https host and path on iOS 17.4+). |
| language | ar or en. Sent as Accept-Language to the Custom Tab. The hosted checkout follows the shopper's browser language and offers an in-page toggle; this hint does not override it today. |
Give the cancel URL a dedicated path. Matching is a prefix on the path at a segment boundary, so a root-path return URL matches every link on that host.
There is no embedded WebView mode in this module. Use the native Android or iOS SDK if you need one.
Handle the result and confirm on your server
type CheckoutResult =
| { type: 'success'; orderId: string; orderReferenceId?: string }
| { type: 'failure'; status: string; orderId?: string; orderReferenceId?: string }
| { type: 'cancelled' };success:status=confirmedwith anorderId.failure:declined,expiredorfailed;unknownwhen the return URL matched but had no usable status, orconfirmedwithout anorderId.cancelled:status=cancelled, the cancel URL without a status, or the shopper dismissed the sheet. A dismissed sheet resolves after a 300 ms grace period so a return URL still in flight throughLinkingwins.
A return URL is navigation, not proof. Before showing "paid" or fulfilling, your server reads the order with its secret key (orders.get in the server SDKs, GET /api/v1/merchant/orders/{orderId}) and acts on the status it returns. Your server also receives the webhook, the durable record. Never change an order from the return URL parameters alone.
Rejections are QasimhaCheckoutError with a code: INVALID_OPTIONS (with field), IN_PROGRESS, NATIVE_MODULE_MISSING, NO_ACTIVITY, NO_VIEW_CONTROLLER, NO_BROWSER, UNSUPPORTED, PRESENTATION_FAILED, HOST_DESTROYED, INVALID_URL, NATIVE_ERROR.
Promo component
import { QasimhaPromo } from '@qasimha/react-native-sdk';
<QasimhaPromo amount={1200} publicKey="pk_test_..." lang="ar" textStyle={{ fontSize: 16, color: '#1f2937' }} />| Prop | Notes |
| :--- | :--- |
| amount | Required. Positive, at most two decimals. |
| publicKey | Required. pk_test_... or pk_live_.... |
| lang | ar (default) or en. |
| apiBaseUrl | Defaults by key prefix (pk_test_ to UAT, pk_live_ to production). https; http for localhost only. |
| renderSymbol | Your own riyal symbol node. |
| showLogo | Default true. |
| onLearnMore | Adds a "Learn more" link that opens your own explainer. |
| onPlan | Called with the headline plan, or null when nothing renders. Use it to hide a surrounding card. |
- Calls
GET /api/v1/widget/planswithX-Public-Key, a 5-second timeout, no cookies, no storage. - Fails closed: renders nothing, never a computed number, on invalid input, a failed request or an out-of-range amount.
- Place it on product, cart and checkout screens, not in every list row: each mounted instance makes one request.
Release builds
- Android: consumer R8 rules are shipped; nothing to add.
- iOS: symbols are stripped in your release build with the rest of the app. The iOS module ships a privacy manifest declaring no tracking.
- Metro and
react-native-svg: the module loadsreact-native-svgwith an optionalrequire. Metro'sresolver.allowOptionalDependenciesisfalseby default, so an app withoutreact-native-svgmay fail to bundle. Installreact-native-svgor set that option totrue.
Security
- No secret on the device. The only credential in the app is the publishable key for the promo, which the API scopes to reading plans.
- TLS validation is the platform's; no option to disable it and no certificate pinning.
- No storage, analytics or fingerprinting. Nothing is written to logs.
- PCI DSS: card entry happens on the hosted checkout and the payment provider. You stay in SAQ-A scope.
- App hardening: root/jailbreak detection, anti-tamper and runtime protection are recommended for apps that present payments. Use a RASP product or the platform's integrity APIs; the module does not perform them.
Troubleshooting
Add manifestPlaceholders = [qasimhaReturnScheme: "..."] to defaultConfig in android/app/build.gradle, even if you only use App Links.
Install react-native-svg 13+, or set Metro's resolver.allowOptionalDependencies to true. Without it, use renderSymbol to draw the riyal symbol.
The return link did not reach handleReturn. Check the Linking listener and getInitialURL() call, the AppDelegate forwarding on iOS, and singleTask on Android. On iOS with Universal Links, the shopper must tap "Return to store". Confirm the order on your server anyway.
A checkout is already open. Only one can run at a time.
Read field on the error. Usually a return URL has a fragment, uses http outside localhost, or uses a refused scheme.
The native module is not linked. Rebuild the app after install, and run pod install in ios/.
Expected today. The hosted checkout follows the shopper's browser language and offers an in-page toggle.
