@applite/duticotac-react 0.0.1

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,677 @@
1
+ "use client";
2
+
3
+ // src/components/payment-modal.tsx
4
+ import * as React2 from "react";
5
+ import {
6
+ DuticotacError,
7
+ getErrorMessage
8
+ } from "@applite/duticotac";
9
+
10
+ // src/components/provider-selector.tsx
11
+ import { getProviderName, getProviderLogo } from "@applite/duticotac";
12
+
13
+ // src/utils/cn.ts
14
+ import { clsx } from "clsx";
15
+ import { twMerge } from "tailwind-merge";
16
+ function cn(...inputs) {
17
+ return twMerge(clsx(inputs));
18
+ }
19
+
20
+ // src/components/provider-selector.tsx
21
+ import { jsx, jsxs } from "react/jsx-runtime";
22
+ function ProviderSelector({
23
+ providers,
24
+ value,
25
+ onChange,
26
+ disabled,
27
+ className
28
+ }) {
29
+ return /* @__PURE__ */ jsx("div", { className: cn("grid gap-2", className), style: {
30
+ gridTemplateColumns: `repeat(${Math.min(providers.length, 4)}, 1fr)`
31
+ }, children: providers.map((provider) => {
32
+ const selected = value === provider;
33
+ return /* @__PURE__ */ jsxs(
34
+ "button",
35
+ {
36
+ type: "button",
37
+ disabled,
38
+ onClick: () => onChange(provider),
39
+ className: cn(
40
+ "relative flex flex-col items-center gap-2 rounded-lg border-2 p-3 transition-all",
41
+ "hover:scale-[1.02] disabled:pointer-events-none disabled:opacity-50",
42
+ selected ? "border-primary bg-primary/5 shadow-sm" : "border-border bg-background hover:border-primary/40"
43
+ ),
44
+ children: [
45
+ selected && /* @__PURE__ */ jsx("div", { className: "absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary", children: /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx(
46
+ "path",
47
+ {
48
+ d: "M5 12l5 5L20 7",
49
+ stroke: "#fff",
50
+ strokeWidth: "3",
51
+ strokeLinecap: "round",
52
+ strokeLinejoin: "round"
53
+ }
54
+ ) }) }),
55
+ /* @__PURE__ */ jsx(
56
+ "img",
57
+ {
58
+ src: getProviderLogo(provider),
59
+ alt: getProviderName(provider),
60
+ width: 44,
61
+ height: 44,
62
+ className: "rounded-lg object-contain"
63
+ }
64
+ ),
65
+ /* @__PURE__ */ jsx(
66
+ "span",
67
+ {
68
+ className: cn(
69
+ "text-center text-xs leading-tight",
70
+ selected ? "font-semibold text-primary" : "font-medium text-muted-foreground"
71
+ ),
72
+ children: getProviderName(provider)
73
+ }
74
+ )
75
+ ]
76
+ },
77
+ provider
78
+ );
79
+ }) });
80
+ }
81
+
82
+ // src/components/phone-input.tsx
83
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
84
+ function PhoneInput({
85
+ value,
86
+ onChange,
87
+ label = "Numero de telephone",
88
+ placeholder = "07 01 02 03 04",
89
+ error,
90
+ disabled,
91
+ className,
92
+ autoFocus
93
+ }) {
94
+ return /* @__PURE__ */ jsxs2("div", { className: cn("space-y-1.5", className), children: [
95
+ label && /* @__PURE__ */ jsx2("label", { className: "block text-sm font-semibold text-foreground", children: label }),
96
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
97
+ /* @__PURE__ */ jsx2("div", { className: "flex h-10 items-center rounded-md border border-border bg-muted px-3 text-sm font-medium text-muted-foreground", children: "+225" }),
98
+ /* @__PURE__ */ jsx2(
99
+ "input",
100
+ {
101
+ type: "tel",
102
+ inputMode: "numeric",
103
+ value,
104
+ onChange: (e) => {
105
+ const cleaned = e.target.value.replace(/[^\d\s]/g, "");
106
+ onChange(cleaned);
107
+ },
108
+ placeholder,
109
+ disabled,
110
+ autoFocus,
111
+ className: cn(
112
+ "h-10 w-full rounded-md border bg-background px-3 text-sm font-medium outline-none transition-colors",
113
+ "placeholder:text-muted-foreground/50",
114
+ "focus:border-primary focus:ring-2 focus:ring-primary/20",
115
+ "disabled:cursor-not-allowed disabled:opacity-50",
116
+ error ? "border-destructive" : "border-border"
117
+ )
118
+ }
119
+ )
120
+ ] }),
121
+ error && /* @__PURE__ */ jsx2("p", { className: "text-xs font-medium text-destructive", children: error })
122
+ ] });
123
+ }
124
+
125
+ // src/components/otp-input.tsx
126
+ import * as React from "react";
127
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
128
+ function OtpInput({
129
+ value,
130
+ onChange,
131
+ length = 4,
132
+ disabled,
133
+ className
134
+ }) {
135
+ const inputRefs = React.useRef([]);
136
+ const digits = value.padEnd(length, "").slice(0, length).split("");
137
+ const setDigit = (index, char) => {
138
+ const next = [...digits];
139
+ next[index] = char;
140
+ onChange(next.join(""));
141
+ };
142
+ const handleKeyDown = (index, e) => {
143
+ if (e.key === "Backspace") {
144
+ e.preventDefault();
145
+ if (digits[index]?.trim()) {
146
+ setDigit(index, "");
147
+ } else if (index > 0) {
148
+ setDigit(index - 1, "");
149
+ inputRefs.current[index - 1]?.focus();
150
+ }
151
+ } else if (e.key === "ArrowLeft" && index > 0) {
152
+ inputRefs.current[index - 1]?.focus();
153
+ } else if (e.key === "ArrowRight" && index < length - 1) {
154
+ inputRefs.current[index + 1]?.focus();
155
+ }
156
+ };
157
+ const handleInput = (index, e) => {
158
+ const val = e.target.value;
159
+ const char = val.replace(/\D/g, "").slice(-1);
160
+ if (!char) return;
161
+ setDigit(index, char);
162
+ if (index < length - 1) {
163
+ inputRefs.current[index + 1]?.focus();
164
+ }
165
+ };
166
+ const handlePaste = (e) => {
167
+ e.preventDefault();
168
+ const pasted = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, length);
169
+ if (!pasted) return;
170
+ onChange(pasted.padEnd(length, "").slice(0, length).replace(/ /g, ""));
171
+ const focusIndex = Math.min(pasted.length, length - 1);
172
+ inputRefs.current[focusIndex]?.focus();
173
+ };
174
+ return /* @__PURE__ */ jsxs3("div", { className: cn("space-y-2", className), children: [
175
+ /* @__PURE__ */ jsx3("label", { className: "block text-sm font-semibold text-muted-foreground", children: "Code OTP" }),
176
+ /* @__PURE__ */ jsx3("div", { className: "flex justify-center gap-3", onPaste: handlePaste, children: Array.from({ length }).map((_, i) => {
177
+ const filled = !!digits[i]?.trim();
178
+ return /* @__PURE__ */ jsx3(
179
+ "input",
180
+ {
181
+ ref: (el) => {
182
+ inputRefs.current[i] = el;
183
+ },
184
+ type: "text",
185
+ inputMode: "numeric",
186
+ maxLength: 1,
187
+ value: digits[i]?.trim() || "",
188
+ onKeyDown: (e) => handleKeyDown(i, e),
189
+ onInput: (e) => handleInput(i, e),
190
+ onFocus: (e) => e.target.select(),
191
+ disabled,
192
+ className: cn(
193
+ "h-13 w-13 rounded-md border-2 text-center text-xl font-bold outline-none transition-all",
194
+ "focus:border-primary focus:ring-2 focus:ring-primary/20 focus:scale-105",
195
+ "disabled:cursor-not-allowed disabled:opacity-50",
196
+ filled ? "border-primary/30 bg-primary/5" : "border-border bg-background"
197
+ )
198
+ },
199
+ i
200
+ );
201
+ }) }),
202
+ /* @__PURE__ */ jsxs3("p", { className: "text-center text-xs text-muted-foreground", children: [
203
+ "Faites ",
204
+ /* @__PURE__ */ jsx3("strong", { className: "text-foreground", children: "#144*82#" }),
205
+ " pour recevoir le code"
206
+ ] })
207
+ ] });
208
+ }
209
+
210
+ // src/components/transaction-status.tsx
211
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
212
+ function Spinner({ className }) {
213
+ return /* @__PURE__ */ jsxs4(
214
+ "svg",
215
+ {
216
+ className: cn("animate-spin", className),
217
+ width: "56",
218
+ height: "56",
219
+ viewBox: "0 0 56 56",
220
+ fill: "none",
221
+ children: [
222
+ /* @__PURE__ */ jsx4(
223
+ "circle",
224
+ {
225
+ cx: "28",
226
+ cy: "28",
227
+ r: "24",
228
+ stroke: "currentColor",
229
+ strokeWidth: "4",
230
+ className: "opacity-20"
231
+ }
232
+ ),
233
+ /* @__PURE__ */ jsx4(
234
+ "path",
235
+ {
236
+ d: "M28 4a24 24 0 0 1 24 24",
237
+ stroke: "currentColor",
238
+ strokeWidth: "4",
239
+ strokeLinecap: "round",
240
+ className: "text-primary"
241
+ }
242
+ )
243
+ ]
244
+ }
245
+ );
246
+ }
247
+ function TransactionStatus({
248
+ status,
249
+ elapsedSeconds = 0,
250
+ message,
251
+ errorMessage,
252
+ paymentUrl,
253
+ onRetry,
254
+ onClose,
255
+ onOpenPaymentUrl,
256
+ className
257
+ }) {
258
+ if (status === "polling") {
259
+ return /* @__PURE__ */ jsxs4("div", { className: cn("flex flex-col items-center gap-4 py-8", className), children: [
260
+ /* @__PURE__ */ jsx4(Spinner, {}),
261
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-2 text-center", children: [
262
+ /* @__PURE__ */ jsx4("p", { className: "text-base font-semibold text-foreground", children: paymentUrl ? "Finalisez le paiement dans l'onglet ouvert..." : "Confirmez le paiement sur votre telephone..." }),
263
+ /* @__PURE__ */ jsx4("p", { className: "text-sm text-muted-foreground", children: paymentUrl ? "Completez le paiement dans l'onglet, puis revenez ici." : "Veuillez valider la transaction sur votre telephone." }),
264
+ elapsedSeconds > 0 && /* @__PURE__ */ jsxs4("p", { className: "text-xs text-muted-foreground/70", children: [
265
+ "Verification en cours... ",
266
+ elapsedSeconds,
267
+ "s"
268
+ ] })
269
+ ] }),
270
+ paymentUrl && /* @__PURE__ */ jsxs4("div", { className: "flex flex-col items-center gap-3", children: [
271
+ /* @__PURE__ */ jsxs4(
272
+ "button",
273
+ {
274
+ type: "button",
275
+ onClick: onOpenPaymentUrl,
276
+ className: "inline-flex items-center gap-2 rounded-md bg-primary px-6 py-2.5 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90",
277
+ children: [
278
+ /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx4(
279
+ "path",
280
+ {
281
+ d: "M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3",
282
+ stroke: "currentColor",
283
+ strokeWidth: "2",
284
+ strokeLinecap: "round",
285
+ strokeLinejoin: "round"
286
+ }
287
+ ) }),
288
+ "Ouvrir Wave pour payer"
289
+ ]
290
+ }
291
+ ),
292
+ /* @__PURE__ */ jsx4(
293
+ "button",
294
+ {
295
+ type: "button",
296
+ onClick: onClose,
297
+ className: "text-sm text-muted-foreground underline hover:text-foreground",
298
+ children: "Annuler"
299
+ }
300
+ )
301
+ ] })
302
+ ] });
303
+ }
304
+ if (status === "success") {
305
+ return /* @__PURE__ */ jsxs4("div", { className: cn("flex flex-col items-center gap-4 py-8", className), children: [
306
+ /* @__PURE__ */ jsx4("div", { className: "flex h-16 w-16 items-center justify-center rounded-full bg-emerald-500 animate-in zoom-in duration-500", children: /* @__PURE__ */ jsx4("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx4(
307
+ "path",
308
+ {
309
+ d: "M5 12l5 5L20 7",
310
+ stroke: "#fff",
311
+ strokeWidth: "3",
312
+ strokeLinecap: "round",
313
+ strokeLinejoin: "round"
314
+ }
315
+ ) }) }),
316
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-2 text-center animate-in fade-in slide-in-from-bottom-4 duration-400 delay-150", children: [
317
+ /* @__PURE__ */ jsx4("p", { className: "text-lg font-bold text-foreground", children: "Paiement reussi !" }),
318
+ message && /* @__PURE__ */ jsx4("p", { className: "text-sm text-muted-foreground", children: message })
319
+ ] }),
320
+ onClose && /* @__PURE__ */ jsx4(
321
+ "button",
322
+ {
323
+ type: "button",
324
+ onClick: onClose,
325
+ className: "rounded-md bg-primary px-6 py-2.5 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90",
326
+ children: "Fermer"
327
+ }
328
+ )
329
+ ] });
330
+ }
331
+ return /* @__PURE__ */ jsxs4("div", { className: cn("flex flex-col items-center gap-4 py-8", className), children: [
332
+ /* @__PURE__ */ jsx4("div", { className: "flex h-16 w-16 items-center justify-center rounded-full bg-red-500 animate-in zoom-in duration-500", children: /* @__PURE__ */ jsx4("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx4(
333
+ "path",
334
+ {
335
+ d: "M6 18L18 6M6 6l12 12",
336
+ stroke: "#fff",
337
+ strokeWidth: "3",
338
+ strokeLinecap: "round"
339
+ }
340
+ ) }) }),
341
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-2 text-center animate-in fade-in slide-in-from-bottom-4 duration-400 delay-150", children: [
342
+ /* @__PURE__ */ jsx4("p", { className: "text-lg font-bold text-foreground", children: "Echec du paiement" }),
343
+ errorMessage && /* @__PURE__ */ jsx4("p", { className: "text-sm text-muted-foreground", children: errorMessage })
344
+ ] }),
345
+ /* @__PURE__ */ jsxs4("div", { className: "flex gap-3", children: [
346
+ onClose && /* @__PURE__ */ jsx4(
347
+ "button",
348
+ {
349
+ type: "button",
350
+ onClick: onClose,
351
+ className: "rounded-md border border-border bg-background px-5 py-2.5 text-sm font-semibold text-foreground transition-colors hover:bg-muted",
352
+ children: "Fermer"
353
+ }
354
+ ),
355
+ onRetry && /* @__PURE__ */ jsx4(
356
+ "button",
357
+ {
358
+ type: "button",
359
+ onClick: onRetry,
360
+ className: "rounded-md bg-primary px-5 py-2.5 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90",
361
+ children: "Reessayer"
362
+ }
363
+ )
364
+ ] })
365
+ ] });
366
+ }
367
+
368
+ // src/utils/format.ts
369
+ function formatCFA(n) {
370
+ return new Intl.NumberFormat("fr-FR").format(n);
371
+ }
372
+
373
+ // src/utils/validation.ts
374
+ function getPhoneRegex(provider) {
375
+ switch (provider) {
376
+ case "OM_CI":
377
+ return /^(\+225)(07)[0-9]{8}$/;
378
+ case "MTN_CI":
379
+ return /^(\+225)(05)[0-9]{8}$/;
380
+ case "MOOV_CI":
381
+ return /^(\+225)(01)[0-9]{8}$/;
382
+ default:
383
+ return /^(\+225)(07|05|01)[0-9]{8}$/;
384
+ }
385
+ }
386
+ function validatePhone(phone, provider) {
387
+ if (!phone) return "Num\xE9ro de t\xE9l\xE9phone requis";
388
+ const intl = phone.startsWith("+225") ? phone : `+225${phone}`;
389
+ const cleaned = intl.replace(/\s/g, "");
390
+ if (!getPhoneRegex(provider).test(cleaned)) {
391
+ return "Num\xE9ro invalide pour ce provider";
392
+ }
393
+ return null;
394
+ }
395
+ function normalizePhone(phone) {
396
+ const cleaned = phone.replace(/\s/g, "");
397
+ return cleaned.startsWith("+225") ? cleaned : `+225${cleaned}`;
398
+ }
399
+ function needsOtp(provider) {
400
+ return provider === "OM_CI";
401
+ }
402
+
403
+ // src/components/payment-modal.tsx
404
+ import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
405
+ var DEFAULT_PROVIDERS = [
406
+ "OM_CI",
407
+ "MTN_CI",
408
+ "MOOV_CI",
409
+ "WAVE_CI"
410
+ ];
411
+ function DuticotacPaymentModal({
412
+ open,
413
+ onClose,
414
+ onSuccess,
415
+ sdk,
416
+ amount,
417
+ providers = DEFAULT_PROVIDERS,
418
+ getTransactionId,
419
+ productReference,
420
+ initialPhone = "",
421
+ name: customerName,
422
+ email: customerEmail,
423
+ customerId,
424
+ kolaboReference,
425
+ app,
426
+ platform,
427
+ successMessage,
428
+ title = "Paiement",
429
+ pollTimeout = 9e4,
430
+ className,
431
+ renderModal
432
+ }) {
433
+ const [step, setStep] = React2.useState("form");
434
+ const [provider, setProvider] = React2.useState(providers[0]);
435
+ const [phone, setPhone] = React2.useState(initialPhone);
436
+ const [otp, setOtp] = React2.useState("");
437
+ const [phoneError, setPhoneError] = React2.useState(null);
438
+ const [errorMessage, setErrorMessage] = React2.useState("");
439
+ const [paymentUrl, setPaymentUrl] = React2.useState(null);
440
+ const [elapsedSeconds, setElapsedSeconds] = React2.useState(0);
441
+ const [lastTransaction, setLastTransaction] = React2.useState(null);
442
+ const abortRef = React2.useRef(null);
443
+ const elapsedRef = React2.useRef(null);
444
+ const cleanup = React2.useCallback(() => {
445
+ abortRef.current?.abort();
446
+ abortRef.current = null;
447
+ if (elapsedRef.current) {
448
+ clearInterval(elapsedRef.current);
449
+ elapsedRef.current = null;
450
+ }
451
+ }, []);
452
+ React2.useEffect(() => {
453
+ if (open) {
454
+ setStep("form");
455
+ setErrorMessage("");
456
+ setOtp("");
457
+ setPaymentUrl(null);
458
+ setElapsedSeconds(0);
459
+ setPhoneError(null);
460
+ setLastTransaction(null);
461
+ } else {
462
+ cleanup();
463
+ }
464
+ }, [open, cleanup]);
465
+ React2.useEffect(() => () => cleanup(), [cleanup]);
466
+ const isFormValid = phone.replace(/\s/g, "").length >= 8 && (needsOtp(provider) ? otp.length === 4 : true);
467
+ const handleSubmit = async () => {
468
+ const phoneErr = validatePhone(phone, provider);
469
+ if (phoneErr) {
470
+ setPhoneError(phoneErr);
471
+ return;
472
+ }
473
+ setPhoneError(null);
474
+ setStep("confirming");
475
+ setErrorMessage("");
476
+ cleanup();
477
+ const controller = new AbortController();
478
+ abortRef.current = controller;
479
+ try {
480
+ const transactionId = await getTransactionId();
481
+ const result = await sdk.mobileMoney.cashout({
482
+ transactionId,
483
+ ref: productReference,
484
+ phone: normalizePhone(phone),
485
+ name: customerName ?? "Duticotac App",
486
+ email: customerEmail ?? "",
487
+ paymentMethod: provider,
488
+ amount,
489
+ otp: needsOtp(provider) ? otp : void 0,
490
+ customerId,
491
+ kolaboReference,
492
+ app,
493
+ plateform: platform
494
+ });
495
+ const tx = result.data;
496
+ if (tx?.paymentUrl) {
497
+ setPaymentUrl(tx.paymentUrl);
498
+ try {
499
+ window.open(tx.paymentUrl, "_blank");
500
+ } catch {
501
+ }
502
+ }
503
+ const startTime = Date.now();
504
+ setElapsedSeconds(0);
505
+ elapsedRef.current = setInterval(() => {
506
+ setElapsedSeconds(Math.floor((Date.now() - startTime) / 1e3));
507
+ }, 1e3);
508
+ const confirmed = await sdk.transaction.poll(tx.id ?? transactionId, {
509
+ timeoutMs: pollTimeout,
510
+ signal: controller.signal,
511
+ onPoll: () => {
512
+ }
513
+ });
514
+ setLastTransaction(confirmed.data);
515
+ setStep("success");
516
+ onSuccess?.(confirmed.data);
517
+ } catch (e) {
518
+ if (controller.signal.aborted) return;
519
+ if (e instanceof DuticotacError) {
520
+ setErrorMessage(e.message);
521
+ } else {
522
+ setErrorMessage(
523
+ getErrorMessage("unknown-error")
524
+ );
525
+ }
526
+ setStep("error");
527
+ }
528
+ };
529
+ const handleRetry = () => {
530
+ setStep("form");
531
+ setErrorMessage("");
532
+ setPaymentUrl(null);
533
+ setElapsedSeconds(0);
534
+ };
535
+ const content = /* @__PURE__ */ jsxs5("div", { className: cn("space-y-5", className), children: [
536
+ step === "form" && /* @__PURE__ */ jsxs5(Fragment, { children: [
537
+ /* @__PURE__ */ jsxs5("div", { className: "rounded-lg border-l-4 border-l-primary bg-muted/50 p-4", children: [
538
+ /* @__PURE__ */ jsx5("p", { className: "text-xs text-muted-foreground", children: "Montant" }),
539
+ /* @__PURE__ */ jsxs5("p", { className: "text-xl font-bold text-primary", children: [
540
+ formatCFA(amount),
541
+ " FCFA"
542
+ ] })
543
+ ] }),
544
+ /* @__PURE__ */ jsxs5("div", { className: "space-y-2", children: [
545
+ /* @__PURE__ */ jsx5("p", { className: "text-sm font-semibold text-foreground", children: "Moyen de paiement" }),
546
+ /* @__PURE__ */ jsx5(
547
+ ProviderSelector,
548
+ {
549
+ providers,
550
+ value: provider,
551
+ onChange: (p) => {
552
+ setProvider(p);
553
+ if (!needsOtp(p)) setOtp("");
554
+ }
555
+ }
556
+ )
557
+ ] }),
558
+ /* @__PURE__ */ jsx5(
559
+ PhoneInput,
560
+ {
561
+ value: phone,
562
+ onChange: (v) => {
563
+ setPhone(v);
564
+ setPhoneError(null);
565
+ },
566
+ error: phoneError,
567
+ autoFocus: true
568
+ }
569
+ ),
570
+ needsOtp(provider) && /* @__PURE__ */ jsx5(OtpInput, { value: otp, onChange: setOtp }),
571
+ /* @__PURE__ */ jsxs5("div", { className: "flex justify-end gap-3 pt-2", children: [
572
+ /* @__PURE__ */ jsx5(
573
+ "button",
574
+ {
575
+ type: "button",
576
+ onClick: onClose,
577
+ className: "rounded-md border border-border bg-background px-5 py-2.5 text-sm font-semibold text-foreground transition-colors hover:bg-muted",
578
+ children: "Annuler"
579
+ }
580
+ ),
581
+ /* @__PURE__ */ jsxs5(
582
+ "button",
583
+ {
584
+ type: "button",
585
+ onClick: handleSubmit,
586
+ disabled: !isFormValid,
587
+ className: "rounded-md bg-primary px-5 py-2.5 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:pointer-events-none disabled:opacity-50",
588
+ children: [
589
+ "Payer ",
590
+ formatCFA(amount),
591
+ " FCFA"
592
+ ]
593
+ }
594
+ )
595
+ ] })
596
+ ] }),
597
+ step === "confirming" && /* @__PURE__ */ jsx5(
598
+ TransactionStatus,
599
+ {
600
+ status: "polling",
601
+ elapsedSeconds,
602
+ paymentUrl,
603
+ onOpenPaymentUrl: () => {
604
+ if (paymentUrl) window.open(paymentUrl, "_blank");
605
+ },
606
+ onClose
607
+ }
608
+ ),
609
+ step === "success" && /* @__PURE__ */ jsx5(
610
+ TransactionStatus,
611
+ {
612
+ status: "success",
613
+ message: successMessage ?? "Le paiement a ete effectue avec succes.",
614
+ onClose
615
+ }
616
+ ),
617
+ step === "error" && /* @__PURE__ */ jsx5(
618
+ TransactionStatus,
619
+ {
620
+ status: "error",
621
+ errorMessage,
622
+ onRetry: handleRetry,
623
+ onClose
624
+ }
625
+ )
626
+ ] });
627
+ if (renderModal) {
628
+ return /* @__PURE__ */ jsx5(Fragment, { children: renderModal({ open, onClose, title, children: content }) });
629
+ }
630
+ if (!open) return null;
631
+ return /* @__PURE__ */ jsxs5("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
632
+ /* @__PURE__ */ jsx5(
633
+ "div",
634
+ {
635
+ className: "absolute inset-0 bg-black/50",
636
+ onClick: onClose
637
+ }
638
+ ),
639
+ /* @__PURE__ */ jsxs5("div", { className: "relative z-10 w-full max-w-md rounded-xl border border-border bg-background p-6 shadow-xl", children: [
640
+ /* @__PURE__ */ jsxs5("div", { className: "mb-4 flex items-center justify-between", children: [
641
+ /* @__PURE__ */ jsx5("h2", { className: "text-lg font-bold text-foreground", children: title }),
642
+ /* @__PURE__ */ jsx5(
643
+ "button",
644
+ {
645
+ type: "button",
646
+ onClick: onClose,
647
+ className: "rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground",
648
+ children: /* @__PURE__ */ jsx5("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx5(
649
+ "path",
650
+ {
651
+ d: "M6 18L18 6M6 6l12 12",
652
+ stroke: "currentColor",
653
+ strokeWidth: "2",
654
+ strokeLinecap: "round"
655
+ }
656
+ ) })
657
+ }
658
+ )
659
+ ] }),
660
+ content
661
+ ] })
662
+ ] });
663
+ }
664
+ export {
665
+ DuticotacPaymentModal,
666
+ OtpInput,
667
+ PhoneInput,
668
+ ProviderSelector,
669
+ TransactionStatus,
670
+ cn,
671
+ formatCFA,
672
+ getPhoneRegex,
673
+ needsOtp,
674
+ normalizePhone,
675
+ validatePhone
676
+ };
677
+ //# sourceMappingURL=index.mjs.map