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.
- package/README.md +171 -0
- package/dist/checkout.d.ts +48 -0
- package/dist/checkout.esm.js +312 -0
- package/dist/checkout.iife.js +313 -0
- package/dist/checkout.js +313 -0
- package/dist/src/checkout.d.ts +81 -0
- package/dist/src/checkout.js +513 -0
- package/package.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# tsapay-checkout-js
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
Beautiful, drop-in payment modal for **TsaPay Mobile Money**. Works with **any** website or JavaScript framework (React, Angular, Vue, Svelte, vanilla JSβ¦).
|
|
7
|
+
|
|
8
|
+
 
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## β¨ Features
|
|
13
|
+
|
|
14
|
+
- π¨ **Beautiful UI** β Professionally designed modal with smooth animations
|
|
15
|
+
- π **Zero dependencies** β No React, no Angular, no jQuery. Works everywhere.
|
|
16
|
+
- π± **Mobile-first** β Responsive design, works on all screen sizes
|
|
17
|
+
- β³ **Built-in polling** β Automatically checks payment status while the user confirms on their phone
|
|
18
|
+
- π **Secure** β Your API key is used server-side or via HTTPS; the checkout never stores sensitive data
|
|
19
|
+
- π **Framework-agnostic** β Works with React, Angular, Vue, Svelte, Next.js, Nuxt, or a simple HTML page
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## π¦ Installation
|
|
24
|
+
|
|
25
|
+
### Option 1: NPM (Recommended for frameworks)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install tsapay-checkout-js
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Option 2: CDN (For simple HTML pages)
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<script src="https://unpkg.com/tsapay-checkout-js/dist/checkout.js"></script>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## π Quick Start
|
|
40
|
+
|
|
41
|
+
### Vanilla HTML / JavaScript
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<button id="pay-btn">Acheter β 5 000 FCFA</button>
|
|
45
|
+
|
|
46
|
+
<script src="https://unpkg.com/tsapay-checkout-js/dist/checkout.js"></script>
|
|
47
|
+
<script>
|
|
48
|
+
const checkout = new TsaPayCheckout('sk_test_YOUR_API_KEY', 'http://your-api.com/v1');
|
|
49
|
+
|
|
50
|
+
document.getElementById('pay-btn').addEventListener('click', function () {
|
|
51
|
+
checkout.open({
|
|
52
|
+
amount: 5000,
|
|
53
|
+
currency: 'XAF',
|
|
54
|
+
description: 'T-Shirt Γdition Dev',
|
|
55
|
+
reference: 'ORDER-001',
|
|
56
|
+
onSuccess: function (payment) {
|
|
57
|
+
alert('Paiement rΓ©ussi ! ID: ' + payment.id);
|
|
58
|
+
},
|
|
59
|
+
onError: function (error) {
|
|
60
|
+
alert('Γchec: ' + error.message);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
</script>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### React / Next.js
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { TsaPayCheckout } from 'tsapay-checkout-js';
|
|
71
|
+
|
|
72
|
+
const checkout = new TsaPayCheckout('sk_test_YOUR_API_KEY');
|
|
73
|
+
|
|
74
|
+
export default function BuyButton() {
|
|
75
|
+
const handleClick = () => {
|
|
76
|
+
checkout.open({
|
|
77
|
+
amount: 1500,
|
|
78
|
+
description: 'Abonnement Premium',
|
|
79
|
+
onSuccess: (payment) => {
|
|
80
|
+
console.log('Payment succeeded:', payment.id);
|
|
81
|
+
// redirect or update UI
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return <button onClick={handleClick}>Payer 1 500 FCFA</button>;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Angular
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { Component } from '@angular/core';
|
|
94
|
+
import { TsaPayCheckout } from 'tsapay-checkout-js';
|
|
95
|
+
|
|
96
|
+
@Component({
|
|
97
|
+
selector: 'app-buy',
|
|
98
|
+
template: `<button (click)="pay()">Payer 5 000 FCFA</button>`,
|
|
99
|
+
})
|
|
100
|
+
export class BuyComponent {
|
|
101
|
+
private checkout = new TsaPayCheckout('sk_test_YOUR_API_KEY');
|
|
102
|
+
|
|
103
|
+
pay() {
|
|
104
|
+
this.checkout.open({
|
|
105
|
+
amount: 5000,
|
|
106
|
+
description: 'Commande #42',
|
|
107
|
+
onSuccess: (payment) => console.log('OK', payment),
|
|
108
|
+
onError: (err) => console.error('KO', err),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Vue 3
|
|
115
|
+
|
|
116
|
+
```vue
|
|
117
|
+
<script setup lang="ts">
|
|
118
|
+
import { TsaPayCheckout } from '@tsapay/checkout';
|
|
119
|
+
|
|
120
|
+
const checkout = new TsaPayCheckout('sk_test_YOUR_API_KEY');
|
|
121
|
+
|
|
122
|
+
function pay() {
|
|
123
|
+
checkout.open({
|
|
124
|
+
amount: 2500,
|
|
125
|
+
description: 'Cours en ligne',
|
|
126
|
+
onSuccess: (p) => alert(`Paiement ${p.id} confirmΓ© !`),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<template>
|
|
132
|
+
<button @click="pay">Payer 2 500 FCFA</button>
|
|
133
|
+
</template>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## βοΈ API Reference
|
|
139
|
+
|
|
140
|
+
### `new TsaPayCheckout(apiKey, baseUrl?)`
|
|
141
|
+
|
|
142
|
+
| Parameter | Type | Description |
|
|
143
|
+
|-----------|----------|------------------------------------------|
|
|
144
|
+
| `apiKey` | `string` | Your TsaPay API key (`sk_test_...`) |
|
|
145
|
+
| `baseUrl` | `string` | Optional. API base URL (default: `https://api.tsapay.com/v1`) |
|
|
146
|
+
|
|
147
|
+
### `checkout.open(options)`
|
|
148
|
+
|
|
149
|
+
| Option | Type | Required | Description |
|
|
150
|
+
|---------------|------------|----------|------------------------------------------|
|
|
151
|
+
| `amount` | `number` | β
| Amount in XAF (e.g. `5000`) |
|
|
152
|
+
| `currency` | `string` | β | Default: `"XAF"` |
|
|
153
|
+
| `description` | `string` | β | Shown in the modal header |
|
|
154
|
+
| `reference` | `string` | β | Your internal order reference |
|
|
155
|
+
| `callbackUrl` | `string` | β | Webhook URL for async notifications |
|
|
156
|
+
| `phoneNumber` | `string` | β | Pre-fill the phone field |
|
|
157
|
+
| `provider` | `string` | β | Pre-select `"mtn_momo"` or `"orange_money"` |
|
|
158
|
+
| `metadata` | `object` | β | Key-value pairs attached to payment |
|
|
159
|
+
| `onSuccess` | `function` | β | Called with `PaymentResult` on success |
|
|
160
|
+
| `onError` | `function` | β | Called with `{ code, message }` on failure |
|
|
161
|
+
| `onClose` | `function` | β | Called when user closes the modal |
|
|
162
|
+
|
|
163
|
+
### `checkout.close()`
|
|
164
|
+
|
|
165
|
+
Programmatically close the modal.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## π License
|
|
170
|
+
|
|
171
|
+
MIT β Built with β€οΈ by [TsaPay](https://tsapay.com)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TsaPay Checkout β Drop-in payment modal.
|
|
3
|
+
* Clean, Stripe-inspired design with real provider logos.
|
|
4
|
+
*/
|
|
5
|
+
export interface CheckoutOptions {
|
|
6
|
+
amount: number;
|
|
7
|
+
currency?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
reference?: string;
|
|
10
|
+
callbackUrl?: string;
|
|
11
|
+
metadata?: Record<string, string>;
|
|
12
|
+
phoneNumber?: string;
|
|
13
|
+
provider?: "mtn_momo" | "orange_money";
|
|
14
|
+
onSuccess?: (payment: PaymentResult) => void;
|
|
15
|
+
onError?: (error: {
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
}) => void;
|
|
19
|
+
onClose?: () => void;
|
|
20
|
+
}
|
|
21
|
+
export interface PaymentResult {
|
|
22
|
+
id: string;
|
|
23
|
+
status: string;
|
|
24
|
+
amount: number;
|
|
25
|
+
currency: string;
|
|
26
|
+
provider: string;
|
|
27
|
+
reference: string;
|
|
28
|
+
}
|
|
29
|
+
export declare class TsaPayCheckout {
|
|
30
|
+
private key;
|
|
31
|
+
private base;
|
|
32
|
+
private el;
|
|
33
|
+
private opts;
|
|
34
|
+
private poll;
|
|
35
|
+
constructor(apiKey: string, baseUrl?: string);
|
|
36
|
+
open(opts: CheckoutOptions): void;
|
|
37
|
+
close(): void;
|
|
38
|
+
private css;
|
|
39
|
+
private fmt;
|
|
40
|
+
private kill;
|
|
41
|
+
private post;
|
|
42
|
+
private get;
|
|
43
|
+
private form;
|
|
44
|
+
private submit;
|
|
45
|
+
private waiting;
|
|
46
|
+
private ok;
|
|
47
|
+
private ko;
|
|
48
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TsaPay Checkout β Drop-in payment modal.
|
|
3
|
+
* Clean, Stripe-inspired design with real provider logos.
|
|
4
|
+
*/
|
|
5
|
+
// ββ Provider logos (base64 inline SVG) ββββββ
|
|
6
|
+
const MTN_LOGO = `<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="48" height="48" rx="10" fill="#FFCC00"/><text x="50%" y="54%" dominant-baseline="middle" text-anchor="middle" font-family="Arial,sans-serif" font-weight="900" font-size="14" fill="#003068">MTN</text></svg>`;
|
|
7
|
+
const ORANGE_LOGO = `<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="48" height="48" rx="10" fill="#FF6600"/><text x="50%" y="42%" dominant-baseline="middle" text-anchor="middle" font-family="Arial,sans-serif" font-weight="900" font-size="9.5" fill="#fff">Orange</text><text x="50%" y="62%" dominant-baseline="middle" text-anchor="middle" font-family="Arial,sans-serif" font-weight="700" font-size="7" fill="rgba(255,255,255,.85)">Money</text></svg>`;
|
|
8
|
+
const CSS = `
|
|
9
|
+
.tp-overlay{position:fixed;inset:0;z-index:999999;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .25s;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}
|
|
10
|
+
.tp-overlay.tp-show{opacity:1}
|
|
11
|
+
.tp-modal{background:#fff;border-radius:12px;width:400px;max-width:92vw;box-shadow:0 20px 60px rgba(0,0,0,.3);transform:scale(.95);transition:transform .25s cubic-bezier(.4,0,.2,1)}
|
|
12
|
+
.tp-overlay.tp-show .tp-modal{transform:scale(1)}
|
|
13
|
+
|
|
14
|
+
/* Header */
|
|
15
|
+
.tp-head{padding:20px 24px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;justify-content:space-between}
|
|
16
|
+
.tp-head-left{display:flex;align-items:center;gap:10px}
|
|
17
|
+
.tp-head-logo{width:28px;height:28px;border-radius:6px;background:linear-gradient(135deg,#eab308,#22c55e);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:12px;color:#fff}
|
|
18
|
+
.tp-head-name{font-size:14px;font-weight:600;color:#111827}
|
|
19
|
+
.tp-close{width:28px;height:28px;border:none;background:none;color:#9ca3af;font-size:20px;cursor:pointer;border-radius:6px;display:flex;align-items:center;justify-content:center}
|
|
20
|
+
.tp-close:hover{background:#f3f4f6;color:#374151}
|
|
21
|
+
|
|
22
|
+
/* Amount bar */
|
|
23
|
+
.tp-amount-bar{padding:20px 24px;background:#fafafa;border-bottom:1px solid #e5e7eb}
|
|
24
|
+
.tp-amount-label{font-size:12px;color:#6b7280;margin-bottom:2px}
|
|
25
|
+
.tp-amount-value{font-size:28px;font-weight:700;color:#111827;letter-spacing:-.5px}
|
|
26
|
+
.tp-amount-value small{font-size:14px;font-weight:500;color:#9ca3af;margin-left:4px}
|
|
27
|
+
.tp-desc{font-size:13px;color:#6b7280;margin-top:2px}
|
|
28
|
+
|
|
29
|
+
/* Body */
|
|
30
|
+
.tp-body{padding:20px 24px}
|
|
31
|
+
.tp-field{margin-bottom:16px}
|
|
32
|
+
.tp-label{display:block;font-size:13px;font-weight:500;color:#374151;margin-bottom:6px}
|
|
33
|
+
.tp-input{width:100%;padding:10px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:14px;color:#111827;outline:none;box-sizing:border-box;font-family:inherit;transition:border-color .15s,box-shadow .15s}
|
|
34
|
+
.tp-input:focus{border-color:#22c55e;box-shadow:0 0 0 3px rgba(34,197,94,.12)}
|
|
35
|
+
.tp-input::placeholder{color:#9ca3af}
|
|
36
|
+
.tp-input-error{border-color:#ef4444 !important}
|
|
37
|
+
|
|
38
|
+
/* Providers */
|
|
39
|
+
.tp-providers{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
|
40
|
+
.tp-prov{position:relative;cursor:pointer}
|
|
41
|
+
.tp-prov input{position:absolute;opacity:0;pointer-events:none}
|
|
42
|
+
.tp-prov-card{display:flex;align-items:center;gap:10px;padding:12px;border:1.5px solid #e5e7eb;border-radius:10px;transition:all .15s;background:#fff}
|
|
43
|
+
.tp-prov-card:hover{border-color:#d1d5db;background:#fafafa}
|
|
44
|
+
.tp-prov input:checked+.tp-prov-card{border-color:#22c55e;background:#f0fdf4}
|
|
45
|
+
.tp-prov-logo{width:36px;height:36px;border-radius:8px;flex-shrink:0}
|
|
46
|
+
.tp-prov-logo svg{width:100%;height:100%}
|
|
47
|
+
.tp-prov-info{line-height:1.3}
|
|
48
|
+
.tp-prov-name{font-size:13px;font-weight:600;color:#111827}
|
|
49
|
+
.tp-prov-sub{font-size:11px;color:#9ca3af}
|
|
50
|
+
|
|
51
|
+
/* Submit */
|
|
52
|
+
.tp-submit{width:100%;padding:12px;border:none;border-radius:8px;font-size:14px;font-weight:600;color:#fff;background:#111827;cursor:pointer;font-family:inherit;transition:background .15s;display:flex;align-items:center;justify-content:center;gap:8px;margin-top:4px}
|
|
53
|
+
.tp-submit:hover{background:#1f2937}
|
|
54
|
+
.tp-submit:disabled{opacity:.5;cursor:not-allowed}
|
|
55
|
+
|
|
56
|
+
/* Spinner */
|
|
57
|
+
.tp-spin{width:16px;height:16px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:tp-r .6s linear infinite}
|
|
58
|
+
@keyframes tp-r{to{transform:rotate(360deg)}}
|
|
59
|
+
|
|
60
|
+
/* Footer */
|
|
61
|
+
.tp-foot{padding:14px 24px;border-top:1px solid #e5e7eb;text-align:center}
|
|
62
|
+
.tp-foot span{font-size:11px;color:#9ca3af}
|
|
63
|
+
.tp-foot a{color:#22c55e;text-decoration:none;font-weight:600}
|
|
64
|
+
|
|
65
|
+
/* Result */
|
|
66
|
+
.tp-result{text-align:center;padding:40px 24px}
|
|
67
|
+
.tp-result-icon{width:56px;height:56px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 16px;font-size:24px;animation:tp-pop .4s cubic-bezier(.22,1,.36,1)}
|
|
68
|
+
@keyframes tp-pop{0%{transform:scale(0)}60%{transform:scale(1.1)}100%{transform:scale(1)}}
|
|
69
|
+
.tp-result-icon.ok{background:#dcfce7;color:#16a34a}
|
|
70
|
+
.tp-result-icon.ko{background:#fee2e2;color:#dc2626}
|
|
71
|
+
.tp-result-title{font-size:16px;font-weight:700;color:#111827;margin-bottom:6px}
|
|
72
|
+
.tp-result-msg{font-size:13px;color:#6b7280;margin-bottom:24px;line-height:1.5}
|
|
73
|
+
.tp-result-btn{padding:10px 24px;border-radius:8px;border:none;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;background:#111827;color:#fff;transition:background .15s}
|
|
74
|
+
.tp-result-btn:hover{background:#1f2937}
|
|
75
|
+
|
|
76
|
+
/* Waiting */
|
|
77
|
+
.tp-wait{text-align:center;padding:40px 24px}
|
|
78
|
+
.tp-wait-phone{font-size:40px;animation:tp-ring 1.2s ease-in-out infinite}
|
|
79
|
+
@keyframes tp-ring{0%,100%{transform:rotate(0)}10%{transform:rotate(10deg)}20%{transform:rotate(-10deg)}30%{transform:rotate(6deg)}40%{transform:rotate(-6deg)}50%{transform:rotate(0)}}
|
|
80
|
+
.tp-wait-title{font-size:15px;font-weight:700;color:#111827;margin:16px 0 6px}
|
|
81
|
+
.tp-wait-msg{font-size:13px;color:#6b7280;line-height:1.6}
|
|
82
|
+
.tp-dots::after{content:'';animation:tp-d 1.5s steps(4) infinite}
|
|
83
|
+
@keyframes tp-d{0%{content:''}25%{content:'.'}50%{content:'..'}75%{content:'...'}}
|
|
84
|
+
`;
|
|
85
|
+
export class TsaPayCheckout {
|
|
86
|
+
constructor(apiKey, baseUrl) {
|
|
87
|
+
this.el = null;
|
|
88
|
+
this.opts = null;
|
|
89
|
+
this.poll = null;
|
|
90
|
+
if (!apiKey)
|
|
91
|
+
throw new Error("TsaPay: API key required");
|
|
92
|
+
this.key = apiKey;
|
|
93
|
+
this.base = (baseUrl || "https://api.tsapay.com/v1").replace(/\/+$/, "");
|
|
94
|
+
}
|
|
95
|
+
open(opts) {
|
|
96
|
+
this.opts = opts;
|
|
97
|
+
this.css();
|
|
98
|
+
this.form(opts);
|
|
99
|
+
}
|
|
100
|
+
close() {
|
|
101
|
+
this.kill();
|
|
102
|
+
this.opts?.onClose?.();
|
|
103
|
+
}
|
|
104
|
+
// ββ CSS βββββββββββββββββββββββββββββββββββ
|
|
105
|
+
css() {
|
|
106
|
+
if (document.getElementById("tp-css"))
|
|
107
|
+
return;
|
|
108
|
+
const s = document.createElement("style");
|
|
109
|
+
s.id = "tp-css";
|
|
110
|
+
s.textContent = CSS;
|
|
111
|
+
document.head.appendChild(s);
|
|
112
|
+
}
|
|
113
|
+
// ββ Helpers βββββββββββββββββββββββββββββββ
|
|
114
|
+
fmt(n) { return new Intl.NumberFormat("fr-FR").format(n); }
|
|
115
|
+
kill() {
|
|
116
|
+
if (this.poll) {
|
|
117
|
+
clearInterval(this.poll);
|
|
118
|
+
this.poll = null;
|
|
119
|
+
}
|
|
120
|
+
if (this.el) {
|
|
121
|
+
this.el.classList.remove("tp-show");
|
|
122
|
+
setTimeout(() => { this.el?.remove(); this.el = null; }, 250);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ββ API βββββββββββββββββββββββββββββββββββ
|
|
126
|
+
async post(phone, prov) {
|
|
127
|
+
const o = this.opts;
|
|
128
|
+
const r = await fetch(`${this.base}/payments`, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: {
|
|
131
|
+
"Authorization": `Bearer ${this.key}`,
|
|
132
|
+
"Content-Type": "application/json",
|
|
133
|
+
"Idempotency-Key": crypto.randomUUID(),
|
|
134
|
+
},
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
amount: o.amount, currency: o.currency || "XAF", provider: prov,
|
|
137
|
+
phone_number: phone, description: o.description || "",
|
|
138
|
+
reference: o.reference || "", callback_url: o.callbackUrl || "",
|
|
139
|
+
metadata: o.metadata || {},
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
if (!r.ok) {
|
|
143
|
+
const e = await r.json().catch(() => ({ message: "Erreur rΓ©seau" }));
|
|
144
|
+
throw new Error(e.message || `HTTP ${r.status}`);
|
|
145
|
+
}
|
|
146
|
+
return r.json();
|
|
147
|
+
}
|
|
148
|
+
async get(id) {
|
|
149
|
+
const r = await fetch(`${this.base}/payments/${id}`, {
|
|
150
|
+
headers: { "Authorization": `Bearer ${this.key}` },
|
|
151
|
+
});
|
|
152
|
+
return r.json();
|
|
153
|
+
}
|
|
154
|
+
// ββ Form ββββββββββββββββββββββββββββββββββ
|
|
155
|
+
form(o) {
|
|
156
|
+
const cur = o.currency || "XAF";
|
|
157
|
+
const ov = document.createElement("div");
|
|
158
|
+
ov.className = "tp-overlay";
|
|
159
|
+
ov.innerHTML = `
|
|
160
|
+
<div class="tp-modal">
|
|
161
|
+
<div class="tp-head">
|
|
162
|
+
<div class="tp-head-left"><div class="tp-head-logo">T</div><span class="tp-head-name">TsaPay</span></div>
|
|
163
|
+
<button class="tp-close" id="tp-x">×</button>
|
|
164
|
+
</div>
|
|
165
|
+
<div class="tp-amount-bar">
|
|
166
|
+
<div class="tp-amount-label">Montant Γ payer</div>
|
|
167
|
+
<div class="tp-amount-value">${this.fmt(o.amount)}<small>${cur}</small></div>
|
|
168
|
+
${o.description ? `<div class="tp-desc">${o.description}</div>` : ""}
|
|
169
|
+
</div>
|
|
170
|
+
<div class="tp-body">
|
|
171
|
+
<div class="tp-field">
|
|
172
|
+
<label class="tp-label">NumΓ©ro de tΓ©lΓ©phone</label>
|
|
173
|
+
<input class="tp-input" id="tp-phone" type="tel" placeholder="+237 6XX XXX XXX" value="${o.phoneNumber || ""}" />
|
|
174
|
+
</div>
|
|
175
|
+
<div class="tp-field">
|
|
176
|
+
<label class="tp-label">Moyen de paiement</label>
|
|
177
|
+
<div class="tp-providers">
|
|
178
|
+
<label class="tp-prov">
|
|
179
|
+
<input type="radio" name="tp-prov" value="mtn_momo" ${(!o.provider || o.provider === "mtn_momo") ? "checked" : ""} />
|
|
180
|
+
<div class="tp-prov-card">
|
|
181
|
+
<div class="tp-prov-logo">${MTN_LOGO}</div>
|
|
182
|
+
<div class="tp-prov-info"><div class="tp-prov-name">MTN MoMo</div><div class="tp-prov-sub">Mobile Money</div></div>
|
|
183
|
+
</div>
|
|
184
|
+
</label>
|
|
185
|
+
<label class="tp-prov">
|
|
186
|
+
<input type="radio" name="tp-prov" value="orange_money" ${o.provider === "orange_money" ? "checked" : ""} />
|
|
187
|
+
<div class="tp-prov-card">
|
|
188
|
+
<div class="tp-prov-logo">${ORANGE_LOGO}</div>
|
|
189
|
+
<div class="tp-prov-info"><div class="tp-prov-name">Orange Money</div><div class="tp-prov-sub">Mobile Money</div></div>
|
|
190
|
+
</div>
|
|
191
|
+
</label>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
<button class="tp-submit" id="tp-pay">Payer ${this.fmt(o.amount)} ${cur}</button>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="tp-foot"><span>SΓ©curisΓ© par <a href="https://tsapay.com" target="_blank">TsaPay</a></span></div>
|
|
197
|
+
</div>`;
|
|
198
|
+
document.body.appendChild(ov);
|
|
199
|
+
this.el = ov;
|
|
200
|
+
requestAnimationFrame(() => ov.classList.add("tp-show"));
|
|
201
|
+
ov.querySelector("#tp-x").addEventListener("click", () => this.close());
|
|
202
|
+
ov.addEventListener("click", (e) => { if (e.target === ov)
|
|
203
|
+
this.close(); });
|
|
204
|
+
ov.querySelector("#tp-pay").addEventListener("click", () => this.submit());
|
|
205
|
+
}
|
|
206
|
+
// ββ Submit ββββββββββββββββββββββββββββββββ
|
|
207
|
+
async submit() {
|
|
208
|
+
const ph = this.el.querySelector("#tp-phone");
|
|
209
|
+
const prov = this.el.querySelector('input[name="tp-prov"]:checked');
|
|
210
|
+
const btn = this.el.querySelector("#tp-pay");
|
|
211
|
+
const phone = ph.value.trim();
|
|
212
|
+
if (!phone) {
|
|
213
|
+
ph.classList.add("tp-input-error");
|
|
214
|
+
ph.focus();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
ph.classList.remove("tp-input-error");
|
|
218
|
+
btn.disabled = true;
|
|
219
|
+
btn.innerHTML = `<span class="tp-spin"></span>Traitementβ¦`;
|
|
220
|
+
try {
|
|
221
|
+
const d = await this.post(phone, prov.value);
|
|
222
|
+
const id = d.payment?.id || d.id;
|
|
223
|
+
if (id)
|
|
224
|
+
this.waiting(id, prov.value);
|
|
225
|
+
else {
|
|
226
|
+
this.ok(d.payment || d);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
this.ko(e.message || "Erreur inattendue");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// ββ Waiting βββββββββββββββββββββββββββββββ
|
|
234
|
+
waiting(id, prov) {
|
|
235
|
+
const label = prov === "mtn_momo" ? "MTN MoMo" : "Orange Money";
|
|
236
|
+
const m = this.el.querySelector(".tp-modal");
|
|
237
|
+
m.innerHTML = `
|
|
238
|
+
<div class="tp-head">
|
|
239
|
+
<div class="tp-head-left"><div class="tp-head-logo">T</div><span class="tp-head-name">TsaPay</span></div>
|
|
240
|
+
<button class="tp-close" id="tp-x">×</button>
|
|
241
|
+
</div>
|
|
242
|
+
<div class="tp-wait">
|
|
243
|
+
<div class="tp-wait-phone">π±</div>
|
|
244
|
+
<div class="tp-wait-title">Confirmez sur votre tΓ©lΓ©phone</div>
|
|
245
|
+
<div class="tp-wait-msg">
|
|
246
|
+
Un message ${label} a Γ©tΓ© envoyΓ©.<br/>Saisissez votre code PIN pour valider<span class="tp-dots"></span>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
<div class="tp-foot"><span>SΓ©curisΓ© par <a href="https://tsapay.com" target="_blank">TsaPay</a></span></div>`;
|
|
250
|
+
m.querySelector("#tp-x").addEventListener("click", () => this.close());
|
|
251
|
+
let n = 0;
|
|
252
|
+
this.poll = window.setInterval(async () => {
|
|
253
|
+
n++;
|
|
254
|
+
try {
|
|
255
|
+
const d = await this.get(id);
|
|
256
|
+
const s = d.payment?.status || d.status;
|
|
257
|
+
if (s === "success") {
|
|
258
|
+
clearInterval(this.poll);
|
|
259
|
+
this.poll = null;
|
|
260
|
+
this.ok(d.payment || d);
|
|
261
|
+
this.opts?.onSuccess?.(d.payment || d);
|
|
262
|
+
}
|
|
263
|
+
else if (s === "failed" || s === "expired") {
|
|
264
|
+
clearInterval(this.poll);
|
|
265
|
+
this.poll = null;
|
|
266
|
+
const r = d.payment?.failure_reason || "Le paiement a Γ©chouΓ©.";
|
|
267
|
+
this.ko(r);
|
|
268
|
+
this.opts?.onError?.({ code: s, message: r });
|
|
269
|
+
}
|
|
270
|
+
else if (n >= 40) {
|
|
271
|
+
clearInterval(this.poll);
|
|
272
|
+
this.poll = null;
|
|
273
|
+
this.ko("DΓ©lai d'attente dΓ©passΓ©.");
|
|
274
|
+
this.opts?.onError?.({ code: "timeout", message: "timeout" });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch { }
|
|
278
|
+
}, 3000);
|
|
279
|
+
}
|
|
280
|
+
// ββ Success βββββββββββββββββββββββββββββββ
|
|
281
|
+
ok(p) {
|
|
282
|
+
const m = this.el.querySelector(".tp-modal");
|
|
283
|
+
m.innerHTML = `
|
|
284
|
+
<div class="tp-result">
|
|
285
|
+
<div class="tp-result-icon ok">β</div>
|
|
286
|
+
<div class="tp-result-title">Paiement confirmΓ©</div>
|
|
287
|
+
<div class="tp-result-msg">
|
|
288
|
+
${this.fmt(p.amount || this.opts?.amount || 0)} ${p.currency || "XAF"} reçus avec succès.
|
|
289
|
+
${p.reference ? `<br/>RΓ©f: <strong>${p.reference}</strong>` : ""}
|
|
290
|
+
</div>
|
|
291
|
+
<button class="tp-result-btn" id="tp-done">Fermer</button>
|
|
292
|
+
</div>`;
|
|
293
|
+
m.querySelector("#tp-done").addEventListener("click", () => this.close());
|
|
294
|
+
}
|
|
295
|
+
// ββ Error βββββββββββββββββββββββββββββββββ
|
|
296
|
+
ko(msg) {
|
|
297
|
+
const m = this.el.querySelector(".tp-modal");
|
|
298
|
+
m.innerHTML = `
|
|
299
|
+
<div class="tp-result">
|
|
300
|
+
<div class="tp-result-icon ko">β</div>
|
|
301
|
+
<div class="tp-result-title">Paiement Γ©chouΓ©</div>
|
|
302
|
+
<div class="tp-result-msg">${msg}</div>
|
|
303
|
+
<button class="tp-result-btn" id="tp-retry">RΓ©essayer</button>
|
|
304
|
+
</div>`;
|
|
305
|
+
m.querySelector("#tp-retry").addEventListener("click", () => {
|
|
306
|
+
if (this.opts)
|
|
307
|
+
this.form(this.opts);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (typeof window !== "undefined")
|
|
312
|
+
window.TsaPayCheckout = TsaPayCheckout;
|