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