npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hadawi/sdk

v1.2.0

Published

Official QattaPay SDK — group contribution checkout for your storefront

Downloads

1,106

Readme

@hadawi/sdk

Official QattaPay SDK — add group contribution checkout to any storefront.

QattaPay lets groups of people split the cost of a purchase. The SDK handles:

  • Server-side: creating checkout intents (a session identifier for one purchase) using your merchant API key.
  • Client-side: official branded checkout buttons plus redirect/popup open helpers.
  • Webhooks: verifying and parsing events sent to your server when a session is funded.

Requirements & platforms

| Surface | Package | Runs on | | ------- | ------- | ------- | | Server (intents, orders, webhooks) | @hadawi/sdk | Node.js ≥ 18 | | Browser (branded button + open checkout) | @hadawi/sdk/browser | Any modern browser |

Supported storefronts: plain HTML, React, Next.js, Vue, Svelte, Angular, Laravel Blade, and Flutter. There is no separate React/Vue package — mount into a DOM node with mountButton(). For Flutter use qattapay_flutter (in-app WebView or system browser).

Not supported yet: native iOS/Android or React Native SDKs. Checkout is the hosted web flow (popup / redirect on web; full-page in-app WebView or external browser on Flutter).


Installation

npm install @hadawi/sdk
# or
pnpm add @hadawi/sdk
# or
yarn add @hadawi/sdk
# or
bun add @hadawi/sdk

Quick start

1 — Browser: mount a branded QattaPay button

Use the SDK button — do not invent your own “Pay / Split” CTA. Official variants keep QattaPay branding consistent on every storefront. On click, call your server (step 2) for an intentId.

React / Next.js

Import from @hadawi/sdk/browser, mount into a ref, and call destroy() on unmount:

"use client"; // Next.js App Router only

import { useEffect, useRef } from "react";
import { QattaPayCheckout } from "@hadawi/sdk/browser";

export function QattaPayPayButton({ productId }: { productId: string }) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!ref.current) return;

    const checkout = new QattaPayCheckout({ mode: "live" }); // or "dev"
    const button = checkout.mountButton({
      container: ref.current,
      variant: "primary",
      label: "split",
      getIntentId: async () => {
        const res = await fetch("/api/create-contribution", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ productId }),
        });
        const data = await res.json();
        return data.intentId;
      },
      open: {
        mode: "popup",
        // Optional: also pass returnUrl so hosted checkout can redirect/deep-link back
        // returnUrl: "https://yourstore.com/thank-you",
        onSuccess: () => {
          window.location.href = "/thank-you";
        },
      },
    });

    return () => button.destroy();
  }, [productId]);

  return <div ref={ref} />;
}

Vue 3

<script setup>
import { onMounted, onBeforeUnmount, ref } from "vue";
import { QattaPayCheckout } from "@hadawi/sdk/browser";

const el = ref(null);
let button;

onMounted(() => {
  const checkout = new QattaPayCheckout({ mode: "live" });
  button = checkout.mountButton({
    container: el.value,
    variant: "primary",
    label: "split",
    getIntentId: async () => {
      const res = await fetch("/api/create-contribution", { method: "POST" });
      const data = await res.json();
      return data.intentId;
    },
    open: {
      mode: "popup",
      onSuccess: () => {
        location.href = "/thank-you";
      },
    },
  });
});

onBeforeUnmount(() => button?.destroy());
</script>

<template>
  <div ref="el" />
</template>

Plain HTML (no bundler)

<div id="qattapay-checkout"></div>
<script src="https://cdn.jsdelivr.net/npm/@hadawi/sdk/dist/browser.iife.js"></script>
<script>
  const checkout = new QattaPay.QattaPayCheckout({ mode: "live" });

  checkout.mountButton({
    container: "#qattapay-checkout",
    variant: "primary",
    label: "split",
    getIntentId: async () => {
      const { intentId } = await fetch("/api/create-contribution", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ productId: "watch-001" }),
      }).then((r) => r.json());
      return intentId;
    },
    open: {
      mode: "popup",
      onSuccess: () => (location.href = "/thank-you"),
    },
  });
</script>

Imperative open() (advanced)

If you already have an intentId and need full control, you can still call checkout.open(intentId, { mode: 'popup' | 'redirect', … }). Prefer mountButton() for the shopper-facing CTA.

2 — Server: create a checkout intent

Power the button’s getIntentId call with your API key from the merchant dashboard.

import { QattaPayClient } from "@hadawi/sdk";

const qattapay = new QattaPayClient({
  apiKey: process.env.QATTAPAY_API_KEY!, // from QattaPay merchant dashboard
  mode: "live", // or "dev" — SDK resolves the API host
  webhookSecret: process.env.QATTAPAY_WEBHOOK_SECRET, // per-merchant secret (Developer → Webhook)
});

