UNPKG

@alexasomba/paystack-inline

Version:

A strongly-typed, Promise-based TypeScript wrapper for Paystack Popup (inline.js).

512 lines (388 loc) • 18.9 kB
# @alexasomba/paystack-inline A **strongly-typed, Promise-based TypeScript wrapper** for the Paystack Popup (`inline.js`). Gives you full IDE autocomplete and type safety, while securely deferring all the heavy lifting — iframe rendering, WebSockets, Apple Pay sessions, PCI compliance — to Paystack's official CDN. > **Why not bundle inline.js directly?** > Paystack ships security and fraud-detection patches silently via their CDN. By dynamically loading the script at runtime, your users always get the latest, most secure version — without you having to cut a new release. --- ## Features - ✅ **Zero dependencies** — no npm bloat, 0.91 kB gzipped - ✅ **Full TypeScript types** for every option, callback, and response - ✅ All documented **and** undocumented aliases from `inline.js` included - ✅ `loadPaystack()` promise prevents race conditions and double-loading - ✅ Supports: **Cards, Bank, USSD, Mobile Money, Bank Transfer, QR, Apple Pay, Subscriptions, Splits** - ✅ Works with **React, Vue, Svelte, Next.js, SvelteKit, plain HTML** — any frontend --- ## Installation ```bash npm install @alexasomba/paystack-inline # or pnpm add @alexasomba/paystack-inline # or yarn add @alexasomba/paystack-inline ``` ## Agent Skills This package ships TanStack Intent skills for agent-assisted Paystack Inline integration: ```bash npx @tanstack/intent@latest list npx @tanstack/intent@latest install --map npx @tanstack/intent@latest load @alexasomba/paystack-inline#paystack-inline-checkout ``` Useful skills include `paystack-inline-checkout`, `paystack-inline-server-verify`, `paystack-inline-popup-options`, and `paystack-inline-popup-lifecycle`. --- ## Quick Start ### 1. Initialize from your server (required) Never call Paystack's API from the browser — your secret key would be exposed. Initialize on your backend and pass the `access_code` to the frontend: ```bash curl https://api.paystack.co/transaction/initialize \ -H "Authorization: Bearer YOUR_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "customer@email.com", "amount": "500000" }' \ -X POST ``` The response contains `data.access_code`. Send this to your frontend. ### 2. Resume the transaction in the browser ```typescript import { loadPaystack } from "@alexasomba/paystack-inline"; const PaystackPop = await loadPaystack(); const popup = new PaystackPop(); popup.resumeTransaction(accessCode, { onSuccess: (response) => { console.log("Payment successful!", response.reference); // Verify on your server before delivering value }, onCancel: () => { console.log("User cancelled payment"); }, }); ``` > **Always verify server-side** after `onSuccess`. Never deliver value based solely on a client callback — confirm with the [Verify Transaction](https://paystack.com/docs/api/transaction/#verify) endpoint. --- ## API Reference ### `loadPaystack()` Dynamically injects `https://js.paystack.co/v2/inline.js` into the page and resolves with the `PaystackPop` constructor. Safe to call multiple times — only one script tag is ever created. ```typescript import { loadPaystack } from "@alexasomba/paystack-inline"; const PaystackPop = await loadPaystack(); const popup = new PaystackPop(); ``` **Throws** if called outside a browser (e.g. server-side rendering). Wrap in a browser check if needed: ```typescript if (typeof window !== "undefined") { const PaystackPop = await loadPaystack(); } ``` --- ### `popup.resumeTransaction(accessCode, callbacks?)` The **recommended** checkout flow. Initialize on your server, then resume on the client. ```typescript popup.resumeTransaction("br6cgmvflhn3qtd", { onSuccess: (res) => console.log(res.reference), onCancel: () => console.log("cancelled"), onError: (err) => console.error(err.message), }); ``` --- ### `popup.newTransaction(options)` Start a transaction directly from the frontend using your public key. > [!WARNING] > The `amount` must be in the **lowest currency denomination** (e.g. kobo for NGN, pesewas for GHS). `500000` = ₦5,000. ```typescript const transaction = popup.newTransaction({ key: "pk_test_xxxxxxxxx", email: "customer@email.com", amount: 500000, currency: "NGN", reference: "unique-ref-" + Date.now(), onSuccess: (response) => { console.log("Transaction ref:", response.reference); }, onCancel: () => { console.log("Checkout closed"); }, }); ``` #### Core Options | Option | Type | Required | Description | | ----------- | ---------- | -------- | ---------------------------------------------------------------------------------------- | | `key` | `string` | ✅ | Your Paystack **public** key (`pk_...`) | | `email` | `string` | ✅ | Customer's email address | | `amount` | `number` | ✅ | Amount in the lowest currency unit (e.g. kobo) | | `currency` | `string` | — | Currency code: `NGN`, `GHS`, `ZAR`, `KES`, `USD` | | `reference` | `string` | — | Unique transaction reference. Auto-generated if unset | | `label` | `string` | — | Customer name shown on the checkout | | `channels` | `string[]` | — | Limit channels: `["card", "bank", "ussd", "mobile_money", "bank_transfer", "qr", "eft"]` | | `metadata` | `object` | — | Arbitrary key-value data attached to the transaction | | `language` | `string` | — | Force checkout language (`en`, `fr`, `pt`, etc.) | #### Customer Options | Option | Type | Description | | -------------- | -------- | ------------------------------- | | `firstName` | `string` | Pre-fill customer first name | | `lastName` | `string` | Pre-fill customer last name | | `phone` | `string` | Customer phone number | | `customerCode` | `string` | Existing Paystack customer code | --- ### `popup.preloadTransaction(options)` → `() => void` Pre-fetches the checkout iframe in the background for a snappy UX. Returns a function to open the pre-loaded popup. ```typescript const open = popup.preloadTransaction({ key: "pk_test_xxxxxxx", email: "customer@email.com", amount: 100000, onSuccess: (res) => verifyOnServer(res.reference), }); // Later, when user clicks "Pay": document.getElementById("pay-btn").addEventListener("click", open); ``` --- ### `popup.checkout(options)` → `Promise<PopupTransaction>` Promise-based checkout. Resolves with the `PopupTransaction` object when the popup opens. ```typescript const transaction = await popup.checkout({ key: "pk_test_xxxxxxx", email: "customer@email.com", amount: 200000, onSuccess: (res) => console.log(res), }); // Check status at any time: console.log(transaction.getStatus()); ``` --- ### `popup.cancelTransaction(idOrTransaction)` Programmatically close an open transaction. ```typescript const tx = popup.newTransaction({ ... }); // ... later popup.cancelTransaction(tx); // or popup.cancelTransaction(tx.id) ``` --- ### `popup.paymentRequest(options)` → `Promise<PopupTransaction>` Renders an **embedded payment element** (Apple Pay button, wallet button) into a DOM container. Perfect for custom checkout UIs. ```typescript const payment = await popup.paymentRequest({ key: "pk_test_xxxxxxx", email: "customer@email.com", amount: 500000, container: "#payment-request-container", // CSS selector or element ID onSuccess: (res) => console.log(res), onElementsMount: (elements) => { console.log("Payment elements mounted:", elements); }, styles: { theme: "dark", applePay: { width: "100%", height: "48px", borderRadius: "8px", type: "buy", // "plain" | "buy" | "donate" locale: "en-US", }, }, }); ``` #### `paymentRequest` Options | Option | Type | Description | | ---------------------------- | ------------------- | ------------------------------------------------------ | | `container` | `string` | ✅ CSS selector or element ID of the mount target | | `loadPaystackCheckoutButton` | `string` | Selector for a custom button that opens the full Popup | | `styles.theme` | `"dark" \| "light"` | Apple Pay button theme | | `styles.applePay.type` | `string` | Apple Pay button label: `"plain"`, `"buy"`, `"donate"` | | `styles.applePay.locale` | `string` | Apple Pay button locale | | `onElementsMount` | `function` | Fires when the embedded elements appear in the DOM | --- ## Callbacks All transaction methods accept the same callback interface: | Callback | Signature | Description | | ----------------------------------- | --------------------------------------------- | --------------------------------------------------- | | `onSuccess` | `(res: { id, reference, message }) => void` | Payment completed successfully | | `onError` | `(err: { message: string }) => void` | Payment failed with an error | | `onCancel` | `() => void` | User closed the popup before completing | | `onClose` | `() => void` | Popup was closed (includes both cancel and success) | | `onLoad` | `(res: { id, customer, accessCode }) => void` | Popup fully loaded and ready | | `onBankTransferConfirmationPending` | `() => void` | Bank transfer initiated, awaiting confirmation | | `callback` | `(res: any) => void` | Legacy v1 callback (use `onSuccess` instead) | --- ## Subscriptions Charge customers on a recurring plan: ```typescript popup.newTransaction({ key: "pk_test_xxxxxxx", email: "customer@email.com", amount: 500000, plan: "PLN_xxxxxxxxx", // or planCode quantity: 1, // number of seats/plan units interval: "monthly", // or planInterval invoice_limit: 12, // stop after N billing cycles start_date: "2025-01-01T00:00:00Z", onSuccess: (res) => console.log(res), }); ``` #### Subscription Options | Option | Type | Aliases | Description | | ----------------------- | -------- | --------------- | -------------------------------------------------- | | `planCode` | `string` | `plan` | Paystack plan code (`PLN_...`) | | `subscriptionCount` | `number` | `quantity` | Number of subscriptions / seats | | `planInterval` | `string` | `interval` | `hourly`, `daily`, `weekly`, `monthly`, `annually` | | `subscriptionLimit` | `number` | `invoice_limit` | Max billing cycles before stopping | | `subscriptionStartDate` | `string` | `start_date` | ISO 8601 date for first charge | --- ## Split Payments Route portions of a payment to subaccounts: ```typescript popup.newTransaction({ key: "pk_test_xxxxxxx", email: "customer@email.com", amount: 500000, subaccount: "ACCT_xxxxxxxxx", // or subaccountCode bearer: "account", // who bears transaction charges transaction_charge: 2000, // flat charge for the main account (kobo) onSuccess: (res) => console.log(res), }); ``` #### Split Options | Option | Type | Aliases | Description | | ------------------- | --------------------------- | -------------------- | ------------------------------------------------ | | `subaccountCode` | `string` | `subaccount` | Subaccount code to split with | | `split_code` | `string` | | Reference a pre-configured split group | | `split` | `object` | | Inline split configuration object | | `connect_split` | `string[]` | | Array of connected account codes for multi-split | | `bearer` | `"account" \| "subaccount"` | | Who absorbs the transaction fee | | `transactionCharge` | `string \| number` | `transaction_charge` | Flat fee (in kobo) retained by the main account | --- ## Framework Examples ### React ```typescript import { loadPaystack } from "@alexasomba/paystack-inline"; function CheckoutButton({ accessCode }: { accessCode: string }) { const handlePay = async () => { const PaystackPop = await loadPaystack(); const popup = new PaystackPop(); popup.resumeTransaction(accessCode, { onSuccess: (response) => { // Call your API to verify + fulfill the order fetch("/api/verify", { method: "POST", body: JSON.stringify({ reference: response.reference }), }); }, onCancel: () => console.log("Payment cancelled"), }); }; return <button onClick={handlePay}>Pay Now</button>; } ``` ### Next.js (App Router) ```typescript "use client"; // Required — loadPaystack() is browser-only import { loadPaystack } from "@alexasomba/paystack-inline"; export default function PayButton({ accessCode }: { accessCode: string }) { const handlePay = async () => { const PaystackPop = await loadPaystack(); const popup = new PaystackPop(); popup.resumeTransaction(accessCode, { onSuccess: (res) => console.log(res.reference), }); }; return <button onClick={handlePay}>Complete Payment</button>; } ``` ### Vue 3 ```typescript <script setup lang="ts"> import { loadPaystack } from "@alexasomba/paystack-inline"; const props = defineProps<{ accessCode: string }>(); const handlePay = async () => { const PaystackPop = await loadPaystack(); const popup = new PaystackPop(); popup.resumeTransaction(props.accessCode, { onSuccess: (res) => console.log(res.reference), }); }; </script> <template> <button @click="handlePay">Pay Now</button> </template> ``` ### Svelte / SvelteKit ```typescript <script lang="ts"> import { loadPaystack } from "@alexasomba/paystack-inline"; export let accessCode: string; async function handlePay() { const PaystackPop = await loadPaystack(); const popup = new PaystackPop(); popup.resumeTransaction(accessCode, { onSuccess: (res) => console.log(res.reference), }); } </script> <button on:click={handlePay}>Pay Now</button> ``` --- ## TypeScript Reference All exported types are available for direct import: ```typescript import type { PaystackPopGeneralOptions, PaystackPopCallbacks, PaystackPopNewTransactionOptions, PaystackPopCheckoutOptions, PaystackPopPaymentRequestOptions, PaystackPopSplitOptions, PaystackPopSubscriptionOptions, PaystackPopInstance, PopupTransaction, PopupTransactionStatus, } from "@alexasomba/paystack-inline"; ``` ### `PopupTransactionStatus` ```typescript interface PopupTransactionStatus { status: "null" | "error" | "abandoned" | "auth" | "failed" | "success" | "pending"; id?: string; errors?: any[]; response?: any; checkoutUrl?: string; } ``` --- ## Complete Payment Flow ``` ┌─────────────────┐ POST /transaction/initialize ┌─────────────────┐ │ Your Backend │ ─────────────────────────────────────► │ Paystack API │ │ (Secret Key) │ ◄───────────────────────────────────── │ │ └────────┬────────┘ { access_code } └─────────────────┘ │ │ access_code ▼ ┌─────────────────┐ loadPaystack() + resumeTransaction() │ Your Frontend │ ─────────────────────────────────────► Paystack Popup UI │ (Public Key) │ ◄─────────────── onSuccess({ reference }) ────────────── └────────┬────────┘ │ │ Verify reference on YOUR server (never trust client callbacks alone) ▼ ┌─────────────────┐ GET /transaction/verify/:reference │ Your Backend │ ─────────────────────────────────────► Paystack API │ │ ◄─────────────── { status: "success" } ───────────────── └─────────────────┘ │ │ Deliver value to customer ✅ ``` --- ## Security Notes - **Never use your Secret Key on the frontend.** Use only your Public Key (`pk_...`). - **Always verify transactions server-side** before delivering value — the `onSuccess` callback can be spoofed. - The script at `https://js.paystack.co/v2/inline.js` is loaded over HTTPS and served with integrity-safe headers by Paystack's CDN. --- ## Related Resources - [Paystack Docs — Accept Payments](https://paystack.com/docs/payments/accept-payment/) - [Paystack Docs — Verify Transactions](https://paystack.com/docs/api/transaction/#verify) - [Paystack Docs — Webhooks](https://paystack.com/docs/payments/webhooks/) - [Paystack Docs — Payment Channels](https://paystack.com/docs/payments/payment-channels/) - [Paystack Docs — Subscriptions](https://paystack.com/docs/payments/subscriptions/) --- ## Source - Monorepo source: [alexasomba/paystack-openapi](https://github.com/alexasomba/paystack-openapi) - Standalone SDK repo: [https://github.com/alexasomba/paystack-inline](https://github.com/alexasomba/paystack-inline) --- ## License MIT © [alexasomba](https://github.com/alexasomba)