canvasframework 0.4.4 → 0.4.5

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,552 @@
1
+ /**
2
+ * StripePayment - Utilitaire pour gérer les paiements Stripe dans Canvas
3
+ * Version avec création/suppression automatique des éléments DOM
4
+ *
5
+ * @example
6
+ * const stripe = new StripePayment('pk_test_xxxxx', {
7
+ * canvasContainer: canvasElement,
8
+ * onElementReady: (element) => { /* afficher dans canvas *\/ }
9
+ * });
10
+ */
11
+ class StripePayment {
12
+ constructor(publishableKey, options = {}) {
13
+ this.publishableKey = publishableKey;
14
+ this.stripe = null;
15
+ this.elements = null;
16
+ this.cardElement = null;
17
+
18
+ // Options spécifiques Canvas
19
+ this.canvasContainer = options.canvasContainer || null;
20
+ this.canvasContext = options.canvasContext || null;
21
+ this.onElementReady = options.onElementReady || null;
22
+ this.onErrorDisplay = options.onErrorDisplay || null;
23
+
24
+ // Options Stripe
25
+ this.locale = options.locale || 'fr';
26
+ this.appearance = options.appearance || {
27
+ theme: 'stripe',
28
+ variables: {
29
+ colorPrimary: '#0570de',
30
+ colorBackground: '#ffffff',
31
+ colorText: '#30313d',
32
+ colorDanger: '#df1b41',
33
+ fontFamily: 'system-ui, sans-serif',
34
+ spacingUnit: '4px',
35
+ borderRadius: '8px'
36
+ }
37
+ };
38
+
39
+ this.onPaymentSuccess = options.onPaymentSuccess || null;
40
+ this.onPaymentError = options.onPaymentError || null;
41
+ this.onPaymentProcessing = options.onPaymentProcessing || null;
42
+
43
+ // Stockage des éléments DOM temporaires
44
+ this.temporaryElements = {
45
+ script: null,
46
+ container: null,
47
+ iframeContainer: null
48
+ };
49
+
50
+ this.isInitialized = false;
51
+ this.isProcessing = false;
52
+ this.paymentFormData = {
53
+ cardNumber: '',
54
+ expiry: '',
55
+ cvc: '',
56
+ postalCode: ''
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Créer un élément DOM temporaire
62
+ */
63
+ createTemporaryElement(tagName, attributes = {}, parent = document.body) {
64
+ if (typeof document === 'undefined') {
65
+ throw new Error('DOM non disponible');
66
+ }
67
+
68
+ const element = document.createElement(tagName);
69
+
70
+ // Appliquer les attributs
71
+ Object.keys(attributes).forEach(key => {
72
+ if (key === 'style' && typeof attributes[key] === 'object') {
73
+ Object.assign(element.style, attributes[key]);
74
+ } else if (key === 'textContent') {
75
+ element.textContent = attributes[key];
76
+ } else if (key === 'innerHTML') {
77
+ element.innerHTML = attributes[key];
78
+ } else {
79
+ element.setAttribute(key, attributes[key]);
80
+ }
81
+ });
82
+
83
+ // Ajouter au parent
84
+ if (parent) {
85
+ parent.appendChild(element);
86
+ }
87
+
88
+ return element;
89
+ }
90
+
91
+ /**
92
+ * Supprimer un élément DOM temporaire
93
+ */
94
+ removeTemporaryElement(element) {
95
+ if (element && element.parentNode) {
96
+ element.parentNode.removeChild(element);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Nettoyer tous les éléments DOM temporaires
102
+ */
103
+ cleanupTemporaryElements() {
104
+ Object.keys(this.temporaryElements).forEach(key => {
105
+ if (this.temporaryElements[key]) {
106
+ this.removeTemporaryElement(this.temporaryElements[key]);
107
+ this.temporaryElements[key] = null;
108
+ }
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Initialiser Stripe.js avec gestion automatique du script
114
+ */
115
+ async initialize() {
116
+ if (this.isInitialized) return;
117
+
118
+ try {
119
+ // Charger Stripe.js dynamiquement
120
+ await this.loadStripeScript();
121
+
122
+ // Initialiser Stripe
123
+ this.stripe = window.Stripe(this.publishableKey, {
124
+ locale: this.locale,
125
+ betas: ['elements_enable_deferred_intent_beta_1']
126
+ });
127
+
128
+ this.isInitialized = true;
129
+
130
+ } catch (error) {
131
+ console.error('❌ Erreur initialisation Stripe:', error);
132
+ this.cleanupTemporaryElements();
133
+ throw error;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Charger Stripe.js avec création/suppression automatique du script
139
+ */
140
+ loadStripeScript() {
141
+ return new Promise((resolve, reject) => {
142
+ // Vérifier si Stripe est déjà chargé
143
+ if (typeof window.Stripe !== 'undefined') {
144
+ resolve();
145
+ return;
146
+ }
147
+
148
+ // Vérifier si un script existe déjà
149
+ const existingScript = document.querySelector('script[src="https://js.stripe.com/v3/"]');
150
+ if (existingScript) {
151
+ // Utiliser le script existant
152
+ this.temporaryElements.script = existingScript;
153
+ resolve();
154
+ return;
155
+ }
156
+
157
+ // Créer un nouveau script
158
+ try {
159
+ this.temporaryElements.script = this.createTemporaryElement('script', {
160
+ src: 'https://js.stripe.com/v3/',
161
+ async: true,
162
+ onload: () => {
163
+ if (typeof window.Stripe === 'undefined') {
164
+ reject(new Error('Stripe.js chargé mais non défini'));
165
+ return;
166
+ }
167
+ resolve();
168
+ },
169
+ onerror: () => {
170
+ reject(new Error('Échec du chargement de Stripe.js'));
171
+ this.cleanupTemporaryElements();
172
+ }
173
+ }, document.head);
174
+
175
+ } catch (error) {
176
+ reject(error);
177
+ }
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Créer un conteneur temporaire pour les éléments de paiement
183
+ */
184
+ createTemporaryContainer(options = {}) {
185
+ if (!this.temporaryElements.container) {
186
+ this.temporaryElements.container = this.createTemporaryElement('div', {
187
+ id: 'stripe-payment-container-' + Date.now(),
188
+ style: {
189
+ position: 'fixed',
190
+ top: '0',
191
+ left: '0',
192
+ width: '100%',
193
+ height: '100%',
194
+ zIndex: '9999',
195
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
196
+ display: 'flex',
197
+ justifyContent: 'center',
198
+ alignItems: 'center'
199
+ }
200
+ }, document.body);
201
+
202
+ // Ajouter un bouton de fermeture
203
+ const closeButton = this.createTemporaryElement('button', {
204
+ style: {
205
+ position: 'absolute',
206
+ top: '20px',
207
+ right: '20px',
208
+ background: 'none',
209
+ border: 'none',
210
+ fontSize: '24px',
211
+ color: '#fff',
212
+ cursor: 'pointer',
213
+ zIndex: '10000'
214
+ },
215
+ textContent: '×',
216
+ onclick: () => this.destroyTemporaryContainer()
217
+ }, this.temporaryElements.container);
218
+
219
+ // Ajouter un conteneur pour le formulaire
220
+ const formContainer = this.createTemporaryElement('div', {
221
+ style: {
222
+ backgroundColor: '#fff',
223
+ padding: '30px',
224
+ borderRadius: '12px',
225
+ width: '400px',
226
+ maxWidth: '90%',
227
+ maxHeight: '90%',
228
+ overflow: 'auto',
229
+ position: 'relative'
230
+ }
231
+ }, this.temporaryElements.container);
232
+
233
+ // Retourner l'ID du conteneur pour Stripe Elements
234
+ return formContainer;
235
+ }
236
+
237
+ return this.temporaryElements.container;
238
+ }
239
+
240
+ /**
241
+ * Détruire le conteneur temporaire
242
+ */
243
+ destroyTemporaryContainer() {
244
+ if (this.temporaryElements.container) {
245
+ this.removeTemporaryElement(this.temporaryElements.container);
246
+ this.temporaryElements.container = null;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Créer et monter un élément de carte dans un conteneur temporaire
252
+ */
253
+ async createCardElementInTemporaryContainer() {
254
+ if (!this.isInitialized) {
255
+ throw new Error('Stripe non initialisé');
256
+ }
257
+
258
+ try {
259
+ // Créer le conteneur temporaire
260
+ const formContainer = this.createTemporaryContainer();
261
+
262
+ // Créer un div pour l'élément de carte
263
+ const cardContainer = this.createTemporaryElement('div', {
264
+ id: 'stripe-card-element-' + Date.now(),
265
+ style: {
266
+ margin: '20px 0'
267
+ }
268
+ }, formContainer);
269
+
270
+ // Créer les éléments Stripe
271
+ this.elements = this.stripe.elements({
272
+ appearance: this.appearance,
273
+ locale: this.locale
274
+ });
275
+
276
+ // Créer et monter l'élément de carte
277
+ this.cardElement = this.elements.create('card', {
278
+ style: {
279
+ base: {
280
+ fontSize: '16px',
281
+ color: '#32325d',
282
+ fontFamily: 'system-ui, sans-serif',
283
+ '::placeholder': {
284
+ color: '#aab7c4'
285
+ }
286
+ },
287
+ invalid: {
288
+ color: '#fa755a',
289
+ iconColor: '#fa755a'
290
+ }
291
+ },
292
+ hidePostalCode: false
293
+ });
294
+
295
+ this.cardElement.mount(cardContainer);
296
+
297
+ // Ajouter un bouton de paiement
298
+ const payButton = this.createTemporaryElement('button', {
299
+ style: {
300
+ backgroundColor: '#5469d4',
301
+ color: '#fff',
302
+ border: 'none',
303
+ padding: '12px 24px',
304
+ borderRadius: '6px',
305
+ fontSize: '16px',
306
+ cursor: 'pointer',
307
+ width: '100%',
308
+ marginTop: '20px'
309
+ },
310
+ textContent: 'Payer',
311
+ onclick: () => this.processPaymentWithTemporaryElement()
312
+ }, formContainer);
313
+
314
+ // Configurer les écouteurs
315
+ this.setupElementListeners();
316
+
317
+ return this.cardElement;
318
+
319
+ } catch (error) {
320
+ console.error('❌ Erreur création élément:', error);
321
+ this.cleanupTemporaryElements();
322
+ throw error;
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Configurer les écouteurs pour l'élément
328
+ */
329
+ setupElementListeners() {
330
+ if (!this.cardElement) return;
331
+
332
+ this.cardElement.on('change', (event) => {
333
+ if (event.error && this.onErrorDisplay) {
334
+ this.onErrorDisplay(event.error.message);
335
+ }
336
+
337
+ if (event.value) {
338
+ this.paymentFormData = {
339
+ ...this.paymentFormData,
340
+ ...event.value
341
+ };
342
+ }
343
+ });
344
+ }
345
+
346
+ /**
347
+ * Traiter le paiement avec l'élément temporaire
348
+ */
349
+ async processPaymentWithTemporaryElement(paymentData) {
350
+ if (!this.cardElement || this.isProcessing) {
351
+ return;
352
+ }
353
+
354
+ this.isProcessing = true;
355
+
356
+ try {
357
+ // Créer un PaymentIntent
358
+ const clientSecret = await this.createPaymentIntent(paymentData);
359
+
360
+ // Confirmer le paiement
361
+ const { error, paymentIntent } = await this.stripe.confirmCardPayment(clientSecret, {
362
+ payment_method: {
363
+ card: this.cardElement
364
+ },
365
+ billing_details: paymentData.billingDetails || {}
366
+ });
367
+
368
+ if (error) {
369
+ throw error;
370
+ }
371
+
372
+ // Succès
373
+ if (this.onPaymentSuccess) {
374
+ this.onPaymentSuccess(paymentIntent);
375
+ }
376
+
377
+ // Nettoyer après succès
378
+ this.destroyTemporaryContainer();
379
+
380
+ return { success: true, paymentIntent };
381
+
382
+ } catch (error) {
383
+ console.error('❌ Erreur paiement:', error);
384
+
385
+ if (this.onPaymentError) {
386
+ this.onPaymentError(error);
387
+ }
388
+
389
+ return { success: false, error: error.message };
390
+
391
+ } finally {
392
+ this.isProcessing = false;
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Créer un PaymentIntent
398
+ */
399
+ async createPaymentIntent(paymentData) {
400
+ const {
401
+ amount,
402
+ currency = 'eur',
403
+ description = '',
404
+ metadata = {},
405
+ serverUrl = '/api/create-payment-intent'
406
+ } = paymentData;
407
+
408
+ try {
409
+ const response = await fetch(serverUrl, {
410
+ method: 'POST',
411
+ headers: {
412
+ 'Content-Type': 'application/json'
413
+ },
414
+ body: JSON.stringify({
415
+ amount,
416
+ currency,
417
+ description,
418
+ metadata
419
+ })
420
+ });
421
+
422
+ if (!response.ok) {
423
+ throw new Error('Erreur création PaymentIntent');
424
+ }
425
+
426
+ const data = await response.json();
427
+ return data.clientSecret;
428
+
429
+ } catch (error) {
430
+ console.error('❌ Erreur createPaymentIntent:', error);
431
+ throw error;
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Interface simplifiée pour le paiement
437
+ */
438
+ async startPaymentFlow(paymentData) {
439
+ try {
440
+ // Initialiser Stripe
441
+ await this.initialize();
442
+
443
+ // Créer l'interface de paiement
444
+ await this.createCardElementInTemporaryContainer();
445
+
446
+ // Stocker les données de paiement pour utilisation ultérieure
447
+ this.currentPaymentData = paymentData;
448
+
449
+ return { success: true, message: 'Interface de paiement prête' };
450
+
451
+ } catch (error) {
452
+ console.error('❌ Erreur démarrage paiement:', error);
453
+ this.cleanupTemporaryElements();
454
+ throw error;
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Méthode Checkout (redirection)
460
+ */
461
+ async redirectToCheckout(paymentData) {
462
+ if (!this.isInitialized) {
463
+ await this.initialize();
464
+ }
465
+
466
+ try {
467
+ // Créer une session de checkout côté serveur
468
+ const response = await fetch('/api/create-checkout-session', {
469
+ method: 'POST',
470
+ headers: {
471
+ 'Content-Type': 'application/json'
472
+ },
473
+ body: JSON.stringify({
474
+ amount: paymentData.amount,
475
+ currency: paymentData.currency || 'eur',
476
+ successUrl: paymentData.successUrl || `${window.location.origin}/success`,
477
+ cancelUrl: paymentData.cancelUrl || `${window.location.origin}/cancel`,
478
+ metadata: paymentData.metadata || {}
479
+ })
480
+ });
481
+
482
+ const session = await response.json();
483
+
484
+ // Rediriger vers Stripe Checkout
485
+ const result = await this.stripe.redirectToCheckout({
486
+ sessionId: session.id
487
+ });
488
+
489
+ if (result.error) {
490
+ throw result.error;
491
+ }
492
+
493
+ } catch (error) {
494
+ console.error('❌ Erreur Checkout:', error);
495
+ throw error;
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Nettoyer toutes les ressources
501
+ */
502
+ destroy() {
503
+ // Détruire les éléments Stripe
504
+ if (this.cardElement) {
505
+ try {
506
+ this.cardElement.destroy();
507
+ } catch (e) {
508
+ // Ignorer les erreurs de destruction
509
+ }
510
+ this.cardElement = null;
511
+ }
512
+
513
+ if (this.elements) {
514
+ this.elements = null;
515
+ }
516
+
517
+ this.stripe = null;
518
+ this.isInitialized = false;
519
+ this.isProcessing = false;
520
+
521
+ // Nettoyer tous les éléments DOM temporaires
522
+ this.cleanupTemporaryElements();
523
+
524
+ }
525
+
526
+ /**
527
+ * Détruire proprement avec callback
528
+ */
529
+ destroyWithCallback(onComplete) {
530
+ this.destroy();
531
+ if (typeof onComplete === 'function') {
532
+ onComplete();
533
+ }
534
+ }
535
+ }
536
+
537
+ // Méthodes statiques utilitaires
538
+ StripePayment.isStripeSupported = function() {
539
+ return typeof window !== 'undefined' &&
540
+ typeof document !== 'undefined' &&
541
+ typeof window.Stripe !== 'undefined';
542
+ };
543
+
544
+ StripePayment.createHiddenInput = function(name, value) {
545
+ const input = document.createElement('input');
546
+ input.type = 'hidden';
547
+ input.name = name;
548
+ input.value = value;
549
+ return input;
550
+ };
551
+
552
+ export default StripePayment;