# react-klasha

React hooks and components for accepting payments with the [Klasha](https://klasha.com) payment gateway.

[![npm](https://img.shields.io/npm/v/react-klasha.svg)](https://www.npmjs.com/package/react-klasha)
[![license](https://img.shields.io/npm/l/react-klasha.svg)](LICENSE.md)

![Demo](App.png?raw=true 'Demo Image')

---

## ⚠️ Version 0.0.x is completely non-functional — upgrade to 1.0.0

Two things were wrong with 0.0.x, and both are fatal:

1. **The script it loaded no longer exists.** 0.0.x fetched
   `https://klastatic.fra1.digitaloceanspaces.com/{test,prod}/js/klasha-integration.js`.
   That bucket has been deleted; both URLs now return a 404. `window.KlashaClient`
   is therefore never defined and **no payment can start at all**. Klasha's
   current script is `https://js.klasha.com/pay.js`, which 1.0.0 loads.
2. **`isTestMode` was never sent to the gateway.** The Klasha client takes the
   test flag as its 9th constructor argument; 0.0.x stopped at 8. Inside the
   Klasha script the flag then read as `undefined`, so the base URL, the iframe
   and the redirect URL all took the production branch. This matters **now**: the current `pay.js` serves one URL for both
   environments and decides which gateway to use from that argument alone, so
   repointing 0.0.x at the new script without adding it would route sandbox
   traffic to production. 1.0.0 passes it, coerced to a real boolean.

   To be clear about the past: while the old CDN still existed, 0.0.x picked
   `/test/js/` or `/prod/js/` correctly based on `isTestMode`, and that old
   script was environment-specific by URL. There is no reason to believe your
   historical sandbox payments went to production, and nothing to reconcile.

1.0.0 also fixes a silently dropped phone number, duplicate DOM ids, a load race
that reported "ready" before the Klasha script had run, and `console.log` calls
that printed your `merchantKey` to the browser console. See
[Migrating from 0.0.x](#migrating-from-00x).

---

## Requirements

- React **16.8 or later** (17, 18 and 19 are supported)
- Node 18+ to build from source

> 0.0.x declared support for React 15, but the library is built on hooks and has never worked below 16.8.

## Install

```sh
npm install react-klasha
```

```sh
yarn add react-klasha
```

## Quick start

```jsx
import {KlashaButton} from 'react-klasha';

export default function Checkout() {
  return (
    <KlashaButton
      text="Make Payment"
      className="payButton"
      merchantKey="YOUR_MERCHANT_KEY"
      businessId="YOUR_BUSINESS_ID"
      amount={1000}
      sourceCurrency="USD"
      destinationCurrency="NGN"
      tx_ref={`tx-${Date.now()}`}
      fullname="John Doe"
      email="john@example.com"
      phone="+2348143108254"
      paymentDescription="Order #1234"
      isTestMode
      callBack={(response) => console.log('payment complete', response)}
    />
  );
}
```

### `isTestMode` selects the environment

There is a single script — `https://js.klasha.com/pay.js` — for both test and
live traffic. The environment is chosen entirely by `isTestMode`, which this
library forwards as the 9th constructor argument:

| `isTestMode` | Gateway used |
| --- | --- |
| `true` | Klasha test environment |
| `false` *(default)* | Klasha live environment |

Use your **test** merchant key with `isTestMode`, and your **live** key without it.

## Usage

### `useKlashaPayment` hook

```jsx
import {useKlashaPayment} from 'react-klasha';

function DonateButton() {
  const config = {
    merchantKey: 'YOUR_MERCHANT_KEY',
    businessId: 'YOUR_BUSINESS_ID',
    amount: 1000,
    sourceCurrency: 'USD',
    destinationCurrency: 'NGN',
    tx_ref: `tx-${Date.now()}`,
    fullname: 'John Doe',
    email: 'john@example.com',
    phone: '+2348143108254',
    isTestMode: true,
  };

  const initializePayment = useKlashaPayment(config);
  const callBack = (response) => console.log('payment complete', response);

  return <button onClick={() => initializePayment(callBack)}>Donate</button>;
}
```

`initializePayment` **throws** if it is called before the Klasha script has
loaded, or if the script failed to load. Gate your button on the script state:

```jsx
import {useKlashaScript, useKlashaPayment} from 'react-klasha';

function PayButton({config, callBack}) {
  const [ready, failed] = useKlashaScript(config.isTestMode);
  const initializePayment = useKlashaPayment(config);

  if (failed) return <p>Payment is unavailable right now.</p>;

  return (
    <button disabled={!ready} onClick={() => initializePayment(callBack)}>
      {ready ? 'Pay' : 'Loading…'}
    </button>
  );
}
```

`useKlashaScript` returns `[loaded, error]`. `loaded` only turns true once
`pay.js` itself has executed — never merely because a load was started.

### `KlashaConsumer` render prop

```jsx
import {KlashaConsumer} from 'react-klasha';

<KlashaConsumer {...config} callBack={callBack}>
  {({initializePayment}) => <button onClick={initializePayment}>Pay with Klasha</button>}
</KlashaConsumer>;
```

`KlashaConsumer` forwards refs. The ref is handed to the render prop:

```jsx
<KlashaConsumer {...config} ref={myRef}>
  {({initializePayment, ref}) => (
    <button ref={ref} onClick={initializePayment}>
      Pay
    </button>
  )}
</KlashaConsumer>;
```

### `KlashaProvider`

Wrap a subtree so nested components share one payment configuration. Nested
components read `initializePayment` and `callBack` from context.

```jsx
import {KlashaProvider} from 'react-klasha';

<KlashaProvider {...config} callBack={callBack}>
  <Checkout />
</KlashaProvider>;
```

## API

### Payment options

Accepted by `useKlashaPayment`, `KlashaButton`, `KlashaConsumer` and `KlashaProvider`.

| Option | Type | Required | Description |
| --- | --- | --- | --- |
| `merchantKey` | `string` | ✅ | Merchant API key from the Klasha dashboard |
| `businessId` | `string \| number` | ✅ | Merchant business id |
| `amount` | `number` | ✅ | Amount to charge |
| `isTestMode` | `boolean` | | Use the Klasha test environment. Defaults to `false` |
| `sourceCurrency` | `string` | | Currency the customer is charged in. Defaults to `NGN` |
| `destinationCurrency` | `string` | | Currency the merchant is settled in. Defaults to `NGN` |
| `tx_ref` | `string` | | Your unique transaction reference. Generated when omitted |
| `fullname` | `string` | | Customer's full name |
| `email` | `string` | | Customer's email |
| `phone` | `string` | | Customer's phone number |
| `phone_number` | `string` | | **Deprecated** alias of `phone` |
| `callbackUrl` | `string` | | URL the gateway calls when the transaction completes |
| `paymentDescription` | `string` | | Description of what is being paid for |
| `metadata` | `object` | | Extra data to keep alongside the transaction |
| `kit` | `KlashaKitOptions` | | Extra fields forwarded to the Klasha client |
| `containerId` | `string` | | Render the checkout into a specific element. One is created per component when omitted |

### The `kit` object

`kit` is passed straight to the Klasha client. The keys `pay.js` actually reads
are `businessId`, `currency`, `redirect_url`, `email`, `phone`, `productType`,
`amount`, `sourceAmount`, `callBack` and `tx_ref`.

You rarely need to set any of them — this library fills in `currency`, `email`,
`fullname`, `phone`, `amount`, `tx_ref` and `callBack` from the top-level options
and builds a **fresh `kit` for every payment** (the Klasha script mutates the
object it is given, so a shared one leaks state between transactions).

```jsx
const config = {
  // …payment options…
  kit: {
    productType: 'digital',
    sourceAmount: 12,
  },
};
```

> `kit.phone` is the key the gateway reads. 0.0.x set only `kit.phone_number`,
> which `pay.js` never looks at, so the customer's phone number was silently
> dropped from the exchange-rate request. 1.0.0 sets both.

### `KlashaButton` props

Everything above, plus:

| Prop | Type | Description |
| --- | --- | --- |
| `text` | `string` | Button label |
| `children` | `ReactNode` | Used when `text` is absent |
| `className` | `string` | CSS class |
| `disabled` | `boolean` | Disable the button |
| `type` | `'button'`, `'submit'` or `'reset'` | Defaults to `'button'` |
| `callBack` | `Function` | Called when the payment flow completes |

### Exports

```js
import {
  useKlashaPayment,
  useKlashaScript,
  useKlashaContainer,
  KlashaButton,
  KlashaConsumer,
  KlashaProvider,
  ensureKlashaContainer,
  removeKlashaContainer,
  getKlashaScriptUrl,
  KLASHA_SDK_URL,
} from 'react-klasha';
```

TypeScript types `KlashaOptionsProps`, `KlashaKitOptions`, `KlashaClientConstructor`,
`KlashaClientInstance` and `KlashaRenderProps` are exported too.

## Migrating from 0.0.x

| Change | Impact |
| --- | --- |
| **Loads `https://js.klasha.com/pay.js`** | The old CDN bucket was deleted and returns 404, so 0.0.x cannot start a payment at all. |
| **`isTestMode` is forwarded to the gateway** | Test-mode integrations no longer hit the live environment. Use your test merchant key when `isTestMode` is set. |
| **jQuery is no longer loaded** | `pay.js` does not need it. If you relied on this library to provide `window.$`, load jQuery yourself. |
| `useKlashaScript` reports `loaded` only once `pay.js` has executed | It used to resolve on whichever of jQuery/Klasha loaded first, so `loaded` was frequently true while `window.KlashaClient` was undefined. |
| Script `error` no longer reports `loaded: true` | `useKlashaScript` returns `[false, true]` on failure instead of `[true, true]`. |
| `initializePayment` throws instead of silently doing nothing when the script is not ready | Gate the call on `useKlashaScript`, or catch the error. |
| The checkout container has a unique per-component id | The hardcoded `<div id="ktest">` was appended on every effect run. Pass `containerId` if you need a specific element. |
| `kit.phone` is populated | The phone number used to be dropped. `kit.phone_number` is still set for compatibility. |
| `tx_ref` is generated when omitted | It is interpolated into the redirect URL and must not be empty. |
| `console.log` of the options (including `merchantKey`) removed | Nothing is written to the console any more. |
| `KlashaConsumer` ref actually reaches the render prop | It was always `undefined` before. |
| Peer range is now `react@^16.8 \|\| ^17 \|\| ^18 \|\| ^19` | React 15 was never actually supported. |
| `react-dom` peer dependency removed | The library never imported it. |
| Build output is `dist/index.cjs` + `dist/index.mjs` with an `exports` map | Deep imports into `dist/` are no longer supported. |

## Development

```sh
npm install
npm run typecheck
npm test
npm run build
```

## Documentation

See the [Klasha documentation](https://docs.klasha.com/) for the full list of
options and for how to verify a transaction server-side.

> **Always verify a payment on your server** before fulfilling an order. Never
> trust the browser callback alone.

## Contributing

1. Fork it
2. Create your feature branch: `git checkout -b feature-name`
3. Commit your changes: `git commit -am 'Some commit message'`
4. Push to the branch: `git push origin feature-name`
5. Submit a pull request 😉

## How can I thank you?

Star the repo, and share it! Don't forget to [follow me on twitter](https://twitter.com/dansteveade).

Thanks!
Adekanbi Dansteve.

## License

MIT — see [LICENSE.md](LICENSE.md).
