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