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

ItemRequirement
Package@qasimha/react (npm)
React18.2+ or 19. Hooks and function components, StrictMode-safe.
Module formatESM with TypeScript types. No runtime dependency besides React.
Key in the browserPublishable key only: pk_test_... (UAT) or pk_live_... (production)
ServerA backend using a Qasimha server SDK (@qasimha/sdk, qasimha/php-sdk, Qasimha.Sdk or com.qasimha:qasimha-sdk)

Install

npm install @qasimha/react

Configure 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>).

OptionDefaultNotes
publicKeynonepk_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).
environmentfrom the key'sandbox' or 'production'. Must agree with the key prefix.
apiBaseUrlfrom the environmenthttps only (http for localhost).
merchantUrlnoneThe 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'.
widgetnonescriptUrl 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 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: call pay() first

Do not await anything before pay() in the click handler. The popup window must open inside the click, or the browser blocks it. The SDK opens the window before it runs createSession. If the browser still blocks it, the SDK falls back to redirect.

redirect (default)popup
What happensThe page goes to the hosted checkout and comes back to your merchantUrl.Checkout opens in a window above your page.
Where the result arrivesYour 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 hookUse
<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:

codeWhen
INVALID_CONFIGBad key, environment, base URL or return URL in the provider config. field names it.
INVALID_OPTIONSpay() got no createSession, or the returned paymentUrl is not https.
NOT_CONFIGUREDA hook used outside <QasimhaProvider>, or update() before the config loaded.
CHECKOUT_IN_PROGRESSpay() 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. QasimhaProvider refuses one.
  • Compute amounts on the server from your own prices.
  • Confirm every outcome with orders.get or the webhook before fulfilling.
  • paymentUrl and the return URLs must be https (http only 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-src for plan quotes (https://api.qasimha.com in production, https://merchant-api-uat.qasimha.com in UAT), and pass nonce to the provider if your style-src uses nonces.

Next steps


Did this page help you?