React SDK
Add Qasimha checkout, return handling and instalment plans to a React app with @qasimha/react.
@qasimha/react presents the Qasimha hosted checkout from a React app, reads the result when the shopper comes back, and shows instalment plans. Your server creates the checkout session with a server SDK; this package never sees your secret key.
Requirements
| Item | Requirement |
|---|---|
| Package | @qasimha/react (npm) |
| React | 18.2+ or 19. Hooks and function components, StrictMode-safe. |
| Module format | ESM with TypeScript types. No runtime dependency besides React. |
| Key in the browser | Publishable key only: pk_test_... (UAT) or pk_live_... (production) |
| Server | A backend using a Qasimha server SDK (@qasimha/sdk, qasimha/php-sdk, Qasimha.Sdk or com.qasimha:qasimha-sdk) |
Install
npm install @qasimha/reactConfigure the provider
Wrap your app once in QasimhaProvider. Load the configuration from your own backend so keys and return URLs live in one place:
// main.tsx
import { createRoot } from 'react-dom/client';
import { QasimhaProvider, type QasimhaConfig } from '@qasimha/react';
const loadConfig = (): Promise<QasimhaConfig> =>
fetch('/api/qasimha/config').then((r) => r.json());
createRoot(document.getElementById('root')!).render(
<QasimhaProvider loadConfig={loadConfig} fallback={<Spinner />}>
<App />
</QasimhaProvider>,
);Or pass a static config:
<QasimhaProvider config={{ publicKey: 'pk_test_...', language: 'ar', merchantUrl: { success, failure, cancel } }}>The provider shows fallback until the config loads, then renders the children. The config is read once, when the provider mounts. A failed loader or invalid config goes to onError if you pass it; otherwise it is thrown to the nearest error boundary.
Provider props: config or loadConfig, fallback, onError, and nonce (CSP nonce for the small stylesheet the components add to <head>).
| Option | Default | Notes |
|---|---|---|
publicKey | none | pk_test_... or pk_live_.... Needed for <QasimhaPromo>, <QasimhaPaymentOption> and usePlans. A secret key (sk_...) is refused. |
language | 'ar' | 'ar' or 'en'. Change at runtime with useQasimha().setLanguage(language). |
environment | from the key | 'sandbox' or 'production'. Must agree with the key prefix. |
apiBaseUrl | from the environment | https only (http for localhost). |
merchantUrl | none | The same success, failure, cancel URLs your server sends when it creates the session. Lets the SDK recognise the return, also inside the popup. |
presentation | 'redirect' | Default for pay(): 'redirect' or 'popup'. |
widget | none | scriptUrl and optional integrity. Loads the on-site widget and keeps it in step with the SDK. |
useQasimha().update(patch) changes other options at runtime. A new key or environment moves plan quotes and the widget to the matching API host.
Start checkout
Your createSession function
createSession functioncreateSession calls your backend, never Qasimha. Send only product IDs and quantities:
const createSession = () =>
fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: cart }),
}).then((r) => r.json()); // -> { paymentUrl, orderId }Your backend prices the basket from its own catalogue, calls checkout.createSession with the secret key and the same merchantUrl set, and returns paymentUrl and orderId. See Checkout flows. The hosted checkout expires after 2 hours.
With the components
<QasimhaPaymentOption amount={total} selected={payWithQasimha} onSelectedChange={setPayWithQasimha} />
<QasimhaPayButton
createSession={createSession}
presentation="redirect"
onResult={onResult}
onError={onError} /><QasimhaPaymentOption> is the Qasimha entry for your payment-method list. Without selected, it keeps its own selection.
With the hook
const { pay, popupState, reopen, cancel } = useQasimhaCheckout();
const onPay = async () => { // call from a click handler
const result = await pay({ createSession, presentation: 'popup' });
// redirect: the page has navigated away (result is null)
// popup: result is { type: 'success' | 'failure' | 'cancelled', ... }
};
Popup mode: callpay()firstDo not
awaitanything beforepay()in the click handler. The popup window must open inside the click, or the browser blocks it. The SDK opens the window before it runscreateSession. If the browser still blocks it, the SDK falls back to redirect.
redirect (default) | popup | |
|---|---|---|
| What happens | The page goes to the hosted checkout and comes back to your merchantUrl. | Checkout opens in a window above your page. |
| Where the result arrives | Your return page (useQasimhaReturn()) | pay() resolves and onResult fires in the original tab |
While the popup is open, <QasimhaPayButton> shows an overlay with "Open it again" and "Cancel". In your own UI, use reopen(), cancel() and popupState (idle, opening, open, lost).
There is no iframe mode. In popup mode, the popup lands on your return URL and your app (started by QasimhaProvider) passes the result to the original tab and closes the popup.
Handle the return
Qasimha appends orderId, orderReferenceId and status to your return URL. status is confirmed, declined, expired, failed or cancelled. See Return URLs.
function ReturnPage() {
const qasimha = useQasimhaReturn();
if (!qasimha) return null; // settling (at most about 0.4 s)
// qasimha.result:
// { type: 'success', orderId, orderReferenceId }
// { type: 'failure', status, ... }
// { type: 'cancelled' }
// null
// qasimha.relayedToOpener: true inside the popup -> show "you can close this window"
}useQasimhaCheckout().parseReturn() reads the same values on any page without side effects, for example on your cart page for the cancel URL.
Confirm on your server
The result above is only what the URL says. Before you show "paid" or fulfil, ask your backend to read the order with the secret key:
// your server, with @qasimha/sdk
const order = await qasimha.orders.get(orderId);
const paid = order.status === 'Confirmed';// your return page
const res = await fetch(`/api/orders/${orderId}`);
const order = await res.json(); // your own response, e.g. { status, paid }An order that completes the hosted checkout is already Confirmed. The demo backend also treats FullyFulfilled and PartiallyFulfilled as paid. Your server should also process the signed webhook, which is the durable record. See Order lifecycle.
Plans and promo components
| Component or hook | Use |
|---|---|
<QasimhaPromo amount={price} /> | Bordered instalment badge |
<QasimhaPromo amount={price} variant="compact" /> | One line: 4 × [symbol] 300, Learn more |
<QasimhaPromo amount={total} variant="text" /> | Text-only line |
usePlans(amount, lang?) | Same data for your own layout: plans, loading, language |
<QasimhaAmount amount={1200} /> | Riyal symbol left of the number, in a left-to-right span |
"Learn more" opens a modal with the full schedule for every plan. <QasimhaPromo> renders nothing, never a computed number, when there is no publishable key, the amount is outside your account's range, or the quote fails. Quotes use fetch with credentials: 'omit', so your own auth headers and cookies never reach Qasimha. Results are cached for the page lifetime. The data comes from the plans endpoint.
Using the on-site widget instead
With widget in the config, the provider loads @qasimha/widget once and keeps its key, language and API host in step. <QasimhaWidget> renders <qasimha-widget> with typed attributes:
<QasimhaWidget type="summary" amount={price} />
<QasimhaWidget type="summary" inlineType={6} amount={cart.total} />
<QasimhaWidget type="faq" />A numeric amount is sent with two decimals. See Install the on-site widget for layouts.
Styling
Set CSS custom properties on any parent element: --qasimha-primary, --qasimha-on-primary, --qasimha-text, --qasimha-muted, --qasimha-border, --qasimha-surface, --qasimha-bg, --qasimha-radius, --qasimha-font-size. Components use your page font; class names start with qasimha- and nothing global is styled.
Errors
The SDK throws QasimhaCheckoutError with a code:
code | When |
|---|---|
INVALID_CONFIG | Bad key, environment, base URL or return URL in the provider config. field names it. |
INVALID_OPTIONS | pay() got no createSession, or the returned paymentUrl is not https. |
NOT_CONFIGURED | A hook used outside <QasimhaProvider>, or update() before the config loaded. |
CHECKOUT_IN_PROGRESS | pay() in popup mode while a popup checkout is already open. |
Errors thrown by your createSession (for example your backend's validation errors) pass through unchanged to onError on the button. For API error codes on your server, see Errors.
Security
- Never put a secret key (
sk_...) in the browser.QasimhaProviderrefuses one. - Compute amounts on the server from your own prices.
- Confirm every outcome with
orders.getor the webhook before fulfilling. paymentUrland the return URLs must behttps(httponly on localhost).- The SDK stores nothing (no cookies, no storage) and sends nothing to Qasimha besides the plan quote.
- Payment happens on the Qasimha hosted page, so no card data touches your app (PCI DSS SAQ-A).
- If you use a CSP, allow the Qasimha API host in
connect-srcfor plan quotes (https://api.qasimha.comin production,https://merchant-api-uat.qasimha.comin UAT), and passnonceto the provider if yourstyle-srcuses nonces.
Next steps
Updated 3 days ago
