@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,755 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/react/index.ts
|
|
31
|
+
var react_exports = {};
|
|
32
|
+
__export(react_exports, {
|
|
33
|
+
ZumboPayModal: () => ZumboPayModal,
|
|
34
|
+
useZumboPay: () => useZumboPay
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(react_exports);
|
|
37
|
+
|
|
38
|
+
// src/react/useZumboPay.ts
|
|
39
|
+
var import_react = require("react");
|
|
40
|
+
|
|
41
|
+
// src/phone.ts
|
|
42
|
+
function normalizePhone(phone) {
|
|
43
|
+
let cleaned = phone.replace(/\D/g, "");
|
|
44
|
+
if (cleaned.startsWith("0")) {
|
|
45
|
+
cleaned = cleaned.slice(1);
|
|
46
|
+
}
|
|
47
|
+
if (cleaned.length === 9) {
|
|
48
|
+
cleaned = "258" + cleaned;
|
|
49
|
+
}
|
|
50
|
+
return cleaned;
|
|
51
|
+
}
|
|
52
|
+
function detectOperator(phone) {
|
|
53
|
+
const normalized = normalizePhone(phone);
|
|
54
|
+
if (normalized.length !== 12 || !normalized.startsWith("258")) {
|
|
55
|
+
return "unknown";
|
|
56
|
+
}
|
|
57
|
+
const prefix = normalized.slice(3, 5);
|
|
58
|
+
if (prefix === "84" || prefix === "85") {
|
|
59
|
+
return "mpesa";
|
|
60
|
+
}
|
|
61
|
+
if (prefix === "86" || prefix === "87") {
|
|
62
|
+
return "emola";
|
|
63
|
+
}
|
|
64
|
+
if (prefix === "82" || prefix === "83") {
|
|
65
|
+
return "mkesh";
|
|
66
|
+
}
|
|
67
|
+
return "unknown";
|
|
68
|
+
}
|
|
69
|
+
function getOperatorLabel(operator) {
|
|
70
|
+
switch (operator) {
|
|
71
|
+
case "mpesa":
|
|
72
|
+
return "Vodacom M-Pesa";
|
|
73
|
+
case "emola":
|
|
74
|
+
return "Movitel e-Mola";
|
|
75
|
+
case "mkesh":
|
|
76
|
+
return "Tmcel mKesh";
|
|
77
|
+
default:
|
|
78
|
+
return "Desconhecida";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function isValidMozPhone(phone) {
|
|
82
|
+
return detectOperator(phone) !== "unknown";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/client.ts
|
|
86
|
+
var ZumboPayClient = class {
|
|
87
|
+
config;
|
|
88
|
+
cachedWallets = null;
|
|
89
|
+
walletsCacheExpiresAt = 0;
|
|
90
|
+
constructor(config) {
|
|
91
|
+
const envWallets = {};
|
|
92
|
+
let defaultEnabled = true;
|
|
93
|
+
if (typeof process !== "undefined" && process?.env) {
|
|
94
|
+
if (process.env.ZUMBOPAY_ENABLED !== void 0) {
|
|
95
|
+
defaultEnabled = process.env.ZUMBOPAY_ENABLED === "true" || process.env.ZUMBOPAY_ENABLED === "1";
|
|
96
|
+
}
|
|
97
|
+
if (process.env.ZUMBOPAY_WALLET_MPESA) {
|
|
98
|
+
envWallets.mpesa = process.env.ZUMBOPAY_WALLET_MPESA;
|
|
99
|
+
}
|
|
100
|
+
if (process.env.ZUMBOPAY_WALLET_EMOLA) {
|
|
101
|
+
envWallets.emola = process.env.ZUMBOPAY_WALLET_EMOLA;
|
|
102
|
+
}
|
|
103
|
+
if (process.env.ZUMBOPAY_WALLET_MKESH) {
|
|
104
|
+
envWallets.mkesh = process.env.ZUMBOPAY_WALLET_MKESH;
|
|
105
|
+
}
|
|
106
|
+
if (process.env.ZUMBOPAY_WALLET_CARD) {
|
|
107
|
+
envWallets.card = process.env.ZUMBOPAY_WALLET_CARD;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
this.config = {
|
|
111
|
+
apiKey: config.apiKey,
|
|
112
|
+
merchantId: config.merchantId,
|
|
113
|
+
baseUrl: config.baseUrl?.replace(/\/+$/, "") || "https://zumbopay.com/api/public/v1",
|
|
114
|
+
webhookSecret: config.webhookSecret,
|
|
115
|
+
wallets: {
|
|
116
|
+
...envWallets,
|
|
117
|
+
...config.wallets || {}
|
|
118
|
+
},
|
|
119
|
+
enabled: config.enabled !== void 0 ? config.enabled : defaultEnabled,
|
|
120
|
+
timeout: config.timeout || 15e3
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Returns whether the ZumboPay client is currently active.
|
|
125
|
+
* If false, all mutations (STK push, checkouts) are gracefully silenced.
|
|
126
|
+
*/
|
|
127
|
+
isEnabled() {
|
|
128
|
+
return Boolean(this.config.enabled);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Programmatically enable or disable the gateway (kill-switch toggle).
|
|
132
|
+
*/
|
|
133
|
+
setEnabled(enabled) {
|
|
134
|
+
this.config.enabled = enabled;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Returns configured base URL.
|
|
138
|
+
*/
|
|
139
|
+
getBaseUrl() {
|
|
140
|
+
return this.config.baseUrl;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Resolves a valid Wallet UUID for a given operator or channel.
|
|
144
|
+
*/
|
|
145
|
+
async resolveWalletId(channel) {
|
|
146
|
+
if (this.config.wallets[channel]) {
|
|
147
|
+
return this.config.wallets[channel];
|
|
148
|
+
}
|
|
149
|
+
const wallets = await this.listWallets();
|
|
150
|
+
const match = wallets.find(
|
|
151
|
+
(w) => w.is_active !== false && (w.method?.toLowerCase() === channel || w.wallet_code?.toLowerCase().includes(channel) || w.name?.toLowerCase().includes(channel))
|
|
152
|
+
);
|
|
153
|
+
if (match) {
|
|
154
|
+
return match.id;
|
|
155
|
+
}
|
|
156
|
+
const active = wallets.find((w) => w.is_active !== false);
|
|
157
|
+
return active?.id;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Resolves the proper wallet ID for a Mozambican phone number.
|
|
161
|
+
*/
|
|
162
|
+
async resolveWalletIdForPhone(phone) {
|
|
163
|
+
const operator = detectOperator(phone);
|
|
164
|
+
if (operator === "unknown") {
|
|
165
|
+
return void 0;
|
|
166
|
+
}
|
|
167
|
+
return this.resolveWalletId(operator);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Initiates a direct STK Push prompt to a mobile phone (M-Pesa, e-Mola, mKesh).
|
|
171
|
+
*/
|
|
172
|
+
async stkPush(request) {
|
|
173
|
+
if (!this.isEnabled()) {
|
|
174
|
+
return {
|
|
175
|
+
success: false,
|
|
176
|
+
status: "disabled",
|
|
177
|
+
reference: null,
|
|
178
|
+
message: "O gateway de pagamento ZumboPay est\xE1 temporariamente desativado."
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const normalizedPhone = normalizePhone(request.phone);
|
|
182
|
+
const walletId = request.walletId || await this.resolveWalletIdForPhone(normalizedPhone);
|
|
183
|
+
const sourceId = request.reference || `stk-${Date.now()}`;
|
|
184
|
+
const payload = {
|
|
185
|
+
phone: normalizedPhone,
|
|
186
|
+
amount: request.amount,
|
|
187
|
+
wallet_id: walletId,
|
|
188
|
+
reference: sourceId,
|
|
189
|
+
customer_name: request.customerName,
|
|
190
|
+
description: request.description
|
|
191
|
+
};
|
|
192
|
+
try {
|
|
193
|
+
const res = await this.fetchWithTimeout("/charges", {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: this.getHeaders(),
|
|
196
|
+
body: JSON.stringify(payload)
|
|
197
|
+
});
|
|
198
|
+
const data = await res.json().catch(() => ({}));
|
|
199
|
+
if (res.ok) {
|
|
200
|
+
const status = (data.data?.status || data.status || "pending").toLowerCase();
|
|
201
|
+
const code = data.code || data.data?.code;
|
|
202
|
+
const isSuccess = status === "success" || status === "succeeded" || status === "completed" || code === "INS-0";
|
|
203
|
+
return {
|
|
204
|
+
success: true,
|
|
205
|
+
status: isSuccess ? "success" : status,
|
|
206
|
+
reference: data.data?.reference || sourceId,
|
|
207
|
+
message: isSuccess ? "Pagamento efetuado com sucesso!" : "Pedido de pagamento enviado para o seu telem\xF3vel. Por favor confirme com o seu PIN.",
|
|
208
|
+
raw: data
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
success: false,
|
|
213
|
+
status: "declined",
|
|
214
|
+
reference: null,
|
|
215
|
+
message: data.error?.message || data.message || "O pagamento foi recusado ou expirou no telem\xF3vel.",
|
|
216
|
+
raw: data
|
|
217
|
+
};
|
|
218
|
+
} catch (err) {
|
|
219
|
+
const error = err;
|
|
220
|
+
if (error.name === "AbortError" || error.message.includes("timeout")) {
|
|
221
|
+
return {
|
|
222
|
+
success: true,
|
|
223
|
+
status: "pending",
|
|
224
|
+
reference: sourceId,
|
|
225
|
+
message: "O pedido foi enviado. Por favor verifique o telem\xF3vel e confirme com o PIN.",
|
|
226
|
+
raw: { timeout: true }
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
success: false,
|
|
231
|
+
status: "failed",
|
|
232
|
+
reference: null,
|
|
233
|
+
message: error.message || "Falha na comunica\xE7\xE3o com o ZumboPay."
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Creates a Hosted Checkout URL for credit/debit card & multicanal payments.
|
|
239
|
+
*/
|
|
240
|
+
async createCheckout(request) {
|
|
241
|
+
if (!this.isEnabled()) {
|
|
242
|
+
return {
|
|
243
|
+
success: false,
|
|
244
|
+
checkoutUrl: null,
|
|
245
|
+
reference: null,
|
|
246
|
+
message: "O gateway de pagamento ZumboPay est\xE1 temporariamente desativado."
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const walletId = request.walletId || await this.resolveWalletId("card") || await this.resolveWalletId("mpesa");
|
|
250
|
+
const sourceId = request.reference || `chk-${Date.now()}`;
|
|
251
|
+
const payload = {
|
|
252
|
+
title: request.title,
|
|
253
|
+
amount: request.amount,
|
|
254
|
+
currency: request.currency || "MZN",
|
|
255
|
+
channels: request.channels || ["card", "mpesa", "emola", "mkesh"],
|
|
256
|
+
wallet_id: walletId,
|
|
257
|
+
reference: sourceId,
|
|
258
|
+
return_url: request.returnUrl,
|
|
259
|
+
redirect_url: request.returnUrl,
|
|
260
|
+
cancel_url: request.cancelUrl,
|
|
261
|
+
metadata: request.metadata
|
|
262
|
+
};
|
|
263
|
+
try {
|
|
264
|
+
const res = await this.fetchWithTimeout("/checkouts", {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: this.getHeaders(),
|
|
267
|
+
body: JSON.stringify(payload)
|
|
268
|
+
});
|
|
269
|
+
const data = await res.json().catch(() => ({}));
|
|
270
|
+
if (res.ok) {
|
|
271
|
+
const checkoutUrl = data.data?.checkout_url || data.checkout_url || data.data?.url || data.url || null;
|
|
272
|
+
return {
|
|
273
|
+
success: true,
|
|
274
|
+
checkoutUrl,
|
|
275
|
+
reference: data.data?.reference || sourceId,
|
|
276
|
+
message: "Sess\xE3o de checkout criada com sucesso.",
|
|
277
|
+
raw: data
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
checkoutUrl: null,
|
|
283
|
+
reference: null,
|
|
284
|
+
message: data.error?.message || data.message || "N\xE3o foi poss\xEDvel gerar a p\xE1gina de checkout.",
|
|
285
|
+
raw: data
|
|
286
|
+
};
|
|
287
|
+
} catch (err) {
|
|
288
|
+
const error = err;
|
|
289
|
+
return {
|
|
290
|
+
success: false,
|
|
291
|
+
checkoutUrl: null,
|
|
292
|
+
reference: null,
|
|
293
|
+
message: error.message || "Erro de conex\xE3o ao criar checkout."
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Queries status of an existing charge/checkout transaction.
|
|
299
|
+
*/
|
|
300
|
+
async getStatus(referenceOrId) {
|
|
301
|
+
const res = await this.fetchWithTimeout(
|
|
302
|
+
`/charges/${encodeURIComponent(referenceOrId)}`,
|
|
303
|
+
{
|
|
304
|
+
method: "GET",
|
|
305
|
+
headers: this.getHeaders()
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
const data = await res.json().catch(() => ({}));
|
|
309
|
+
const payload = data.data || data;
|
|
310
|
+
const status = (payload.status || "pending").toLowerCase();
|
|
311
|
+
const isPaid = status === "success" || status === "succeeded" || status === "completed" || payload.is_paid === true;
|
|
312
|
+
return {
|
|
313
|
+
success: res.ok,
|
|
314
|
+
status,
|
|
315
|
+
paid: isPaid,
|
|
316
|
+
reference: payload.reference || referenceOrId,
|
|
317
|
+
amount: payload.amount,
|
|
318
|
+
currency: payload.currency || "MZN",
|
|
319
|
+
channel: payload.channel,
|
|
320
|
+
paidAt: payload.paid_at || payload.updated_at || null,
|
|
321
|
+
raw: data
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Lists all wallets associated with the merchant account (cached for 10 minutes).
|
|
326
|
+
*/
|
|
327
|
+
async listWallets() {
|
|
328
|
+
const now = Date.now();
|
|
329
|
+
if (this.cachedWallets && now < this.walletsCacheExpiresAt) {
|
|
330
|
+
return this.cachedWallets;
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const res = await this.fetchWithTimeout("/wallets", {
|
|
334
|
+
method: "GET",
|
|
335
|
+
headers: this.getHeaders()
|
|
336
|
+
});
|
|
337
|
+
if (res.ok) {
|
|
338
|
+
const data = await res.json();
|
|
339
|
+
this.cachedWallets = data.data || data || [];
|
|
340
|
+
this.walletsCacheExpiresAt = now + 10 * 60 * 1e3;
|
|
341
|
+
return this.cachedWallets;
|
|
342
|
+
}
|
|
343
|
+
return [];
|
|
344
|
+
} catch {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
getHeaders() {
|
|
349
|
+
return {
|
|
350
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
351
|
+
"X-Merchant-Id": this.config.merchantId,
|
|
352
|
+
"Content-Type": "application/json",
|
|
353
|
+
Accept: "application/json"
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
async fetchWithTimeout(endpoint, options) {
|
|
357
|
+
const controller = new AbortController();
|
|
358
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeout);
|
|
359
|
+
const url = `${this.config.baseUrl}${endpoint}`;
|
|
360
|
+
try {
|
|
361
|
+
return await fetch(url, {
|
|
362
|
+
...options,
|
|
363
|
+
signal: controller.signal
|
|
364
|
+
});
|
|
365
|
+
} finally {
|
|
366
|
+
clearTimeout(timer);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// src/react/useZumboPay.ts
|
|
372
|
+
function useZumboPay({
|
|
373
|
+
config,
|
|
374
|
+
onSuccess,
|
|
375
|
+
onError,
|
|
376
|
+
pollingIntervalMs = 3e3
|
|
377
|
+
}) {
|
|
378
|
+
const clientRef = (0, import_react.useRef)(new ZumboPayClient(config));
|
|
379
|
+
(0, import_react.useEffect)(() => {
|
|
380
|
+
clientRef.current = new ZumboPayClient(config);
|
|
381
|
+
}, [config]);
|
|
382
|
+
const [phone, setPhone] = (0, import_react.useState)("");
|
|
383
|
+
const [operator, setOperator] = (0, import_react.useState)("unknown");
|
|
384
|
+
const [status, setStatus] = (0, import_react.useState)("pending");
|
|
385
|
+
const [isLoading, setIsLoading] = (0, import_react.useState)(false);
|
|
386
|
+
const [reference, setReference] = (0, import_react.useState)(null);
|
|
387
|
+
const [errorMessage, setErrorMessage] = (0, import_react.useState)(null);
|
|
388
|
+
const pollingRef = (0, import_react.useRef)(null);
|
|
389
|
+
const isEnabled = clientRef.current.isEnabled();
|
|
390
|
+
const stopPolling = (0, import_react.useCallback)(() => {
|
|
391
|
+
if (pollingRef.current) {
|
|
392
|
+
clearInterval(pollingRef.current);
|
|
393
|
+
pollingRef.current = null;
|
|
394
|
+
}
|
|
395
|
+
}, []);
|
|
396
|
+
(0, import_react.useEffect)(() => {
|
|
397
|
+
return () => stopPolling();
|
|
398
|
+
}, [stopPolling]);
|
|
399
|
+
const handlePhoneChange = (0, import_react.useCallback)((newPhone) => {
|
|
400
|
+
setPhone(newPhone);
|
|
401
|
+
setOperator(detectOperator(newPhone));
|
|
402
|
+
}, []);
|
|
403
|
+
const startPolling = (0, import_react.useCallback)(
|
|
404
|
+
(targetRef) => {
|
|
405
|
+
stopPolling();
|
|
406
|
+
let attempts = 0;
|
|
407
|
+
const maxAttempts = 40;
|
|
408
|
+
pollingRef.current = setInterval(async () => {
|
|
409
|
+
attempts++;
|
|
410
|
+
if (attempts > maxAttempts) {
|
|
411
|
+
stopPolling();
|
|
412
|
+
setStatus("expired");
|
|
413
|
+
setErrorMessage("O tempo limite para inser\xE7\xE3o do PIN expirou.");
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
const res = await clientRef.current.getStatus(targetRef);
|
|
418
|
+
setStatus(res.status);
|
|
419
|
+
if (res.paid) {
|
|
420
|
+
stopPolling();
|
|
421
|
+
setIsLoading(false);
|
|
422
|
+
onSuccess?.(res);
|
|
423
|
+
} else if (res.status === "failed" || res.status === "declined" || res.status === "cancelled") {
|
|
424
|
+
stopPolling();
|
|
425
|
+
setIsLoading(false);
|
|
426
|
+
setErrorMessage("O pagamento foi recusado ou cancelado.");
|
|
427
|
+
onError?.({ message: "Pagamento recusado." });
|
|
428
|
+
}
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
431
|
+
}, pollingIntervalMs);
|
|
432
|
+
},
|
|
433
|
+
[stopPolling, pollingIntervalMs, onSuccess, onError]
|
|
434
|
+
);
|
|
435
|
+
const initiateStk = (0, import_react.useCallback)(
|
|
436
|
+
async (req) => {
|
|
437
|
+
const targetPhone = req.phone || phone;
|
|
438
|
+
if (!isEnabled) {
|
|
439
|
+
const msg = "O gateway ZumboPay est\xE1 temporariamente desativado.";
|
|
440
|
+
setErrorMessage(msg);
|
|
441
|
+
setStatus("disabled");
|
|
442
|
+
onError?.({ message: msg });
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
if (!isValidMozPhone(targetPhone)) {
|
|
446
|
+
const msg = "N\xFAmero de telefone mo\xE7ambicano inv\xE1lido.";
|
|
447
|
+
setErrorMessage(msg);
|
|
448
|
+
onError?.({ message: msg });
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
setIsLoading(true);
|
|
452
|
+
setErrorMessage(null);
|
|
453
|
+
const res = await clientRef.current.stkPush({
|
|
454
|
+
...req,
|
|
455
|
+
phone: normalizePhone(targetPhone)
|
|
456
|
+
});
|
|
457
|
+
if (res.success) {
|
|
458
|
+
setStatus(res.status);
|
|
459
|
+
if (res.reference) {
|
|
460
|
+
setReference(res.reference);
|
|
461
|
+
}
|
|
462
|
+
if (res.status === "success") {
|
|
463
|
+
setIsLoading(false);
|
|
464
|
+
onSuccess?.({
|
|
465
|
+
success: true,
|
|
466
|
+
status: "success",
|
|
467
|
+
paid: true,
|
|
468
|
+
reference: res.reference || ""
|
|
469
|
+
});
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
if (res.reference) {
|
|
473
|
+
startPolling(res.reference);
|
|
474
|
+
}
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
setIsLoading(false);
|
|
478
|
+
setStatus(res.status);
|
|
479
|
+
setErrorMessage(res.message);
|
|
480
|
+
onError?.({ message: res.message });
|
|
481
|
+
return false;
|
|
482
|
+
},
|
|
483
|
+
[phone, isEnabled, startPolling, onSuccess, onError]
|
|
484
|
+
);
|
|
485
|
+
const createCheckout = (0, import_react.useCallback)(
|
|
486
|
+
async (req) => {
|
|
487
|
+
if (!isEnabled) {
|
|
488
|
+
const msg = "O gateway ZumboPay est\xE1 temporariamente desativado.";
|
|
489
|
+
setErrorMessage(msg);
|
|
490
|
+
setStatus("disabled");
|
|
491
|
+
onError?.({ message: msg });
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
setIsLoading(true);
|
|
495
|
+
setErrorMessage(null);
|
|
496
|
+
const res = await clientRef.current.createCheckout(req);
|
|
497
|
+
setIsLoading(false);
|
|
498
|
+
if (res.success && res.checkoutUrl) {
|
|
499
|
+
setReference(res.reference);
|
|
500
|
+
return res.checkoutUrl;
|
|
501
|
+
}
|
|
502
|
+
setErrorMessage(res.message);
|
|
503
|
+
onError?.({ message: res.message });
|
|
504
|
+
return null;
|
|
505
|
+
},
|
|
506
|
+
[isEnabled, onError]
|
|
507
|
+
);
|
|
508
|
+
const reset = (0, import_react.useCallback)(() => {
|
|
509
|
+
stopPolling();
|
|
510
|
+
setIsLoading(false);
|
|
511
|
+
setStatus("pending");
|
|
512
|
+
setErrorMessage(null);
|
|
513
|
+
setReference(null);
|
|
514
|
+
}, [stopPolling]);
|
|
515
|
+
return {
|
|
516
|
+
client: clientRef.current,
|
|
517
|
+
isEnabled,
|
|
518
|
+
phone,
|
|
519
|
+
operator,
|
|
520
|
+
status,
|
|
521
|
+
isLoading,
|
|
522
|
+
reference,
|
|
523
|
+
errorMessage,
|
|
524
|
+
setPhone: handlePhoneChange,
|
|
525
|
+
initiateStk,
|
|
526
|
+
createCheckout,
|
|
527
|
+
reset,
|
|
528
|
+
stopPolling
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/react/ZumboPayModal.tsx
|
|
533
|
+
var import_react2 = __toESM(require("react"));
|
|
534
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
535
|
+
function ZumboPayModal({
|
|
536
|
+
isOpen,
|
|
537
|
+
onClose,
|
|
538
|
+
config,
|
|
539
|
+
amount,
|
|
540
|
+
currency = "MZN",
|
|
541
|
+
reference,
|
|
542
|
+
title = "Pagamento Online",
|
|
543
|
+
customerName,
|
|
544
|
+
initialPhone = "",
|
|
545
|
+
onPaymentSuccess,
|
|
546
|
+
returnUrl,
|
|
547
|
+
cancelUrl
|
|
548
|
+
}) {
|
|
549
|
+
const [activeTab, setActiveTab] = (0, import_react2.useState)("mobile");
|
|
550
|
+
const {
|
|
551
|
+
isEnabled,
|
|
552
|
+
phone,
|
|
553
|
+
operator,
|
|
554
|
+
status,
|
|
555
|
+
isLoading,
|
|
556
|
+
errorMessage,
|
|
557
|
+
setPhone,
|
|
558
|
+
initiateStk,
|
|
559
|
+
createCheckout,
|
|
560
|
+
reset
|
|
561
|
+
} = useZumboPay({
|
|
562
|
+
config,
|
|
563
|
+
onSuccess: (res) => {
|
|
564
|
+
onPaymentSuccess?.(res);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
import_react2.default.useEffect(() => {
|
|
568
|
+
if (initialPhone && !phone) {
|
|
569
|
+
setPhone(initialPhone);
|
|
570
|
+
}
|
|
571
|
+
}, [initialPhone, phone, setPhone]);
|
|
572
|
+
if (!isOpen) {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
const handleMobileSubmit = async (e) => {
|
|
576
|
+
e.preventDefault();
|
|
577
|
+
await initiateStk({
|
|
578
|
+
amount,
|
|
579
|
+
phone,
|
|
580
|
+
reference,
|
|
581
|
+
customerName,
|
|
582
|
+
description: title
|
|
583
|
+
});
|
|
584
|
+
};
|
|
585
|
+
const handleCardCheckout = async () => {
|
|
586
|
+
const checkoutUrl = await createCheckout({
|
|
587
|
+
amount,
|
|
588
|
+
currency,
|
|
589
|
+
title,
|
|
590
|
+
reference,
|
|
591
|
+
returnUrl,
|
|
592
|
+
cancelUrl,
|
|
593
|
+
channels: ["card", "mpesa", "emola", "mkesh"]
|
|
594
|
+
});
|
|
595
|
+
if (checkoutUrl) {
|
|
596
|
+
window.location.href = checkoutUrl;
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
const handleClose = () => {
|
|
600
|
+
reset();
|
|
601
|
+
onClose();
|
|
602
|
+
};
|
|
603
|
+
const formatCurrency = (val) => {
|
|
604
|
+
return new Intl.NumberFormat("pt-MZ", {
|
|
605
|
+
style: "currency",
|
|
606
|
+
currency
|
|
607
|
+
}).format(val);
|
|
608
|
+
};
|
|
609
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.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__ */ (0, import_jsx_runtime.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: [
|
|
610
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between px-6 py-4 border-b border-neutral-100 dark:border-neutral-800", children: [
|
|
611
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
612
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "text-base font-bold text-neutral-900 dark:text-white", children: title }),
|
|
613
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs text-neutral-500 dark:text-neutral-400", children: reference ? `Ref: ${reference}` : "ZumboPay Gateway" })
|
|
614
|
+
] }),
|
|
615
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
616
|
+
"button",
|
|
617
|
+
{
|
|
618
|
+
type: "button",
|
|
619
|
+
onClick: handleClose,
|
|
620
|
+
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",
|
|
621
|
+
children: [
|
|
622
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "sr-only", children: "Fechar" }),
|
|
623
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: "w-5 h-5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
|
|
624
|
+
]
|
|
625
|
+
}
|
|
626
|
+
)
|
|
627
|
+
] }),
|
|
628
|
+
!isEnabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "p-6 text-center", children: [
|
|
629
|
+
/* @__PURE__ */ (0, import_jsx_runtime.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__ */ (0, import_jsx_runtime.jsx)("svg", { className: "w-6 h-6", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime.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" }) }) }),
|
|
630
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h4", { className: "text-sm font-bold text-neutral-900 dark:text-white", children: "Pagamentos Temporariamente Indispon\xEDveis" }),
|
|
631
|
+
/* @__PURE__ */ (0, import_jsx_runtime.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." }),
|
|
632
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
633
|
+
"button",
|
|
634
|
+
{
|
|
635
|
+
type: "button",
|
|
636
|
+
onClick: handleClose,
|
|
637
|
+
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",
|
|
638
|
+
children: "Fechar"
|
|
639
|
+
}
|
|
640
|
+
)
|
|
641
|
+
] }),
|
|
642
|
+
isEnabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "p-6", children: [
|
|
643
|
+
/* @__PURE__ */ (0, import_jsx_runtime.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: [
|
|
644
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-xs font-medium text-neutral-500 dark:text-neutral-400", children: "Total a Pagar" }),
|
|
645
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-2xl font-black text-neutral-900 dark:text-white", children: formatCurrency(amount) })
|
|
646
|
+
] }),
|
|
647
|
+
status === "success" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "py-6 text-center space-y-3", children: [
|
|
648
|
+
/* @__PURE__ */ (0, import_jsx_runtime.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__ */ (0, import_jsx_runtime.jsx)("svg", { className: "w-8 h-8", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M5 13l4 4L19 7" }) }) }),
|
|
649
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h4", { className: "text-base font-bold text-emerald-900 dark:text-emerald-300", children: "Pagamento Confirmado!" }),
|
|
650
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs text-neutral-500 dark:text-neutral-400", children: "O seu pagamento foi recebido e processado com sucesso." }),
|
|
651
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
652
|
+
"button",
|
|
653
|
+
{
|
|
654
|
+
type: "button",
|
|
655
|
+
onClick: handleClose,
|
|
656
|
+
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",
|
|
657
|
+
children: "Concluir"
|
|
658
|
+
}
|
|
659
|
+
)
|
|
660
|
+
] }),
|
|
661
|
+
isLoading && status !== "success" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "py-6 text-center space-y-3", children: [
|
|
662
|
+
/* @__PURE__ */ (0, import_jsx_runtime.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__ */ (0, import_jsx_runtime.jsxs)("svg", { className: "w-6 h-6 animate-spin", fill: "none", viewBox: "0 0 24 24", children: [
|
|
663
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
|
|
664
|
+
/* @__PURE__ */ (0, import_jsx_runtime.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" })
|
|
665
|
+
] }) }),
|
|
666
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h4", { className: "text-sm font-bold text-neutral-900 dark:text-white", children: "Confirme no seu telem\xF3vel" }),
|
|
667
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "text-xs text-neutral-500 dark:text-neutral-400 max-w-xs mx-auto", children: [
|
|
668
|
+
"Envi\xE1mos um pedido para o n\xFAmero ",
|
|
669
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { className: "font-mono text-neutral-900 dark:text-white", children: phone }),
|
|
670
|
+
". Introduza o seu PIN na notifica\xE7\xE3o USSD para confirmar."
|
|
671
|
+
] })
|
|
672
|
+
] }),
|
|
673
|
+
!isLoading && status !== "success" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
674
|
+
/* @__PURE__ */ (0, import_jsx_runtime.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: [
|
|
675
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
676
|
+
"button",
|
|
677
|
+
{
|
|
678
|
+
type: "button",
|
|
679
|
+
onClick: () => setActiveTab("mobile"),
|
|
680
|
+
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"}`,
|
|
681
|
+
children: "Carteira M\xF3vel"
|
|
682
|
+
}
|
|
683
|
+
),
|
|
684
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
685
|
+
"button",
|
|
686
|
+
{
|
|
687
|
+
type: "button",
|
|
688
|
+
onClick: () => setActiveTab("card"),
|
|
689
|
+
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"}`,
|
|
690
|
+
children: "Cart\xE3o Banc\xE1rio"
|
|
691
|
+
}
|
|
692
|
+
)
|
|
693
|
+
] }),
|
|
694
|
+
errorMessage && /* @__PURE__ */ (0, import_jsx_runtime.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 }),
|
|
695
|
+
activeTab === "mobile" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: handleMobileSubmit, className: "space-y-4", children: [
|
|
696
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
697
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "block text-xs font-semibold text-neutral-700 dark:text-neutral-300 mb-1", children: "N\xFAmero de Telem\xF3vel" }),
|
|
698
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative", children: [
|
|
699
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
700
|
+
"input",
|
|
701
|
+
{
|
|
702
|
+
type: "tel",
|
|
703
|
+
required: true,
|
|
704
|
+
placeholder: "84 / 85 / 86 / 87 / 82 / 83...",
|
|
705
|
+
value: phone,
|
|
706
|
+
onChange: (e) => setPhone(e.target.value),
|
|
707
|
+
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"
|
|
708
|
+
}
|
|
709
|
+
),
|
|
710
|
+
operator !== "unknown" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
711
|
+
"span",
|
|
712
|
+
{
|
|
713
|
+
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"}`,
|
|
714
|
+
children: getOperatorLabel(operator)
|
|
715
|
+
}
|
|
716
|
+
)
|
|
717
|
+
] }),
|
|
718
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mt-1.5 text-[11px] text-neutral-400", children: "Compat\xEDvel com M-Pesa (Vodacom), e-Mola (Movitel) e mKesh (Tmcel)." })
|
|
719
|
+
] }),
|
|
720
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
721
|
+
"button",
|
|
722
|
+
{
|
|
723
|
+
type: "submit",
|
|
724
|
+
disabled: isLoading,
|
|
725
|
+
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",
|
|
726
|
+
children: [
|
|
727
|
+
"Pagar ",
|
|
728
|
+
formatCurrency(amount)
|
|
729
|
+
]
|
|
730
|
+
}
|
|
731
|
+
)
|
|
732
|
+
] }),
|
|
733
|
+
activeTab === "card" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-4 text-center", children: [
|
|
734
|
+
/* @__PURE__ */ (0, import_jsx_runtime.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." }),
|
|
735
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
736
|
+
"button",
|
|
737
|
+
{
|
|
738
|
+
type: "button",
|
|
739
|
+
onClick: handleCardCheckout,
|
|
740
|
+
disabled: isLoading,
|
|
741
|
+
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",
|
|
742
|
+
children: "Continuar para Checkout com Cart\xE3o"
|
|
743
|
+
}
|
|
744
|
+
)
|
|
745
|
+
] })
|
|
746
|
+
] })
|
|
747
|
+
] })
|
|
748
|
+
] }) });
|
|
749
|
+
}
|
|
750
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
751
|
+
0 && (module.exports = {
|
|
752
|
+
ZumboPayModal,
|
|
753
|
+
useZumboPay
|
|
754
|
+
});
|
|
755
|
+
//# sourceMappingURL=index.js.map
|