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 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