// e.g. POST /api/create-contribution
const { intent, redirectUrl } = await qattapay.intents.create({
  itemSnapshot: [
    {
      name: "Luxury Watch Gift Set",
      nameAr: "طقم ساعة فاخرة",
      price: 150000, // amount in halalas (150,000 = 1,500.00 SAR)
      image: "https://example.com/watch.jpg",
      reference: "watch-001", // your internal SKU
    },
  ],
  totalAmount: 150000, // must equal sum of itemSnapshot[].price
  currency: "SAR",
  metadata: { orderId: "ord_abc123" }, // echoed back in webhook events
});

// Return intentId to the browser button
res.json({ intentId: intent.id, redirectUrl });

Note on amounts: All monetary values are in the currency's smallest unit. For SAR, that is halalas (100 halalas = 1 SAR).


Button variants

| variant | Look | | ---------- | ------------------------------------------------- | | primary | Purple gradient fill (default) | | dark | Solid deep purple | | light | White fill, purple text | | outline | Transparent with purple border |

| label | English text | | ------------- | ---------------------------- | | split | Split with Friends (default) | | split_cart | Split Cart with Friends | | pay | Pay with QattaPay | | (string) | Your custom text |

locale: 'ar' switches copy to Arabic and sets dir="rtl".

Checkout modes

| Mode | Behaviour | Best for | | ---------- | ------------------------------------------------ | ------------------------------------- | | redirect | Navigates the current tab to the hosted checkout | Simple integrations | | popup | Opens a new browser window (~520×700 px) | SPAs that want to keep page state |

Returning to the merchant

Hosted checkout accepts returnUrl on open() / mountButton({ open }):

| Option | When to use | | --- | --- | | onSuccess / onCancel | Popup — listen for qattapay:success / qattapay:cancel postMessage | | returnUrl | Redirect (and popup/mobile fallback) — QattaPay navigates here after success/cancel with intentId, sessionId, and status=success\|cancel\|failed |

checkout.open(intentId, {
  mode: "redirect",
  returnUrl: "https://yourstore.com/thank-you",
});

// Lands on:
// https://yourstore.com/thank-you?intentId=…&sessionId=…&status=success

Laravel Blade mirrors this as success-url (popup onSuccess) and return-url (returnUrl). Flutter uses returnUrl as a deep link or https URL so the in-app WebView can close.

checkout.open() returns a close() function — call it to dismiss a popup programmatically:

const close = checkout.open(intentId, { mode: "popup", onCancel: () => {} });
// …later:
close();

Why there is no modal / iframe mode

The hosted payment page responds with X-Frame-Options: deny, so browsers refuse to render it inside any <iframe> (including nested frames).

If checkout were embedded in a merchant-site iframe, the user would hit a blank/blocked frame at pay time. For that reason the SDK only supports:

  • redirect — full top-level navigation
  • popup — a separate top-level window (same-origin browsing context for QattaPay, then top-level navigation to the payment page)

Do not wrap /checkout/{intentId} in your own iframe either — payment will fail for the same reason.


Webhook handling

QattaPay POSTs a signed JSON event to your webhookUrl (set in the merchant dashboard) whenever a session changes state.

Each merchant has a unique signing secret (whsec_…) issued by QattaPay. Copy it from Developer → Webhook → Reveal into QATTAPAY_WEBHOOK_SECRET. Merchants cannot choose this value — only reveal or rotate it.

Register your webhook URL (one-time setup)

Set the webhook URL in the QattaPay merchant dashboard:

  1. Log in to the merchant portal
  2. Open Developer → Webhook
  3. Enter your HTTPS endpoint (e.g. https://yourstore.com/webhooks/qattapay)
  4. Save, then Reveal the signing secret (whsec_…) and store it as QATTAPAY_WEBHOOK_SECRET

Webhook URL configuration is dashboard-only — it is not exposed through the SDK.

Verify and handle events

import { QattaPayClient } from "@hadawi/sdk";
import express from "express";

const qattapay = new QattaPayClient({
  apiKey: process.env.QATTAPAY_API_KEY!,
  mode: "live",
  webhookSecret: process.env.QATTAPAY_WEBHOOK_SECRET!,
});

const app = express();

// ⚠️  Use raw body parser — do NOT parse JSON before this route
app.post(
  "/webhooks/qattapay",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const signature = req.headers["x-qattapay-signature"] as string;

    let event;
    try {
      event = qattapay.webhooks.constructEvent(req.body, signature);
    } catch (err) {
      console.error("Webhook signature mismatch:", err);
      return res.status(400).send("Invalid signature");
    }

    switch (event.type) {
      case "order.funded":
        await qattapay.orders.fulfill(event.payload.order_id!);
        console.log(
          "Order funded, fulfillment started:",
          event.payload.order_id,
        );
        break;

      case "order.partially_funded":
        console.log("Partial funding — awaiting organiser decision");
        break;

      case "order.cancelled":
      case "order.expired":
        console.log("Session ended:", event.type, event.payload.session_id);
        break;
    }

    res.sendStatus(200);
  },
);

