tsapay-checkout-js 1.0.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.
@@ -0,0 +1,81 @@
1
+ /**
2
+ * TsaPay Checkout — Drop-in payment modal for any website.
3
+ *
4
+ * Usage (vanilla):
5
+ * <script src="https://cdn.tsapay.com/checkout.js"></script>
6
+ * <script>
7
+ * const checkout = new TsaPayCheckout('pk_test_xxx');
8
+ * checkout.open({ amount: 5000 });
9
+ * </script>
10
+ *
11
+ * Usage (npm / ES module):
12
+ * import { TsaPayCheckout } from '@tsapay/checkout';
13
+ * const checkout = new TsaPayCheckout('pk_test_xxx');
14
+ */
15
+ export interface CheckoutOptions {
16
+ /** Amount in the smallest currency unit (e.g. 5000 for 5 000 XAF). */
17
+ amount: number;
18
+ /** ISO currency code. Default: "XAF". */
19
+ currency?: string;
20
+ /** Short description shown in the modal header. */
21
+ description?: string;
22
+ /** Your internal order reference. */
23
+ reference?: string;
24
+ /** URL to receive webhook after payment. */
25
+ callbackUrl?: string;
26
+ /** Arbitrary metadata to attach to the payment. */
27
+ metadata?: Record<string, string>;
28
+ /** Pre-fill the phone number field. */
29
+ phoneNumber?: string;
30
+ /** Pre-select a provider: "mtn_momo" | "orange_money". */
31
+ provider?: "mtn_momo" | "orange_money";
32
+ /** Called when payment succeeds. */
33
+ onSuccess?: (payment: PaymentResult) => void;
34
+ /** Called when payment fails or is cancelled. */
35
+ onError?: (error: {
36
+ code: string;
37
+ message: string;
38
+ }) => void;
39
+ /** Called when the user closes the modal. */
40
+ onClose?: () => void;
41
+ }
42
+ export interface PaymentResult {
43
+ id: string;
44
+ status: string;
45
+ amount: number;
46
+ currency: string;
47
+ provider: string;
48
+ reference: string;
49
+ }
50
+ export declare class TsaPayCheckout {
51
+ private apiKey;
52
+ private baseUrl;
53
+ private overlay;
54
+ private styleEl;
55
+ private currentOpts;
56
+ private pollTimer;
57
+ /**
58
+ * Create a new TsaPayCheckout instance.
59
+ * @param apiKey Your TsaPay API key (sk_test_... or sk_live_...).
60
+ * @param baseUrl Optional. Override the API base URL.
61
+ */
62
+ constructor(apiKey: string, baseUrl?: string);
63
+ /**
64
+ * Open the checkout modal with the given options.
65
+ */
66
+ open(opts: CheckoutOptions): void;
67
+ /**
68
+ * Programmatically close the checkout modal.
69
+ */
70
+ close(): void;
71
+ private injectCSS;
72
+ private formatAmount;
73
+ private destroy;
74
+ private createPayment;
75
+ private fetchPayment;
76
+ private renderForm;
77
+ private handleSubmit;
78
+ private renderWaiting;
79
+ private renderSuccess;
80
+ private renderError;
81
+ }
@@ -0,0 +1,513 @@
1
+ /**
2
+ * TsaPay Checkout — Drop-in payment modal for any website.
3
+ *
4
+ * Usage (vanilla):
5
+ * <script src="https://cdn.tsapay.com/checkout.js"></script>
6
+ * <script>
7
+ * const checkout = new TsaPayCheckout('pk_test_xxx');
8
+ * checkout.open({ amount: 5000 });
9
+ * </script>
10
+ *
11
+ * Usage (npm / ES module):
12
+ * import { TsaPayCheckout } from '@tsapay/checkout';
13
+ * const checkout = new TsaPayCheckout('pk_test_xxx');
14
+ */
15
+ // ─────────────────────────────────────────────
16
+ // Inline CSS (injected once)
17
+ // ─────────────────────────────────────────────
18
+ const CHECKOUT_CSS = `
19
+ /* ── TsaPay Checkout Overlay ───────────────── */
20
+ .tsapay-overlay {
21
+ position: fixed; inset: 0; z-index: 999999;
22
+ background: rgba(0,0,0,.55);
23
+ backdrop-filter: blur(6px);
24
+ display: flex; align-items: center; justify-content: center;
25
+ opacity: 0; transition: opacity .3s ease;
26
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
27
+ }
28
+ .tsapay-overlay.tsapay-visible { opacity: 1; }
29
+
30
+ /* ── Modal Card ────────────────────────────── */
31
+ .tsapay-modal {
32
+ background: #fff; border-radius: 20px;
33
+ width: 420px; max-width: 94vw; max-height: 96vh;
34
+ overflow-y: auto; box-shadow: 0 25px 60px rgba(0,0,0,.35);
35
+ transform: translateY(30px) scale(.96);
36
+ transition: transform .35s cubic-bezier(.22,1,.36,1);
37
+ }
38
+ .tsapay-overlay.tsapay-visible .tsapay-modal {
39
+ transform: translateY(0) scale(1);
40
+ }
41
+
42
+ /* ── Header ────────────────────────────────── */
43
+ .tsapay-header {
44
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
45
+ padding: 28px 28px 22px; border-radius: 20px 20px 0 0;
46
+ position: relative;
47
+ }
48
+ .tsapay-header-brand {
49
+ display: flex; align-items: center; gap: 10px; margin-bottom: 16px;
50
+ }
51
+ .tsapay-logo {
52
+ width: 32px; height: 32px; border-radius: 8px;
53
+ background: linear-gradient(135deg, #eab308, #22c55e);
54
+ display: flex; align-items: center; justify-content: center;
55
+ font-weight: 800; font-size: 14px; color: #0f172a;
56
+ }
57
+ .tsapay-brand-name {
58
+ font-size: 16px; font-weight: 700; color: #f1f5f9; letter-spacing: -.3px;
59
+ }
60
+ .tsapay-close-btn {
61
+ position: absolute; top: 18px; right: 18px;
62
+ width: 32px; height: 32px; border-radius: 50%;
63
+ background: rgba(255,255,255,.1); border: none;
64
+ color: #94a3b8; font-size: 18px; cursor: pointer;
65
+ display: flex; align-items: center; justify-content: center;
66
+ transition: background .2s, color .2s;
67
+ }
68
+ .tsapay-close-btn:hover { background: rgba(255,255,255,.2); color: #fff; }
69
+ .tsapay-amount {
70
+ font-size: 36px; font-weight: 800; color: #fff; letter-spacing: -1px;
71
+ }
72
+ .tsapay-amount-currency {
73
+ font-size: 16px; font-weight: 500; color: #94a3b8; margin-left: 6px;
74
+ }
75
+ .tsapay-description {
76
+ font-size: 13px; color: #94a3b8; margin-top: 4px;
77
+ }
78
+
79
+ /* ── Body ──────────────────────────────────── */
80
+ .tsapay-body { padding: 28px; }
81
+
82
+ .tsapay-field { margin-bottom: 20px; }
83
+ .tsapay-label {
84
+ display: block; font-size: 13px; font-weight: 600;
85
+ color: #334155; margin-bottom: 8px;
86
+ }
87
+ .tsapay-input {
88
+ width: 100%; padding: 14px 16px; border-radius: 12px;
89
+ border: 1.5px solid #e2e8f0; font-size: 15px; color: #0f172a;
90
+ outline: none; transition: border-color .2s, box-shadow .2s;
91
+ box-sizing: border-box; font-family: inherit;
92
+ }
93
+ .tsapay-input:focus {
94
+ border-color: #22c55e; box-shadow: 0 0 0 3px rgba(34,197,94,.15);
95
+ }
96
+ .tsapay-input::placeholder { color: #94a3b8; }
97
+
98
+ /* ── Provider Selector ─────────────────────── */
99
+ .tsapay-providers { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
100
+ .tsapay-provider {
101
+ position: relative; cursor: pointer;
102
+ }
103
+ .tsapay-provider input { position: absolute; opacity: 0; pointer-events: none; }
104
+ .tsapay-provider-card {
105
+ padding: 16px; border-radius: 14px;
106
+ border: 2px solid #e2e8f0; text-align: center;
107
+ transition: all .2s ease; background: #fff;
108
+ }
109
+ .tsapay-provider-card:hover { border-color: #cbd5e1; background: #f8fafc; }
110
+ .tsapay-provider input:checked + .tsapay-provider-card {
111
+ border-color: #22c55e; background: #f0fdf4;
112
+ }
113
+ .tsapay-provider-icon {
114
+ font-size: 28px; margin-bottom: 6px; display: block;
115
+ }
116
+ .tsapay-provider-name {
117
+ font-size: 13px; font-weight: 700; color: #0f172a;
118
+ }
119
+ .tsapay-provider-sub {
120
+ font-size: 11px; color: #64748b; margin-top: 2px;
121
+ }
122
+
123
+ /* ── Submit Button ─────────────────────────── */
124
+ .tsapay-submit {
125
+ width: 100%; padding: 16px; border-radius: 14px;
126
+ border: none; cursor: pointer; font-size: 15px;
127
+ font-weight: 700; color: #fff; font-family: inherit;
128
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
129
+ transition: all .25s ease; display: flex;
130
+ align-items: center; justify-content: center; gap: 8px;
131
+ margin-top: 8px;
132
+ }
133
+ .tsapay-submit:hover { transform: translateY(-1px); box-shadow: 0 8px 24px rgba(15,23,42,.3); }
134
+ .tsapay-submit:active { transform: translateY(0); }
135
+ .tsapay-submit:disabled {
136
+ opacity: .6; cursor: not-allowed; transform: none !important;
137
+ box-shadow: none !important;
138
+ }
139
+
140
+ /* ── Spinner ───────────────────────────────── */
141
+ .tsapay-spinner {
142
+ width: 20px; height: 20px; border: 2.5px solid rgba(255,255,255,.3);
143
+ border-top-color: #fff; border-radius: 50%;
144
+ animation: tsapay-spin .7s linear infinite;
145
+ }
146
+ @keyframes tsapay-spin { to { transform: rotate(360deg); } }
147
+
148
+ /* ── Footer ────────────────────────────────── */
149
+ .tsapay-footer {
150
+ text-align: center; padding: 16px 28px 22px;
151
+ border-top: 1px solid #f1f5f9;
152
+ }
153
+ .tsapay-footer-text {
154
+ font-size: 11px; color: #94a3b8;
155
+ }
156
+ .tsapay-footer-text a { color: #22c55e; text-decoration: none; font-weight: 600; }
157
+
158
+ /* ── Result screens ────────────────────────── */
159
+ .tsapay-result {
160
+ text-align: center; padding: 48px 28px;
161
+ }
162
+ .tsapay-result-icon {
163
+ width: 72px; height: 72px; border-radius: 50%;
164
+ display: flex; align-items: center; justify-content: center;
165
+ margin: 0 auto 20px; font-size: 36px;
166
+ animation: tsapay-pop .5s cubic-bezier(.22,1,.36,1);
167
+ }
168
+ @keyframes tsapay-pop {
169
+ 0% { transform: scale(0); } 60% { transform: scale(1.15); } 100% { transform: scale(1); }
170
+ }
171
+ .tsapay-result-icon.success { background: #dcfce7; color: #16a34a; }
172
+ .tsapay-result-icon.error { background: #fee2e2; color: #dc2626; }
173
+ .tsapay-result-icon.pending { background: #dbeafe; color: #2563eb; }
174
+ .tsapay-result-title {
175
+ font-size: 20px; font-weight: 800; color: #0f172a; margin-bottom: 8px;
176
+ }
177
+ .tsapay-result-msg {
178
+ font-size: 14px; color: #64748b; margin-bottom: 28px; line-height: 1.6;
179
+ }
180
+ .tsapay-result-btn {
181
+ padding: 14px 32px; border-radius: 12px; border: none;
182
+ font-size: 14px; font-weight: 700; cursor: pointer;
183
+ font-family: inherit; transition: all .2s;
184
+ }
185
+ .tsapay-result-btn.primary {
186
+ background: #0f172a; color: #fff;
187
+ }
188
+ .tsapay-result-btn.primary:hover { background: #1e293b; }
189
+
190
+ /* ── Waiting / polling screen ──────────────── */
191
+ .tsapay-waiting {
192
+ text-align: center; padding: 48px 28px;
193
+ }
194
+ .tsapay-waiting-animation {
195
+ width: 80px; height: 80px; margin: 0 auto 24px; position: relative;
196
+ }
197
+ .tsapay-phone-icon {
198
+ font-size: 48px; animation: tsapay-ring 1.5s ease-in-out infinite;
199
+ }
200
+ @keyframes tsapay-ring {
201
+ 0%, 100% { transform: rotate(0deg); }
202
+ 15% { transform: rotate(12deg); }
203
+ 30% { transform: rotate(-12deg); }
204
+ 45% { transform: rotate(8deg); }
205
+ 60% { transform: rotate(-8deg); }
206
+ 75% { transform: rotate(0deg); }
207
+ }
208
+ .tsapay-waiting-title {
209
+ font-size: 18px; font-weight: 800; color: #0f172a; margin-bottom: 8px;
210
+ }
211
+ .tsapay-waiting-msg {
212
+ font-size: 13px; color: #64748b; line-height: 1.7;
213
+ }
214
+ .tsapay-waiting-dots::after {
215
+ content: ''; animation: tsapay-dots 1.5s steps(4, end) infinite;
216
+ }
217
+ @keyframes tsapay-dots {
218
+ 0% { content: ''; }
219
+ 25% { content: '.'; }
220
+ 50% { content: '..'; }
221
+ 75% { content: '...'; }
222
+ }
223
+ `;
224
+ // ─────────────────────────────────────────────
225
+ // Main Class
226
+ // ─────────────────────────────────────────────
227
+ export class TsaPayCheckout {
228
+ /**
229
+ * Create a new TsaPayCheckout instance.
230
+ * @param apiKey Your TsaPay API key (sk_test_... or sk_live_...).
231
+ * @param baseUrl Optional. Override the API base URL.
232
+ */
233
+ constructor(apiKey, baseUrl) {
234
+ this.overlay = null;
235
+ this.styleEl = null;
236
+ this.currentOpts = null;
237
+ this.pollTimer = null;
238
+ if (!apiKey)
239
+ throw new Error("[TsaPay] An API key is required.");
240
+ this.apiKey = apiKey;
241
+ this.baseUrl = (baseUrl || "https://api.tsapay.com/v1").replace(/\/+$/, "");
242
+ }
243
+ // ── Public API ────────────────────────────
244
+ /**
245
+ * Open the checkout modal with the given options.
246
+ */
247
+ open(opts) {
248
+ this.currentOpts = opts;
249
+ this.injectCSS();
250
+ this.renderForm(opts);
251
+ }
252
+ /**
253
+ * Programmatically close the checkout modal.
254
+ */
255
+ close() {
256
+ this.destroy();
257
+ this.currentOpts?.onClose?.();
258
+ }
259
+ // ── CSS Injection ─────────────────────────
260
+ injectCSS() {
261
+ if (document.getElementById("tsapay-checkout-css"))
262
+ return;
263
+ const style = document.createElement("style");
264
+ style.id = "tsapay-checkout-css";
265
+ style.textContent = CHECKOUT_CSS;
266
+ document.head.appendChild(style);
267
+ this.styleEl = style;
268
+ }
269
+ // ── Helpers ───────────────────────────────
270
+ formatAmount(amount) {
271
+ return new Intl.NumberFormat("fr-FR").format(amount);
272
+ }
273
+ destroy() {
274
+ if (this.pollTimer) {
275
+ clearInterval(this.pollTimer);
276
+ this.pollTimer = null;
277
+ }
278
+ if (this.overlay) {
279
+ this.overlay.classList.remove("tsapay-visible");
280
+ setTimeout(() => {
281
+ this.overlay?.remove();
282
+ this.overlay = null;
283
+ }, 300);
284
+ }
285
+ }
286
+ // ── API Calls ─────────────────────────────
287
+ async createPayment(phone, provider) {
288
+ const opts = this.currentOpts;
289
+ const res = await fetch(`${this.baseUrl}/payments`, {
290
+ method: "POST",
291
+ headers: {
292
+ "Authorization": `Bearer ${this.apiKey}`,
293
+ "Content-Type": "application/json",
294
+ "Idempotency-Key": crypto.randomUUID(),
295
+ },
296
+ body: JSON.stringify({
297
+ amount: opts.amount,
298
+ currency: opts.currency || "XAF",
299
+ provider,
300
+ phone_number: phone,
301
+ description: opts.description || "",
302
+ reference: opts.reference || "",
303
+ callback_url: opts.callbackUrl || "",
304
+ metadata: opts.metadata || {},
305
+ }),
306
+ });
307
+ if (!res.ok) {
308
+ const err = await res.json().catch(() => ({ message: "Network error" }));
309
+ throw new Error(err.message || `HTTP ${res.status}`);
310
+ }
311
+ return res.json();
312
+ }
313
+ async fetchPayment(paymentId) {
314
+ const res = await fetch(`${this.baseUrl}/payments/${paymentId}`, {
315
+ headers: { "Authorization": `Bearer ${this.apiKey}` },
316
+ });
317
+ return res.json();
318
+ }
319
+ // ── Render: Form ──────────────────────────
320
+ renderForm(opts) {
321
+ const currency = opts.currency || "XAF";
322
+ const overlay = document.createElement("div");
323
+ overlay.className = "tsapay-overlay";
324
+ overlay.innerHTML = `
325
+ <div class="tsapay-modal">
326
+ <div class="tsapay-header">
327
+ <div class="tsapay-header-brand">
328
+ <div class="tsapay-logo">T</div>
329
+ <span class="tsapay-brand-name">TsaPay</span>
330
+ </div>
331
+ <button class="tsapay-close-btn" aria-label="Fermer">&times;</button>
332
+ <div class="tsapay-amount">${this.formatAmount(opts.amount)}<span class="tsapay-amount-currency">${currency}</span></div>
333
+ ${opts.description ? `<div class="tsapay-description">${opts.description}</div>` : ""}
334
+ </div>
335
+
336
+ <div class="tsapay-body">
337
+ <div class="tsapay-field">
338
+ <label class="tsapay-label">Numéro de téléphone</label>
339
+ <input class="tsapay-input" id="tsapay-phone" type="tel"
340
+ placeholder="+237 6XX XXX XXX" value="${opts.phoneNumber || ""}" required />
341
+ </div>
342
+
343
+ <div class="tsapay-field">
344
+ <label class="tsapay-label">Moyen de paiement</label>
345
+ <div class="tsapay-providers">
346
+ <label class="tsapay-provider">
347
+ <input type="radio" name="tsapay-provider" value="mtn_momo" ${(!opts.provider || opts.provider === "mtn_momo") ? "checked" : ""} />
348
+ <div class="tsapay-provider-card">
349
+ <span class="tsapay-provider-icon">🟡</span>
350
+ <div class="tsapay-provider-name">MTN MoMo</div>
351
+ <div class="tsapay-provider-sub">Mobile Money</div>
352
+ </div>
353
+ </label>
354
+ <label class="tsapay-provider">
355
+ <input type="radio" name="tsapay-provider" value="orange_money" ${opts.provider === "orange_money" ? "checked" : ""} />
356
+ <div class="tsapay-provider-card">
357
+ <span class="tsapay-provider-icon">🟠</span>
358
+ <div class="tsapay-provider-name">Orange Money</div>
359
+ <div class="tsapay-provider-sub">Mobile Money</div>
360
+ </div>
361
+ </label>
362
+ </div>
363
+ </div>
364
+
365
+ <button class="tsapay-submit" id="tsapay-submit-btn">
366
+ Payer ${this.formatAmount(opts.amount)} ${currency}
367
+ </button>
368
+ </div>
369
+
370
+ <div class="tsapay-footer">
371
+ <span class="tsapay-footer-text">🔒 Paiement sécurisé par <a href="https://tsapay.com" target="_blank">TsaPay</a></span>
372
+ </div>
373
+ </div>
374
+ `;
375
+ document.body.appendChild(overlay);
376
+ this.overlay = overlay;
377
+ // Animate in
378
+ requestAnimationFrame(() => overlay.classList.add("tsapay-visible"));
379
+ // Close on overlay click or close button
380
+ overlay.querySelector(".tsapay-close-btn").addEventListener("click", () => this.close());
381
+ overlay.addEventListener("click", (e) => {
382
+ if (e.target === overlay)
383
+ this.close();
384
+ });
385
+ // Submit handler
386
+ overlay.querySelector("#tsapay-submit-btn").addEventListener("click", () => this.handleSubmit());
387
+ }
388
+ // ── Submit ────────────────────────────────
389
+ async handleSubmit() {
390
+ const phoneInput = this.overlay.querySelector("#tsapay-phone");
391
+ const providerInput = this.overlay.querySelector('input[name="tsapay-provider"]:checked');
392
+ const btn = this.overlay.querySelector("#tsapay-submit-btn");
393
+ const phone = phoneInput.value.trim();
394
+ if (!phone) {
395
+ phoneInput.style.borderColor = "#ef4444";
396
+ phoneInput.focus();
397
+ return;
398
+ }
399
+ // Loading state
400
+ btn.disabled = true;
401
+ btn.innerHTML = `<span class="tsapay-spinner"></span> Traitement en cours…`;
402
+ try {
403
+ const data = await this.createPayment(phone, providerInput.value);
404
+ const paymentId = data.payment?.id || data.id;
405
+ if (paymentId) {
406
+ this.renderWaiting(paymentId, providerInput.value);
407
+ }
408
+ else {
409
+ this.renderSuccess(data.payment || data);
410
+ }
411
+ }
412
+ catch (err) {
413
+ this.renderError(err.message || "Une erreur est survenue.");
414
+ }
415
+ }
416
+ // ── Render: Waiting (USSD push) ───────────
417
+ renderWaiting(paymentId, provider) {
418
+ const modal = this.overlay.querySelector(".tsapay-modal");
419
+ const providerLabel = provider === "mtn_momo" ? "MTN MoMo" : "Orange Money";
420
+ modal.innerHTML = `
421
+ <div class="tsapay-header">
422
+ <div class="tsapay-header-brand">
423
+ <div class="tsapay-logo">T</div>
424
+ <span class="tsapay-brand-name">TsaPay</span>
425
+ </div>
426
+ <button class="tsapay-close-btn" aria-label="Fermer">&times;</button>
427
+ </div>
428
+ <div class="tsapay-waiting">
429
+ <div class="tsapay-waiting-animation">
430
+ <span class="tsapay-phone-icon">📱</span>
431
+ </div>
432
+ <div class="tsapay-waiting-title">Vérifiez votre téléphone</div>
433
+ <div class="tsapay-waiting-msg">
434
+ Un message USSD ${providerLabel} a été envoyé sur votre téléphone.<br/>
435
+ Composez votre code PIN pour confirmer le paiement<span class="tsapay-waiting-dots"></span>
436
+ </div>
437
+ </div>
438
+ <div class="tsapay-footer">
439
+ <span class="tsapay-footer-text">🔒 Paiement sécurisé par <a href="https://tsapay.com" target="_blank">TsaPay</a></span>
440
+ </div>
441
+ `;
442
+ modal.querySelector(".tsapay-close-btn").addEventListener("click", () => this.close());
443
+ // Poll for status every 3s
444
+ let attempts = 0;
445
+ const maxAttempts = 40; // ~2 minutes
446
+ this.pollTimer = window.setInterval(async () => {
447
+ attempts++;
448
+ try {
449
+ const data = await this.fetchPayment(paymentId);
450
+ const status = data.payment?.status || data.status;
451
+ if (status === "success") {
452
+ clearInterval(this.pollTimer);
453
+ this.pollTimer = null;
454
+ const result = data.payment || data;
455
+ this.renderSuccess(result);
456
+ this.currentOpts?.onSuccess?.(result);
457
+ }
458
+ else if (status === "failed" || status === "expired") {
459
+ clearInterval(this.pollTimer);
460
+ this.pollTimer = null;
461
+ const reason = data.payment?.failure_reason || "Le paiement a échoué.";
462
+ this.renderError(reason);
463
+ this.currentOpts?.onError?.({ code: status, message: reason });
464
+ }
465
+ else if (attempts >= maxAttempts) {
466
+ clearInterval(this.pollTimer);
467
+ this.pollTimer = null;
468
+ this.renderError("Délai d'attente dépassé. Veuillez réessayer.");
469
+ this.currentOpts?.onError?.({ code: "timeout", message: "Payment timeout" });
470
+ }
471
+ }
472
+ catch {
473
+ // Silently retry on network error
474
+ }
475
+ }, 3000);
476
+ }
477
+ // ── Render: Success ───────────────────────
478
+ renderSuccess(payment) {
479
+ const modal = this.overlay.querySelector(".tsapay-modal");
480
+ modal.innerHTML = `
481
+ <div class="tsapay-result">
482
+ <div class="tsapay-result-icon success">✓</div>
483
+ <div class="tsapay-result-title">Paiement réussi !</div>
484
+ <div class="tsapay-result-msg">
485
+ Votre paiement de <strong>${this.formatAmount(payment.amount || this.currentOpts?.amount || 0)} ${payment.currency || "XAF"}</strong> a été confirmé.<br/>
486
+ ${payment.reference ? `Référence : <strong>${payment.reference}</strong>` : ""}
487
+ </div>
488
+ <button class="tsapay-result-btn primary" id="tsapay-done-btn">Terminé</button>
489
+ </div>
490
+ `;
491
+ modal.querySelector("#tsapay-done-btn").addEventListener("click", () => this.close());
492
+ }
493
+ // ── Render: Error ─────────────────────────
494
+ renderError(message) {
495
+ const modal = this.overlay.querySelector(".tsapay-modal");
496
+ modal.innerHTML = `
497
+ <div class="tsapay-result">
498
+ <div class="tsapay-result-icon error">✕</div>
499
+ <div class="tsapay-result-title">Paiement échoué</div>
500
+ <div class="tsapay-result-msg">${message}</div>
501
+ <button class="tsapay-result-btn primary" id="tsapay-retry-btn">Réessayer</button>
502
+ </div>
503
+ `;
504
+ modal.querySelector("#tsapay-retry-btn").addEventListener("click", () => {
505
+ if (this.currentOpts)
506
+ this.renderForm(this.currentOpts);
507
+ });
508
+ }
509
+ }
510
+ // ── Auto-register on window for <script> usage ──
511
+ if (typeof window !== "undefined") {
512
+ window.TsaPayCheckout = TsaPayCheckout;
513
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "tsapay-checkout-js",
3
+ "version": "1.0.0",
4
+ "description": "TsaPay Drop-in Checkout UI — Beautiful payment modal for any website or JS framework",
5
+ "main": "dist/checkout.js",
6
+ "module": "dist/checkout.esm.js",
7
+ "types": "dist/checkout.d.ts",
8
+ "files": ["dist"],
9
+ "scripts": {
10
+ "build": "tsc && node scripts/bundle.js",
11
+ "prepublishOnly": "npm run build"
12
+ },
13
+ "keywords": ["tsapay", "checkout", "mobile-money", "payment-ui", "mtn", "orange-money", "drop-in", "modal"],
14
+ "author": "TsaPay",
15
+ "license": "MIT",
16
+ "devDependencies": {
17
+ "@types/node": "^25.9.1",
18
+ "typescript": "^6.0.3"
19
+ }
20
+ }