cinetpay-seamless 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +658 -0
- package/dist/checkout.d.ts +73 -0
- package/dist/checkout.d.ts.map +1 -0
- package/dist/cinetpay-seamless.js +401 -0
- package/dist/cinetpay-seamless.js.map +1 -0
- package/dist/cinetpay-seamless.umd.cjs +82 -0
- package/dist/cinetpay-seamless.umd.cjs.map +1 -0
- package/dist/emitter.d.ts +99 -0
- package/dist/emitter.d.ts.map +1 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/logger.d.ts +18 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/styles.d.ts +5 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
# cinetpay-seamless
|
|
2
|
+
|
|
3
|
+
CinetPay Seamless — paiement inline sans redirection pour applications web.
|
|
4
|
+
|
|
5
|
+
Ouvre la passerelle de paiement CinetPay dans une popup. L'utilisateur reste sur votre page. Le client ne quitte jamais votre site.
|
|
6
|
+
|
|
7
|
+
## Comment ça marche
|
|
8
|
+
|
|
9
|
+
1. Votre **backend** initialise le paiement via l'API CinetPay (`POST /v1/payment`) et obtient un `paymentToken`
|
|
10
|
+
2. Votre **frontend** passe ce token au Seamless qui ouvre la popup de paiement
|
|
11
|
+
3. Le client paie dans la popup — votre backend reçoit la confirmation via webhook
|
|
12
|
+
|
|
13
|
+
```mermaid
|
|
14
|
+
sequenceDiagram
|
|
15
|
+
participant F as Frontend
|
|
16
|
+
participant B as Backend
|
|
17
|
+
participant C as CinetPay API
|
|
18
|
+
|
|
19
|
+
F->>B: 1. fetch('/api/pay')
|
|
20
|
+
B->>C: 2. POST /v1/payment
|
|
21
|
+
C-->>B: 3. paymentToken
|
|
22
|
+
B-->>F: 4. { paymentToken }
|
|
23
|
+
F->>F: 5. CinetPaySeamless.open({ paymentToken })
|
|
24
|
+
F->>C: 6. Popup → page checkout
|
|
25
|
+
Note over F,C: Le client paie dans la popup
|
|
26
|
+
C-->>B: 7. Webhook (notifyUrl)
|
|
27
|
+
C-->>F: 8. postMessage → onPaymentSuccess
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install cinetpay-seamless
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### CDN
|
|
37
|
+
|
|
38
|
+
```html
|
|
39
|
+
<script src="https://unpkg.com/cinetpay-seamless/dist/cinetpay-seamless.umd.cjs"></script>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Démarrage rapide
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { CinetPaySeamless } from 'cinetpay-seamless'
|
|
46
|
+
|
|
47
|
+
// 1. Obtenir le paymentToken depuis votre backend
|
|
48
|
+
const { paymentToken } = await fetch('/api/pay', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({ amount: 5000, orderId: 'ORDER-001' }),
|
|
52
|
+
}).then(r => r.json())
|
|
53
|
+
|
|
54
|
+
// 2. Ouvrir la popup
|
|
55
|
+
CinetPaySeamless.open({
|
|
56
|
+
paymentToken,
|
|
57
|
+
onPaymentSuccess: (data) => {
|
|
58
|
+
console.log('Paiement réussi !', data.amount, data.currency)
|
|
59
|
+
},
|
|
60
|
+
onPaymentFailed: (data) => {
|
|
61
|
+
console.log('Paiement refusé')
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## CDN / Vanilla JS
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<button id="pay-btn">Payer 5 000 XOF</button>
|
|
70
|
+
|
|
71
|
+
<script src="https://unpkg.com/cinetpay-seamless/dist/cinetpay-seamless.umd.cjs"></script>
|
|
72
|
+
<script>
|
|
73
|
+
document.getElementById('pay-btn').addEventListener('click', function() {
|
|
74
|
+
// Appeler votre backend pour obtenir le paymentToken
|
|
75
|
+
fetch('/api/pay', {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
body: JSON.stringify({ amount: 5000 })
|
|
79
|
+
})
|
|
80
|
+
.then(function(res) { return res.json() })
|
|
81
|
+
.then(function(data) {
|
|
82
|
+
CinetPaySeamless.open({
|
|
83
|
+
paymentToken: data.paymentToken,
|
|
84
|
+
onPaymentSuccess: function(result) {
|
|
85
|
+
alert('Merci ! ' + result.amount + ' ' + result.currency)
|
|
86
|
+
},
|
|
87
|
+
onPaymentFailed: function() {
|
|
88
|
+
alert('Paiement échoué')
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
</script>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Event Listeners (style événementiel)
|
|
97
|
+
|
|
98
|
+
En plus des callbacks dans `open()`, écoutez les événements globalement :
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { CinetPaySeamless } from 'cinetpay-seamless'
|
|
102
|
+
|
|
103
|
+
// Enregistrer les listeners AVANT d'ouvrir la popup
|
|
104
|
+
CinetPaySeamless.on('ready', () => {
|
|
105
|
+
console.log('Passerelle chargée')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
CinetPaySeamless.on('payment.success', (data) => {
|
|
109
|
+
console.log('Payé !', data.amount, data.currency)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
CinetPaySeamless.on('payment.failed', (data) => {
|
|
113
|
+
console.log('Refusé', data.transactionId)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
CinetPaySeamless.on('payment.pending', (data) => {
|
|
117
|
+
console.log('En attente...', data.status)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
CinetPaySeamless.on('close', ({ status }) => {
|
|
121
|
+
console.log('Modal fermé:', status)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
CinetPaySeamless.on('error', (err) => {
|
|
125
|
+
console.error(err.code, err.message)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Ouvrir — les listeners sont déjà en place
|
|
129
|
+
CinetPaySeamless.open({ paymentToken: 'votre-payment-token-ici' })
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Désabonnement
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// on() retourne une fonction de désabonnement
|
|
136
|
+
const unsubscribe = CinetPaySeamless.on('payment.success', handler)
|
|
137
|
+
unsubscribe()
|
|
138
|
+
|
|
139
|
+
// Ou avec off()
|
|
140
|
+
CinetPaySeamless.off('payment.success', handler)
|
|
141
|
+
|
|
142
|
+
// once() — appelé une seule fois
|
|
143
|
+
CinetPaySeamless.once('payment.success', (data) => { ... })
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## API
|
|
147
|
+
|
|
148
|
+
### `CinetPaySeamless.open(config)`
|
|
149
|
+
|
|
150
|
+
Ouvre la popup de paiement CinetPay.
|
|
151
|
+
|
|
152
|
+
| Option | Type | Default | Description |
|
|
153
|
+
|---|---|---|---|
|
|
154
|
+
| `paymentToken` | `string` | **requis** | Token obtenu via votre backend (`POST /v1/payment`) |
|
|
155
|
+
| `debug` | `boolean` | `false` | Logs console `[CinetPay Seamless]` |
|
|
156
|
+
| `onReady` | `() => void` | - | Iframe chargée |
|
|
157
|
+
| `onPaymentSuccess` | `(data) => void` | - | Paiement accepté |
|
|
158
|
+
| `onPaymentFailed` | `(data) => void` | - | Paiement refusé |
|
|
159
|
+
| `onPaymentPending` | `(data) => void` | - | En attente (PENDING, INITIATED, EXPIRED) |
|
|
160
|
+
| `onClose` | `({ status }) => void` | - | Modal fermé |
|
|
161
|
+
| `onError` | `(error) => void` | - | Erreur technique |
|
|
162
|
+
|
|
163
|
+
### `CinetPaySeamless.on(event, handler)`
|
|
164
|
+
|
|
165
|
+
| Événement | Donnée | Description |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| `ready` | — | Iframe chargée |
|
|
168
|
+
| `payment.success` | `PaymentResponse` | Paiement accepté |
|
|
169
|
+
| `payment.failed` | `PaymentResponse` | Paiement refusé |
|
|
170
|
+
| `payment.pending` | `PaymentResponse` | En attente |
|
|
171
|
+
| `close` | `{ status: string }` | Modal fermé |
|
|
172
|
+
| `error` | `PaymentError` | Erreur technique |
|
|
173
|
+
|
|
174
|
+
### `CinetPaySeamless.off(event, handler)`
|
|
175
|
+
|
|
176
|
+
Supprime un listener.
|
|
177
|
+
|
|
178
|
+
### `CinetPaySeamless.once(event, handler)`
|
|
179
|
+
|
|
180
|
+
Listener appelé une seule fois.
|
|
181
|
+
|
|
182
|
+
### `CinetPaySeamless.close()`
|
|
183
|
+
|
|
184
|
+
Ferme la popup et l'overlay.
|
|
185
|
+
|
|
186
|
+
### PaymentResponse
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
{
|
|
190
|
+
amount: number
|
|
191
|
+
currency: string
|
|
192
|
+
status: 'ACCEPTED' | 'REFUSED' | 'PENDING' | 'INITIATED' | 'EXPIRED' | 'UNKNOWN'
|
|
193
|
+
paymentMethod: string
|
|
194
|
+
description: string
|
|
195
|
+
transactionId: string
|
|
196
|
+
metadata?: string
|
|
197
|
+
operatorId?: string
|
|
198
|
+
paymentDate?: string
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Exemples d'intégration
|
|
203
|
+
|
|
204
|
+
> Le Seamless a besoin d'un `paymentToken` obtenu côté serveur. Les exemples ci-dessous
|
|
205
|
+
> utilisent `cinetpay-js` (Node.js), mais vous pouvez utiliser n'importe quel langage/SDK :
|
|
206
|
+
> `cinetpay-laravel-sdk` (PHP), un appel API direct (Python, Go, Ruby, etc.), ou tout autre outil
|
|
207
|
+
> qui appelle `POST /v1/payment` sur l'API CinetPay.
|
|
208
|
+
|
|
209
|
+
### Node.js (cinetpay-js) + Frontend (Seamless)
|
|
210
|
+
|
|
211
|
+
**Backend — Express :**
|
|
212
|
+
```typescript
|
|
213
|
+
import express from 'express'
|
|
214
|
+
import { CinetPayClient } from 'cinetpay-js'
|
|
215
|
+
|
|
216
|
+
const app = express()
|
|
217
|
+
app.use(express.json())
|
|
218
|
+
|
|
219
|
+
const client = new CinetPayClient({
|
|
220
|
+
credentials: {
|
|
221
|
+
CI: {
|
|
222
|
+
apiKey: process.env.CINETPAY_API_KEY_CI!,
|
|
223
|
+
apiPassword: process.env.CINETPAY_API_PASSWORD_CI!,
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
app.post('/api/pay', async (req, res) => {
|
|
229
|
+
const { amount, orderId, email, firstName, lastName, phone } = req.body
|
|
230
|
+
|
|
231
|
+
const payment = await client.payment.initialize({
|
|
232
|
+
currency: 'XOF',
|
|
233
|
+
merchantTransactionId: orderId,
|
|
234
|
+
amount,
|
|
235
|
+
lang: 'fr',
|
|
236
|
+
designation: `Commande ${orderId}`,
|
|
237
|
+
clientEmail: email,
|
|
238
|
+
clientFirstName: firstName,
|
|
239
|
+
clientLastName: lastName,
|
|
240
|
+
clientPhoneNumber: phone,
|
|
241
|
+
successUrl: `${process.env.APP_URL}/success`,
|
|
242
|
+
failedUrl: `${process.env.APP_URL}/failed`,
|
|
243
|
+
notifyUrl: `${process.env.APP_URL}/api/webhook`,
|
|
244
|
+
channel: 'PUSH',
|
|
245
|
+
}, 'CI')
|
|
246
|
+
|
|
247
|
+
res.json({ paymentToken: payment.paymentToken })
|
|
248
|
+
})
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Frontend :**
|
|
252
|
+
```typescript
|
|
253
|
+
import { CinetPaySeamless } from 'cinetpay-seamless'
|
|
254
|
+
|
|
255
|
+
async function pay(amount: number) {
|
|
256
|
+
const res = await fetch('/api/pay', {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: { 'Content-Type': 'application/json' },
|
|
259
|
+
body: JSON.stringify({
|
|
260
|
+
amount,
|
|
261
|
+
orderId: `CMD-${Date.now()}`,
|
|
262
|
+
email: 'client@email.com',
|
|
263
|
+
firstName: 'Jean',
|
|
264
|
+
lastName: 'Dupont',
|
|
265
|
+
phone: '+2250707000000',
|
|
266
|
+
}),
|
|
267
|
+
})
|
|
268
|
+
const { paymentToken } = await res.json()
|
|
269
|
+
|
|
270
|
+
CinetPaySeamless.open({ paymentToken, debug: true })
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### PHP / Laravel (cinetpay-laravel-sdk)
|
|
275
|
+
|
|
276
|
+
```php
|
|
277
|
+
// routes/api.php
|
|
278
|
+
Route::post('/pay', function (Request $request) {
|
|
279
|
+
$payment = CinetPay::payment()->initialize([
|
|
280
|
+
'currency' => 'XOF',
|
|
281
|
+
'merchant_transaction_id' => 'CMD-' . time(),
|
|
282
|
+
'amount' => $request->amount,
|
|
283
|
+
'lang' => 'fr',
|
|
284
|
+
'designation' => 'Commande ' . $request->orderId,
|
|
285
|
+
'client_email' => $request->email,
|
|
286
|
+
'client_first_name' => $request->firstName,
|
|
287
|
+
'client_last_name' => $request->lastName,
|
|
288
|
+
'success_url' => config('app.url') . '/success',
|
|
289
|
+
'failed_url' => config('app.url') . '/failed',
|
|
290
|
+
'notify_url' => config('app.url') . '/api/webhook',
|
|
291
|
+
'channel' => 'PUSH',
|
|
292
|
+
], 'CI');
|
|
293
|
+
|
|
294
|
+
return response()->json(['paymentToken' => $payment->paymentToken]);
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### API directe (n'importe quel langage)
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
# 1. Authentification
|
|
302
|
+
TOKEN=$(curl -s -X POST https://api.cinetpay.net/v1/oauth/login \
|
|
303
|
+
-H "Content-Type: application/json" \
|
|
304
|
+
-d '{"api_key":"sk_test_...","api_password":"..."}' \
|
|
305
|
+
| jq -r '.access_token')
|
|
306
|
+
|
|
307
|
+
# 2. Initialisation du paiement
|
|
308
|
+
PAYMENT_TOKEN=$(curl -s -X POST https://api.cinetpay.net/v1/payment \
|
|
309
|
+
-H "Content-Type: application/json" \
|
|
310
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
311
|
+
-d '{
|
|
312
|
+
"currency": "XOF",
|
|
313
|
+
"merchant_transaction_id": "CMD-'$(date +%s)'",
|
|
314
|
+
"amount": 5000,
|
|
315
|
+
"lang": "fr",
|
|
316
|
+
"designation": "Commande",
|
|
317
|
+
"client_email": "client@email.com",
|
|
318
|
+
"client_first_name": "Jean",
|
|
319
|
+
"client_last_name": "Dupont",
|
|
320
|
+
"success_url": "https://monsite.com/success",
|
|
321
|
+
"failed_url": "https://monsite.com/failed",
|
|
322
|
+
"notify_url": "https://monsite.com/webhook",
|
|
323
|
+
"channel": "PUSH",
|
|
324
|
+
"direct_pay": false
|
|
325
|
+
}' | jq -r '.payment_token')
|
|
326
|
+
|
|
327
|
+
# 3. Passer $PAYMENT_TOKEN au frontend
|
|
328
|
+
echo "paymentToken: $PAYMENT_TOKEN"
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Next.js (App Router)
|
|
332
|
+
|
|
333
|
+
**`app/api/pay/route.ts` :**
|
|
334
|
+
```typescript
|
|
335
|
+
import { CinetPayClient } from 'cinetpay-js'
|
|
336
|
+
import { NextResponse } from 'next/server'
|
|
337
|
+
|
|
338
|
+
const client = new CinetPayClient({
|
|
339
|
+
credentials: {
|
|
340
|
+
CI: {
|
|
341
|
+
apiKey: process.env.CINETPAY_API_KEY_CI!,
|
|
342
|
+
apiPassword: process.env.CINETPAY_API_PASSWORD_CI!,
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
export async function POST(req: Request) {
|
|
348
|
+
const { amount, orderId, email, firstName, lastName, phone } = await req.json()
|
|
349
|
+
|
|
350
|
+
const payment = await client.payment.initialize({
|
|
351
|
+
currency: 'XOF',
|
|
352
|
+
merchantTransactionId: orderId,
|
|
353
|
+
amount,
|
|
354
|
+
lang: 'fr',
|
|
355
|
+
designation: `Commande ${orderId}`,
|
|
356
|
+
clientEmail: email,
|
|
357
|
+
clientFirstName: firstName,
|
|
358
|
+
clientLastName: lastName,
|
|
359
|
+
clientPhoneNumber: phone,
|
|
360
|
+
successUrl: `${process.env.APP_URL}/orders/${orderId}/success`,
|
|
361
|
+
failedUrl: `${process.env.APP_URL}/orders/${orderId}/failed`,
|
|
362
|
+
notifyUrl: `${process.env.APP_URL}/api/webhook`,
|
|
363
|
+
channel: 'PUSH',
|
|
364
|
+
}, 'CI')
|
|
365
|
+
|
|
366
|
+
return NextResponse.json({ paymentToken: payment.paymentToken })
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**`app/checkout/page.tsx` :**
|
|
371
|
+
```tsx
|
|
372
|
+
'use client'
|
|
373
|
+
import { useEffect } from 'react'
|
|
374
|
+
import { CinetPaySeamless } from 'cinetpay-seamless'
|
|
375
|
+
|
|
376
|
+
export default function CheckoutPage() {
|
|
377
|
+
useEffect(() => {
|
|
378
|
+
const unsub = CinetPaySeamless.on('payment.success', (data) => {
|
|
379
|
+
window.location.href = `/orders/${data.transactionId}/success`
|
|
380
|
+
})
|
|
381
|
+
return unsub
|
|
382
|
+
}, [])
|
|
383
|
+
|
|
384
|
+
const handlePay = async () => {
|
|
385
|
+
const res = await fetch('/api/pay', {
|
|
386
|
+
method: 'POST',
|
|
387
|
+
headers: { 'Content-Type': 'application/json' },
|
|
388
|
+
body: JSON.stringify({
|
|
389
|
+
amount: 5000,
|
|
390
|
+
orderId: `CMD-${Date.now()}`,
|
|
391
|
+
email: 'client@email.com',
|
|
392
|
+
firstName: 'Jean',
|
|
393
|
+
lastName: 'Dupont',
|
|
394
|
+
phone: '+2250707000000',
|
|
395
|
+
}),
|
|
396
|
+
})
|
|
397
|
+
const { paymentToken } = await res.json()
|
|
398
|
+
CinetPaySeamless.open({ paymentToken, debug: true })
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return <button onClick={handlePay}>Payer 5 000 XOF</button>
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### React
|
|
406
|
+
|
|
407
|
+
```tsx
|
|
408
|
+
import { useEffect, useState } from 'react'
|
|
409
|
+
import { CinetPaySeamless } from 'cinetpay-seamless'
|
|
410
|
+
|
|
411
|
+
function PayButton({ amount }: { amount: number }) {
|
|
412
|
+
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
|
|
413
|
+
|
|
414
|
+
useEffect(() => {
|
|
415
|
+
const unsub1 = CinetPaySeamless.on('payment.success', () => setStatus('success'))
|
|
416
|
+
const unsub2 = CinetPaySeamless.on('payment.failed', () => setStatus('error'))
|
|
417
|
+
const unsub3 = CinetPaySeamless.on('close', () => setStatus('idle'))
|
|
418
|
+
return () => { unsub1(); unsub2(); unsub3() }
|
|
419
|
+
}, []) // Pas de deps — les listeners sont stables
|
|
420
|
+
|
|
421
|
+
const handlePay = async () => {
|
|
422
|
+
setStatus('loading')
|
|
423
|
+
const res = await fetch('/api/pay', {
|
|
424
|
+
method: 'POST',
|
|
425
|
+
headers: { 'Content-Type': 'application/json' },
|
|
426
|
+
body: JSON.stringify({ amount }),
|
|
427
|
+
})
|
|
428
|
+
const { paymentToken } = await res.json()
|
|
429
|
+
CinetPaySeamless.open({ paymentToken })
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return (
|
|
433
|
+
<div>
|
|
434
|
+
<button onClick={handlePay} disabled={status === 'loading'}>
|
|
435
|
+
{status === 'loading' ? 'Chargement...' : `Payer ${amount} XOF`}
|
|
436
|
+
</button>
|
|
437
|
+
{status === 'success' && <p>Paiement réussi !</p>}
|
|
438
|
+
{status === 'error' && <p>Paiement échoué</p>}
|
|
439
|
+
</div>
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Vue 3
|
|
445
|
+
|
|
446
|
+
```vue
|
|
447
|
+
<script setup lang="ts">
|
|
448
|
+
import { onMounted, onUnmounted, ref } from 'vue'
|
|
449
|
+
import { CinetPaySeamless } from 'cinetpay-seamless'
|
|
450
|
+
|
|
451
|
+
const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle')
|
|
452
|
+
let unsubs: (() => void)[] = []
|
|
453
|
+
|
|
454
|
+
onMounted(() => {
|
|
455
|
+
unsubs.push(
|
|
456
|
+
CinetPaySeamless.on('payment.success', () => { status.value = 'success' }),
|
|
457
|
+
CinetPaySeamless.on('payment.failed', () => { status.value = 'error' }),
|
|
458
|
+
CinetPaySeamless.on('close', () => { if (status.value === 'loading') status.value = 'idle' }),
|
|
459
|
+
)
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
onUnmounted(() => unsubs.forEach(u => u()))
|
|
463
|
+
|
|
464
|
+
async function pay() {
|
|
465
|
+
status.value = 'loading'
|
|
466
|
+
const res = await fetch('/api/pay', {
|
|
467
|
+
method: 'POST',
|
|
468
|
+
headers: { 'Content-Type': 'application/json' },
|
|
469
|
+
body: JSON.stringify({ amount: 5000 }),
|
|
470
|
+
})
|
|
471
|
+
const { paymentToken } = await res.json()
|
|
472
|
+
CinetPaySeamless.open({ paymentToken })
|
|
473
|
+
}
|
|
474
|
+
</script>
|
|
475
|
+
|
|
476
|
+
<template>
|
|
477
|
+
<button @click="pay" :disabled="status === 'loading'">
|
|
478
|
+
{{ status === 'loading' ? 'Chargement...' : 'Payer 5 000 XOF' }}
|
|
479
|
+
</button>
|
|
480
|
+
<p v-if="status === 'success'">Paiement réussi !</p>
|
|
481
|
+
<p v-if="status === 'error'">Paiement échoué</p>
|
|
482
|
+
</template>
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Formulaire HTML complet
|
|
486
|
+
|
|
487
|
+
```html
|
|
488
|
+
<!DOCTYPE html>
|
|
489
|
+
<html lang="fr">
|
|
490
|
+
<head>
|
|
491
|
+
<meta charset="UTF-8">
|
|
492
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
493
|
+
<title>Paiement</title>
|
|
494
|
+
<script src="https://unpkg.com/cinetpay-seamless/dist/cinetpay-seamless.umd.cjs"></script>
|
|
495
|
+
</head>
|
|
496
|
+
<body>
|
|
497
|
+
<form id="payment-form">
|
|
498
|
+
<input type="text" id="firstName" required placeholder="Prénom">
|
|
499
|
+
<input type="text" id="lastName" required placeholder="Nom">
|
|
500
|
+
<input type="email" id="email" required placeholder="Email">
|
|
501
|
+
<input type="tel" id="phone" required placeholder="+2250707000000">
|
|
502
|
+
<input type="number" id="amount" value="5000" min="100">
|
|
503
|
+
<button type="submit" id="payBtn">Payer</button>
|
|
504
|
+
</form>
|
|
505
|
+
|
|
506
|
+
<div id="status"></div>
|
|
507
|
+
|
|
508
|
+
<script>
|
|
509
|
+
CinetPaySeamless.on('payment.success', function(data) {
|
|
510
|
+
document.getElementById('status').textContent = 'Payé ! ' + data.amount + ' ' + data.currency
|
|
511
|
+
document.getElementById('payBtn').disabled = false
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
CinetPaySeamless.on('payment.failed', function() {
|
|
515
|
+
document.getElementById('status').textContent = 'Paiement refusé'
|
|
516
|
+
document.getElementById('payBtn').disabled = false
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
CinetPaySeamless.on('close', function() {
|
|
520
|
+
document.getElementById('payBtn').disabled = false
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
document.getElementById('payment-form').addEventListener('submit', function(e) {
|
|
524
|
+
e.preventDefault()
|
|
525
|
+
document.getElementById('payBtn').disabled = true
|
|
526
|
+
|
|
527
|
+
// Appeler votre backend pour obtenir le paymentToken
|
|
528
|
+
fetch('/api/pay', {
|
|
529
|
+
method: 'POST',
|
|
530
|
+
headers: { 'Content-Type': 'application/json' },
|
|
531
|
+
body: JSON.stringify({
|
|
532
|
+
amount: parseInt(document.getElementById('amount').value),
|
|
533
|
+
firstName: document.getElementById('firstName').value,
|
|
534
|
+
lastName: document.getElementById('lastName').value,
|
|
535
|
+
email: document.getElementById('email').value,
|
|
536
|
+
phone: document.getElementById('phone').value,
|
|
537
|
+
})
|
|
538
|
+
})
|
|
539
|
+
.then(function(res) { return res.json() })
|
|
540
|
+
.then(function(data) {
|
|
541
|
+
CinetPaySeamless.open({ paymentToken: data.paymentToken, debug: true })
|
|
542
|
+
})
|
|
543
|
+
.catch(function(err) {
|
|
544
|
+
document.getElementById('status').textContent = 'Erreur: ' + err.message
|
|
545
|
+
document.getElementById('payBtn').disabled = false
|
|
546
|
+
})
|
|
547
|
+
})
|
|
548
|
+
</script>
|
|
549
|
+
</body>
|
|
550
|
+
</html>
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Gestion d'erreur complète
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
CinetPaySeamless.on('ready', () => {
|
|
557
|
+
disablePayButton() // Empêcher les doubles clics
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
CinetPaySeamless.on('payment.success', (data) => {
|
|
561
|
+
showToast('success', `${data.amount} ${data.currency} payés !`)
|
|
562
|
+
redirectTo(`/orders/${data.transactionId}/success`)
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
CinetPaySeamless.on('payment.failed', () => {
|
|
566
|
+
showToast('error', 'Le paiement a été refusé.')
|
|
567
|
+
enablePayButton()
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
CinetPaySeamless.on('payment.pending', (data) => {
|
|
571
|
+
showToast('info', `En attente (${data.status})...`)
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
CinetPaySeamless.on('error', (err) => {
|
|
575
|
+
showToast('error', `Erreur: ${err.message}`)
|
|
576
|
+
enablePayButton()
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
CinetPaySeamless.on('close', ({ status }) => {
|
|
580
|
+
if (status === 'UNKNOWN') {
|
|
581
|
+
showToast('warning', 'Paiement annulé.')
|
|
582
|
+
}
|
|
583
|
+
enablePayButton()
|
|
584
|
+
})
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
## Debug
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
CinetPaySeamless.open({ paymentToken: 'abc...', debug: true })
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
```
|
|
594
|
+
[CinetPay Seamless] CinetPaySeamless.open() called
|
|
595
|
+
[CinetPay Seamless] Opening popup { paymentUrl: 'https://secure.cinetpay.net/checkout/abc...' }
|
|
596
|
+
[CinetPay Seamless] Iframe loaded — checkout ready
|
|
597
|
+
[CinetPay Seamless] Payment response: ACCEPTED { amount: 5000, currency: 'XOF', ... }
|
|
598
|
+
[CinetPay Seamless] Payment accepted
|
|
599
|
+
[CinetPay Seamless] Modal closed { lastStatus: 'ACCEPTED' }
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
## Sécurité
|
|
603
|
+
|
|
604
|
+
### Protection des clés API
|
|
605
|
+
|
|
606
|
+
Les clés API (`apiKey` / `apiPassword`) ne sont **jamais** utilisées côté frontend. Le Seamless reçoit uniquement un `paymentToken` opaque et à usage unique, généré par votre backend.
|
|
607
|
+
|
|
608
|
+
```
|
|
609
|
+
NE FAITES PAS FAITES
|
|
610
|
+
────────────────────────────────────────────────────────────────────────
|
|
611
|
+
Mettre apiKey dans le code frontend Initialiser le paiement côté serveur
|
|
612
|
+
Exposer apiPassword dans le JavaScript Passer uniquement le paymentToken au frontend
|
|
613
|
+
Stocker les clés dans le code source Utiliser des variables d'environnement (.env)
|
|
614
|
+
Utiliser les mêmes clés partout Clés sandbox (sk_test_) en dev, prod (sk_live_) en prod
|
|
615
|
+
Partager vos clés par email/chat Utiliser un gestionnaire de secrets
|
|
616
|
+
Commiter le .env dans git Ajouter .env dans .gitignore
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### Environnements
|
|
620
|
+
|
|
621
|
+
| Préfixe de clé | Environnement | Usage |
|
|
622
|
+
|---|---|---|
|
|
623
|
+
| `sk_test_...` | Sandbox (`api.cinetpay.net`) | Développement et tests |
|
|
624
|
+
| `sk_live_...` | Production (`api.cinetpay.co`) | Transactions réelles |
|
|
625
|
+
|
|
626
|
+
**Règles importantes :**
|
|
627
|
+
- Ne **jamais** utiliser des clés `sk_live_` en développement
|
|
628
|
+
- Ne **jamais** mélanger des clés `sk_test_` et `sk_live_` dans le même environnement
|
|
629
|
+
- Les SDKs backend (cinetpay-js, cinetpay-laravel-sdk) détectent automatiquement l'environnement depuis le préfixe de la clé
|
|
630
|
+
- En cas de compromission, changez immédiatement vos clés depuis le dashboard CinetPay
|
|
631
|
+
|
|
632
|
+
### Architecture recommandée
|
|
633
|
+
|
|
634
|
+
```mermaid
|
|
635
|
+
flowchart LR
|
|
636
|
+
F["Frontend\ncinetpay-seamless\n(aucun secret)"] -->|"fetch /api/pay"| B["Backend\ncinetpay-js / Laravel / API directe\n(apiKey + apiPassword en .env)"]
|
|
637
|
+
B -->|"POST /v1/payment"| C["CinetPay API"]
|
|
638
|
+
C -->|"paymentToken"| B
|
|
639
|
+
B -->|"paymentToken"| F
|
|
640
|
+
F -->|"popup"| D["CinetPay Checkout"]
|
|
641
|
+
D -->|"webhook"| B
|
|
642
|
+
D -->|"postMessage"| F
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Autres protections
|
|
646
|
+
|
|
647
|
+
- **postMessage** : whitelist stricte des domaines CinetPay (bloque les domaines lookalike)
|
|
648
|
+
- **paymentToken validé** : regex `[a-zA-Z0-9_-]{10,128}` avant injection dans l'URL
|
|
649
|
+
- **Popup bloquée** : détection et callback `onError` avec code `POPUP_BLOCKED`
|
|
650
|
+
- **Zero dépendance** runtime — aucun risque supply chain
|
|
651
|
+
|
|
652
|
+
## Support
|
|
653
|
+
|
|
654
|
+
Pour toute question : **support@cinetpay.com**
|
|
655
|
+
|
|
656
|
+
## Licence
|
|
657
|
+
|
|
658
|
+
MIT
|