@dryinov8/zumbopay-ts 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +375 -0
- package/dist/chunk-YULBG3E6.mjs +338 -0
- package/dist/chunk-YULBG3E6.mjs.map +1 -0
- package/dist/client-CO8dBkq7.d.mts +188 -0
- package/dist/client-CO8dBkq7.d.ts +188 -0
- package/dist/index.d.mts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +400 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +36 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react/index.d.mts +46 -0
- package/dist/react/index.d.ts +46 -0
- package/dist/react/index.js +755 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +393 -0
- package/dist/react/index.mjs.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/phone.ts","../src/webhook.ts","../src/client.ts"],"sourcesContent":["export * from './types.js';\nexport * from './phone.js';\nexport * from './webhook.js';\nexport * from './client.js';\n","import type { MobileOperator } from './types.js';\n\n/**\n * Normalizes any Mozambican telephone string into the standard 12-digit format `258XXXXXXXXX`.\n * Removes spaces, hyphens, parentheses, and prefixes (+258, 258, 0).\n */\nexport function normalizePhone(phone: string): string {\n let cleaned = phone.replace(/\\D/g, '');\n\n // Remove leading 0 (e.g. 0841234567 -> 841234567)\n if (cleaned.startsWith('0')) {\n cleaned = cleaned.slice(1);\n }\n\n // Prepend 258 if 9-digit national number\n if (cleaned.length === 9) {\n cleaned = '258' + cleaned;\n }\n\n return cleaned;\n}\n\n/**\n * Detects mobile network operator from phone number:\n * - Vodacom (M-Pesa): 84, 85\n * - Movitel (e-Mola): 86, 87\n * - Tmcel (mKesh): 82, 83\n */\nexport function detectOperator(phone: string): MobileOperator {\n const normalized = normalizePhone(phone);\n\n if (normalized.length !== 12 || !normalized.startsWith('258')) {\n return 'unknown';\n }\n\n const prefix = normalized.slice(3, 5);\n\n if (prefix === '84' || prefix === '85') {\n return 'mpesa';\n }\n if (prefix === '86' || prefix === '87') {\n return 'emola';\n }\n if (prefix === '82' || prefix === '83') {\n return 'mkesh';\n }\n\n return 'unknown';\n}\n\n/**\n * Returns human-readable label for a mobile operator.\n */\nexport function getOperatorLabel(operator: MobileOperator): string {\n switch (operator) {\n case 'mpesa':\n return 'Vodacom M-Pesa';\n case 'emola':\n return 'Movitel e-Mola';\n case 'mkesh':\n return 'Tmcel mKesh';\n default:\n return 'Desconhecida';\n }\n}\n\n/**\n * Validates whether the number is a valid 9-digit Mozambican mobile number (with or without 258).\n */\nexport function isValidMozPhone(phone: string): boolean {\n return detectOperator(phone) !== 'unknown';\n}\n","import crypto from 'node:crypto';\n\n/**\n * Validates HMAC-SHA256 webhook signature sent by ZumboPay.\n * Resists timing attacks using constant-time buffer comparison.\n *\n * @param rawBody - Raw body string or Buffer from the webhook request\n * @param receivedSignature - Signature sent in `X-Signature`, `Signature`, or `X-ZumboPay-Signature` header\n * @param secret - Webhook secret configured in ZumboPay dashboard\n */\nexport function verifyWebhookSignature(\n rawBody: string | Buffer,\n receivedSignature: string,\n secret: string\n): boolean {\n if (!receivedSignature || !secret) {\n return false;\n }\n\n // Remove any algorithm prefix like sha256= or v1=\n const cleanReceived = receivedSignature.replace(/^(sha256=|v1=)/i, '').trim();\n\n try {\n const expected = crypto\n .createHmac('sha256', secret)\n .update(typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8'))\n .digest('hex');\n\n const expectedBuf = Buffer.from(expected, 'hex');\n const receivedBuf = Buffer.from(cleanReceived, 'hex');\n\n if (expectedBuf.length !== receivedBuf.length) {\n return false;\n }\n\n return crypto.timingSafeEqual(expectedBuf, receivedBuf);\n } catch {\n return false;\n }\n}\n","import { detectOperator, normalizePhone } from './phone.js';\nimport type {\n CheckoutRequest,\n CheckoutResponse,\n StkPushRequest,\n StkPushResponse,\n TransactionStatusResponse,\n WalletItem,\n ZumboPayConfig,\n ZumboPayWallets,\n} from './types.js';\n\nexport class ZumboPayClient {\n private readonly config: Required<\n Omit<ZumboPayConfig, 'webhookSecret' | 'wallets'>\n > & {\n webhookSecret?: string;\n wallets: ZumboPayWallets;\n };\n\n private cachedWallets: WalletItem[] | null = null;\n private walletsCacheExpiresAt = 0;\n\n constructor(config: ZumboPayConfig) {\n const envWallets: ZumboPayWallets = {};\n let defaultEnabled = true;\n\n // Read environment variables if available in Node.js / server environment\n if (typeof process !== 'undefined' && process?.env) {\n if (process.env.ZUMBOPAY_ENABLED !== undefined) {\n defaultEnabled =\n process.env.ZUMBOPAY_ENABLED === 'true' ||\n process.env.ZUMBOPAY_ENABLED === '1';\n }\n\n if (process.env.ZUMBOPAY_WALLET_MPESA) {\n envWallets.mpesa = process.env.ZUMBOPAY_WALLET_MPESA;\n }\n if (process.env.ZUMBOPAY_WALLET_EMOLA) {\n envWallets.emola = process.env.ZUMBOPAY_WALLET_EMOLA;\n }\n if (process.env.ZUMBOPAY_WALLET_MKESH) {\n envWallets.mkesh = process.env.ZUMBOPAY_WALLET_MKESH;\n }\n if (process.env.ZUMBOPAY_WALLET_CARD) {\n envWallets.card = process.env.ZUMBOPAY_WALLET_CARD;\n }\n }\n\n this.config = {\n apiKey: config.apiKey,\n merchantId: config.merchantId,\n baseUrl:\n config.baseUrl?.replace(/\\/+$/, '') ||\n 'https://zumbopay.com/api/public/v1',\n webhookSecret: config.webhookSecret,\n wallets: {\n ...envWallets,\n ...(config.wallets || {}),\n },\n enabled: config.enabled !== undefined ? config.enabled : defaultEnabled,\n timeout: config.timeout || 15000,\n };\n }\n\n /**\n * Returns whether the ZumboPay client is currently active.\n * If false, all mutations (STK push, checkouts) are gracefully silenced.\n */\n public isEnabled(): boolean {\n return Boolean(this.config.enabled);\n }\n\n /**\n * Programmatically enable or disable the gateway (kill-switch toggle).\n */\n public setEnabled(enabled: boolean): void {\n this.config.enabled = enabled;\n }\n\n /**\n * Returns configured base URL.\n */\n public getBaseUrl(): string {\n return this.config.baseUrl;\n }\n\n /**\n * Resolves a valid Wallet UUID for a given operator or channel.\n */\n public async resolveWalletId(\n channel: 'mpesa' | 'emola' | 'mkesh' | 'card'\n ): Promise<string | undefined> {\n // 1. Check explicitly configured wallet\n if (this.config.wallets[channel]) {\n return this.config.wallets[channel];\n }\n\n // 2. Discover dynamically from list of wallets\n const wallets = await this.listWallets();\n const match = wallets.find(\n (w) =>\n w.is_active !== false &&\n (w.method?.toLowerCase() === channel ||\n w.wallet_code?.toLowerCase().includes(channel) ||\n w.name?.toLowerCase().includes(channel))\n );\n\n if (match) {\n return match.id;\n }\n\n // 3. Fallback to any active wallet\n const active = wallets.find((w) => w.is_active !== false);\n return active?.id;\n }\n\n /**\n * Resolves the proper wallet ID for a Mozambican phone number.\n */\n public async resolveWalletIdForPhone(\n phone: string\n ): Promise<string | undefined> {\n const operator = detectOperator(phone);\n if (operator === 'unknown') {\n return undefined;\n }\n return this.resolveWalletId(operator);\n }\n\n /**\n * Initiates a direct STK Push prompt to a mobile phone (M-Pesa, e-Mola, mKesh).\n */\n public async stkPush(request: StkPushRequest): Promise<StkPushResponse> {\n if (!this.isEnabled()) {\n return {\n success: false,\n status: 'disabled',\n reference: null,\n message:\n 'O gateway de pagamento ZumboPay está temporariamente desativado.',\n };\n }\n\n const normalizedPhone = normalizePhone(request.phone);\n const walletId =\n request.walletId ||\n (await this.resolveWalletIdForPhone(normalizedPhone));\n\n const sourceId = request.reference || `stk-${Date.now()}`;\n\n const payload = {\n phone: normalizedPhone,\n amount: request.amount,\n wallet_id: walletId,\n reference: sourceId,\n customer_name: request.customerName,\n description: request.description,\n };\n\n try {\n const res = await this.fetchWithTimeout('/charges', {\n method: 'POST',\n headers: this.getHeaders(),\n body: JSON.stringify(payload),\n });\n\n const data = await res.json().catch(() => ({}));\n\n if (res.ok) {\n const status = (\n data.data?.status ||\n data.status ||\n 'pending'\n ).toLowerCase();\n const code = data.code || data.data?.code;\n const isSuccess =\n status === 'success' ||\n status === 'succeeded' ||\n status === 'completed' ||\n code === 'INS-0';\n\n return {\n success: true,\n status: isSuccess ? 'success' : status,\n reference: data.data?.reference || sourceId,\n message: isSuccess\n ? 'Pagamento efetuado com sucesso!'\n : 'Pedido de pagamento enviado para o seu telemóvel. Por favor confirme com o seu PIN.',\n raw: data,\n };\n }\n\n return {\n success: false,\n status: 'declined',\n reference: null,\n message:\n data.error?.message ||\n data.message ||\n 'O pagamento foi recusado ou expirou no telemóvel.',\n raw: data,\n };\n } catch (err: unknown) {\n const error = err as Error;\n\n // Handle USSD network timeouts gracefully as pending\n if (error.name === 'AbortError' || error.message.includes('timeout')) {\n return {\n success: true,\n status: 'pending',\n reference: sourceId,\n message:\n 'O pedido foi enviado. Por favor verifique o telemóvel e confirme com o PIN.',\n raw: { timeout: true },\n };\n }\n\n return {\n success: false,\n status: 'failed',\n reference: null,\n message: error.message || 'Falha na comunicação com o ZumboPay.',\n };\n }\n }\n\n /**\n * Creates a Hosted Checkout URL for credit/debit card & multicanal payments.\n */\n public async createCheckout(\n request: CheckoutRequest\n ): Promise<CheckoutResponse> {\n if (!this.isEnabled()) {\n return {\n success: false,\n checkoutUrl: null,\n reference: null,\n message:\n 'O gateway de pagamento ZumboPay está temporariamente desativado.',\n };\n }\n\n const walletId =\n request.walletId ||\n (await this.resolveWalletId('card')) ||\n (await this.resolveWalletId('mpesa'));\n\n const sourceId = request.reference || `chk-${Date.now()}`;\n\n const payload = {\n title: request.title,\n amount: request.amount,\n currency: request.currency || 'MZN',\n channels: request.channels || ['card', 'mpesa', 'emola', 'mkesh'],\n wallet_id: walletId,\n reference: sourceId,\n return_url: request.returnUrl,\n redirect_url: request.returnUrl,\n cancel_url: request.cancelUrl,\n metadata: request.metadata,\n };\n\n try {\n const res = await this.fetchWithTimeout('/checkouts', {\n method: 'POST',\n headers: this.getHeaders(),\n body: JSON.stringify(payload),\n });\n\n const data = await res.json().catch(() => ({}));\n\n if (res.ok) {\n const checkoutUrl =\n data.data?.checkout_url ||\n data.checkout_url ||\n data.data?.url ||\n data.url ||\n null;\n\n return {\n success: true,\n checkoutUrl,\n reference: data.data?.reference || sourceId,\n message: 'Sessão de checkout criada com sucesso.',\n raw: data,\n };\n }\n\n return {\n success: false,\n checkoutUrl: null,\n reference: null,\n message:\n data.error?.message ||\n data.message ||\n 'Não foi possível gerar a página de checkout.',\n raw: data,\n };\n } catch (err: unknown) {\n const error = err as Error;\n return {\n success: false,\n checkoutUrl: null,\n reference: null,\n message: error.message || 'Erro de conexão ao criar checkout.',\n };\n }\n }\n\n /**\n * Queries status of an existing charge/checkout transaction.\n */\n public async getStatus(\n referenceOrId: string\n ): Promise<TransactionStatusResponse> {\n const res = await this.fetchWithTimeout(\n `/charges/${encodeURIComponent(referenceOrId)}`,\n {\n method: 'GET',\n headers: this.getHeaders(),\n }\n );\n\n const data = await res.json().catch(() => ({}));\n const payload = data.data || data;\n const status = (payload.status || 'pending').toLowerCase();\n const isPaid =\n status === 'success' ||\n status === 'succeeded' ||\n status === 'completed' ||\n payload.is_paid === true;\n\n return {\n success: res.ok,\n status,\n paid: isPaid,\n reference: payload.reference || referenceOrId,\n amount: payload.amount,\n currency: payload.currency || 'MZN',\n channel: payload.channel,\n paidAt: payload.paid_at || payload.updated_at || null,\n raw: data,\n };\n }\n\n /**\n * Lists all wallets associated with the merchant account (cached for 10 minutes).\n */\n public async listWallets(): Promise<WalletItem[]> {\n const now = Date.now();\n if (this.cachedWallets && now < this.walletsCacheExpiresAt) {\n return this.cachedWallets;\n }\n\n try {\n const res = await this.fetchWithTimeout('/wallets', {\n method: 'GET',\n headers: this.getHeaders(),\n });\n\n if (res.ok) {\n const data = await res.json();\n this.cachedWallets = (data.data || data || []) as WalletItem[];\n this.walletsCacheExpiresAt = now + 10 * 60 * 1000;\n return this.cachedWallets;\n }\n return [];\n } catch {\n return [];\n }\n }\n\n private getHeaders(): Record<string, string> {\n return {\n Authorization: `Bearer ${this.config.apiKey}`,\n 'X-Merchant-Id': this.config.merchantId,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n }\n\n private async fetchWithTimeout(\n endpoint: string,\n options: RequestInit\n ): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.config.timeout);\n\n const url = `${this.config.baseUrl}${endpoint}`;\n\n try {\n return await fetch(url, {\n ...options,\n signal: controller.signal,\n });\n } finally {\n clearTimeout(timer);\n }\n }\n}\n"],"mappings":"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","names":["crypto"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ZumboPayClient,
|
|
3
|
+
detectOperator,
|
|
4
|
+
getOperatorLabel,
|
|
5
|
+
isValidMozPhone,
|
|
6
|
+
normalizePhone
|
|
7
|
+
} from "./chunk-YULBG3E6.mjs";
|
|
8
|
+
|
|
9
|
+
// src/webhook.ts
|
|
10
|
+
import crypto from "crypto";
|
|
11
|
+
function verifyWebhookSignature(rawBody, receivedSignature, secret) {
|
|
12
|
+
if (!receivedSignature || !secret) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const cleanReceived = receivedSignature.replace(/^(sha256=|v1=)/i, "").trim();
|
|
16
|
+
try {
|
|
17
|
+
const expected = crypto.createHmac("sha256", secret).update(typeof rawBody === "string" ? rawBody : rawBody.toString("utf8")).digest("hex");
|
|
18
|
+
const expectedBuf = Buffer.from(expected, "hex");
|
|
19
|
+
const receivedBuf = Buffer.from(cleanReceived, "hex");
|
|
20
|
+
if (expectedBuf.length !== receivedBuf.length) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return crypto.timingSafeEqual(expectedBuf, receivedBuf);
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
ZumboPayClient,
|
|
30
|
+
detectOperator,
|
|
31
|
+
getOperatorLabel,
|
|
32
|
+
isValidMozPhone,
|
|
33
|
+
normalizePhone,
|
|
34
|
+
verifyWebhookSignature
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/webhook.ts"],"sourcesContent":["import crypto from 'node:crypto';\n\n/**\n * Validates HMAC-SHA256 webhook signature sent by ZumboPay.\n * Resists timing attacks using constant-time buffer comparison.\n *\n * @param rawBody - Raw body string or Buffer from the webhook request\n * @param receivedSignature - Signature sent in `X-Signature`, `Signature`, or `X-ZumboPay-Signature` header\n * @param secret - Webhook secret configured in ZumboPay dashboard\n */\nexport function verifyWebhookSignature(\n rawBody: string | Buffer,\n receivedSignature: string,\n secret: string\n): boolean {\n if (!receivedSignature || !secret) {\n return false;\n }\n\n // Remove any algorithm prefix like sha256= or v1=\n const cleanReceived = receivedSignature.replace(/^(sha256=|v1=)/i, '').trim();\n\n try {\n const expected = crypto\n .createHmac('sha256', secret)\n .update(typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8'))\n .digest('hex');\n\n const expectedBuf = Buffer.from(expected, 'hex');\n const receivedBuf = Buffer.from(cleanReceived, 'hex');\n\n if (expectedBuf.length !== receivedBuf.length) {\n return false;\n }\n\n return crypto.timingSafeEqual(expectedBuf, receivedBuf);\n } catch {\n return false;\n }\n}\n"],"mappings":";;;;;;;;;AAAA,OAAO,YAAY;AAUZ,SAAS,uBACd,SACA,mBACA,QACS;AACT,MAAI,CAAC,qBAAqB,CAAC,QAAQ;AACjC,WAAO;AAAA,EACT;AAGA,QAAM,gBAAgB,kBAAkB,QAAQ,mBAAmB,EAAE,EAAE,KAAK;AAE5E,MAAI;AACF,UAAM,WAAW,OACd,WAAW,UAAU,MAAM,EAC3B,OAAO,OAAO,YAAY,WAAW,UAAU,QAAQ,SAAS,MAAM,CAAC,EACvE,OAAO,KAAK;AAEf,UAAM,cAAc,OAAO,KAAK,UAAU,KAAK;AAC/C,UAAM,cAAc,OAAO,KAAK,eAAe,KAAK;AAEpD,QAAI,YAAY,WAAW,YAAY,QAAQ;AAC7C,aAAO;AAAA,IACT;AAEA,WAAO,OAAO,gBAAgB,aAAa,WAAW;AAAA,EACxD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { c as ZumboPayConfig, T as TransactionStatusResponse, Z as ZumboPayClient, M as MobileOperator, P as PaymentStatus, S as StkPushRequest, C as CheckoutRequest } from '../client-CO8dBkq7.mjs';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface UseZumboPayOptions {
|
|
5
|
+
config: ZumboPayConfig;
|
|
6
|
+
onSuccess?: (response: TransactionStatusResponse) => void;
|
|
7
|
+
onError?: (error: Error | {
|
|
8
|
+
message: string;
|
|
9
|
+
}) => void;
|
|
10
|
+
pollingIntervalMs?: number;
|
|
11
|
+
}
|
|
12
|
+
declare function useZumboPay({ config, onSuccess, onError, pollingIntervalMs, }: UseZumboPayOptions): {
|
|
13
|
+
client: ZumboPayClient;
|
|
14
|
+
isEnabled: boolean;
|
|
15
|
+
phone: string;
|
|
16
|
+
operator: MobileOperator;
|
|
17
|
+
status: PaymentStatus;
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
reference: string | null;
|
|
20
|
+
errorMessage: string | null;
|
|
21
|
+
setPhone: (newPhone: string) => void;
|
|
22
|
+
initiateStk: (req: Omit<StkPushRequest, "phone"> & {
|
|
23
|
+
phone?: string;
|
|
24
|
+
}) => Promise<boolean>;
|
|
25
|
+
createCheckout: (req: CheckoutRequest) => Promise<string | null>;
|
|
26
|
+
reset: () => void;
|
|
27
|
+
stopPolling: () => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
interface ZumboPayModalProps {
|
|
31
|
+
isOpen: boolean;
|
|
32
|
+
onClose: () => void;
|
|
33
|
+
config: ZumboPayConfig;
|
|
34
|
+
amount: number;
|
|
35
|
+
currency?: string;
|
|
36
|
+
reference?: string;
|
|
37
|
+
title?: string;
|
|
38
|
+
customerName?: string;
|
|
39
|
+
initialPhone?: string;
|
|
40
|
+
onPaymentSuccess?: (response: TransactionStatusResponse) => void;
|
|
41
|
+
returnUrl?: string;
|
|
42
|
+
cancelUrl?: string;
|
|
43
|
+
}
|
|
44
|
+
declare function ZumboPayModal({ isOpen, onClose, config, amount, currency, reference, title, customerName, initialPhone, onPaymentSuccess, returnUrl, cancelUrl, }: ZumboPayModalProps): React.JSX.Element | null;
|
|
45
|
+
|
|
46
|
+
export { type UseZumboPayOptions, ZumboPayModal, type ZumboPayModalProps, useZumboPay };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { c as ZumboPayConfig, T as TransactionStatusResponse, Z as ZumboPayClient, M as MobileOperator, P as PaymentStatus, S as StkPushRequest, C as CheckoutRequest } from '../client-CO8dBkq7.js';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface UseZumboPayOptions {
|
|
5
|
+
config: ZumboPayConfig;
|
|
6
|
+
onSuccess?: (response: TransactionStatusResponse) => void;
|
|
7
|
+
onError?: (error: Error | {
|
|
8
|
+
message: string;
|
|
9
|
+
}) => void;
|
|
10
|
+
pollingIntervalMs?: number;
|
|
11
|
+
}
|
|
12
|
+
declare function useZumboPay({ config, onSuccess, onError, pollingIntervalMs, }: UseZumboPayOptions): {
|
|
13
|
+
client: ZumboPayClient;
|
|
14
|
+
isEnabled: boolean;
|
|
15
|
+
phone: string;
|
|
16
|
+
operator: MobileOperator;
|
|
17
|
+
status: PaymentStatus;
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
reference: string | null;
|
|
20
|
+
errorMessage: string | null;
|
|
21
|
+
setPhone: (newPhone: string) => void;
|
|
22
|
+
initiateStk: (req: Omit<StkPushRequest, "phone"> & {
|
|
23
|
+
phone?: string;
|
|
24
|
+
}) => Promise<boolean>;
|
|
25
|
+
createCheckout: (req: CheckoutRequest) => Promise<string | null>;
|
|
26
|
+
reset: () => void;
|
|
27
|
+
stopPolling: () => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
interface ZumboPayModalProps {
|
|
31
|
+
isOpen: boolean;
|
|
32
|
+
onClose: () => void;
|
|
33
|
+
config: ZumboPayConfig;
|
|
34
|
+
amount: number;
|
|
35
|
+
currency?: string;
|
|
36
|
+
reference?: string;
|
|
37
|
+
title?: string;
|
|
38
|
+
customerName?: string;
|
|
39
|
+
initialPhone?: string;
|
|
40
|
+
onPaymentSuccess?: (response: TransactionStatusResponse) => void;
|
|
41
|
+
returnUrl?: string;
|
|
42
|
+
cancelUrl?: string;
|
|
43
|
+
}
|
|
44
|
+
declare function ZumboPayModal({ isOpen, onClose, config, amount, currency, reference, title, customerName, initialPhone, onPaymentSuccess, returnUrl, cancelUrl, }: ZumboPayModalProps): React.JSX.Element | null;
|
|
45
|
+
|
|
46
|
+
export { type UseZumboPayOptions, ZumboPayModal, type ZumboPayModalProps, useZumboPay };
|