@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.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