Webhook event shape

interface QattaPayWebhookEvent {
  type:
    | "order.funded"
    | "order.partially_funded"
    | "order.cancelled"
    | "order.expired";
  payload: {
    event: string;
    order_id?: string; // present when an Order record exists
    session_id: string;
    merchant_id: string;
    items: ItemSnapshot[];
    total_amount: number; // halalas
    currency: string;
    funded_at: string; // ISO 8601
  };
}

The X-QattaPay-Signature header is an HMAC-SHA256 hex digest of the raw JSON body, keyed with your per-merchant QATTAPAY_WEBHOOK_SECRET (not a global platform secret).


Orders API

Once a session is funded and an order exists, use qattapay.orders to manage fulfillment:

// List all orders
const { orders } = await qattapay.orders.list();

// Get detail for a single order (includes contribution breakdown)
const { order, contributions } = await qattapay.orders.get(orderId);

// Mark as being fulfilled (you've started processing)
await qattapay.orders.fulfill(orderId);

// Mark as delivered (item shipped / handed over)
await qattapay.orders.deliver(orderId);

// Refund every captured contribution on the order (e.g. out of stock)
await qattapay.orders.refund(orderId, { reason: 'Out of stock' });

// Refund a single contributor within the order (partial refund)
await qattapay.orders.refundContribution(orderId, contributionId, {
  reason: 'Contributor requested to back out',
});

Order status lifecycle

pending_funding → funded → notified → fulfilling → delivered
                         ↘ cancelled

Refunds

orders.refund() and orders.refundContribution() process the refund synchronously against the original payment method and notify each affected contributor. Both accept an optional reason string that's stored on the refund record (not shown to contributors).

A refund request is rejected with a QattaPayApiError (400) when:

  • The order has already been included in a payout request — refunds must go through support at that point.
  • The underlying session is already refunding, refunded, or cancelled.
  • (refundContribution only) the specific contribution was never captured or was already refunded.

Refunding via API key is scoped to that key's environment — a dev key cannot refund an order that was processed with live credentials, and vice versa.


TypeScript

The SDK is written in TypeScript and ships full declaration files. All types are exported from @hadawi/sdk (server) and @hadawi/sdk/browser (client).

import type {
  QattaPayClientConfig,
  CreateIntentParams,
  CreateIntentResponse,
  Order,
  OrderDetail,
  QattaPayWebhookEvent,
  WebhookEventType,
} from "@hadawi/sdk";

import type {
  QattaPayCheckoutConfig,
  CheckoutOpenOptions,
  CheckoutMode,
} from "@hadawi/sdk/browser";

Environments (mode)

Merchants only set mode — URLs are resolved inside the SDK:

| mode | Checkout (web) | API | |--------|----------------|-----| | dev | https://dev.qatta.sa | https://dev.qatta.sa/api | | live | https://qatta.sa | https://qatta.sa/api |

Checkout opens {host}/checkout/{intentId}.

// Production
new QattaPayClient({ apiKey, mode: "live", webhookSecret });
new QattaPayCheckout({ mode: "live" });

// Staging / sandbox
new QattaPayClient({ apiKey, mode: "dev", webhookSecret });
new QattaPayCheckout({ mode: "dev" });

Local development

Set these environment variables (.env):

QATTAPAY_API_KEY=mk_test_...          # from seed output / merchant dashboard
QATTAPAY_WEBHOOK_SECRET=change-me-in-production

Override hosts only when running against a local stack (baseUrl wins over mode):

// Server
const qattapay = new QattaPayClient({
  apiKey: process.env.QATTAPAY_API_KEY!,
  baseUrl: "http://localhost:4000",
});

// Browser
const checkout = new QattaPayCheckout({ baseUrl: "http://localhost:3000" });

Demo store reference integration

The demo-store/ app (sibling of this monorepo, under infra/) is a complete working example. See demo-store/server.js for the server-side intent creation and demo-store/public/index.html for the browser-side checkout trigger. It depends on this SDK via file:../hadawi/packages/sdk, so run pnpm sdk:build here before starting it.


API reference docs

API reference page: /docs/sdk/api (embeds TypeDoc from /sdk-api/). Merchant-facing quick start: /docs/sdk.

cd packages/sdk
pnpm run docs          # local preview → packages/sdk/docs/
pnpm run docs:web      # sync into packages/web/public/sdk-api/ for deploy

See PUBLISHING.md for the release checklist.


Publishing

Releases are published to npm as @hadawi/sdk via GitHub Actions when you push a matching tag:

# after bumping version + CHANGELOG
git tag sdk-v0.1.0
git push origin sdk-v0.1.0

Full checklist (secrets, provenance, dry-run): PUBLISHING.md.


License

MIT