# Paymob Installments Widget

`paymob-widget`

A drop-in widget you embed in any web page (product page, cart, or checkout) to show your customers the **installment plans** available for a given amount. You can use it purely to display plans, or let customers pick a plan and hand the selection back to your code.

No framework required — it works on any HTML page with a single `<script>` tag,
and it also drops into React, Angular or Vue apps (see
[Use in a framework](#use-in-a-framework-react-angular-vue)).

---

## How it works

1. You add the script and a container element to your page.
2. You create a `PaymobWidget` with your public key and the order amount.
3. The widget calls Paymob, fetches the installment plans for that amount, and renders them inside your container.

There are two modes:

| Mode | What the customer sees | When to use it |
| --- | --- | --- |
| **Display only** (default) | A read-only list of available installment plans. | You just want to promote installments (e.g. "Pay in 12 months") on a product page. |
| **Selectable** (`customerCanSelect: true`) | The same plans, but the customer can **select** one and click **Buy now**. | You want the customer to choose a plan and continue the purchase in your own flow. |

---

## Quick start

### 1. Add the script

Using the jsDelivr CDN:

```html
<script src="https://cdn.jsdelivr.net/npm/paymob-widget@latest/main.js" type="module"></script>
```

### 2. Add a container

Place an empty element wherever you want the widget to appear:

```html
<div id="paymob-widget"></div>
```

### 3. Create the widget

```js
new PaymobWidget({
  publicKey: 'egy_pk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
  elementId: 'paymob-widget',
  amount: 100000, // order total in cents — this is 1,000 EGP
  currency: 'EGP',
});
```

That's it. If installment plans are available for that amount, the widget renders them.

> **⚠️ `amount` is in cents, not whole currency units.**
> To show plans for **1,000 EGP**, pass `amount: 100000` (1,000 × 100).
> If `amount` is missing, zero, negative, or not a number, the widget will **not** render and an error is logged to the browser console.

---

## Options

| Option | Type | Required | Description |
| --- | --- | --- | --- |
| `publicKey` | `string` | **Yes** | Your Paymob public key. Authenticates requests and selects the correct regional API (based on the key's country prefix, e.g. `egy_`, `are_`, `sau_`, `pak_`, `omn_`). |
| `elementId` | `string` | **Yes** | The `id` of the HTML element the widget renders into. |
| `amount` | `number` | **Yes** | The order total **in cents** (a positive number). No default — the widget won't render without it. |
| `currency` | `string` | No | Currency code. Default: `"EGP"`. |
| `integrationId` | `number \| number[]` | No | One or more Paymob integration IDs. When provided, plans are fetched for those integrations only. |
| `theme` | `"primary" \| "light" \| "dark"` | No | Visual theme. Default: `"primary"`. |
| `customerCanSelect` | `boolean` | No | Set to `true` to let the customer select a plan and enable **Buy now**. Default: `false` (read-only). |
| `onSubmit` | `function` | No | Called when the customer clicks **Buy now**. If you do not pass it, we will hide the **Buy now** button. See [Handling the selection](#handling-the-selection). |

---

## Letting customers select a plan

Set `customerCanSelect: true` and provide an `onSubmit` callback. Your callback runs when the customer picks a plan and clicks **Buy now**, so you can continue the purchase in your own flow.

```js
new PaymobWidget({
  publicKey: 'egy_pk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
  elementId: 'paymob-widget',
  amount: 100000, // 1,000 EGP
  currency: 'EGP',
  theme: 'primary',
  customerCanSelect: true,
  onSubmit: (plan) => {
    // plan = { id: 123, tenure: 12, amount: 25000 }
    console.log('Customer selected plan', plan);
    // → continue your purchase flow here
  },
});
```

### Handling the selection

When `customerCanSelect` is `true`, `onSubmit` receives the selected plan:

```js
{
  id: string | number,   // the installment plan ID
  tenure: number,        // number of months
  amount: number         // monthly installment amount, in cents
}
```

When `customerCanSelect` is `false`, the plans are read-only. **Buy now** is not part of that flow, and if `onSubmit` is called it receives no payload.

The built-in **Learn more** dialog also adapts: with `customerCanSelect: true` it includes a "Select installment plan" step; with `false` it omits that step.

---

## Creating the intention with a pre-selected plan

You can use the plan ID when creating the intention by passing the `pre_selected_plan` flag, for example:

```json
"pre_selected_plan": 123
```

---

## Full HTML example

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Paymob Widget Example</title>
    <base href="/" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style>
      #paymob-widget {
        width: 100%;
        margin: 10% auto 0;
      }
      body {
        margin: 0;
      }
    </style>
  </head>

  <body>
    <div id="paymob-widget"></div>

    <script src="https://cdn.jsdelivr.net/npm/paymob-widget@latest/main.js" type="module"></script>

    <script>
      window.onload = () => {
        new PaymobWidget({
          publicKey: 'egy_pk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
          elementId: 'paymob-widget',
          amount: 100000, // 1,000 EGP
          currency: 'EGP',
          integrationId: 123,
          theme: 'primary',
          customerCanSelect: true,
          onSubmit: (plan) => {
            console.log('Selected plan', plan);
          },
        });
      };
    </script>
  </body>
</html>
```

---

## Use in a framework (React, Angular, Vue)

The SDK ships as a single self-contained `main.js` — styles, fonts and icons are
bundled in, so there are **no extra CSS files to import**. Loading the bundle
registers the widget on `window.PaymobWidget`; you then create it against a
container element from your component's lifecycle hook.

You can load the bundle in either of two ways:

- **npm** — `npm install paymob-widget`, then `import 'paymob-widget';` (a
  side-effect import that registers `window.PaymobWidget`).
- **CDN** — add `<script src="https://cdn.jsdelivr.net/npm/paymob-widget@latest/main.js" type="module"></script>` once (e.g. in `index.html`); no import needed.

> The published bundle exposes the class on `window` (it does **not** provide a
> named ESM export), so always construct it as `new window.PaymobWidget({...})`.
> Use a **unique `elementId` per instance** if you render more than one widget.

**TypeScript:** add this once (e.g. `paymob-widget.d.ts`) so `window.PaymobWidget` is typed:

```ts
interface PaymobWidgetPlan {
  id: string | number;
  tenure: number;
  amount: number;
}

interface PaymobWidgetOptions {
  publicKey: string;
  elementId: string;
  amount: number;
  currency?: string;
  integrationId?: number | number[];
  theme?: 'primary' | 'light' | 'dark';
  customerCanSelect?: boolean;
  onSubmit?: (plan?: PaymobWidgetPlan) => void;
}

declare global {
  interface Window {
    PaymobWidget: new (options: PaymobWidgetOptions) => unknown;
  }
}

export {};
```

### React

```tsx
import { useEffect } from 'react';
import 'paymob-widget'; // registers window.PaymobWidget

export function InstallmentsWidget() {
  useEffect(() => {
    new window.PaymobWidget({
      publicKey: 'egy_pk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
      elementId: 'paymob-widget',
      amount: 100000, // 1,000 EGP
      currency: 'EGP',
      customerCanSelect: true,
      onSubmit: (plan) => console.log('Selected plan', plan),
    });
  }, []);

  return <div id="paymob-widget" />;
}
```

### Angular

Add `import 'paymob-widget';` (or the CDN `<script>` in `index.html`), then
initialise the widget after the view is ready:

```ts
import { Component, AfterViewInit } from '@angular/core';
import 'paymob-widget'; // registers window.PaymobWidget

@Component({
  selector: 'app-installments',
  template: '<div id="paymob-widget"></div>',
})
export class InstallmentsComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    new (window as any).PaymobWidget({
      publicKey: 'egy_pk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
      elementId: 'paymob-widget',
      amount: 100000, // 1,000 EGP
      currency: 'EGP',
      customerCanSelect: true,
      onSubmit: (plan: unknown) => console.log('Selected plan', plan),
    });
  }
}
```

### Vue 3

```vue
<script setup lang="ts">
import { onMounted } from 'vue';
import 'paymob-widget'; // registers window.PaymobWidget

onMounted(() => {
  new window.PaymobWidget({
    publicKey: 'egy_pk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
    elementId: 'paymob-widget',
    amount: 100000, // 1,000 EGP
    currency: 'EGP',
    customerCanSelect: true,
    onSubmit: (plan) => console.log('Selected plan', plan),
  });
});
</script>

<template>
  <div id="paymob-widget"></div>
</template>
```

---

## Troubleshooting — "the widget isn't showing"

The widget renders **only** when all of these are true:

- `amount` is a valid positive number (in cents).
- The plans request to Paymob succeeds (returns a `2xx` status).
- At least one installment plan is available for that amount.

If any of these fail, the widget stays hidden and the reason is logged to the **browser console** (prefixed with `[PaymobWidget]`):

| Symptom | Likely cause |
| --- | --- |
| Nothing renders, console shows an `amount` error | `amount` is missing, `0`, negative, or not a number. |
| Nothing renders, console shows a status code (e.g. `400`) | The request to Paymob failed — check your `publicKey`, `integrationId`, and `currency`. |
| Request succeeds but nothing renders | No plans are available — e.g. the amount is below the minimum, the integration has no bank installments configured, or the `integrationId` is invalid. |

> **Note:** These errors are for developers only. Your customers never see error messages on the page or in the dialog.

---

## Good to know

- **Self-contained:** everything — CSS, the widget font, and icons — is inlined into `main.js`. There are no extra files to host or import; the single file is all you ship. (At runtime the widget still calls the Paymob API, and the design system may pull a few supplementary web fonts from Google Fonts, with graceful fallback.)
- **Style isolation:** the widget renders inside a Shadow DOM, so its styles won't clash with your page's CSS (and your CSS won't bleed into it).
- **Region:** the API region is chosen automatically from your public key's country prefix (`egy_`, `are_`, `sau_`, `pak_`, `omn_`).
