Angular SDK
Add Qasimha checkout, return handling and instalment plans to an Angular app with @qasimha/angular.
@qasimha/angular presents the Qasimha hosted checkout from an Angular 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/angular (npm) |
| Angular | 19 or later (@angular/core, @angular/common, @angular/router). Standalone components, signals, zoneless-ready. |
| 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/angularConfigure
Register the SDK once with provideQasimha. Load the configuration from your own backend so keys and return URLs live in one place:
// app.config.ts
import { ApplicationConfig, inject } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { HttpClient, provideHttpClient } from '@angular/common/http';
import { provideQasimha, QasimhaConfig } from '@qasimha/angular';
import { firstValueFrom } from 'rxjs';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(),
provideQasimha(() => firstValueFrom(inject(HttpClient).get<QasimhaConfig>('/api/qasimha/config'))),
],
};Or pass a static config:
provideQasimha({ publicKey: 'pk_test_...', language: 'ar', merchantUrl: { success, failure, cancel } });The loader runs during bootstrap (provideAppInitializer), so the config is ready before the first component renders.
| Option | Default | Notes |
|---|---|---|
publicKey | none | pk_test_... or pk_live_.... Needed for <qasimha-promo>, <qasimha-payment-option> and QasimhaPlans. A secret key (sk_...) is refused. |
language | 'ar' | 'ar' or 'en'. Change at runtime with inject(QasimhaConfigStore).update({ 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. |
QasimhaConfigStore.update() with 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:
createSession = () =>
firstValueFrom(this.http.post<{ paymentUrl: string; orderId: string }>('/api/checkout', { items: this.cart() }));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
<qasimha-payment-option [amount]="total()" [(selected)]="payWithQasimha" />
<qasimha-pay-button
[createSession]="createSession"
presentation="redirect"
(result)="onResult($event)"
(failed)="onError($event)" /><qasimha-payment-option> is the Qasimha entry for your payment-method list.
With the service
const checkout = inject(QasimhaCheckout);
async onPay() { // call from a click handler
const result = await checkout.pay({ createSession: this.createSession, presentation: 'popup' });
// redirect: the page has navigated away (result is null)
// popup: result is { type: 'success' | 'failure' | 'cancelled', ... }
}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 route (qasimhaReturnResolver) | pay() resolves and (result) emits in the original tab |
| Popup blocked | Not applicable | The SDK opens the window inside the click, before createSession runs. If the browser still blocks it, the SDK falls back to redirect. |
Call pay() directly from the click handler so the popup can open. While the popup is open, <qasimha-pay-button> shows an overlay with "Open it again" and "Cancel". In code, use checkout.reopen(), checkout.cancel() and the popupState signal.
There is no iframe mode. In popup mode, the popup lands on your return URL and your app (started by provideQasimha) 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.
Add the resolver to your return route:
// app.routes.ts
import { qasimhaReturnResolver } from '@qasimha/angular';
{ path: 'checkout/return', component: ReturnPage, resolve: { qasimha: qasimhaReturnResolver } }Read it as an input (needs withComponentInputBinding()):
// return.page.ts
readonly qasimha = input.required<QasimhaReturn>();
// qasimha().result:
// { type: 'success', orderId, orderReferenceId }
// { type: 'failure', status, ... }
// { type: 'cancelled' }
// null
// qasimha().relayedToOpener: true inside the popup -> show "you can close this window"inject(QasimhaCheckout).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 order = await firstValueFrom(this.http.get<{ status: string; paid: boolean }>(`/api/orders/${orderId}`));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 service | Use |
|---|---|
<qasimha-promo [amount]="price" /> | Bordered instalment badge |
<qasimha-promo [amount]="price" variant="compact" /> | One line: 4 × [symbol] 300, Learn more |
<qasimha-promo [amount]="total" variant="text" /> | Text-only line |
QasimhaPlans.getPlans(amount, lang?) | Same data for your own layout (Plans or null) |
<qasimha-amount [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. <qasimha-promo> 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' instead of HttpClient, so your interceptors (auth headers, 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, provideQasimha loads @qasimha/widget once and keeps its key, language and API host in step. Use the element in any component that declares CUSTOM_ELEMENTS_SCHEMA:
@Component({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<qasimha-widget type="summary" [attr.amount]="price.toFixed(2)"></qasimha-widget>
<qasimha-widget type="summary" inline-type="6" [attr.amount]="total.toFixed(2)"></qasimha-widget>
<qasimha-widget type="faq"></qasimha-widget>
`,
})Pass amount as a plain decimal string. 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.
Errors
The SDK throws QasimhaCheckoutError with a code:
code | When |
|---|---|
INVALID_CONFIG | provideQasimha received a bad key, environment, base URL or return URL. field names it. |
INVALID_OPTIONS | pay() got no createSession, or the returned paymentUrl is not https. |
NOT_CONFIGURED | QasimhaConfigStore.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 (failed) on the button. For API error codes on your server, see Errors.
Security
- Never put a secret key (
sk_...) in the browser.provideQasimharefuses 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).
Next steps
Updated 3 days ago
