@reevit/react 0.1.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 +211 -0
- package/dist/index.d.mts +628 -0
- package/dist/index.d.ts +628 -0
- package/dist/index.js +1636 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1612 -0
- package/dist/index.mjs.map +1 -0
- package/dist/styles.css +526 -0
- package/package.json +59 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1612 @@
|
|
|
1
|
+
import { createContext, useReducer, useRef, useEffect, useCallback, useState, useContext } from 'react';
|
|
2
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/components/ReevitCheckout.tsx
|
|
5
|
+
|
|
6
|
+
// src/utils/index.ts
|
|
7
|
+
function formatAmount(amount, currency) {
|
|
8
|
+
const formatter = new Intl.NumberFormat("en-US", {
|
|
9
|
+
style: "currency",
|
|
10
|
+
currency,
|
|
11
|
+
minimumFractionDigits: 2
|
|
12
|
+
});
|
|
13
|
+
return formatter.format(amount / 100);
|
|
14
|
+
}
|
|
15
|
+
function generateReference() {
|
|
16
|
+
const timestamp = Date.now().toString(36);
|
|
17
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
18
|
+
return `reevit_${timestamp}_${random}`;
|
|
19
|
+
}
|
|
20
|
+
function validatePhone(phone, network) {
|
|
21
|
+
const cleaned = phone.replace(/[\s-]/g, "");
|
|
22
|
+
const patterns = {
|
|
23
|
+
mtn: /^(0|233|\+233)?(24|54|55|59)\d{7}$/,
|
|
24
|
+
vodafone: /^(0|233|\+233)?(20|50)\d{7}$/,
|
|
25
|
+
airteltigo: /^(0|233|\+233)?(26|27|56|57)\d{7}$/
|
|
26
|
+
};
|
|
27
|
+
if (network && patterns[network]) {
|
|
28
|
+
return patterns[network].test(cleaned);
|
|
29
|
+
}
|
|
30
|
+
return Object.values(patterns).some((pattern) => pattern.test(cleaned));
|
|
31
|
+
}
|
|
32
|
+
function formatPhone(phone) {
|
|
33
|
+
const cleaned = phone.replace(/[\s-]/g, "");
|
|
34
|
+
if (cleaned.startsWith("0") && cleaned.length === 10) {
|
|
35
|
+
return `${cleaned.slice(0, 3)} ${cleaned.slice(3, 6)} ${cleaned.slice(6)}`;
|
|
36
|
+
}
|
|
37
|
+
if (cleaned.startsWith("+233") && cleaned.length === 13) {
|
|
38
|
+
return `${cleaned.slice(0, 4)} ${cleaned.slice(4, 6)} ${cleaned.slice(6, 9)} ${cleaned.slice(9)}`;
|
|
39
|
+
}
|
|
40
|
+
return phone;
|
|
41
|
+
}
|
|
42
|
+
function detectNetwork(phone) {
|
|
43
|
+
const cleaned = phone.replace(/[\s-]/g, "");
|
|
44
|
+
const prefixes = {
|
|
45
|
+
mtn: ["024", "054", "055", "059", "23324", "23354", "23355", "23359"],
|
|
46
|
+
vodafone: ["020", "050", "23320", "23350"],
|
|
47
|
+
airteltigo: ["026", "027", "056", "057", "23326", "23327", "23356", "23357"]
|
|
48
|
+
};
|
|
49
|
+
for (const [network, networkPrefixes] of Object.entries(prefixes)) {
|
|
50
|
+
for (const prefix of networkPrefixes) {
|
|
51
|
+
if (cleaned.startsWith(prefix) || cleaned.startsWith("0" + prefix.slice(3))) {
|
|
52
|
+
return network;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function createThemeVariables(theme) {
|
|
59
|
+
const variables = {};
|
|
60
|
+
if (theme.primaryColor) {
|
|
61
|
+
variables["--reevit-primary"] = theme.primaryColor;
|
|
62
|
+
}
|
|
63
|
+
if (theme.backgroundColor) {
|
|
64
|
+
variables["--reevit-background"] = theme.backgroundColor;
|
|
65
|
+
}
|
|
66
|
+
if (theme.textColor) {
|
|
67
|
+
variables["--reevit-text"] = theme.textColor;
|
|
68
|
+
}
|
|
69
|
+
if (theme.borderRadius) {
|
|
70
|
+
variables["--reevit-radius"] = theme.borderRadius;
|
|
71
|
+
}
|
|
72
|
+
if (theme.fontFamily) {
|
|
73
|
+
variables["--reevit-font"] = theme.fontFamily;
|
|
74
|
+
}
|
|
75
|
+
return variables;
|
|
76
|
+
}
|
|
77
|
+
function cn(...classes) {
|
|
78
|
+
return classes.filter(Boolean).join(" ");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/api/client.ts
|
|
82
|
+
var API_BASE_URL_PRODUCTION = "https://api.reevit.io";
|
|
83
|
+
var API_BASE_URL_SANDBOX = "https://sandbox-api.reevit.io";
|
|
84
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
85
|
+
function isSandboxKey(publicKey) {
|
|
86
|
+
return publicKey.startsWith("pk_test_") || publicKey.startsWith("pk_sandbox_");
|
|
87
|
+
}
|
|
88
|
+
function createPaymentError(response, errorData) {
|
|
89
|
+
return {
|
|
90
|
+
code: errorData.code || "api_error",
|
|
91
|
+
message: errorData.message || "An unexpected error occurred",
|
|
92
|
+
details: {
|
|
93
|
+
httpStatus: response.status,
|
|
94
|
+
...errorData.details
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
var ReevitAPIClient = class {
|
|
99
|
+
constructor(config) {
|
|
100
|
+
this.publicKey = config.publicKey;
|
|
101
|
+
this.baseUrl = config.baseUrl || (isSandboxKey(config.publicKey) ? API_BASE_URL_SANDBOX : API_BASE_URL_PRODUCTION);
|
|
102
|
+
this.timeout = config.timeout || DEFAULT_TIMEOUT;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Makes an authenticated API request
|
|
106
|
+
*/
|
|
107
|
+
async request(method, path, body) {
|
|
108
|
+
const controller = new AbortController();
|
|
109
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
110
|
+
try {
|
|
111
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
112
|
+
method,
|
|
113
|
+
headers: {
|
|
114
|
+
"Content-Type": "application/json",
|
|
115
|
+
"Authorization": `Bearer ${this.publicKey}`,
|
|
116
|
+
"X-Reevit-Client": "@reevit/react",
|
|
117
|
+
"X-Reevit-Client-Version": "1.0.0"
|
|
118
|
+
},
|
|
119
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
120
|
+
signal: controller.signal
|
|
121
|
+
});
|
|
122
|
+
clearTimeout(timeoutId);
|
|
123
|
+
const responseData = await response.json().catch(() => ({}));
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
return {
|
|
126
|
+
error: createPaymentError(response, responseData)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { data: responseData };
|
|
130
|
+
} catch (err) {
|
|
131
|
+
clearTimeout(timeoutId);
|
|
132
|
+
if (err instanceof Error) {
|
|
133
|
+
if (err.name === "AbortError") {
|
|
134
|
+
return {
|
|
135
|
+
error: {
|
|
136
|
+
code: "request_timeout",
|
|
137
|
+
message: "The request timed out. Please try again."
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (err.message.includes("Failed to fetch") || err.message.includes("NetworkError")) {
|
|
142
|
+
return {
|
|
143
|
+
error: {
|
|
144
|
+
code: "network_error",
|
|
145
|
+
message: "Unable to connect to Reevit. Please check your internet connection."
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
error: {
|
|
152
|
+
code: "unknown_error",
|
|
153
|
+
message: "An unexpected error occurred. Please try again."
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Creates a payment intent
|
|
160
|
+
*/
|
|
161
|
+
async createPaymentIntent(config, method, country = "GH") {
|
|
162
|
+
const request = {
|
|
163
|
+
amount: config.amount,
|
|
164
|
+
currency: config.currency,
|
|
165
|
+
method: this.mapPaymentMethod(method),
|
|
166
|
+
country,
|
|
167
|
+
customer_id: config.metadata?.customerId,
|
|
168
|
+
metadata: config.metadata
|
|
169
|
+
};
|
|
170
|
+
return this.request("POST", "/v1/payments/intents", request);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Retrieves a payment intent by ID
|
|
174
|
+
*/
|
|
175
|
+
async getPaymentIntent(paymentId) {
|
|
176
|
+
return this.request("GET", `/v1/payments/${paymentId}`);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Confirms a payment after PSP callback
|
|
180
|
+
*/
|
|
181
|
+
async confirmPayment(paymentId) {
|
|
182
|
+
return this.request("POST", `/v1/payments/${paymentId}/confirm`);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Cancels a payment intent
|
|
186
|
+
*/
|
|
187
|
+
async cancelPaymentIntent(paymentId) {
|
|
188
|
+
return this.request("POST", `/v1/payments/${paymentId}/cancel`);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Maps SDK payment method to backend format
|
|
192
|
+
*/
|
|
193
|
+
mapPaymentMethod(method) {
|
|
194
|
+
switch (method) {
|
|
195
|
+
case "card":
|
|
196
|
+
return "card";
|
|
197
|
+
case "mobile_money":
|
|
198
|
+
return "mobile_money";
|
|
199
|
+
case "bank_transfer":
|
|
200
|
+
return "bank_transfer";
|
|
201
|
+
default:
|
|
202
|
+
return method;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
function createReevitClient(config) {
|
|
207
|
+
return new ReevitAPIClient(config);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/hooks/useReevit.ts
|
|
211
|
+
var initialState = {
|
|
212
|
+
status: "idle",
|
|
213
|
+
paymentIntent: null,
|
|
214
|
+
selectedMethod: null,
|
|
215
|
+
error: null,
|
|
216
|
+
result: null
|
|
217
|
+
};
|
|
218
|
+
function reevitReducer(state, action) {
|
|
219
|
+
switch (action.type) {
|
|
220
|
+
case "INIT_START":
|
|
221
|
+
return { ...state, status: "loading", error: null };
|
|
222
|
+
case "INIT_SUCCESS":
|
|
223
|
+
return { ...state, status: "ready", paymentIntent: action.payload };
|
|
224
|
+
case "INIT_ERROR":
|
|
225
|
+
return { ...state, status: "failed", error: action.payload };
|
|
226
|
+
case "SELECT_METHOD":
|
|
227
|
+
return { ...state, status: "method_selected", selectedMethod: action.payload };
|
|
228
|
+
case "PROCESS_START":
|
|
229
|
+
return { ...state, status: "processing", error: null };
|
|
230
|
+
case "PROCESS_SUCCESS":
|
|
231
|
+
return { ...state, status: "success", result: action.payload };
|
|
232
|
+
case "PROCESS_ERROR":
|
|
233
|
+
return { ...state, status: "failed", error: action.payload };
|
|
234
|
+
case "RESET":
|
|
235
|
+
return { ...initialState, status: "ready", paymentIntent: state.paymentIntent };
|
|
236
|
+
case "CLOSE":
|
|
237
|
+
return { ...state, status: "closed" };
|
|
238
|
+
default:
|
|
239
|
+
return state;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function mapProviderToPsp(provider) {
|
|
243
|
+
const providerLower = provider.toLowerCase();
|
|
244
|
+
if (providerLower.includes("paystack")) return "paystack";
|
|
245
|
+
if (providerLower.includes("hubtel")) return "hubtel";
|
|
246
|
+
if (providerLower.includes("flutterwave")) return "flutterwave";
|
|
247
|
+
return "paystack";
|
|
248
|
+
}
|
|
249
|
+
function mapToPaymentIntent(response, config) {
|
|
250
|
+
return {
|
|
251
|
+
id: response.id,
|
|
252
|
+
clientSecret: response.client_secret,
|
|
253
|
+
amount: response.amount,
|
|
254
|
+
currency: response.currency,
|
|
255
|
+
status: response.status,
|
|
256
|
+
recommendedPsp: mapProviderToPsp(response.provider),
|
|
257
|
+
availableMethods: config.paymentMethods || ["card", "mobile_money"],
|
|
258
|
+
connectionId: response.connection_id,
|
|
259
|
+
provider: response.provider,
|
|
260
|
+
feeAmount: response.fee_amount,
|
|
261
|
+
feeCurrency: response.fee_currency,
|
|
262
|
+
netAmount: response.net_amount,
|
|
263
|
+
metadata: config.metadata
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function useReevit(options) {
|
|
267
|
+
const { config, onSuccess, onError, onClose, onStateChange, apiBaseUrl } = options;
|
|
268
|
+
const [state, dispatch] = useReducer(reevitReducer, initialState);
|
|
269
|
+
const apiClientRef = useRef(null);
|
|
270
|
+
if (!apiClientRef.current) {
|
|
271
|
+
apiClientRef.current = new ReevitAPIClient({
|
|
272
|
+
publicKey: config.publicKey,
|
|
273
|
+
baseUrl: apiBaseUrl
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
onStateChange?.(state.status);
|
|
278
|
+
}, [state.status, onStateChange]);
|
|
279
|
+
const initialize = useCallback(
|
|
280
|
+
async (method) => {
|
|
281
|
+
dispatch({ type: "INIT_START" });
|
|
282
|
+
try {
|
|
283
|
+
const apiClient = apiClientRef.current;
|
|
284
|
+
if (!apiClient) {
|
|
285
|
+
throw new Error("API client not initialized");
|
|
286
|
+
}
|
|
287
|
+
const reference = config.reference || generateReference();
|
|
288
|
+
const country = detectCountryFromCurrency(config.currency);
|
|
289
|
+
const paymentMethod = method || config.paymentMethods?.[0] || "card";
|
|
290
|
+
const { data, error } = await apiClient.createPaymentIntent(
|
|
291
|
+
{ ...config, reference },
|
|
292
|
+
paymentMethod,
|
|
293
|
+
country
|
|
294
|
+
);
|
|
295
|
+
if (error) {
|
|
296
|
+
dispatch({ type: "INIT_ERROR", payload: error });
|
|
297
|
+
onError?.(error);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (!data) {
|
|
301
|
+
const noDataError = {
|
|
302
|
+
code: "INIT_FAILED",
|
|
303
|
+
message: "No data received from API",
|
|
304
|
+
recoverable: true
|
|
305
|
+
};
|
|
306
|
+
dispatch({ type: "INIT_ERROR", payload: noDataError });
|
|
307
|
+
onError?.(noDataError);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const paymentIntent = mapToPaymentIntent(data, { ...config, reference });
|
|
311
|
+
dispatch({ type: "INIT_SUCCESS", payload: paymentIntent });
|
|
312
|
+
} catch (err) {
|
|
313
|
+
const error = {
|
|
314
|
+
code: "INIT_FAILED",
|
|
315
|
+
message: err instanceof Error ? err.message : "Failed to initialize checkout",
|
|
316
|
+
recoverable: true,
|
|
317
|
+
originalError: err
|
|
318
|
+
};
|
|
319
|
+
dispatch({ type: "INIT_ERROR", payload: error });
|
|
320
|
+
onError?.(error);
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
[config, onError, apiBaseUrl]
|
|
324
|
+
);
|
|
325
|
+
const selectMethod = useCallback((method) => {
|
|
326
|
+
dispatch({ type: "SELECT_METHOD", payload: method });
|
|
327
|
+
}, []);
|
|
328
|
+
const processPayment = useCallback(
|
|
329
|
+
async (paymentData) => {
|
|
330
|
+
if (!state.paymentIntent || !state.selectedMethod) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
dispatch({ type: "PROCESS_START" });
|
|
334
|
+
try {
|
|
335
|
+
const apiClient = apiClientRef.current;
|
|
336
|
+
if (!apiClient) {
|
|
337
|
+
throw new Error("API client not initialized");
|
|
338
|
+
}
|
|
339
|
+
const { data, error } = await apiClient.confirmPayment(state.paymentIntent.id);
|
|
340
|
+
if (error) {
|
|
341
|
+
dispatch({ type: "PROCESS_ERROR", payload: error });
|
|
342
|
+
onError?.(error);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const result = {
|
|
346
|
+
paymentId: state.paymentIntent.id,
|
|
347
|
+
reference: paymentData.reference || state.paymentIntent.metadata?.reference || "",
|
|
348
|
+
amount: state.paymentIntent.amount,
|
|
349
|
+
currency: state.paymentIntent.currency,
|
|
350
|
+
paymentMethod: state.selectedMethod,
|
|
351
|
+
psp: state.paymentIntent.recommendedPsp,
|
|
352
|
+
pspReference: paymentData.pspReference || data?.provider_ref_id || "",
|
|
353
|
+
status: "success",
|
|
354
|
+
metadata: paymentData
|
|
355
|
+
};
|
|
356
|
+
dispatch({ type: "PROCESS_SUCCESS", payload: result });
|
|
357
|
+
onSuccess?.(result);
|
|
358
|
+
} catch (err) {
|
|
359
|
+
const error = {
|
|
360
|
+
code: "PAYMENT_FAILED",
|
|
361
|
+
message: err instanceof Error ? err.message : "Payment failed. Please try again.",
|
|
362
|
+
recoverable: true,
|
|
363
|
+
originalError: err
|
|
364
|
+
};
|
|
365
|
+
dispatch({ type: "PROCESS_ERROR", payload: error });
|
|
366
|
+
onError?.(error);
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
[state.paymentIntent, state.selectedMethod, onSuccess, onError]
|
|
370
|
+
);
|
|
371
|
+
const handlePspSuccess = useCallback(
|
|
372
|
+
async (pspData) => {
|
|
373
|
+
await processPayment(pspData);
|
|
374
|
+
},
|
|
375
|
+
[processPayment]
|
|
376
|
+
);
|
|
377
|
+
const handlePspError = useCallback(
|
|
378
|
+
(error) => {
|
|
379
|
+
dispatch({ type: "PROCESS_ERROR", payload: error });
|
|
380
|
+
onError?.(error);
|
|
381
|
+
},
|
|
382
|
+
[onError]
|
|
383
|
+
);
|
|
384
|
+
const reset = useCallback(() => {
|
|
385
|
+
dispatch({ type: "RESET" });
|
|
386
|
+
}, []);
|
|
387
|
+
const close = useCallback(async () => {
|
|
388
|
+
if (state.paymentIntent && state.status !== "success") {
|
|
389
|
+
try {
|
|
390
|
+
const apiClient = apiClientRef.current;
|
|
391
|
+
if (apiClient) {
|
|
392
|
+
await apiClient.cancelPaymentIntent(state.paymentIntent.id);
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
dispatch({ type: "CLOSE" });
|
|
398
|
+
onClose?.();
|
|
399
|
+
}, [onClose, state.paymentIntent, state.status]);
|
|
400
|
+
return {
|
|
401
|
+
// State
|
|
402
|
+
status: state.status,
|
|
403
|
+
paymentIntent: state.paymentIntent,
|
|
404
|
+
selectedMethod: state.selectedMethod,
|
|
405
|
+
error: state.error,
|
|
406
|
+
result: state.result,
|
|
407
|
+
// Actions
|
|
408
|
+
initialize,
|
|
409
|
+
selectMethod,
|
|
410
|
+
processPayment,
|
|
411
|
+
handlePspSuccess,
|
|
412
|
+
handlePspError,
|
|
413
|
+
reset,
|
|
414
|
+
close,
|
|
415
|
+
// Computed
|
|
416
|
+
isLoading: state.status === "loading" || state.status === "processing",
|
|
417
|
+
isReady: state.status === "ready" || state.status === "method_selected",
|
|
418
|
+
isComplete: state.status === "success",
|
|
419
|
+
canRetry: state.error?.recoverable ?? false
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function detectCountryFromCurrency(currency) {
|
|
423
|
+
const currencyToCountry = {
|
|
424
|
+
GHS: "GH",
|
|
425
|
+
// Ghana
|
|
426
|
+
NGN: "NG",
|
|
427
|
+
// Nigeria
|
|
428
|
+
KES: "KE",
|
|
429
|
+
// Kenya
|
|
430
|
+
UGX: "UG",
|
|
431
|
+
// Uganda
|
|
432
|
+
TZS: "TZ",
|
|
433
|
+
// Tanzania
|
|
434
|
+
ZAR: "ZA",
|
|
435
|
+
// South Africa
|
|
436
|
+
XOF: "CI",
|
|
437
|
+
// Côte d'Ivoire (CFA)
|
|
438
|
+
XAF: "CM",
|
|
439
|
+
// Cameroon (CFA)
|
|
440
|
+
USD: "US",
|
|
441
|
+
// United States
|
|
442
|
+
EUR: "DE",
|
|
443
|
+
// Europe (default to Germany)
|
|
444
|
+
GBP: "GB"
|
|
445
|
+
// United Kingdom
|
|
446
|
+
};
|
|
447
|
+
return currencyToCountry[currency.toUpperCase()] || "GH";
|
|
448
|
+
}
|
|
449
|
+
var methodConfig = {
|
|
450
|
+
card: {
|
|
451
|
+
label: "Card",
|
|
452
|
+
icon: "\u{1F4B3}",
|
|
453
|
+
description: "Pay with Visa, Mastercard, or other cards"
|
|
454
|
+
},
|
|
455
|
+
mobile_money: {
|
|
456
|
+
label: "Mobile Money",
|
|
457
|
+
icon: "\u{1F4F1}",
|
|
458
|
+
description: "MTN, Vodafone Cash, AirtelTigo Money"
|
|
459
|
+
},
|
|
460
|
+
bank_transfer: {
|
|
461
|
+
label: "Bank Transfer",
|
|
462
|
+
icon: "\u{1F3E6}",
|
|
463
|
+
description: "Pay directly from your bank account"
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
function PaymentMethodSelector({
|
|
467
|
+
methods,
|
|
468
|
+
selectedMethod,
|
|
469
|
+
onSelect,
|
|
470
|
+
disabled = false
|
|
471
|
+
}) {
|
|
472
|
+
return /* @__PURE__ */ jsxs("div", { className: "reevit-method-selector", children: [
|
|
473
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-method-selector__label", children: "Select payment method" }),
|
|
474
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-method-selector__options", children: methods.map((method) => {
|
|
475
|
+
const config = methodConfig[method];
|
|
476
|
+
const isSelected = selectedMethod === method;
|
|
477
|
+
return /* @__PURE__ */ jsxs(
|
|
478
|
+
"button",
|
|
479
|
+
{
|
|
480
|
+
type: "button",
|
|
481
|
+
className: cn(
|
|
482
|
+
"reevit-method-option",
|
|
483
|
+
isSelected && "reevit-method-option--selected",
|
|
484
|
+
disabled && "reevit-method-option--disabled"
|
|
485
|
+
),
|
|
486
|
+
onClick: () => onSelect(method),
|
|
487
|
+
disabled,
|
|
488
|
+
"aria-pressed": isSelected,
|
|
489
|
+
children: [
|
|
490
|
+
/* @__PURE__ */ jsx("span", { className: "reevit-method-option__icon", children: config.icon }),
|
|
491
|
+
/* @__PURE__ */ jsxs("div", { className: "reevit-method-option__content", children: [
|
|
492
|
+
/* @__PURE__ */ jsx("span", { className: "reevit-method-option__label", children: config.label }),
|
|
493
|
+
/* @__PURE__ */ jsx("span", { className: "reevit-method-option__description", children: config.description })
|
|
494
|
+
] }),
|
|
495
|
+
isSelected && /* @__PURE__ */ jsx("span", { className: "reevit-method-option__check", children: /* @__PURE__ */ jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: /* @__PURE__ */ jsx(
|
|
496
|
+
"path",
|
|
497
|
+
{
|
|
498
|
+
d: "M16.667 5L7.5 14.167 3.333 10",
|
|
499
|
+
stroke: "currentColor",
|
|
500
|
+
strokeWidth: "2",
|
|
501
|
+
strokeLinecap: "round",
|
|
502
|
+
strokeLinejoin: "round"
|
|
503
|
+
}
|
|
504
|
+
) }) })
|
|
505
|
+
]
|
|
506
|
+
},
|
|
507
|
+
method
|
|
508
|
+
);
|
|
509
|
+
}) })
|
|
510
|
+
] });
|
|
511
|
+
}
|
|
512
|
+
var networks = [
|
|
513
|
+
{ id: "mtn", name: "MTN", color: "#FFCC00" },
|
|
514
|
+
{ id: "vodafone", name: "Vodafone", color: "#E60000" },
|
|
515
|
+
{ id: "airteltigo", name: "AirtelTigo", color: "#E4002B" }
|
|
516
|
+
];
|
|
517
|
+
function MobileMoneyForm({
|
|
518
|
+
onSubmit,
|
|
519
|
+
onCancel,
|
|
520
|
+
isLoading = false,
|
|
521
|
+
initialPhone = ""
|
|
522
|
+
}) {
|
|
523
|
+
const [phone, setPhone] = useState(initialPhone);
|
|
524
|
+
const [network, setNetwork] = useState(null);
|
|
525
|
+
const [error, setError] = useState(null);
|
|
526
|
+
const [touched, setTouched] = useState(false);
|
|
527
|
+
useEffect(() => {
|
|
528
|
+
if (phone.length >= 3) {
|
|
529
|
+
const detected = detectNetwork(phone);
|
|
530
|
+
if (detected) {
|
|
531
|
+
setNetwork(detected);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}, [phone]);
|
|
535
|
+
useEffect(() => {
|
|
536
|
+
if (touched && phone) {
|
|
537
|
+
if (!validatePhone(phone)) {
|
|
538
|
+
setError("Please enter a valid Ghana phone number");
|
|
539
|
+
} else if (network && !validatePhone(phone, network)) {
|
|
540
|
+
setError(`This number doesn't match the selected network`);
|
|
541
|
+
} else {
|
|
542
|
+
setError(null);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}, [phone, network, touched]);
|
|
546
|
+
const handlePhoneChange = useCallback((e) => {
|
|
547
|
+
const value = e.target.value.replace(/[^0-9+]/g, "");
|
|
548
|
+
setPhone(value);
|
|
549
|
+
}, []);
|
|
550
|
+
const handleSubmit = useCallback(
|
|
551
|
+
(e) => {
|
|
552
|
+
e.preventDefault();
|
|
553
|
+
setTouched(true);
|
|
554
|
+
if (!phone || !network) {
|
|
555
|
+
setError("Please enter your phone number and select a network");
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (!validatePhone(phone, network)) {
|
|
559
|
+
setError("Invalid phone number for selected network");
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
onSubmit({ phone, network });
|
|
563
|
+
},
|
|
564
|
+
[phone, network, onSubmit]
|
|
565
|
+
);
|
|
566
|
+
const isValid = phone && network && validatePhone(phone, network);
|
|
567
|
+
return /* @__PURE__ */ jsxs("form", { className: "reevit-momo-form", onSubmit: handleSubmit, children: [
|
|
568
|
+
/* @__PURE__ */ jsxs("div", { className: "reevit-momo-form__field", children: [
|
|
569
|
+
/* @__PURE__ */ jsx("label", { htmlFor: "reevit-phone", className: "reevit-momo-form__label", children: "Phone Number" }),
|
|
570
|
+
/* @__PURE__ */ jsx(
|
|
571
|
+
"input",
|
|
572
|
+
{
|
|
573
|
+
id: "reevit-phone",
|
|
574
|
+
type: "tel",
|
|
575
|
+
className: cn("reevit-momo-form__input", !!error && "reevit-momo-form__input--error"),
|
|
576
|
+
placeholder: "024 XXX XXXX",
|
|
577
|
+
value: phone,
|
|
578
|
+
onChange: handlePhoneChange,
|
|
579
|
+
onBlur: () => setTouched(true),
|
|
580
|
+
disabled: isLoading,
|
|
581
|
+
autoComplete: "tel"
|
|
582
|
+
}
|
|
583
|
+
),
|
|
584
|
+
phone && !error && /* @__PURE__ */ jsx("div", { className: "reevit-momo-form__formatted", children: formatPhone(phone) }),
|
|
585
|
+
error && /* @__PURE__ */ jsx("div", { className: "reevit-momo-form__error", children: error })
|
|
586
|
+
] }),
|
|
587
|
+
/* @__PURE__ */ jsxs("div", { className: "reevit-momo-form__field", children: [
|
|
588
|
+
/* @__PURE__ */ jsx("label", { className: "reevit-momo-form__label", children: "Select Network" }),
|
|
589
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-momo-form__networks", children: networks.map((n) => /* @__PURE__ */ jsx(
|
|
590
|
+
"button",
|
|
591
|
+
{
|
|
592
|
+
type: "button",
|
|
593
|
+
className: cn(
|
|
594
|
+
"reevit-network-btn",
|
|
595
|
+
network === n.id && "reevit-network-btn--selected"
|
|
596
|
+
),
|
|
597
|
+
style: { "--network-color": n.color },
|
|
598
|
+
onClick: () => setNetwork(n.id),
|
|
599
|
+
disabled: isLoading,
|
|
600
|
+
children: n.name
|
|
601
|
+
},
|
|
602
|
+
n.id
|
|
603
|
+
)) })
|
|
604
|
+
] }),
|
|
605
|
+
/* @__PURE__ */ jsxs("div", { className: "reevit-momo-form__actions", children: [
|
|
606
|
+
/* @__PURE__ */ jsx(
|
|
607
|
+
"button",
|
|
608
|
+
{
|
|
609
|
+
type: "button",
|
|
610
|
+
className: "reevit-btn reevit-btn--secondary",
|
|
611
|
+
onClick: onCancel,
|
|
612
|
+
disabled: isLoading,
|
|
613
|
+
children: "Back"
|
|
614
|
+
}
|
|
615
|
+
),
|
|
616
|
+
/* @__PURE__ */ jsx(
|
|
617
|
+
"button",
|
|
618
|
+
{
|
|
619
|
+
type: "submit",
|
|
620
|
+
className: "reevit-btn reevit-btn--primary",
|
|
621
|
+
disabled: !isValid || isLoading,
|
|
622
|
+
children: isLoading ? /* @__PURE__ */ jsx("span", { className: "reevit-spinner" }) : "Continue"
|
|
623
|
+
}
|
|
624
|
+
)
|
|
625
|
+
] }),
|
|
626
|
+
/* @__PURE__ */ jsx("p", { className: "reevit-momo-form__hint", children: "You will receive a USSD prompt on your phone to authorize the payment." })
|
|
627
|
+
] });
|
|
628
|
+
}
|
|
629
|
+
function loadPaystackScript() {
|
|
630
|
+
return new Promise((resolve, reject) => {
|
|
631
|
+
if (window.PaystackPop) {
|
|
632
|
+
resolve();
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const script = document.createElement("script");
|
|
636
|
+
script.src = "https://js.paystack.co/v2/inline.js";
|
|
637
|
+
script.async = true;
|
|
638
|
+
script.onload = () => resolve();
|
|
639
|
+
script.onerror = () => reject(new Error("Failed to load Paystack script"));
|
|
640
|
+
document.head.appendChild(script);
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
function PaystackBridge({
|
|
644
|
+
publicKey,
|
|
645
|
+
email,
|
|
646
|
+
amount,
|
|
647
|
+
currency = "GHS",
|
|
648
|
+
reference,
|
|
649
|
+
metadata,
|
|
650
|
+
channels = ["card", "mobile_money"],
|
|
651
|
+
onSuccess,
|
|
652
|
+
onError,
|
|
653
|
+
onClose,
|
|
654
|
+
autoStart = true
|
|
655
|
+
}) {
|
|
656
|
+
const initialized = useRef(false);
|
|
657
|
+
const startPayment = useCallback(async () => {
|
|
658
|
+
try {
|
|
659
|
+
await loadPaystackScript();
|
|
660
|
+
if (!window.PaystackPop) {
|
|
661
|
+
throw new Error("Paystack not available");
|
|
662
|
+
}
|
|
663
|
+
const handler = window.PaystackPop.setup({
|
|
664
|
+
key: publicKey,
|
|
665
|
+
email,
|
|
666
|
+
amount,
|
|
667
|
+
// Paystack expects amount in kobo/pesewas (smallest unit)
|
|
668
|
+
currency,
|
|
669
|
+
ref: reference,
|
|
670
|
+
metadata,
|
|
671
|
+
channels,
|
|
672
|
+
callback: (response) => {
|
|
673
|
+
const result = {
|
|
674
|
+
paymentId: response.transaction,
|
|
675
|
+
reference: response.reference,
|
|
676
|
+
amount,
|
|
677
|
+
currency,
|
|
678
|
+
paymentMethod: "card",
|
|
679
|
+
// Paystack handles this internally
|
|
680
|
+
psp: "paystack",
|
|
681
|
+
pspReference: response.trans,
|
|
682
|
+
status: response.status === "success" ? "success" : "pending",
|
|
683
|
+
metadata: { trxref: response.trxref }
|
|
684
|
+
};
|
|
685
|
+
onSuccess(result);
|
|
686
|
+
},
|
|
687
|
+
onClose: () => {
|
|
688
|
+
onClose();
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
handler.openIframe();
|
|
692
|
+
} catch (err) {
|
|
693
|
+
const error = {
|
|
694
|
+
code: "PSP_ERROR",
|
|
695
|
+
message: "Failed to initialize Paystack",
|
|
696
|
+
recoverable: true,
|
|
697
|
+
originalError: err
|
|
698
|
+
};
|
|
699
|
+
onError(error);
|
|
700
|
+
}
|
|
701
|
+
}, [publicKey, email, amount, currency, reference, metadata, channels, onSuccess, onError, onClose]);
|
|
702
|
+
useEffect(() => {
|
|
703
|
+
if (autoStart && !initialized.current) {
|
|
704
|
+
initialized.current = true;
|
|
705
|
+
startPayment();
|
|
706
|
+
}
|
|
707
|
+
}, [autoStart, startPayment]);
|
|
708
|
+
return /* @__PURE__ */ jsx("div", { className: "reevit-psp-bridge reevit-psp-bridge--paystack", children: /* @__PURE__ */ jsxs("div", { className: "reevit-psp-bridge__loading", children: [
|
|
709
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-spinner" }),
|
|
710
|
+
/* @__PURE__ */ jsx("p", { children: "Connecting to Paystack..." })
|
|
711
|
+
] }) });
|
|
712
|
+
}
|
|
713
|
+
var ReevitContext = createContext(null);
|
|
714
|
+
function useReevitContext() {
|
|
715
|
+
const context = useContext(ReevitContext);
|
|
716
|
+
if (!context) {
|
|
717
|
+
throw new Error("useReevitContext must be used within ReevitCheckout");
|
|
718
|
+
}
|
|
719
|
+
return context;
|
|
720
|
+
}
|
|
721
|
+
function ReevitCheckout({
|
|
722
|
+
// Config
|
|
723
|
+
publicKey,
|
|
724
|
+
amount,
|
|
725
|
+
currency,
|
|
726
|
+
email = "",
|
|
727
|
+
phone = "",
|
|
728
|
+
reference,
|
|
729
|
+
metadata,
|
|
730
|
+
paymentMethods = ["card", "mobile_money"],
|
|
731
|
+
// Callbacks
|
|
732
|
+
onSuccess,
|
|
733
|
+
onError,
|
|
734
|
+
onClose,
|
|
735
|
+
onStateChange,
|
|
736
|
+
// UI
|
|
737
|
+
children,
|
|
738
|
+
autoOpen = false,
|
|
739
|
+
theme
|
|
740
|
+
}) {
|
|
741
|
+
const [isOpen, setIsOpen] = useState(autoOpen);
|
|
742
|
+
const [showPSPBridge, setShowPSPBridge] = useState(false);
|
|
743
|
+
const [momoData, setMomoData] = useState(null);
|
|
744
|
+
const {
|
|
745
|
+
status,
|
|
746
|
+
paymentIntent,
|
|
747
|
+
selectedMethod,
|
|
748
|
+
error,
|
|
749
|
+
result,
|
|
750
|
+
initialize,
|
|
751
|
+
selectMethod,
|
|
752
|
+
processPayment,
|
|
753
|
+
reset,
|
|
754
|
+
close: closeCheckout,
|
|
755
|
+
isLoading,
|
|
756
|
+
isComplete
|
|
757
|
+
} = useReevit({
|
|
758
|
+
config: { publicKey, amount, currency, email, phone, reference, metadata, paymentMethods },
|
|
759
|
+
onSuccess: (result2) => {
|
|
760
|
+
onSuccess?.(result2);
|
|
761
|
+
setTimeout(() => {
|
|
762
|
+
setIsOpen(false);
|
|
763
|
+
}, 2e3);
|
|
764
|
+
},
|
|
765
|
+
onError,
|
|
766
|
+
onClose: () => {
|
|
767
|
+
setIsOpen(false);
|
|
768
|
+
onClose?.();
|
|
769
|
+
},
|
|
770
|
+
onStateChange
|
|
771
|
+
});
|
|
772
|
+
useEffect(() => {
|
|
773
|
+
if (isOpen && status === "idle") {
|
|
774
|
+
initialize();
|
|
775
|
+
}
|
|
776
|
+
}, [isOpen, status, initialize]);
|
|
777
|
+
const handleOpen = useCallback(() => {
|
|
778
|
+
setIsOpen(true);
|
|
779
|
+
setShowPSPBridge(false);
|
|
780
|
+
setMomoData(null);
|
|
781
|
+
}, []);
|
|
782
|
+
const handleClose = useCallback(() => {
|
|
783
|
+
closeCheckout();
|
|
784
|
+
setIsOpen(false);
|
|
785
|
+
setShowPSPBridge(false);
|
|
786
|
+
setMomoData(null);
|
|
787
|
+
}, [closeCheckout]);
|
|
788
|
+
const handleMethodSelect = useCallback(
|
|
789
|
+
(method) => {
|
|
790
|
+
selectMethod(method);
|
|
791
|
+
},
|
|
792
|
+
[selectMethod]
|
|
793
|
+
);
|
|
794
|
+
const handleContinue = useCallback(() => {
|
|
795
|
+
if (!selectedMethod) return;
|
|
796
|
+
if (selectedMethod === "card") {
|
|
797
|
+
setShowPSPBridge(true);
|
|
798
|
+
}
|
|
799
|
+
}, [selectedMethod]);
|
|
800
|
+
const handleMomoSubmit = useCallback(
|
|
801
|
+
(data) => {
|
|
802
|
+
setMomoData(data);
|
|
803
|
+
setShowPSPBridge(true);
|
|
804
|
+
},
|
|
805
|
+
[]
|
|
806
|
+
);
|
|
807
|
+
const handlePSPSuccess = useCallback(
|
|
808
|
+
(pspResult) => {
|
|
809
|
+
processPayment({ ...pspResult, momoData });
|
|
810
|
+
},
|
|
811
|
+
[processPayment, momoData]
|
|
812
|
+
);
|
|
813
|
+
const handlePSPError = useCallback(
|
|
814
|
+
(error2) => {
|
|
815
|
+
setShowPSPBridge(false);
|
|
816
|
+
onError?.(error2);
|
|
817
|
+
},
|
|
818
|
+
[onError]
|
|
819
|
+
);
|
|
820
|
+
const handlePSPClose = useCallback(() => {
|
|
821
|
+
setShowPSPBridge(false);
|
|
822
|
+
}, []);
|
|
823
|
+
const handleBack = useCallback(() => {
|
|
824
|
+
reset();
|
|
825
|
+
setMomoData(null);
|
|
826
|
+
setShowPSPBridge(false);
|
|
827
|
+
}, [reset]);
|
|
828
|
+
const themeStyles = theme ? createThemeVariables(theme) : {};
|
|
829
|
+
const trigger = children ? /* @__PURE__ */ jsx("span", { onClick: handleOpen, role: "button", tabIndex: 0, children }) : /* @__PURE__ */ jsxs("button", { className: "reevit-trigger-btn", onClick: handleOpen, children: [
|
|
830
|
+
"Pay ",
|
|
831
|
+
formatAmount(amount, currency)
|
|
832
|
+
] });
|
|
833
|
+
const renderContent = () => {
|
|
834
|
+
if (status === "loading") {
|
|
835
|
+
return /* @__PURE__ */ jsxs("div", { className: "reevit-loading", children: [
|
|
836
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-spinner" }),
|
|
837
|
+
/* @__PURE__ */ jsx("p", { children: "Preparing checkout..." })
|
|
838
|
+
] });
|
|
839
|
+
}
|
|
840
|
+
if (status === "success" && result) {
|
|
841
|
+
return /* @__PURE__ */ jsxs("div", { className: "reevit-success", children: [
|
|
842
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-success__icon", children: "\u2713" }),
|
|
843
|
+
/* @__PURE__ */ jsx("h3", { children: "Payment Successful" }),
|
|
844
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
845
|
+
"Reference: ",
|
|
846
|
+
result.reference
|
|
847
|
+
] })
|
|
848
|
+
] });
|
|
849
|
+
}
|
|
850
|
+
if (status === "failed" && error && !error.recoverable) {
|
|
851
|
+
return /* @__PURE__ */ jsxs("div", { className: "reevit-error", children: [
|
|
852
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-error__icon", children: "\u2715" }),
|
|
853
|
+
/* @__PURE__ */ jsx("h3", { children: "Payment Failed" }),
|
|
854
|
+
/* @__PURE__ */ jsx("p", { children: error.message }),
|
|
855
|
+
/* @__PURE__ */ jsx("button", { className: "reevit-btn reevit-btn--primary", onClick: handleBack, children: "Try Again" })
|
|
856
|
+
] });
|
|
857
|
+
}
|
|
858
|
+
if (showPSPBridge) {
|
|
859
|
+
return /* @__PURE__ */ jsx(
|
|
860
|
+
PaystackBridge,
|
|
861
|
+
{
|
|
862
|
+
publicKey,
|
|
863
|
+
email,
|
|
864
|
+
amount,
|
|
865
|
+
currency,
|
|
866
|
+
reference,
|
|
867
|
+
metadata,
|
|
868
|
+
channels: selectedMethod === "mobile_money" ? ["mobile_money"] : ["card"],
|
|
869
|
+
onSuccess: handlePSPSuccess,
|
|
870
|
+
onError: handlePSPError,
|
|
871
|
+
onClose: handlePSPClose
|
|
872
|
+
}
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
if (selectedMethod === "mobile_money" && !showPSPBridge) {
|
|
876
|
+
return /* @__PURE__ */ jsx(
|
|
877
|
+
MobileMoneyForm,
|
|
878
|
+
{
|
|
879
|
+
onSubmit: handleMomoSubmit,
|
|
880
|
+
onCancel: handleBack,
|
|
881
|
+
isLoading,
|
|
882
|
+
initialPhone: phone
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
return /* @__PURE__ */ jsxs("div", { className: "reevit-method-step", children: [
|
|
887
|
+
/* @__PURE__ */ jsx(
|
|
888
|
+
PaymentMethodSelector,
|
|
889
|
+
{
|
|
890
|
+
methods: paymentMethods,
|
|
891
|
+
selectedMethod,
|
|
892
|
+
onSelect: handleMethodSelect,
|
|
893
|
+
disabled: isLoading
|
|
894
|
+
}
|
|
895
|
+
),
|
|
896
|
+
selectedMethod && selectedMethod !== "mobile_money" && /* @__PURE__ */ jsx("div", { className: "reevit-method-step__actions", children: /* @__PURE__ */ jsx(
|
|
897
|
+
"button",
|
|
898
|
+
{
|
|
899
|
+
className: "reevit-btn reevit-btn--primary",
|
|
900
|
+
onClick: handleContinue,
|
|
901
|
+
disabled: isLoading,
|
|
902
|
+
children: "Continue"
|
|
903
|
+
}
|
|
904
|
+
) })
|
|
905
|
+
] });
|
|
906
|
+
};
|
|
907
|
+
return /* @__PURE__ */ jsxs(ReevitContext.Provider, { value: { publicKey, amount, currency }, children: [
|
|
908
|
+
trigger,
|
|
909
|
+
isOpen && /* @__PURE__ */ jsx("div", { className: "reevit-overlay", onClick: handleClose, children: /* @__PURE__ */ jsxs(
|
|
910
|
+
"div",
|
|
911
|
+
{
|
|
912
|
+
className: cn("reevit-modal", isComplete && "reevit-modal--success"),
|
|
913
|
+
style: themeStyles,
|
|
914
|
+
onClick: (e) => e.stopPropagation(),
|
|
915
|
+
role: "dialog",
|
|
916
|
+
"aria-modal": "true",
|
|
917
|
+
children: [
|
|
918
|
+
/* @__PURE__ */ jsxs("div", { className: "reevit-modal__header", children: [
|
|
919
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-modal__branding", children: /* @__PURE__ */ jsx("span", { className: "reevit-modal__logo", children: "Reevit" }) }),
|
|
920
|
+
/* @__PURE__ */ jsx(
|
|
921
|
+
"button",
|
|
922
|
+
{
|
|
923
|
+
className: "reevit-modal__close",
|
|
924
|
+
onClick: handleClose,
|
|
925
|
+
"aria-label": "Close",
|
|
926
|
+
children: "\u2715"
|
|
927
|
+
}
|
|
928
|
+
)
|
|
929
|
+
] }),
|
|
930
|
+
/* @__PURE__ */ jsxs("div", { className: "reevit-modal__amount", children: [
|
|
931
|
+
/* @__PURE__ */ jsx("span", { className: "reevit-modal__amount-label", children: "Amount" }),
|
|
932
|
+
/* @__PURE__ */ jsx("span", { className: "reevit-modal__amount-value", children: formatAmount(amount, currency) })
|
|
933
|
+
] }),
|
|
934
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-modal__content", children: renderContent() }),
|
|
935
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-modal__footer", children: /* @__PURE__ */ jsx("span", { className: "reevit-modal__secured", children: "\u{1F512} Secured by Reevit" }) })
|
|
936
|
+
]
|
|
937
|
+
}
|
|
938
|
+
) })
|
|
939
|
+
] });
|
|
940
|
+
}
|
|
941
|
+
function loadHubtelScript() {
|
|
942
|
+
return new Promise((resolve, reject) => {
|
|
943
|
+
if (window.HubtelCheckout) {
|
|
944
|
+
resolve();
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const script = document.createElement("script");
|
|
948
|
+
script.src = "https://checkout.hubtel.com/checkout.js";
|
|
949
|
+
script.async = true;
|
|
950
|
+
script.onload = () => resolve();
|
|
951
|
+
script.onerror = () => reject(new Error("Failed to load Hubtel script"));
|
|
952
|
+
document.head.appendChild(script);
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
function HubtelBridge({
|
|
956
|
+
merchantAccount,
|
|
957
|
+
amount,
|
|
958
|
+
currency = "GHS",
|
|
959
|
+
reference,
|
|
960
|
+
email,
|
|
961
|
+
phone,
|
|
962
|
+
description = "Payment",
|
|
963
|
+
onSuccess,
|
|
964
|
+
onError,
|
|
965
|
+
onClose,
|
|
966
|
+
autoStart = true
|
|
967
|
+
}) {
|
|
968
|
+
const initialized = useRef(false);
|
|
969
|
+
const startPayment = useCallback(async () => {
|
|
970
|
+
try {
|
|
971
|
+
await loadHubtelScript();
|
|
972
|
+
if (!window.HubtelCheckout) {
|
|
973
|
+
throw new Error("Hubtel checkout not available");
|
|
974
|
+
}
|
|
975
|
+
window.HubtelCheckout.initPayment({
|
|
976
|
+
merchantAccount,
|
|
977
|
+
basicDescription: description,
|
|
978
|
+
totalAmount: amount / 100,
|
|
979
|
+
// Hubtel expects amount in major units (GHS, not pesewas)
|
|
980
|
+
currency,
|
|
981
|
+
clientReference: reference || `hubtel_${Date.now()}`,
|
|
982
|
+
customerEmail: email,
|
|
983
|
+
customerMsisdn: phone,
|
|
984
|
+
onComplete: (response) => {
|
|
985
|
+
if (response.status === "Success") {
|
|
986
|
+
const result = {
|
|
987
|
+
paymentId: response.transactionId,
|
|
988
|
+
reference: response.clientReference,
|
|
989
|
+
amount: Math.round(response.amount * 100),
|
|
990
|
+
// Convert back to pesewas
|
|
991
|
+
currency: response.currency,
|
|
992
|
+
paymentMethod: "mobile_money",
|
|
993
|
+
psp: "hubtel",
|
|
994
|
+
pspReference: response.transactionId,
|
|
995
|
+
status: "success"
|
|
996
|
+
};
|
|
997
|
+
onSuccess(result);
|
|
998
|
+
} else {
|
|
999
|
+
const error = {
|
|
1000
|
+
code: "PAYMENT_FAILED",
|
|
1001
|
+
message: response.message || "Payment failed",
|
|
1002
|
+
recoverable: true
|
|
1003
|
+
};
|
|
1004
|
+
onError(error);
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
onCancel: () => {
|
|
1008
|
+
onClose();
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
const error = {
|
|
1013
|
+
code: "PSP_ERROR",
|
|
1014
|
+
message: "Failed to initialize Hubtel",
|
|
1015
|
+
recoverable: true,
|
|
1016
|
+
originalError: err
|
|
1017
|
+
};
|
|
1018
|
+
onError(error);
|
|
1019
|
+
}
|
|
1020
|
+
}, [merchantAccount, amount, currency, reference, email, phone, description, onSuccess, onError, onClose]);
|
|
1021
|
+
useEffect(() => {
|
|
1022
|
+
if (autoStart && !initialized.current) {
|
|
1023
|
+
initialized.current = true;
|
|
1024
|
+
startPayment();
|
|
1025
|
+
}
|
|
1026
|
+
}, [autoStart, startPayment]);
|
|
1027
|
+
return /* @__PURE__ */ jsx("div", { className: "reevit-psp-bridge reevit-psp-bridge--hubtel", children: /* @__PURE__ */ jsxs("div", { className: "reevit-psp-bridge__loading", children: [
|
|
1028
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-spinner" }),
|
|
1029
|
+
/* @__PURE__ */ jsx("p", { children: "Connecting to Hubtel..." })
|
|
1030
|
+
] }) });
|
|
1031
|
+
}
|
|
1032
|
+
function loadFlutterwaveScript() {
|
|
1033
|
+
return new Promise((resolve, reject) => {
|
|
1034
|
+
if (window.FlutterwaveCheckout) {
|
|
1035
|
+
resolve();
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
const script = document.createElement("script");
|
|
1039
|
+
script.src = "https://checkout.flutterwave.com/v3.js";
|
|
1040
|
+
script.async = true;
|
|
1041
|
+
script.onload = () => resolve();
|
|
1042
|
+
script.onerror = () => reject(new Error("Failed to load Flutterwave script"));
|
|
1043
|
+
document.head.appendChild(script);
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
function FlutterwaveBridge({
|
|
1047
|
+
publicKey,
|
|
1048
|
+
amount,
|
|
1049
|
+
currency = "GHS",
|
|
1050
|
+
reference,
|
|
1051
|
+
email,
|
|
1052
|
+
phone,
|
|
1053
|
+
name,
|
|
1054
|
+
paymentOptions = "card,mobilemoney,ussd",
|
|
1055
|
+
title,
|
|
1056
|
+
description,
|
|
1057
|
+
logo,
|
|
1058
|
+
metadata,
|
|
1059
|
+
onSuccess,
|
|
1060
|
+
onError,
|
|
1061
|
+
onClose,
|
|
1062
|
+
autoStart = true
|
|
1063
|
+
}) {
|
|
1064
|
+
const initialized = useRef(false);
|
|
1065
|
+
const startPayment = useCallback(async () => {
|
|
1066
|
+
try {
|
|
1067
|
+
await loadFlutterwaveScript();
|
|
1068
|
+
if (!window.FlutterwaveCheckout) {
|
|
1069
|
+
throw new Error("Flutterwave checkout not available");
|
|
1070
|
+
}
|
|
1071
|
+
const txRef = reference || `flw_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
1072
|
+
window.FlutterwaveCheckout({
|
|
1073
|
+
public_key: publicKey,
|
|
1074
|
+
tx_ref: txRef,
|
|
1075
|
+
amount: amount / 100,
|
|
1076
|
+
// Flutterwave expects amount in major units
|
|
1077
|
+
currency,
|
|
1078
|
+
payment_options: paymentOptions,
|
|
1079
|
+
customer: {
|
|
1080
|
+
email,
|
|
1081
|
+
phone_number: phone,
|
|
1082
|
+
name
|
|
1083
|
+
},
|
|
1084
|
+
customizations: {
|
|
1085
|
+
title,
|
|
1086
|
+
description,
|
|
1087
|
+
logo
|
|
1088
|
+
},
|
|
1089
|
+
meta: metadata,
|
|
1090
|
+
callback: (response) => {
|
|
1091
|
+
if (response.status === "successful") {
|
|
1092
|
+
const result = {
|
|
1093
|
+
paymentId: response.transaction_id.toString(),
|
|
1094
|
+
reference: response.tx_ref,
|
|
1095
|
+
amount: Math.round(response.amount * 100),
|
|
1096
|
+
currency: response.currency,
|
|
1097
|
+
paymentMethod: response.payment_type === "mobilemoney" ? "mobile_money" : "card",
|
|
1098
|
+
psp: "flutterwave",
|
|
1099
|
+
pspReference: response.flw_ref,
|
|
1100
|
+
status: "success",
|
|
1101
|
+
metadata: {
|
|
1102
|
+
charged_amount: response.charged_amount,
|
|
1103
|
+
payment_type: response.payment_type
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
onSuccess(result);
|
|
1107
|
+
} else {
|
|
1108
|
+
const error = {
|
|
1109
|
+
code: response.status === "cancelled" ? "CANCELLED" : "PAYMENT_FAILED",
|
|
1110
|
+
message: response.status === "cancelled" ? "Payment was cancelled" : "Payment failed",
|
|
1111
|
+
recoverable: true
|
|
1112
|
+
};
|
|
1113
|
+
onError(error);
|
|
1114
|
+
}
|
|
1115
|
+
},
|
|
1116
|
+
onclose: () => {
|
|
1117
|
+
onClose();
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
const error = {
|
|
1122
|
+
code: "PSP_ERROR",
|
|
1123
|
+
message: "Failed to initialize Flutterwave",
|
|
1124
|
+
recoverable: true,
|
|
1125
|
+
originalError: err
|
|
1126
|
+
};
|
|
1127
|
+
onError(error);
|
|
1128
|
+
}
|
|
1129
|
+
}, [
|
|
1130
|
+
publicKey,
|
|
1131
|
+
amount,
|
|
1132
|
+
currency,
|
|
1133
|
+
reference,
|
|
1134
|
+
email,
|
|
1135
|
+
phone,
|
|
1136
|
+
name,
|
|
1137
|
+
paymentOptions,
|
|
1138
|
+
title,
|
|
1139
|
+
description,
|
|
1140
|
+
logo,
|
|
1141
|
+
metadata,
|
|
1142
|
+
onSuccess,
|
|
1143
|
+
onError,
|
|
1144
|
+
onClose
|
|
1145
|
+
]);
|
|
1146
|
+
useEffect(() => {
|
|
1147
|
+
if (autoStart && !initialized.current) {
|
|
1148
|
+
initialized.current = true;
|
|
1149
|
+
startPayment();
|
|
1150
|
+
}
|
|
1151
|
+
}, [autoStart, startPayment]);
|
|
1152
|
+
return /* @__PURE__ */ jsx("div", { className: "reevit-psp-bridge reevit-psp-bridge--flutterwave", children: /* @__PURE__ */ jsxs("div", { className: "reevit-psp-bridge__loading", children: [
|
|
1153
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-spinner" }),
|
|
1154
|
+
/* @__PURE__ */ jsx("p", { children: "Connecting to Flutterwave..." })
|
|
1155
|
+
] }) });
|
|
1156
|
+
}
|
|
1157
|
+
var STRIPE_SCRIPT_URL = "https://js.stripe.com/v3/";
|
|
1158
|
+
var stripeScriptPromise = null;
|
|
1159
|
+
function loadStripeScript() {
|
|
1160
|
+
if (stripeScriptPromise) return stripeScriptPromise;
|
|
1161
|
+
if (document.getElementById("stripe-js-script")) {
|
|
1162
|
+
stripeScriptPromise = Promise.resolve();
|
|
1163
|
+
return stripeScriptPromise;
|
|
1164
|
+
}
|
|
1165
|
+
stripeScriptPromise = new Promise((resolve, reject) => {
|
|
1166
|
+
const script = document.createElement("script");
|
|
1167
|
+
script.id = "stripe-js-script";
|
|
1168
|
+
script.src = STRIPE_SCRIPT_URL;
|
|
1169
|
+
script.async = true;
|
|
1170
|
+
script.onload = () => resolve();
|
|
1171
|
+
script.onerror = () => reject(new Error("Failed to load Stripe.js"));
|
|
1172
|
+
document.head.appendChild(script);
|
|
1173
|
+
});
|
|
1174
|
+
return stripeScriptPromise;
|
|
1175
|
+
}
|
|
1176
|
+
function StripeBridge({
|
|
1177
|
+
publishableKey,
|
|
1178
|
+
clientSecret,
|
|
1179
|
+
amount,
|
|
1180
|
+
currency,
|
|
1181
|
+
appearance,
|
|
1182
|
+
onSuccess,
|
|
1183
|
+
onError,
|
|
1184
|
+
onReady,
|
|
1185
|
+
onCancel
|
|
1186
|
+
}) {
|
|
1187
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
1188
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
1189
|
+
const [error, setError] = useState(null);
|
|
1190
|
+
const stripeRef = useRef(null);
|
|
1191
|
+
const elementsRef = useRef(null);
|
|
1192
|
+
const paymentElementRef = useRef(null);
|
|
1193
|
+
const containerRef = useRef(null);
|
|
1194
|
+
useEffect(() => {
|
|
1195
|
+
let mounted = true;
|
|
1196
|
+
const initStripe = async () => {
|
|
1197
|
+
try {
|
|
1198
|
+
await loadStripeScript();
|
|
1199
|
+
if (!mounted || !window.Stripe) {
|
|
1200
|
+
throw new Error("Stripe not available");
|
|
1201
|
+
}
|
|
1202
|
+
stripeRef.current = window.Stripe(publishableKey);
|
|
1203
|
+
elementsRef.current = stripeRef.current.elements({
|
|
1204
|
+
clientSecret,
|
|
1205
|
+
appearance: appearance || { theme: "stripe" }
|
|
1206
|
+
});
|
|
1207
|
+
paymentElementRef.current = elementsRef.current.create("payment");
|
|
1208
|
+
if (containerRef.current) {
|
|
1209
|
+
paymentElementRef.current.mount(containerRef.current);
|
|
1210
|
+
}
|
|
1211
|
+
paymentElementRef.current.on("ready", () => {
|
|
1212
|
+
if (mounted) {
|
|
1213
|
+
setIsLoading(false);
|
|
1214
|
+
onReady?.();
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
paymentElementRef.current.on("change", (event) => {
|
|
1218
|
+
if (event.error) {
|
|
1219
|
+
setError(event.error.message);
|
|
1220
|
+
} else {
|
|
1221
|
+
setError(null);
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
} catch (err) {
|
|
1225
|
+
if (mounted) {
|
|
1226
|
+
const message = err instanceof Error ? err.message : "Failed to initialize Stripe";
|
|
1227
|
+
setError(message);
|
|
1228
|
+
setIsLoading(false);
|
|
1229
|
+
onError({ code: "STRIPE_INIT_ERROR", message });
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
initStripe();
|
|
1234
|
+
return () => {
|
|
1235
|
+
mounted = false;
|
|
1236
|
+
paymentElementRef.current?.destroy();
|
|
1237
|
+
};
|
|
1238
|
+
}, [publishableKey, clientSecret, appearance, onReady, onError]);
|
|
1239
|
+
const handleSubmit = useCallback(async () => {
|
|
1240
|
+
if (!stripeRef.current || !elementsRef.current) {
|
|
1241
|
+
onError({ code: "NOT_INITIALIZED", message: "Stripe not initialized" });
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
setIsSubmitting(true);
|
|
1245
|
+
setError(null);
|
|
1246
|
+
try {
|
|
1247
|
+
const { error: submitError } = await elementsRef.current.submit();
|
|
1248
|
+
if (submitError) {
|
|
1249
|
+
setError(submitError.message);
|
|
1250
|
+
onError({ code: submitError.code || "VALIDATION_ERROR", message: submitError.message });
|
|
1251
|
+
setIsSubmitting(false);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const { error: confirmError, paymentIntent } = await stripeRef.current.confirmPayment({
|
|
1255
|
+
elements: elementsRef.current,
|
|
1256
|
+
redirect: "if_required"
|
|
1257
|
+
});
|
|
1258
|
+
if (confirmError) {
|
|
1259
|
+
setError(confirmError.message);
|
|
1260
|
+
onError({ code: confirmError.code || "PAYMENT_ERROR", message: confirmError.message });
|
|
1261
|
+
} else if (paymentIntent) {
|
|
1262
|
+
onSuccess({
|
|
1263
|
+
paymentIntentId: paymentIntent.id,
|
|
1264
|
+
status: paymentIntent.status
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
} catch (err) {
|
|
1268
|
+
const message = err instanceof Error ? err.message : "Payment failed";
|
|
1269
|
+
setError(message);
|
|
1270
|
+
onError({ code: "UNKNOWN_ERROR", message });
|
|
1271
|
+
} finally {
|
|
1272
|
+
setIsSubmitting(false);
|
|
1273
|
+
}
|
|
1274
|
+
}, [onSuccess, onError]);
|
|
1275
|
+
return /* @__PURE__ */ jsxs("div", { className: "reevit-stripe-bridge", children: [
|
|
1276
|
+
isLoading && /* @__PURE__ */ jsxs("div", { className: "reevit-stripe-loading", children: [
|
|
1277
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-spinner" }),
|
|
1278
|
+
/* @__PURE__ */ jsx("p", { children: "Loading secure payment form..." })
|
|
1279
|
+
] }),
|
|
1280
|
+
/* @__PURE__ */ jsx(
|
|
1281
|
+
"div",
|
|
1282
|
+
{
|
|
1283
|
+
ref: containerRef,
|
|
1284
|
+
className: "reevit-stripe-element",
|
|
1285
|
+
style: { display: isLoading ? "none" : "block", minHeight: "200px" }
|
|
1286
|
+
}
|
|
1287
|
+
),
|
|
1288
|
+
error && /* @__PURE__ */ jsx("div", { className: "reevit-stripe-error", children: /* @__PURE__ */ jsx("p", { children: error }) }),
|
|
1289
|
+
/* @__PURE__ */ jsxs("div", { className: "reevit-stripe-actions", children: [
|
|
1290
|
+
/* @__PURE__ */ jsx(
|
|
1291
|
+
"button",
|
|
1292
|
+
{
|
|
1293
|
+
type: "button",
|
|
1294
|
+
className: "reevit-submit-btn",
|
|
1295
|
+
onClick: handleSubmit,
|
|
1296
|
+
disabled: isLoading || isSubmitting,
|
|
1297
|
+
children: isSubmitting ? /* @__PURE__ */ jsx("span", { className: "reevit-spinner" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1298
|
+
"Pay ",
|
|
1299
|
+
currency,
|
|
1300
|
+
" ",
|
|
1301
|
+
(amount / 100).toFixed(2)
|
|
1302
|
+
] })
|
|
1303
|
+
}
|
|
1304
|
+
),
|
|
1305
|
+
onCancel && /* @__PURE__ */ jsx(
|
|
1306
|
+
"button",
|
|
1307
|
+
{
|
|
1308
|
+
type: "button",
|
|
1309
|
+
className: "reevit-cancel-btn",
|
|
1310
|
+
onClick: onCancel,
|
|
1311
|
+
disabled: isSubmitting,
|
|
1312
|
+
children: "Cancel"
|
|
1313
|
+
}
|
|
1314
|
+
)
|
|
1315
|
+
] })
|
|
1316
|
+
] });
|
|
1317
|
+
}
|
|
1318
|
+
var MONNIFY_SCRIPT_URL = "https://sdk.monnify.com/plugin/monnify.js";
|
|
1319
|
+
var monnifyScriptPromise = null;
|
|
1320
|
+
function loadMonnifyScript() {
|
|
1321
|
+
if (monnifyScriptPromise) return monnifyScriptPromise;
|
|
1322
|
+
if (document.getElementById("monnify-sdk-script")) {
|
|
1323
|
+
monnifyScriptPromise = Promise.resolve();
|
|
1324
|
+
return monnifyScriptPromise;
|
|
1325
|
+
}
|
|
1326
|
+
monnifyScriptPromise = new Promise((resolve, reject) => {
|
|
1327
|
+
const script = document.createElement("script");
|
|
1328
|
+
script.id = "monnify-sdk-script";
|
|
1329
|
+
script.src = MONNIFY_SCRIPT_URL;
|
|
1330
|
+
script.async = true;
|
|
1331
|
+
script.onload = () => resolve();
|
|
1332
|
+
script.onerror = () => reject(new Error("Failed to load Monnify SDK"));
|
|
1333
|
+
document.head.appendChild(script);
|
|
1334
|
+
});
|
|
1335
|
+
return monnifyScriptPromise;
|
|
1336
|
+
}
|
|
1337
|
+
function MonnifyBridge({
|
|
1338
|
+
apiKey,
|
|
1339
|
+
contractCode,
|
|
1340
|
+
amount,
|
|
1341
|
+
currency,
|
|
1342
|
+
reference,
|
|
1343
|
+
customerName,
|
|
1344
|
+
customerEmail,
|
|
1345
|
+
customerPhone,
|
|
1346
|
+
paymentDescription,
|
|
1347
|
+
isTestMode = false,
|
|
1348
|
+
metadata,
|
|
1349
|
+
autoOpen = true,
|
|
1350
|
+
onSuccess,
|
|
1351
|
+
onError,
|
|
1352
|
+
onClose
|
|
1353
|
+
}) {
|
|
1354
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
1355
|
+
const [isReady, setIsReady] = useState(false);
|
|
1356
|
+
const openMonnify = useCallback(async () => {
|
|
1357
|
+
try {
|
|
1358
|
+
await loadMonnifyScript();
|
|
1359
|
+
if (!window.MonnifySDK) {
|
|
1360
|
+
throw new Error("Monnify SDK not available");
|
|
1361
|
+
}
|
|
1362
|
+
window.MonnifySDK.initialize({
|
|
1363
|
+
amount,
|
|
1364
|
+
currency: currency || "NGN",
|
|
1365
|
+
reference,
|
|
1366
|
+
customerName,
|
|
1367
|
+
customerEmail,
|
|
1368
|
+
customerMobileNumber: customerPhone,
|
|
1369
|
+
apiKey,
|
|
1370
|
+
contractCode,
|
|
1371
|
+
paymentDescription: paymentDescription || "Payment",
|
|
1372
|
+
isTestMode,
|
|
1373
|
+
metadata,
|
|
1374
|
+
onComplete: (response) => {
|
|
1375
|
+
if (response.status === "SUCCESS") {
|
|
1376
|
+
onSuccess({
|
|
1377
|
+
transactionReference: response.transactionReference,
|
|
1378
|
+
paymentReference: response.paymentReference,
|
|
1379
|
+
amount: response.authorizedAmount || amount
|
|
1380
|
+
});
|
|
1381
|
+
} else {
|
|
1382
|
+
onError({
|
|
1383
|
+
code: "MONNIFY_PAYMENT_FAILED",
|
|
1384
|
+
message: response.message || "Payment was not successful"
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
onClose: () => {
|
|
1389
|
+
onClose?.();
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
const message = err instanceof Error ? err.message : "Failed to open Monnify";
|
|
1394
|
+
onError({ code: "MONNIFY_INIT_ERROR", message });
|
|
1395
|
+
}
|
|
1396
|
+
}, [
|
|
1397
|
+
amount,
|
|
1398
|
+
currency,
|
|
1399
|
+
reference,
|
|
1400
|
+
customerName,
|
|
1401
|
+
customerEmail,
|
|
1402
|
+
customerPhone,
|
|
1403
|
+
apiKey,
|
|
1404
|
+
contractCode,
|
|
1405
|
+
paymentDescription,
|
|
1406
|
+
isTestMode,
|
|
1407
|
+
metadata,
|
|
1408
|
+
onSuccess,
|
|
1409
|
+
onError,
|
|
1410
|
+
onClose
|
|
1411
|
+
]);
|
|
1412
|
+
useEffect(() => {
|
|
1413
|
+
const init = async () => {
|
|
1414
|
+
try {
|
|
1415
|
+
await loadMonnifyScript();
|
|
1416
|
+
setIsReady(true);
|
|
1417
|
+
setIsLoading(false);
|
|
1418
|
+
if (autoOpen) {
|
|
1419
|
+
openMonnify();
|
|
1420
|
+
}
|
|
1421
|
+
} catch (err) {
|
|
1422
|
+
setIsLoading(false);
|
|
1423
|
+
const message = err instanceof Error ? err.message : "Failed to load Monnify";
|
|
1424
|
+
onError({ code: "MONNIFY_LOAD_ERROR", message });
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
init();
|
|
1428
|
+
}, [autoOpen, openMonnify, onError]);
|
|
1429
|
+
if (isLoading) {
|
|
1430
|
+
return /* @__PURE__ */ jsx("div", { className: "reevit-monnify-bridge", children: /* @__PURE__ */ jsxs("div", { className: "reevit-monnify-loading", children: [
|
|
1431
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-spinner" }),
|
|
1432
|
+
/* @__PURE__ */ jsx("p", { children: "Loading Monnify checkout..." })
|
|
1433
|
+
] }) });
|
|
1434
|
+
}
|
|
1435
|
+
if (!autoOpen && isReady) {
|
|
1436
|
+
return /* @__PURE__ */ jsx("div", { className: "reevit-monnify-bridge", children: /* @__PURE__ */ jsx(
|
|
1437
|
+
"button",
|
|
1438
|
+
{
|
|
1439
|
+
type: "button",
|
|
1440
|
+
className: "reevit-submit-btn",
|
|
1441
|
+
onClick: openMonnify,
|
|
1442
|
+
children: "Pay with Monnify"
|
|
1443
|
+
}
|
|
1444
|
+
) });
|
|
1445
|
+
}
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
function MPesaBridge({
|
|
1449
|
+
apiEndpoint,
|
|
1450
|
+
phoneNumber,
|
|
1451
|
+
amount,
|
|
1452
|
+
currency,
|
|
1453
|
+
reference,
|
|
1454
|
+
description,
|
|
1455
|
+
onInitiated,
|
|
1456
|
+
onSuccess,
|
|
1457
|
+
onError,
|
|
1458
|
+
headers = {}
|
|
1459
|
+
}) {
|
|
1460
|
+
const [state, setState] = useState("idle");
|
|
1461
|
+
const [error, setError] = useState(null);
|
|
1462
|
+
const [checkoutRequestId, setCheckoutRequestId] = useState(null);
|
|
1463
|
+
const initiateSTKPush = useCallback(async () => {
|
|
1464
|
+
setState("initiating");
|
|
1465
|
+
setError(null);
|
|
1466
|
+
try {
|
|
1467
|
+
const response = await fetch(apiEndpoint, {
|
|
1468
|
+
method: "POST",
|
|
1469
|
+
headers: {
|
|
1470
|
+
"Content-Type": "application/json",
|
|
1471
|
+
...headers
|
|
1472
|
+
},
|
|
1473
|
+
body: JSON.stringify({
|
|
1474
|
+
phone_number: phoneNumber,
|
|
1475
|
+
amount,
|
|
1476
|
+
currency,
|
|
1477
|
+
reference,
|
|
1478
|
+
description: description || `Payment ${reference}`
|
|
1479
|
+
})
|
|
1480
|
+
});
|
|
1481
|
+
if (!response.ok) {
|
|
1482
|
+
const errorData = await response.json().catch(() => ({}));
|
|
1483
|
+
throw new Error(errorData.message || `Request failed: ${response.status}`);
|
|
1484
|
+
}
|
|
1485
|
+
const data = await response.json();
|
|
1486
|
+
const requestId = data.checkout_request_id || data.checkoutRequestId || data.transaction_id;
|
|
1487
|
+
setCheckoutRequestId(requestId);
|
|
1488
|
+
setState("waiting");
|
|
1489
|
+
onInitiated?.(requestId);
|
|
1490
|
+
} catch (err) {
|
|
1491
|
+
const message = err instanceof Error ? err.message : "Failed to initiate M-Pesa payment";
|
|
1492
|
+
setError(message);
|
|
1493
|
+
setState("failed");
|
|
1494
|
+
onError({ code: "MPESA_INIT_ERROR", message });
|
|
1495
|
+
}
|
|
1496
|
+
}, [apiEndpoint, phoneNumber, amount, currency, reference, description, headers, onInitiated, onError]);
|
|
1497
|
+
const handleRetry = useCallback(() => {
|
|
1498
|
+
setState("idle");
|
|
1499
|
+
setError(null);
|
|
1500
|
+
initiateSTKPush();
|
|
1501
|
+
}, [initiateSTKPush]);
|
|
1502
|
+
if (state === "idle") {
|
|
1503
|
+
return /* @__PURE__ */ jsxs("div", { className: "reevit-mpesa-bridge", children: [
|
|
1504
|
+
/* @__PURE__ */ jsxs("div", { className: "reevit-mpesa-info", children: [
|
|
1505
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-mpesa-icon", children: "\u{1F4F1}" }),
|
|
1506
|
+
/* @__PURE__ */ jsx("h3", { children: "Pay with M-Pesa" }),
|
|
1507
|
+
/* @__PURE__ */ jsx("p", { children: "You will receive a prompt on your phone to complete the payment." }),
|
|
1508
|
+
/* @__PURE__ */ jsxs("p", { className: "reevit-mpesa-phone", children: [
|
|
1509
|
+
"Phone: ",
|
|
1510
|
+
phoneNumber
|
|
1511
|
+
] })
|
|
1512
|
+
] }),
|
|
1513
|
+
/* @__PURE__ */ jsx(
|
|
1514
|
+
"button",
|
|
1515
|
+
{
|
|
1516
|
+
type: "button",
|
|
1517
|
+
className: "reevit-submit-btn reevit-mpesa-btn",
|
|
1518
|
+
onClick: initiateSTKPush,
|
|
1519
|
+
children: "Send Payment Request"
|
|
1520
|
+
}
|
|
1521
|
+
)
|
|
1522
|
+
] });
|
|
1523
|
+
}
|
|
1524
|
+
if (state === "initiating") {
|
|
1525
|
+
return /* @__PURE__ */ jsx("div", { className: "reevit-mpesa-bridge", children: /* @__PURE__ */ jsxs("div", { className: "reevit-mpesa-loading", children: [
|
|
1526
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-spinner reevit-spinner--large" }),
|
|
1527
|
+
/* @__PURE__ */ jsx("p", { children: "Sending payment request to your phone..." })
|
|
1528
|
+
] }) });
|
|
1529
|
+
}
|
|
1530
|
+
if (state === "waiting") {
|
|
1531
|
+
return /* @__PURE__ */ jsx("div", { className: "reevit-mpesa-bridge", children: /* @__PURE__ */ jsxs("div", { className: "reevit-mpesa-waiting", children: [
|
|
1532
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-mpesa-phone-icon", children: "\u{1F4F2}" }),
|
|
1533
|
+
/* @__PURE__ */ jsx("h3", { children: "Check Your Phone" }),
|
|
1534
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
1535
|
+
"An M-Pesa payment request has been sent to ",
|
|
1536
|
+
/* @__PURE__ */ jsx("strong", { children: phoneNumber }),
|
|
1537
|
+
"."
|
|
1538
|
+
] }),
|
|
1539
|
+
/* @__PURE__ */ jsxs("ol", { className: "reevit-mpesa-steps", children: [
|
|
1540
|
+
/* @__PURE__ */ jsx("li", { children: "Check for the M-Pesa prompt on your phone" }),
|
|
1541
|
+
/* @__PURE__ */ jsx("li", { children: "Enter your M-Pesa PIN to authorize" }),
|
|
1542
|
+
/* @__PURE__ */ jsx("li", { children: "Wait for confirmation" })
|
|
1543
|
+
] }),
|
|
1544
|
+
/* @__PURE__ */ jsxs("div", { className: "reevit-mpesa-waiting-indicator", children: [
|
|
1545
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-spinner" }),
|
|
1546
|
+
/* @__PURE__ */ jsx("span", { children: "Waiting for payment confirmation..." })
|
|
1547
|
+
] }),
|
|
1548
|
+
checkoutRequestId && /* @__PURE__ */ jsxs("p", { className: "reevit-mpesa-ref", children: [
|
|
1549
|
+
"Request ID: ",
|
|
1550
|
+
checkoutRequestId
|
|
1551
|
+
] })
|
|
1552
|
+
] }) });
|
|
1553
|
+
}
|
|
1554
|
+
if (state === "failed") {
|
|
1555
|
+
return /* @__PURE__ */ jsx("div", { className: "reevit-mpesa-bridge", children: /* @__PURE__ */ jsxs("div", { className: "reevit-mpesa-error", children: [
|
|
1556
|
+
/* @__PURE__ */ jsx("div", { className: "reevit-error-icon", children: "\u26A0\uFE0F" }),
|
|
1557
|
+
/* @__PURE__ */ jsx("h3", { children: "Payment Request Failed" }),
|
|
1558
|
+
/* @__PURE__ */ jsx("p", { children: error || "Something went wrong. Please try again." }),
|
|
1559
|
+
/* @__PURE__ */ jsx(
|
|
1560
|
+
"button",
|
|
1561
|
+
{
|
|
1562
|
+
type: "button",
|
|
1563
|
+
className: "reevit-retry-btn",
|
|
1564
|
+
onClick: handleRetry,
|
|
1565
|
+
children: "Try Again"
|
|
1566
|
+
}
|
|
1567
|
+
)
|
|
1568
|
+
] }) });
|
|
1569
|
+
}
|
|
1570
|
+
return null;
|
|
1571
|
+
}
|
|
1572
|
+
function useMPesaStatusPolling(statusEndpoint, checkoutRequestId, options) {
|
|
1573
|
+
const { interval = 5e3, maxAttempts = 24, headers = {}, onSuccess, onFailed, onTimeout } = options;
|
|
1574
|
+
const startPolling = useCallback(async () => {
|
|
1575
|
+
if (!checkoutRequestId) return;
|
|
1576
|
+
let attempts = 0;
|
|
1577
|
+
const poll = async () => {
|
|
1578
|
+
if (attempts >= maxAttempts) {
|
|
1579
|
+
onTimeout();
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
try {
|
|
1583
|
+
const response = await fetch(`${statusEndpoint}?checkout_request_id=${checkoutRequestId}`, {
|
|
1584
|
+
headers
|
|
1585
|
+
});
|
|
1586
|
+
if (!response.ok) {
|
|
1587
|
+
attempts++;
|
|
1588
|
+
setTimeout(poll, interval);
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
const data = await response.json();
|
|
1592
|
+
if (data.status === "success" || data.status === "completed") {
|
|
1593
|
+
onSuccess({ transactionId: data.transaction_id || data.mpesa_receipt });
|
|
1594
|
+
} else if (data.status === "failed" || data.status === "cancelled") {
|
|
1595
|
+
onFailed({ message: data.message || "Payment failed or was cancelled" });
|
|
1596
|
+
} else {
|
|
1597
|
+
attempts++;
|
|
1598
|
+
setTimeout(poll, interval);
|
|
1599
|
+
}
|
|
1600
|
+
} catch {
|
|
1601
|
+
attempts++;
|
|
1602
|
+
setTimeout(poll, interval);
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
poll();
|
|
1606
|
+
}, [checkoutRequestId, statusEndpoint, interval, maxAttempts, headers, onSuccess, onFailed, onTimeout]);
|
|
1607
|
+
return { startPolling };
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
export { FlutterwaveBridge, HubtelBridge, MPesaBridge, MobileMoneyForm, MonnifyBridge, PaymentMethodSelector, PaystackBridge, ReevitAPIClient, ReevitCheckout, StripeBridge, createReevitClient, detectNetwork, formatAmount, formatPhone, loadFlutterwaveScript, loadHubtelScript, loadMonnifyScript, loadPaystackScript, loadStripeScript, useMPesaStatusPolling, useReevit, useReevitContext, validatePhone };
|
|
1611
|
+
//# sourceMappingURL=index.mjs.map
|
|
1612
|
+
//# sourceMappingURL=index.mjs.map
|