@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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/index.ts","../../src/react/useZumboPay.ts","../../src/phone.ts","../../src/client.ts","../../src/react/ZumboPayModal.tsx"],"sourcesContent":["export * from './useZumboPay.js';\nexport * from './ZumboPayModal.js';\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport { ZumboPayClient } from '../client.js';\nimport { detectOperator, isValidMozPhone, normalizePhone } from '../phone.js';\nimport type {\n CheckoutRequest,\n MobileOperator,\n PaymentStatus,\n StkPushRequest,\n TransactionStatusResponse,\n ZumboPayConfig,\n} from '../types.js';\n\nexport interface UseZumboPayOptions {\n config: ZumboPayConfig;\n onSuccess?: (response: TransactionStatusResponse) => void;\n onError?: (error: Error | { message: string }) => void;\n pollingIntervalMs?: number;\n}\n\nexport function useZumboPay({\n config,\n onSuccess,\n onError,\n pollingIntervalMs = 3000,\n}: UseZumboPayOptions) {\n const clientRef = useRef<ZumboPayClient>(new ZumboPayClient(config));\n\n // Update client config if dependencies change\n useEffect(() => {\n clientRef.current = new ZumboPayClient(config);\n }, [config]);\n\n const [phone, setPhone] = useState('');\n const [operator, setOperator] = useState<MobileOperator>('unknown');\n const [status, setStatus] = useState<PaymentStatus>('pending');\n const [isLoading, setIsLoading] = useState(false);\n const [reference, setReference] = useState<string | null>(null);\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n\n const pollingRef = useRef<NodeJS.Timeout | null>(null);\n const isEnabled = clientRef.current.isEnabled();\n\n // Stop polling helper\n const stopPolling = useCallback(() => {\n if (pollingRef.current) {\n clearInterval(pollingRef.current);\n pollingRef.current = null;\n }\n }, []);\n\n useEffect(() => {\n return () => stopPolling();\n }, [stopPolling]);\n\n // Handle phone input change with live operator detection\n const handlePhoneChange = useCallback((newPhone: string) => {\n setPhone(newPhone);\n setOperator(detectOperator(newPhone));\n }, []);\n\n // Poll status until terminal state\n const startPolling = useCallback(\n (targetRef: string) => {\n stopPolling();\n\n let attempts = 0;\n const maxAttempts = 40; // ~2 minutes\n\n pollingRef.current = setInterval(async () => {\n attempts++;\n if (attempts > maxAttempts) {\n stopPolling();\n setStatus('expired');\n setErrorMessage('O tempo limite para inserção do PIN expirou.');\n return;\n }\n\n try {\n const res = await clientRef.current.getStatus(targetRef);\n setStatus(res.status);\n\n if (res.paid) {\n stopPolling();\n setIsLoading(false);\n onSuccess?.(res);\n } else if (\n res.status === 'failed' ||\n res.status === 'declined' ||\n res.status === 'cancelled'\n ) {\n stopPolling();\n setIsLoading(false);\n setErrorMessage('O pagamento foi recusado ou cancelado.');\n onError?.({ message: 'Pagamento recusado.' });\n }\n } catch {\n // Ignore network glitch during polling\n }\n }, pollingIntervalMs);\n },\n [stopPolling, pollingIntervalMs, onSuccess, onError]\n );\n\n // Trigger STK Push\n const initiateStk = useCallback(\n async (\n req: Omit<StkPushRequest, 'phone'> & { phone?: string }\n ): Promise<boolean> => {\n const targetPhone = req.phone || phone;\n\n if (!isEnabled) {\n const msg = 'O gateway ZumboPay está temporariamente desativado.';\n setErrorMessage(msg);\n setStatus('disabled');\n onError?.({ message: msg });\n return false;\n }\n\n if (!isValidMozPhone(targetPhone)) {\n const msg = 'Número de telefone moçambicano inválido.';\n setErrorMessage(msg);\n onError?.({ message: msg });\n return false;\n }\n\n setIsLoading(true);\n setErrorMessage(null);\n\n const res = await clientRef.current.stkPush({\n ...req,\n phone: normalizePhone(targetPhone),\n });\n\n if (res.success) {\n setStatus(res.status);\n if (res.reference) {\n setReference(res.reference);\n }\n\n if (res.status === 'success') {\n setIsLoading(false);\n onSuccess?.({\n success: true,\n status: 'success',\n paid: true,\n reference: res.reference || '',\n });\n return true;\n }\n\n // Start polling for PIN confirmation\n if (res.reference) {\n startPolling(res.reference);\n }\n return true;\n }\n\n setIsLoading(false);\n setStatus(res.status);\n setErrorMessage(res.message);\n onError?.({ message: res.message });\n return false;\n },\n [phone, isEnabled, startPolling, onSuccess, onError]\n );\n\n // Trigger Hosted Checkout\n const createCheckout = useCallback(\n async (req: CheckoutRequest): Promise<string | null> => {\n if (!isEnabled) {\n const msg = 'O gateway ZumboPay está temporariamente desativado.';\n setErrorMessage(msg);\n setStatus('disabled');\n onError?.({ message: msg });\n return null;\n }\n\n setIsLoading(true);\n setErrorMessage(null);\n\n const res = await clientRef.current.createCheckout(req);\n setIsLoading(false);\n\n if (res.success && res.checkoutUrl) {\n setReference(res.reference);\n return res.checkoutUrl;\n }\n\n setErrorMessage(res.message);\n onError?.({ message: res.message });\n return null;\n },\n [isEnabled, onError]\n );\n\n const reset = useCallback(() => {\n stopPolling();\n setIsLoading(false);\n setStatus('pending');\n setErrorMessage(null);\n setReference(null);\n }, [stopPolling]);\n\n return {\n client: clientRef.current,\n isEnabled,\n phone,\n operator,\n status,\n isLoading,\n reference,\n errorMessage,\n setPhone: handlePhoneChange,\n initiateStk,\n createCheckout,\n reset,\n stopPolling,\n };\n}\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 { 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","import React, { useState } from 'react';\nimport { getOperatorLabel } from '../phone.js';\nimport type {\n TransactionStatusResponse,\n ZumboPayConfig,\n} from '../types.js';\nimport { useZumboPay } from './useZumboPay.js';\n\nexport interface ZumboPayModalProps {\n isOpen: boolean;\n onClose: () => void;\n config: ZumboPayConfig;\n amount: number;\n currency?: string;\n reference?: string;\n title?: string;\n customerName?: string;\n initialPhone?: string;\n onPaymentSuccess?: (response: TransactionStatusResponse) => void;\n returnUrl?: string;\n cancelUrl?: string;\n}\n\nexport function ZumboPayModal({\n isOpen,\n onClose,\n config,\n amount,\n currency = 'MZN',\n reference,\n title = 'Pagamento Online',\n customerName,\n initialPhone = '',\n onPaymentSuccess,\n returnUrl,\n cancelUrl,\n}: ZumboPayModalProps) {\n const [activeTab, setActiveTab] = useState<'mobile' | 'card'>('mobile');\n\n const {\n isEnabled,\n phone,\n operator,\n status,\n isLoading,\n errorMessage,\n setPhone,\n initiateStk,\n createCheckout,\n reset,\n } = useZumboPay({\n config,\n onSuccess: (res) => {\n onPaymentSuccess?.(res);\n },\n });\n\n // Pre-fill initial phone\n React.useEffect(() => {\n if (initialPhone && !phone) {\n setPhone(initialPhone);\n }\n }, [initialPhone, phone, setPhone]);\n\n if (!isOpen) {\n return null;\n }\n\n const handleMobileSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n await initiateStk({\n amount,\n phone,\n reference,\n customerName,\n description: title,\n });\n };\n\n const handleCardCheckout = async () => {\n const checkoutUrl = await createCheckout({\n amount,\n currency,\n title,\n reference,\n returnUrl,\n cancelUrl,\n channels: ['card', 'mpesa', 'emola', 'mkesh'],\n });\n\n if (checkoutUrl) {\n window.location.href = checkoutUrl;\n }\n };\n\n const handleClose = () => {\n reset();\n onClose();\n };\n\n const formatCurrency = (val: number) => {\n return new Intl.NumberFormat('pt-MZ', {\n style: 'currency',\n currency,\n }).format(val);\n };\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs animate-in fade-in duration-200\">\n <div className=\"relative w-full max-w-md overflow-hidden bg-white dark:bg-neutral-900 rounded-2xl shadow-2xl border border-neutral-200 dark:border-neutral-800\">\n {/* Header */}\n <div className=\"flex items-center justify-between px-6 py-4 border-b border-neutral-100 dark:border-neutral-800\">\n <div>\n <h3 className=\"text-base font-bold text-neutral-900 dark:text-white\">\n {title}\n </h3>\n <p className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n {reference ? `Ref: ${reference}` : 'ZumboPay Gateway'}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={handleClose}\n className=\"p-1 rounded-full text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-800 transition\"\n >\n <span className=\"sr-only\">Fechar</span>\n <svg className=\"w-5 h-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n\n {/* Disabled Banner (Kill-switch activated) */}\n {!isEnabled && (\n <div className=\"p-6 text-center\">\n <div className=\"inline-flex items-center justify-center w-12 h-12 mb-3 rounded-full bg-amber-100 text-amber-600 dark:bg-amber-950/50 dark:text-amber-400\">\n <svg className=\"w-6 h-6\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\" />\n </svg>\n </div>\n <h4 className=\"text-sm font-bold text-neutral-900 dark:text-white\">\n Pagamentos Temporariamente Indisponíveis\n </h4>\n <p className=\"mt-1 text-xs text-neutral-500 dark:text-neutral-400\">\n O serviço de pagamentos online está temporariamente desativado pela administração.\n </p>\n <button\n type=\"button\"\n onClick={handleClose}\n className=\"mt-5 w-full py-2.5 px-4 text-xs font-semibold rounded-xl bg-neutral-100 dark:bg-neutral-800 hover:bg-neutral-200 text-neutral-700 dark:text-neutral-200 transition\"\n >\n Fechar\n </button>\n </div>\n )}\n\n {/* Active Content */}\n {isEnabled && (\n <div className=\"p-6\">\n {/* Amount Banner */}\n <div className=\"mb-5 p-4 rounded-xl bg-neutral-50 dark:bg-neutral-800/60 border border-neutral-100 dark:border-neutral-800 text-center\">\n <span className=\"text-xs font-medium text-neutral-500 dark:text-neutral-400\">\n Total a Pagar\n </span>\n <div className=\"text-2xl font-black text-neutral-900 dark:text-white\">\n {formatCurrency(amount)}\n </div>\n </div>\n\n {/* Status: SUCCESS */}\n {status === 'success' && (\n <div className=\"py-6 text-center space-y-3\">\n <div className=\"inline-flex items-center justify-center w-14 h-14 rounded-full bg-emerald-100 text-emerald-600 dark:bg-emerald-950/50 dark:text-emerald-400\">\n <svg className=\"w-8 h-8\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2.5} d=\"M5 13l4 4L19 7\" />\n </svg>\n </div>\n <h4 className=\"text-base font-bold text-emerald-900 dark:text-emerald-300\">\n Pagamento Confirmado!\n </h4>\n <p className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n O seu pagamento foi recebido e processado com sucesso.\n </p>\n <button\n type=\"button\"\n onClick={handleClose}\n className=\"mt-4 w-full py-2.5 px-4 text-xs font-semibold rounded-xl bg-emerald-600 hover:bg-emerald-700 text-white transition shadow-sm\"\n >\n Concluir\n </button>\n </div>\n )}\n\n {/* Status: WAITING FOR PIN */}\n {isLoading && status !== 'success' && (\n <div className=\"py-6 text-center space-y-3\">\n <div className=\"inline-flex items-center justify-center w-12 h-12 rounded-full bg-primary/10 text-blue-600 dark:text-blue-400 animate-pulse\">\n <svg className=\"w-6 h-6 animate-spin\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\" />\n </svg>\n </div>\n <h4 className=\"text-sm font-bold text-neutral-900 dark:text-white\">\n Confirme no seu telemóvel\n </h4>\n <p className=\"text-xs text-neutral-500 dark:text-neutral-400 max-w-xs mx-auto\">\n Enviámos um pedido para o número <strong className=\"font-mono text-neutral-900 dark:text-white\">{phone}</strong>. Introduza o seu PIN na notificação USSD para confirmar.\n </p>\n </div>\n )}\n\n {/* Form when not waiting */}\n {!isLoading && status !== 'success' && (\n <>\n {/* Tabs */}\n <div className=\"grid grid-cols-2 p-1 mb-4 rounded-xl bg-neutral-100 dark:bg-neutral-800 text-xs font-semibold\">\n <button\n type=\"button\"\n onClick={() => setActiveTab('mobile')}\n className={`py-2 rounded-lg transition ${\n activeTab === 'mobile'\n ? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-xs'\n : 'text-neutral-500 hover:text-neutral-900 dark:hover:text-white'\n }`}\n >\n Carteira Móvel\n </button>\n <button\n type=\"button\"\n onClick={() => setActiveTab('card')}\n className={`py-2 rounded-lg transition ${\n activeTab === 'card'\n ? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-xs'\n : 'text-neutral-500 hover:text-neutral-900 dark:hover:text-white'\n }`}\n >\n Cartão Bancário\n </button>\n </div>\n\n {/* Error Alert */}\n {errorMessage && (\n <div className=\"mb-4 p-3 rounded-xl bg-rose-50 dark:bg-rose-950/40 border border-rose-200 dark:border-rose-900 text-xs text-rose-700 dark:text-rose-300\">\n {errorMessage}\n </div>\n )}\n\n {/* Mobile Tab */}\n {activeTab === 'mobile' && (\n <form onSubmit={handleMobileSubmit} className=\"space-y-4\">\n <div>\n <label className=\"block text-xs font-semibold text-neutral-700 dark:text-neutral-300 mb-1\">\n Número de Telemóvel\n </label>\n <div className=\"relative\">\n <input\n type=\"tel\"\n required\n placeholder=\"84 / 85 / 86 / 87 / 82 / 83...\"\n value={phone}\n onChange={(e) => setPhone(e.target.value)}\n className=\"w-full px-3.5 py-2.5 text-sm rounded-xl border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white focus:outline-hidden focus:ring-2 focus:ring-blue-500\"\n />\n {operator !== 'unknown' && (\n <span\n className={`absolute right-2.5 top-2.5 px-2 py-0.5 text-[10px] font-bold rounded-md ${\n operator === 'mpesa'\n ? 'bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300'\n : operator === 'emola'\n ? 'bg-orange-100 text-orange-700 dark:bg-orange-950 dark:text-orange-300'\n : 'bg-yellow-100 text-yellow-800 dark:bg-yellow-950 dark:text-yellow-300'\n }`}\n >\n {getOperatorLabel(operator)}\n </span>\n )}\n </div>\n <p className=\"mt-1.5 text-[11px] text-neutral-400\">\n Compatível com M-Pesa (Vodacom), e-Mola (Movitel) e mKesh (Tmcel).\n </p>\n </div>\n\n <button\n type=\"submit\"\n disabled={isLoading}\n className=\"w-full py-3 px-4 text-xs font-bold rounded-xl bg-blue-600 hover:bg-blue-700 text-white transition shadow-sm\"\n >\n Pagar {formatCurrency(amount)}\n </button>\n </form>\n )}\n\n {/* Card Tab */}\n {activeTab === 'card' && (\n <div className=\"space-y-4 text-center\">\n <p className=\"text-xs text-neutral-600 dark:text-neutral-400\">\n Será redirecionado para a página segura do ZumboPay para inserir os dados do seu cartão Visa ou Mastercard.\n </p>\n <button\n type=\"button\"\n onClick={handleCardCheckout}\n disabled={isLoading}\n className=\"w-full py-3 px-4 text-xs font-bold rounded-xl bg-neutral-900 hover:bg-neutral-800 dark:bg-white dark:hover:bg-neutral-100 text-white dark:text-neutral-900 transition shadow-sm\"\n >\n Continuar para Checkout com Cartão\n </button>\n </div>\n )}\n </>\n )}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["import_react","React"]}
@@ -0,0 +1,393 @@
1
+ import {
2
+ ZumboPayClient,
3
+ detectOperator,
4
+ getOperatorLabel,
5
+ isValidMozPhone,
6
+ normalizePhone
7
+ } from "../chunk-YULBG3E6.mjs";
8
+
9
+ // src/react/useZumboPay.ts
10
+ import { useCallback, useEffect, useRef, useState } from "react";
11
+ function useZumboPay({
12
+ config,
13
+ onSuccess,
14
+ onError,
15
+ pollingIntervalMs = 3e3
16
+ }) {
17
+ const clientRef = useRef(new ZumboPayClient(config));
18
+ useEffect(() => {
19
+ clientRef.current = new ZumboPayClient(config);
20
+ }, [config]);
21
+ const [phone, setPhone] = useState("");
22
+ const [operator, setOperator] = useState("unknown");
23
+ const [status, setStatus] = useState("pending");
24
+ const [isLoading, setIsLoading] = useState(false);
25
+ const [reference, setReference] = useState(null);
26
+ const [errorMessage, setErrorMessage] = useState(null);
27
+ const pollingRef = useRef(null);
28
+ const isEnabled = clientRef.current.isEnabled();
29
+ const stopPolling = useCallback(() => {
30
+ if (pollingRef.current) {
31
+ clearInterval(pollingRef.current);
32
+ pollingRef.current = null;
33
+ }
34
+ }, []);
35
+ useEffect(() => {
36
+ return () => stopPolling();
37
+ }, [stopPolling]);
38
+ const handlePhoneChange = useCallback((newPhone) => {
39
+ setPhone(newPhone);
40
+ setOperator(detectOperator(newPhone));
41
+ }, []);
42
+ const startPolling = useCallback(
43
+ (targetRef) => {
44
+ stopPolling();
45
+ let attempts = 0;
46
+ const maxAttempts = 40;
47
+ pollingRef.current = setInterval(async () => {
48
+ attempts++;
49
+ if (attempts > maxAttempts) {
50
+ stopPolling();
51
+ setStatus("expired");
52
+ setErrorMessage("O tempo limite para inser\xE7\xE3o do PIN expirou.");
53
+ return;
54
+ }
55
+ try {
56
+ const res = await clientRef.current.getStatus(targetRef);
57
+ setStatus(res.status);
58
+ if (res.paid) {
59
+ stopPolling();
60
+ setIsLoading(false);
61
+ onSuccess?.(res);
62
+ } else if (res.status === "failed" || res.status === "declined" || res.status === "cancelled") {
63
+ stopPolling();
64
+ setIsLoading(false);
65
+ setErrorMessage("O pagamento foi recusado ou cancelado.");
66
+ onError?.({ message: "Pagamento recusado." });
67
+ }
68
+ } catch {
69
+ }
70
+ }, pollingIntervalMs);
71
+ },
72
+ [stopPolling, pollingIntervalMs, onSuccess, onError]
73
+ );
74
+ const initiateStk = useCallback(
75
+ async (req) => {
76
+ const targetPhone = req.phone || phone;
77
+ if (!isEnabled) {
78
+ const msg = "O gateway ZumboPay est\xE1 temporariamente desativado.";
79
+ setErrorMessage(msg);
80
+ setStatus("disabled");
81
+ onError?.({ message: msg });
82
+ return false;
83
+ }
84
+ if (!isValidMozPhone(targetPhone)) {
85
+ const msg = "N\xFAmero de telefone mo\xE7ambicano inv\xE1lido.";
86
+ setErrorMessage(msg);
87
+ onError?.({ message: msg });
88
+ return false;
89
+ }
90
+ setIsLoading(true);
91
+ setErrorMessage(null);
92
+ const res = await clientRef.current.stkPush({
93
+ ...req,
94
+ phone: normalizePhone(targetPhone)
95
+ });
96
+ if (res.success) {
97
+ setStatus(res.status);
98
+ if (res.reference) {
99
+ setReference(res.reference);
100
+ }
101
+ if (res.status === "success") {
102
+ setIsLoading(false);
103
+ onSuccess?.({
104
+ success: true,
105
+ status: "success",
106
+ paid: true,
107
+ reference: res.reference || ""
108
+ });
109
+ return true;
110
+ }
111
+ if (res.reference) {
112
+ startPolling(res.reference);
113
+ }
114
+ return true;
115
+ }
116
+ setIsLoading(false);
117
+ setStatus(res.status);
118
+ setErrorMessage(res.message);
119
+ onError?.({ message: res.message });
120
+ return false;
121
+ },
122
+ [phone, isEnabled, startPolling, onSuccess, onError]
123
+ );
124
+ const createCheckout = useCallback(
125
+ async (req) => {
126
+ if (!isEnabled) {
127
+ const msg = "O gateway ZumboPay est\xE1 temporariamente desativado.";
128
+ setErrorMessage(msg);
129
+ setStatus("disabled");
130
+ onError?.({ message: msg });
131
+ return null;
132
+ }
133
+ setIsLoading(true);
134
+ setErrorMessage(null);
135
+ const res = await clientRef.current.createCheckout(req);
136
+ setIsLoading(false);
137
+ if (res.success && res.checkoutUrl) {
138
+ setReference(res.reference);
139
+ return res.checkoutUrl;
140
+ }
141
+ setErrorMessage(res.message);
142
+ onError?.({ message: res.message });
143
+ return null;
144
+ },
145
+ [isEnabled, onError]
146
+ );
147
+ const reset = useCallback(() => {
148
+ stopPolling();
149
+ setIsLoading(false);
150
+ setStatus("pending");
151
+ setErrorMessage(null);
152
+ setReference(null);
153
+ }, [stopPolling]);
154
+ return {
155
+ client: clientRef.current,
156
+ isEnabled,
157
+ phone,
158
+ operator,
159
+ status,
160
+ isLoading,
161
+ reference,
162
+ errorMessage,
163
+ setPhone: handlePhoneChange,
164
+ initiateStk,
165
+ createCheckout,
166
+ reset,
167
+ stopPolling
168
+ };
169
+ }
170
+
171
+ // src/react/ZumboPayModal.tsx
172
+ import React, { useState as useState2 } from "react";
173
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
174
+ function ZumboPayModal({
175
+ isOpen,
176
+ onClose,
177
+ config,
178
+ amount,
179
+ currency = "MZN",
180
+ reference,
181
+ title = "Pagamento Online",
182
+ customerName,
183
+ initialPhone = "",
184
+ onPaymentSuccess,
185
+ returnUrl,
186
+ cancelUrl
187
+ }) {
188
+ const [activeTab, setActiveTab] = useState2("mobile");
189
+ const {
190
+ isEnabled,
191
+ phone,
192
+ operator,
193
+ status,
194
+ isLoading,
195
+ errorMessage,
196
+ setPhone,
197
+ initiateStk,
198
+ createCheckout,
199
+ reset
200
+ } = useZumboPay({
201
+ config,
202
+ onSuccess: (res) => {
203
+ onPaymentSuccess?.(res);
204
+ }
205
+ });
206
+ React.useEffect(() => {
207
+ if (initialPhone && !phone) {
208
+ setPhone(initialPhone);
209
+ }
210
+ }, [initialPhone, phone, setPhone]);
211
+ if (!isOpen) {
212
+ return null;
213
+ }
214
+ const handleMobileSubmit = async (e) => {
215
+ e.preventDefault();
216
+ await initiateStk({
217
+ amount,
218
+ phone,
219
+ reference,
220
+ customerName,
221
+ description: title
222
+ });
223
+ };
224
+ const handleCardCheckout = async () => {
225
+ const checkoutUrl = await createCheckout({
226
+ amount,
227
+ currency,
228
+ title,
229
+ reference,
230
+ returnUrl,
231
+ cancelUrl,
232
+ channels: ["card", "mpesa", "emola", "mkesh"]
233
+ });
234
+ if (checkoutUrl) {
235
+ window.location.href = checkoutUrl;
236
+ }
237
+ };
238
+ const handleClose = () => {
239
+ reset();
240
+ onClose();
241
+ };
242
+ const formatCurrency = (val) => {
243
+ return new Intl.NumberFormat("pt-MZ", {
244
+ style: "currency",
245
+ currency
246
+ }).format(val);
247
+ };
248
+ return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs animate-in fade-in duration-200", children: /* @__PURE__ */ jsxs("div", { className: "relative w-full max-w-md overflow-hidden bg-white dark:bg-neutral-900 rounded-2xl shadow-2xl border border-neutral-200 dark:border-neutral-800", children: [
249
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-neutral-100 dark:border-neutral-800", children: [
250
+ /* @__PURE__ */ jsxs("div", { children: [
251
+ /* @__PURE__ */ jsx("h3", { className: "text-base font-bold text-neutral-900 dark:text-white", children: title }),
252
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-neutral-500 dark:text-neutral-400", children: reference ? `Ref: ${reference}` : "ZumboPay Gateway" })
253
+ ] }),
254
+ /* @__PURE__ */ jsxs(
255
+ "button",
256
+ {
257
+ type: "button",
258
+ onClick: handleClose,
259
+ className: "p-1 rounded-full text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-800 transition",
260
+ children: [
261
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Fechar" }),
262
+ /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
263
+ ]
264
+ }
265
+ )
266
+ ] }),
267
+ !isEnabled && /* @__PURE__ */ jsxs("div", { className: "p-6 text-center", children: [
268
+ /* @__PURE__ */ jsx("div", { className: "inline-flex items-center justify-center w-12 h-12 mb-3 rounded-full bg-amber-100 text-amber-600 dark:bg-amber-950/50 dark:text-amber-400", children: /* @__PURE__ */ jsx("svg", { className: "w-6 h-6", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" }) }) }),
269
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-bold text-neutral-900 dark:text-white", children: "Pagamentos Temporariamente Indispon\xEDveis" }),
270
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-neutral-500 dark:text-neutral-400", children: "O servi\xE7o de pagamentos online est\xE1 temporariamente desativado pela administra\xE7\xE3o." }),
271
+ /* @__PURE__ */ jsx(
272
+ "button",
273
+ {
274
+ type: "button",
275
+ onClick: handleClose,
276
+ className: "mt-5 w-full py-2.5 px-4 text-xs font-semibold rounded-xl bg-neutral-100 dark:bg-neutral-800 hover:bg-neutral-200 text-neutral-700 dark:text-neutral-200 transition",
277
+ children: "Fechar"
278
+ }
279
+ )
280
+ ] }),
281
+ isEnabled && /* @__PURE__ */ jsxs("div", { className: "p-6", children: [
282
+ /* @__PURE__ */ jsxs("div", { className: "mb-5 p-4 rounded-xl bg-neutral-50 dark:bg-neutral-800/60 border border-neutral-100 dark:border-neutral-800 text-center", children: [
283
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-neutral-500 dark:text-neutral-400", children: "Total a Pagar" }),
284
+ /* @__PURE__ */ jsx("div", { className: "text-2xl font-black text-neutral-900 dark:text-white", children: formatCurrency(amount) })
285
+ ] }),
286
+ status === "success" && /* @__PURE__ */ jsxs("div", { className: "py-6 text-center space-y-3", children: [
287
+ /* @__PURE__ */ jsx("div", { className: "inline-flex items-center justify-center w-14 h-14 rounded-full bg-emerald-100 text-emerald-600 dark:bg-emerald-950/50 dark:text-emerald-400", children: /* @__PURE__ */ jsx("svg", { className: "w-8 h-8", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M5 13l4 4L19 7" }) }) }),
288
+ /* @__PURE__ */ jsx("h4", { className: "text-base font-bold text-emerald-900 dark:text-emerald-300", children: "Pagamento Confirmado!" }),
289
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-neutral-500 dark:text-neutral-400", children: "O seu pagamento foi recebido e processado com sucesso." }),
290
+ /* @__PURE__ */ jsx(
291
+ "button",
292
+ {
293
+ type: "button",
294
+ onClick: handleClose,
295
+ className: "mt-4 w-full py-2.5 px-4 text-xs font-semibold rounded-xl bg-emerald-600 hover:bg-emerald-700 text-white transition shadow-sm",
296
+ children: "Concluir"
297
+ }
298
+ )
299
+ ] }),
300
+ isLoading && status !== "success" && /* @__PURE__ */ jsxs("div", { className: "py-6 text-center space-y-3", children: [
301
+ /* @__PURE__ */ jsx("div", { className: "inline-flex items-center justify-center w-12 h-12 rounded-full bg-primary/10 text-blue-600 dark:text-blue-400 animate-pulse", children: /* @__PURE__ */ jsxs("svg", { className: "w-6 h-6 animate-spin", fill: "none", viewBox: "0 0 24 24", children: [
302
+ /* @__PURE__ */ jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
303
+ /* @__PURE__ */ jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })
304
+ ] }) }),
305
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-bold text-neutral-900 dark:text-white", children: "Confirme no seu telem\xF3vel" }),
306
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-neutral-500 dark:text-neutral-400 max-w-xs mx-auto", children: [
307
+ "Envi\xE1mos um pedido para o n\xFAmero ",
308
+ /* @__PURE__ */ jsx("strong", { className: "font-mono text-neutral-900 dark:text-white", children: phone }),
309
+ ". Introduza o seu PIN na notifica\xE7\xE3o USSD para confirmar."
310
+ ] })
311
+ ] }),
312
+ !isLoading && status !== "success" && /* @__PURE__ */ jsxs(Fragment, { children: [
313
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 p-1 mb-4 rounded-xl bg-neutral-100 dark:bg-neutral-800 text-xs font-semibold", children: [
314
+ /* @__PURE__ */ jsx(
315
+ "button",
316
+ {
317
+ type: "button",
318
+ onClick: () => setActiveTab("mobile"),
319
+ className: `py-2 rounded-lg transition ${activeTab === "mobile" ? "bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-xs" : "text-neutral-500 hover:text-neutral-900 dark:hover:text-white"}`,
320
+ children: "Carteira M\xF3vel"
321
+ }
322
+ ),
323
+ /* @__PURE__ */ jsx(
324
+ "button",
325
+ {
326
+ type: "button",
327
+ onClick: () => setActiveTab("card"),
328
+ className: `py-2 rounded-lg transition ${activeTab === "card" ? "bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-xs" : "text-neutral-500 hover:text-neutral-900 dark:hover:text-white"}`,
329
+ children: "Cart\xE3o Banc\xE1rio"
330
+ }
331
+ )
332
+ ] }),
333
+ errorMessage && /* @__PURE__ */ jsx("div", { className: "mb-4 p-3 rounded-xl bg-rose-50 dark:bg-rose-950/40 border border-rose-200 dark:border-rose-900 text-xs text-rose-700 dark:text-rose-300", children: errorMessage }),
334
+ activeTab === "mobile" && /* @__PURE__ */ jsxs("form", { onSubmit: handleMobileSubmit, className: "space-y-4", children: [
335
+ /* @__PURE__ */ jsxs("div", { children: [
336
+ /* @__PURE__ */ jsx("label", { className: "block text-xs font-semibold text-neutral-700 dark:text-neutral-300 mb-1", children: "N\xFAmero de Telem\xF3vel" }),
337
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
338
+ /* @__PURE__ */ jsx(
339
+ "input",
340
+ {
341
+ type: "tel",
342
+ required: true,
343
+ placeholder: "84 / 85 / 86 / 87 / 82 / 83...",
344
+ value: phone,
345
+ onChange: (e) => setPhone(e.target.value),
346
+ className: "w-full px-3.5 py-2.5 text-sm rounded-xl border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white focus:outline-hidden focus:ring-2 focus:ring-blue-500"
347
+ }
348
+ ),
349
+ operator !== "unknown" && /* @__PURE__ */ jsx(
350
+ "span",
351
+ {
352
+ className: `absolute right-2.5 top-2.5 px-2 py-0.5 text-[10px] font-bold rounded-md ${operator === "mpesa" ? "bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300" : operator === "emola" ? "bg-orange-100 text-orange-700 dark:bg-orange-950 dark:text-orange-300" : "bg-yellow-100 text-yellow-800 dark:bg-yellow-950 dark:text-yellow-300"}`,
353
+ children: getOperatorLabel(operator)
354
+ }
355
+ )
356
+ ] }),
357
+ /* @__PURE__ */ jsx("p", { className: "mt-1.5 text-[11px] text-neutral-400", children: "Compat\xEDvel com M-Pesa (Vodacom), e-Mola (Movitel) e mKesh (Tmcel)." })
358
+ ] }),
359
+ /* @__PURE__ */ jsxs(
360
+ "button",
361
+ {
362
+ type: "submit",
363
+ disabled: isLoading,
364
+ className: "w-full py-3 px-4 text-xs font-bold rounded-xl bg-blue-600 hover:bg-blue-700 text-white transition shadow-sm",
365
+ children: [
366
+ "Pagar ",
367
+ formatCurrency(amount)
368
+ ]
369
+ }
370
+ )
371
+ ] }),
372
+ activeTab === "card" && /* @__PURE__ */ jsxs("div", { className: "space-y-4 text-center", children: [
373
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-neutral-600 dark:text-neutral-400", children: "Ser\xE1 redirecionado para a p\xE1gina segura do ZumboPay para inserir os dados do seu cart\xE3o Visa ou Mastercard." }),
374
+ /* @__PURE__ */ jsx(
375
+ "button",
376
+ {
377
+ type: "button",
378
+ onClick: handleCardCheckout,
379
+ disabled: isLoading,
380
+ className: "w-full py-3 px-4 text-xs font-bold rounded-xl bg-neutral-900 hover:bg-neutral-800 dark:bg-white dark:hover:bg-neutral-100 text-white dark:text-neutral-900 transition shadow-sm",
381
+ children: "Continuar para Checkout com Cart\xE3o"
382
+ }
383
+ )
384
+ ] })
385
+ ] })
386
+ ] })
387
+ ] }) });
388
+ }
389
+ export {
390
+ ZumboPayModal,
391
+ useZumboPay
392
+ };
393
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/useZumboPay.ts","../../src/react/ZumboPayModal.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport { ZumboPayClient } from '../client.js';\nimport { detectOperator, isValidMozPhone, normalizePhone } from '../phone.js';\nimport type {\n CheckoutRequest,\n MobileOperator,\n PaymentStatus,\n StkPushRequest,\n TransactionStatusResponse,\n ZumboPayConfig,\n} from '../types.js';\n\nexport interface UseZumboPayOptions {\n config: ZumboPayConfig;\n onSuccess?: (response: TransactionStatusResponse) => void;\n onError?: (error: Error | { message: string }) => void;\n pollingIntervalMs?: number;\n}\n\nexport function useZumboPay({\n config,\n onSuccess,\n onError,\n pollingIntervalMs = 3000,\n}: UseZumboPayOptions) {\n const clientRef = useRef<ZumboPayClient>(new ZumboPayClient(config));\n\n // Update client config if dependencies change\n useEffect(() => {\n clientRef.current = new ZumboPayClient(config);\n }, [config]);\n\n const [phone, setPhone] = useState('');\n const [operator, setOperator] = useState<MobileOperator>('unknown');\n const [status, setStatus] = useState<PaymentStatus>('pending');\n const [isLoading, setIsLoading] = useState(false);\n const [reference, setReference] = useState<string | null>(null);\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n\n const pollingRef = useRef<NodeJS.Timeout | null>(null);\n const isEnabled = clientRef.current.isEnabled();\n\n // Stop polling helper\n const stopPolling = useCallback(() => {\n if (pollingRef.current) {\n clearInterval(pollingRef.current);\n pollingRef.current = null;\n }\n }, []);\n\n useEffect(() => {\n return () => stopPolling();\n }, [stopPolling]);\n\n // Handle phone input change with live operator detection\n const handlePhoneChange = useCallback((newPhone: string) => {\n setPhone(newPhone);\n setOperator(detectOperator(newPhone));\n }, []);\n\n // Poll status until terminal state\n const startPolling = useCallback(\n (targetRef: string) => {\n stopPolling();\n\n let attempts = 0;\n const maxAttempts = 40; // ~2 minutes\n\n pollingRef.current = setInterval(async () => {\n attempts++;\n if (attempts > maxAttempts) {\n stopPolling();\n setStatus('expired');\n setErrorMessage('O tempo limite para inserção do PIN expirou.');\n return;\n }\n\n try {\n const res = await clientRef.current.getStatus(targetRef);\n setStatus(res.status);\n\n if (res.paid) {\n stopPolling();\n setIsLoading(false);\n onSuccess?.(res);\n } else if (\n res.status === 'failed' ||\n res.status === 'declined' ||\n res.status === 'cancelled'\n ) {\n stopPolling();\n setIsLoading(false);\n setErrorMessage('O pagamento foi recusado ou cancelado.');\n onError?.({ message: 'Pagamento recusado.' });\n }\n } catch {\n // Ignore network glitch during polling\n }\n }, pollingIntervalMs);\n },\n [stopPolling, pollingIntervalMs, onSuccess, onError]\n );\n\n // Trigger STK Push\n const initiateStk = useCallback(\n async (\n req: Omit<StkPushRequest, 'phone'> & { phone?: string }\n ): Promise<boolean> => {\n const targetPhone = req.phone || phone;\n\n if (!isEnabled) {\n const msg = 'O gateway ZumboPay está temporariamente desativado.';\n setErrorMessage(msg);\n setStatus('disabled');\n onError?.({ message: msg });\n return false;\n }\n\n if (!isValidMozPhone(targetPhone)) {\n const msg = 'Número de telefone moçambicano inválido.';\n setErrorMessage(msg);\n onError?.({ message: msg });\n return false;\n }\n\n setIsLoading(true);\n setErrorMessage(null);\n\n const res = await clientRef.current.stkPush({\n ...req,\n phone: normalizePhone(targetPhone),\n });\n\n if (res.success) {\n setStatus(res.status);\n if (res.reference) {\n setReference(res.reference);\n }\n\n if (res.status === 'success') {\n setIsLoading(false);\n onSuccess?.({\n success: true,\n status: 'success',\n paid: true,\n reference: res.reference || '',\n });\n return true;\n }\n\n // Start polling for PIN confirmation\n if (res.reference) {\n startPolling(res.reference);\n }\n return true;\n }\n\n setIsLoading(false);\n setStatus(res.status);\n setErrorMessage(res.message);\n onError?.({ message: res.message });\n return false;\n },\n [phone, isEnabled, startPolling, onSuccess, onError]\n );\n\n // Trigger Hosted Checkout\n const createCheckout = useCallback(\n async (req: CheckoutRequest): Promise<string | null> => {\n if (!isEnabled) {\n const msg = 'O gateway ZumboPay está temporariamente desativado.';\n setErrorMessage(msg);\n setStatus('disabled');\n onError?.({ message: msg });\n return null;\n }\n\n setIsLoading(true);\n setErrorMessage(null);\n\n const res = await clientRef.current.createCheckout(req);\n setIsLoading(false);\n\n if (res.success && res.checkoutUrl) {\n setReference(res.reference);\n return res.checkoutUrl;\n }\n\n setErrorMessage(res.message);\n onError?.({ message: res.message });\n return null;\n },\n [isEnabled, onError]\n );\n\n const reset = useCallback(() => {\n stopPolling();\n setIsLoading(false);\n setStatus('pending');\n setErrorMessage(null);\n setReference(null);\n }, [stopPolling]);\n\n return {\n client: clientRef.current,\n isEnabled,\n phone,\n operator,\n status,\n isLoading,\n reference,\n errorMessage,\n setPhone: handlePhoneChange,\n initiateStk,\n createCheckout,\n reset,\n stopPolling,\n };\n}\n","import React, { useState } from 'react';\nimport { getOperatorLabel } from '../phone.js';\nimport type {\n TransactionStatusResponse,\n ZumboPayConfig,\n} from '../types.js';\nimport { useZumboPay } from './useZumboPay.js';\n\nexport interface ZumboPayModalProps {\n isOpen: boolean;\n onClose: () => void;\n config: ZumboPayConfig;\n amount: number;\n currency?: string;\n reference?: string;\n title?: string;\n customerName?: string;\n initialPhone?: string;\n onPaymentSuccess?: (response: TransactionStatusResponse) => void;\n returnUrl?: string;\n cancelUrl?: string;\n}\n\nexport function ZumboPayModal({\n isOpen,\n onClose,\n config,\n amount,\n currency = 'MZN',\n reference,\n title = 'Pagamento Online',\n customerName,\n initialPhone = '',\n onPaymentSuccess,\n returnUrl,\n cancelUrl,\n}: ZumboPayModalProps) {\n const [activeTab, setActiveTab] = useState<'mobile' | 'card'>('mobile');\n\n const {\n isEnabled,\n phone,\n operator,\n status,\n isLoading,\n errorMessage,\n setPhone,\n initiateStk,\n createCheckout,\n reset,\n } = useZumboPay({\n config,\n onSuccess: (res) => {\n onPaymentSuccess?.(res);\n },\n });\n\n // Pre-fill initial phone\n React.useEffect(() => {\n if (initialPhone && !phone) {\n setPhone(initialPhone);\n }\n }, [initialPhone, phone, setPhone]);\n\n if (!isOpen) {\n return null;\n }\n\n const handleMobileSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n await initiateStk({\n amount,\n phone,\n reference,\n customerName,\n description: title,\n });\n };\n\n const handleCardCheckout = async () => {\n const checkoutUrl = await createCheckout({\n amount,\n currency,\n title,\n reference,\n returnUrl,\n cancelUrl,\n channels: ['card', 'mpesa', 'emola', 'mkesh'],\n });\n\n if (checkoutUrl) {\n window.location.href = checkoutUrl;\n }\n };\n\n const handleClose = () => {\n reset();\n onClose();\n };\n\n const formatCurrency = (val: number) => {\n return new Intl.NumberFormat('pt-MZ', {\n style: 'currency',\n currency,\n }).format(val);\n };\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs animate-in fade-in duration-200\">\n <div className=\"relative w-full max-w-md overflow-hidden bg-white dark:bg-neutral-900 rounded-2xl shadow-2xl border border-neutral-200 dark:border-neutral-800\">\n {/* Header */}\n <div className=\"flex items-center justify-between px-6 py-4 border-b border-neutral-100 dark:border-neutral-800\">\n <div>\n <h3 className=\"text-base font-bold text-neutral-900 dark:text-white\">\n {title}\n </h3>\n <p className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n {reference ? `Ref: ${reference}` : 'ZumboPay Gateway'}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={handleClose}\n className=\"p-1 rounded-full text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-800 transition\"\n >\n <span className=\"sr-only\">Fechar</span>\n <svg className=\"w-5 h-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n\n {/* Disabled Banner (Kill-switch activated) */}\n {!isEnabled && (\n <div className=\"p-6 text-center\">\n <div className=\"inline-flex items-center justify-center w-12 h-12 mb-3 rounded-full bg-amber-100 text-amber-600 dark:bg-amber-950/50 dark:text-amber-400\">\n <svg className=\"w-6 h-6\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\" />\n </svg>\n </div>\n <h4 className=\"text-sm font-bold text-neutral-900 dark:text-white\">\n Pagamentos Temporariamente Indisponíveis\n </h4>\n <p className=\"mt-1 text-xs text-neutral-500 dark:text-neutral-400\">\n O serviço de pagamentos online está temporariamente desativado pela administração.\n </p>\n <button\n type=\"button\"\n onClick={handleClose}\n className=\"mt-5 w-full py-2.5 px-4 text-xs font-semibold rounded-xl bg-neutral-100 dark:bg-neutral-800 hover:bg-neutral-200 text-neutral-700 dark:text-neutral-200 transition\"\n >\n Fechar\n </button>\n </div>\n )}\n\n {/* Active Content */}\n {isEnabled && (\n <div className=\"p-6\">\n {/* Amount Banner */}\n <div className=\"mb-5 p-4 rounded-xl bg-neutral-50 dark:bg-neutral-800/60 border border-neutral-100 dark:border-neutral-800 text-center\">\n <span className=\"text-xs font-medium text-neutral-500 dark:text-neutral-400\">\n Total a Pagar\n </span>\n <div className=\"text-2xl font-black text-neutral-900 dark:text-white\">\n {formatCurrency(amount)}\n </div>\n </div>\n\n {/* Status: SUCCESS */}\n {status === 'success' && (\n <div className=\"py-6 text-center space-y-3\">\n <div className=\"inline-flex items-center justify-center w-14 h-14 rounded-full bg-emerald-100 text-emerald-600 dark:bg-emerald-950/50 dark:text-emerald-400\">\n <svg className=\"w-8 h-8\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2.5} d=\"M5 13l4 4L19 7\" />\n </svg>\n </div>\n <h4 className=\"text-base font-bold text-emerald-900 dark:text-emerald-300\">\n Pagamento Confirmado!\n </h4>\n <p className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n O seu pagamento foi recebido e processado com sucesso.\n </p>\n <button\n type=\"button\"\n onClick={handleClose}\n className=\"mt-4 w-full py-2.5 px-4 text-xs font-semibold rounded-xl bg-emerald-600 hover:bg-emerald-700 text-white transition shadow-sm\"\n >\n Concluir\n </button>\n </div>\n )}\n\n {/* Status: WAITING FOR PIN */}\n {isLoading && status !== 'success' && (\n <div className=\"py-6 text-center space-y-3\">\n <div className=\"inline-flex items-center justify-center w-12 h-12 rounded-full bg-primary/10 text-blue-600 dark:text-blue-400 animate-pulse\">\n <svg className=\"w-6 h-6 animate-spin\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\" />\n </svg>\n </div>\n <h4 className=\"text-sm font-bold text-neutral-900 dark:text-white\">\n Confirme no seu telemóvel\n </h4>\n <p className=\"text-xs text-neutral-500 dark:text-neutral-400 max-w-xs mx-auto\">\n Enviámos um pedido para o número <strong className=\"font-mono text-neutral-900 dark:text-white\">{phone}</strong>. Introduza o seu PIN na notificação USSD para confirmar.\n </p>\n </div>\n )}\n\n {/* Form when not waiting */}\n {!isLoading && status !== 'success' && (\n <>\n {/* Tabs */}\n <div className=\"grid grid-cols-2 p-1 mb-4 rounded-xl bg-neutral-100 dark:bg-neutral-800 text-xs font-semibold\">\n <button\n type=\"button\"\n onClick={() => setActiveTab('mobile')}\n className={`py-2 rounded-lg transition ${\n activeTab === 'mobile'\n ? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-xs'\n : 'text-neutral-500 hover:text-neutral-900 dark:hover:text-white'\n }`}\n >\n Carteira Móvel\n </button>\n <button\n type=\"button\"\n onClick={() => setActiveTab('card')}\n className={`py-2 rounded-lg transition ${\n activeTab === 'card'\n ? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-xs'\n : 'text-neutral-500 hover:text-neutral-900 dark:hover:text-white'\n }`}\n >\n Cartão Bancário\n </button>\n </div>\n\n {/* Error Alert */}\n {errorMessage && (\n <div className=\"mb-4 p-3 rounded-xl bg-rose-50 dark:bg-rose-950/40 border border-rose-200 dark:border-rose-900 text-xs text-rose-700 dark:text-rose-300\">\n {errorMessage}\n </div>\n )}\n\n {/* Mobile Tab */}\n {activeTab === 'mobile' && (\n <form onSubmit={handleMobileSubmit} className=\"space-y-4\">\n <div>\n <label className=\"block text-xs font-semibold text-neutral-700 dark:text-neutral-300 mb-1\">\n Número de Telemóvel\n </label>\n <div className=\"relative\">\n <input\n type=\"tel\"\n required\n placeholder=\"84 / 85 / 86 / 87 / 82 / 83...\"\n value={phone}\n onChange={(e) => setPhone(e.target.value)}\n className=\"w-full px-3.5 py-2.5 text-sm rounded-xl border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white focus:outline-hidden focus:ring-2 focus:ring-blue-500\"\n />\n {operator !== 'unknown' && (\n <span\n className={`absolute right-2.5 top-2.5 px-2 py-0.5 text-[10px] font-bold rounded-md ${\n operator === 'mpesa'\n ? 'bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300'\n : operator === 'emola'\n ? 'bg-orange-100 text-orange-700 dark:bg-orange-950 dark:text-orange-300'\n : 'bg-yellow-100 text-yellow-800 dark:bg-yellow-950 dark:text-yellow-300'\n }`}\n >\n {getOperatorLabel(operator)}\n </span>\n )}\n </div>\n <p className=\"mt-1.5 text-[11px] text-neutral-400\">\n Compatível com M-Pesa (Vodacom), e-Mola (Movitel) e mKesh (Tmcel).\n </p>\n </div>\n\n <button\n type=\"submit\"\n disabled={isLoading}\n className=\"w-full py-3 px-4 text-xs font-bold rounded-xl bg-blue-600 hover:bg-blue-700 text-white transition shadow-sm\"\n >\n Pagar {formatCurrency(amount)}\n </button>\n </form>\n )}\n\n {/* Card Tab */}\n {activeTab === 'card' && (\n <div className=\"space-y-4 text-center\">\n <p className=\"text-xs text-neutral-600 dark:text-neutral-400\">\n Será redirecionado para a página segura do ZumboPay para inserir os dados do seu cartão Visa ou Mastercard.\n </p>\n <button\n type=\"button\"\n onClick={handleCardCheckout}\n disabled={isLoading}\n className=\"w-full py-3 px-4 text-xs font-bold rounded-xl bg-neutral-900 hover:bg-neutral-800 dark:bg-white dark:hover:bg-neutral-100 text-white dark:text-neutral-900 transition shadow-sm\"\n >\n Continuar para Checkout com Cartão\n </button>\n </div>\n )}\n </>\n )}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["useState","useState"]}