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,678 @@
1
+ /**
2
+ * PayPalPayment - Utilitaire pour gérer les paiements PayPal
3
+ * Version avec création/suppression automatique des éléments DOM
4
+ *
5
+ * @example
6
+ * const paypal = new PayPalPayment('YOUR_CLIENT_ID');
7
+ * await paypal.initialize();
8
+ * await paypal.createTemporaryButton({
9
+ * amount: '50.00',
10
+ * currency: 'EUR'
11
+ * });
12
+ */
13
+ class PayPalPayment {
14
+ constructor(clientId, options = {}) {
15
+ this.clientId = clientId;
16
+ this.currency = options.currency || 'EUR';
17
+ this.locale = options.locale || 'fr_FR';
18
+ this.intent = options.intent || 'capture';
19
+ this.environment = options.environment || 'sandbox';
20
+
21
+ // Callbacks
22
+ this.onPaymentSuccess = options.onPaymentSuccess || null;
23
+ this.onPaymentError = options.onPaymentError || null;
24
+ this.onPaymentCancel = options.onPaymentCancel || null;
25
+ this.onPaymentProcessing = options.onPaymentProcessing || null;
26
+
27
+ // Stockage des éléments DOM temporaires
28
+ this.temporaryElements = {
29
+ script: null,
30
+ container: null,
31
+ buttonContainer: null,
32
+ overlay: null
33
+ };
34
+
35
+ this.isInitialized = false;
36
+ this.buttons = [];
37
+ }
38
+
39
+ /**
40
+ * Créer un élément DOM temporaire
41
+ */
42
+ createTemporaryElement(tagName, attributes = {}, parent = document.body) {
43
+ if (typeof document === 'undefined') {
44
+ throw new Error('DOM non disponible');
45
+ }
46
+
47
+ const element = document.createElement(tagName);
48
+
49
+ // Appliquer les attributs
50
+ Object.keys(attributes).forEach(key => {
51
+ if (key === 'style' && typeof attributes[key] === 'object') {
52
+ Object.assign(element.style, attributes[key]);
53
+ } else if (key === 'textContent') {
54
+ element.textContent = attributes[key];
55
+ } else if (key === 'innerHTML') {
56
+ element.innerHTML = attributes[key];
57
+ } else if (key.startsWith('on')) {
58
+ element[key] = attributes[key];
59
+ } else {
60
+ element.setAttribute(key, attributes[key]);
61
+ }
62
+ });
63
+
64
+ // Ajouter au parent
65
+ if (parent) {
66
+ parent.appendChild(element);
67
+ }
68
+
69
+ return element;
70
+ }
71
+
72
+ /**
73
+ * Supprimer un élément DOM temporaire
74
+ */
75
+ removeTemporaryElement(element) {
76
+ if (element && element.parentNode) {
77
+ element.parentNode.removeChild(element);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Nettoyer tous les éléments DOM temporaires
83
+ */
84
+ cleanupTemporaryElements() {
85
+ Object.keys(this.temporaryElements).forEach(key => {
86
+ if (this.temporaryElements[key]) {
87
+ this.removeTemporaryElement(this.temporaryElements[key]);
88
+ this.temporaryElements[key] = null;
89
+ }
90
+ });
91
+
92
+ // Fermer les boutons PayPal
93
+ this.buttons.forEach(button => {
94
+ if (button && typeof button.close === 'function') {
95
+ try {
96
+ button.close();
97
+ } catch (e) {
98
+ // Ignorer les erreurs de fermeture
99
+ }
100
+ }
101
+ });
102
+ this.buttons = [];
103
+ }
104
+
105
+ /**
106
+ * Créer un overlay temporaire
107
+ */
108
+ createOverlay(options = {}) {
109
+ if (this.temporaryElements.overlay) {
110
+ return this.temporaryElements.overlay;
111
+ }
112
+
113
+ this.temporaryElements.overlay = this.createTemporaryElement('div', {
114
+ id: 'paypal-overlay-' + Date.now(),
115
+ style: {
116
+ position: 'fixed',
117
+ top: '0',
118
+ left: '0',
119
+ width: '100%',
120
+ height: '100%',
121
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
122
+ zIndex: '9998',
123
+ display: 'flex',
124
+ justifyContent: 'center',
125
+ alignItems: 'center',
126
+ opacity: '0',
127
+ transition: 'opacity 0.3s ease'
128
+ }
129
+ }, document.body);
130
+
131
+ // Animer l'apparition
132
+ setTimeout(() => {
133
+ if (this.temporaryElements.overlay) {
134
+ this.temporaryElements.overlay.style.opacity = '1';
135
+ }
136
+ }, 10);
137
+
138
+ return this.temporaryElements.overlay;
139
+ }
140
+
141
+ /**
142
+ * Créer un conteneur modal temporaire
143
+ */
144
+ createTemporaryModal(options = {}) {
145
+ if (this.temporaryElements.container) {
146
+ return this.temporaryElements.container;
147
+ }
148
+
149
+ // Créer l'overlay
150
+ this.createOverlay();
151
+
152
+ // Créer la modal
153
+ this.temporaryElements.container = this.createTemporaryElement('div', {
154
+ id: 'paypal-modal-' + Date.now(),
155
+ style: {
156
+ position: 'fixed',
157
+ top: '50%',
158
+ left: '50%',
159
+ transform: 'translate(-50%, -50%)',
160
+ backgroundColor: '#fff',
161
+ borderRadius: '12px',
162
+ padding: '30px',
163
+ width: '400px',
164
+ maxWidth: '90%',
165
+ maxHeight: '90%',
166
+ overflow: 'auto',
167
+ zIndex: '9999',
168
+ boxShadow: '0 10px 30px rgba(0, 0, 0, 0.3)',
169
+ opacity: '0',
170
+ transition: 'opacity 0.3s ease, transform 0.3s ease'
171
+ }
172
+ }, document.body);
173
+
174
+ // Ajouter un bouton de fermeture
175
+ const closeButton = this.createTemporaryElement('button', {
176
+ style: {
177
+ position: 'absolute',
178
+ top: '15px',
179
+ right: '15px',
180
+ background: 'none',
181
+ border: 'none',
182
+ fontSize: '24px',
183
+ color: '#666',
184
+ cursor: 'pointer',
185
+ width: '30px',
186
+ height: '30px',
187
+ display: 'flex',
188
+ alignItems: 'center',
189
+ justifyContent: 'center',
190
+ borderRadius: '50%',
191
+ transition: 'background-color 0.2s'
192
+ },
193
+ textContent: '×',
194
+ onmouseover: function() { this.style.backgroundColor = '#f0f0f0'; },
195
+ onmouseout: function() { this.style.backgroundColor = 'transparent'; },
196
+ onclick: () => this.destroyTemporaryModal()
197
+ }, this.temporaryElements.container);
198
+
199
+ // Ajouter un titre
200
+ if (options.title) {
201
+ const title = this.createTemporaryElement('h2', {
202
+ style: {
203
+ margin: '0 0 20px 0',
204
+ color: '#333',
205
+ fontSize: '24px',
206
+ textAlign: 'center'
207
+ },
208
+ textContent: options.title
209
+ }, this.temporaryElements.container);
210
+ }
211
+
212
+ // Animer l'apparition
213
+ setTimeout(() => {
214
+ if (this.temporaryElements.container) {
215
+ this.temporaryElements.container.style.opacity = '1';
216
+ this.temporaryElements.container.style.transform = 'translate(-50%, -50%)';
217
+ }
218
+ }, 10);
219
+
220
+ return this.temporaryElements.container;
221
+ }
222
+
223
+ /**
224
+ * Détruire la modal temporaire
225
+ */
226
+ destroyTemporaryModal() {
227
+ if (this.temporaryElements.container) {
228
+ // Animation de disparition
229
+ this.temporaryElements.container.style.opacity = '0';
230
+ this.temporaryElements.container.style.transform = 'translate(-50%, -60%)';
231
+
232
+ setTimeout(() => {
233
+ this.removeTemporaryElement(this.temporaryElements.container);
234
+ this.temporaryElements.container = null;
235
+ }, 300);
236
+ }
237
+
238
+ if (this.temporaryElements.overlay) {
239
+ // Animation de disparition de l'overlay
240
+ this.temporaryElements.overlay.style.opacity = '0';
241
+
242
+ setTimeout(() => {
243
+ this.removeTemporaryElement(this.temporaryElements.overlay);
244
+ this.temporaryElements.overlay = null;
245
+ }, 300);
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Charger le script PayPal SDK avec gestion temporaire
251
+ */
252
+ loadPayPalScript() {
253
+ return new Promise((resolve, reject) => {
254
+ if (typeof paypal !== 'undefined') {
255
+ resolve();
256
+ return;
257
+ }
258
+
259
+ // Vérifier si un script existe déjà
260
+ const existingScript = document.querySelector('script[src*="paypal.com/sdk/js"]');
261
+ if (existingScript) {
262
+ this.temporaryElements.script = existingScript;
263
+
264
+ // Vérifier que PayPal est chargé
265
+ const checkPayPal = () => {
266
+ if (typeof paypal !== 'undefined') {
267
+ resolve();
268
+ } else {
269
+ setTimeout(checkPayPal, 100);
270
+ }
271
+ };
272
+ checkPayPal();
273
+ return;
274
+ }
275
+
276
+ try {
277
+ // Créer le script temporaire
278
+ const scriptUrl = `https://www.paypal.com/sdk/js?client-id=${this.clientId}&currency=${this.currency}&locale=${this.locale}&intent=${this.intent}`;
279
+
280
+ this.temporaryElements.script = this.createTemporaryElement('script', {
281
+ src: scriptUrl,
282
+ async: true,
283
+ onload: () => {
284
+ if (typeof paypal === 'undefined') {
285
+ reject(new Error('SDK PayPal chargé mais non défini'));
286
+ return;
287
+ }
288
+ resolve();
289
+ },
290
+ onerror: () => {
291
+ reject(new Error('Échec du chargement du SDK PayPal'));
292
+ this.cleanupTemporaryElements();
293
+ }
294
+ }, document.head);
295
+
296
+ } catch (error) {
297
+ reject(error);
298
+ }
299
+ });
300
+ }
301
+
302
+ /**
303
+ * Initialiser PayPal SDK
304
+ */
305
+ async initialize() {
306
+ if (this.isInitialized) return;
307
+
308
+ try {
309
+ await this.loadPayPalScript();
310
+ this.isInitialized = true;
311
+ console.log('✅ PayPal initialisé');
312
+ } catch (error) {
313
+ console.error('❌ Erreur initialisation PayPal:', error);
314
+ this.cleanupTemporaryElements();
315
+ throw error;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Créer un bouton PayPal dans un conteneur temporaire
321
+ */
322
+ async createTemporaryButton(orderData) {
323
+ if (!this.isInitialized) {
324
+ await this.initialize();
325
+ }
326
+
327
+ try {
328
+ // Créer la modal temporaire
329
+ const modal = this.createTemporaryModal({
330
+ title: orderData.title || 'Paiement sécurisé'
331
+ });
332
+
333
+ // Créer un conteneur pour le bouton
334
+ this.temporaryElements.buttonContainer = this.createTemporaryElement('div', {
335
+ id: 'paypal-button-container-' + Date.now(),
336
+ style: {
337
+ margin: '20px 0',
338
+ minHeight: '50px'
339
+ }
340
+ }, modal);
341
+
342
+ // Créer le bouton PayPal
343
+ const button = paypal.Buttons({
344
+ style: orderData.style || {
345
+ layout: 'vertical',
346
+ color: 'gold',
347
+ shape: 'rect',
348
+ label: 'paypal',
349
+ height: 45
350
+ },
351
+
352
+ // Créer la commande
353
+ createOrder: async (data, actions) => {
354
+ if (this.onPaymentProcessing) {
355
+ this.onPaymentProcessing();
356
+ }
357
+
358
+ try {
359
+ // Si serverUrl fourni, créer la commande côté serveur
360
+ if (orderData.serverUrl) {
361
+ const response = await fetch(orderData.serverUrl, {
362
+ method: 'POST',
363
+ headers: { 'Content-Type': 'application/json' },
364
+ body: JSON.stringify({
365
+ amount: orderData.amount,
366
+ currency: orderData.currency || this.currency,
367
+ description: orderData.description || '',
368
+ items: orderData.items || []
369
+ })
370
+ });
371
+
372
+ const data = await response.json();
373
+ return data.orderID;
374
+ }
375
+
376
+ // Sinon, créer la commande directement
377
+ return actions.order.create({
378
+ purchase_units: [{
379
+ amount: {
380
+ currency_code: orderData.currency || this.currency,
381
+ value: orderData.amount,
382
+ breakdown: orderData.breakdown || undefined
383
+ },
384
+ description: orderData.description || '',
385
+ items: orderData.items || undefined,
386
+ shipping: orderData.shipping || undefined
387
+ }],
388
+ application_context: {
389
+ brand_name: orderData.brandName || '',
390
+ shipping_preference: orderData.shippingPreference || 'NO_SHIPPING'
391
+ }
392
+ });
393
+
394
+ } catch (error) {
395
+ console.error('❌ Erreur createOrder:', error);
396
+ if (this.onPaymentError) {
397
+ this.onPaymentError(error);
398
+ }
399
+ throw error;
400
+ }
401
+ },
402
+
403
+ // Approuver le paiement
404
+ onApprove: async (data, actions) => {
405
+ try {
406
+ // Capturer le paiement
407
+ const details = await actions.order.capture();
408
+
409
+ console.log('✅ Paiement PayPal réussi:', details);
410
+
411
+ // Appeler le callback de succès
412
+ if (this.onPaymentSuccess) {
413
+ this.onPaymentSuccess(details);
414
+ }
415
+
416
+ // Nettoyer la modal après succès
417
+ setTimeout(() => {
418
+ this.destroyTemporaryModal();
419
+ }, 1000);
420
+
421
+ // Notifier le serveur si serverUrl fourni
422
+ if (orderData.onApproveUrl) {
423
+ await fetch(orderData.onApproveUrl, {
424
+ method: 'POST',
425
+ headers: { 'Content-Type': 'application/json' },
426
+ body: JSON.stringify({
427
+ orderID: data.orderID,
428
+ details: details
429
+ })
430
+ });
431
+ }
432
+
433
+ return details;
434
+
435
+ } catch (error) {
436
+ console.error('❌ Erreur onApprove:', error);
437
+ if (this.onPaymentError) {
438
+ this.onPaymentError(error);
439
+ }
440
+ throw error;
441
+ }
442
+ },
443
+
444
+ // Annulation du paiement
445
+ onCancel: (data) => {
446
+ console.log('⚠️ Paiement annulé:', data);
447
+ if (this.onPaymentCancel) {
448
+ this.onPaymentCancel(data);
449
+ }
450
+ },
451
+
452
+ // Erreur
453
+ onError: (err) => {
454
+ console.error('❌ Erreur PayPal:', err);
455
+ if (this.onPaymentError) {
456
+ this.onPaymentError(err);
457
+ }
458
+ }
459
+ });
460
+
461
+ // Rendre le bouton
462
+ if (button.isEligible()) {
463
+ await button.render(`#${this.temporaryElements.buttonContainer.id}`);
464
+ this.buttons.push(button);
465
+
466
+ // Ajouter un message d'information
467
+ const info = this.createTemporaryElement('p', {
468
+ style: {
469
+ textAlign: 'center',
470
+ color: '#666',
471
+ fontSize: '14px',
472
+ marginTop: '20px'
473
+ },
474
+ textContent: 'Paiement 100% sécurisé par PayPal'
475
+ }, modal);
476
+
477
+ console.log('✅ Bouton PayPal créé dans modal temporaire');
478
+ return button;
479
+
480
+ } else {
481
+ throw new Error('Bouton PayPal non éligible');
482
+ }
483
+
484
+ } catch (error) {
485
+ console.error('❌ Erreur création bouton:', error);
486
+ this.cleanupTemporaryElements();
487
+ throw error;
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Créer un formulaire de paiement alternatif (cartes bancaires)
493
+ */
494
+ async createAlternativePaymentForm(orderData) {
495
+ if (!this.isInitialized) {
496
+ await this.initialize();
497
+ }
498
+
499
+ try {
500
+ // Créer la modal temporaire
501
+ const modal = this.createTemporaryModal({
502
+ title: 'Paiement par carte bancaire'
503
+ });
504
+
505
+ // Créer un conteneur pour les boutons de paiement
506
+ this.temporaryElements.buttonContainer = this.createTemporaryElement('div', {
507
+ id: 'paypal-cards-container-' + Date.now(),
508
+ style: {
509
+ margin: '20px 0'
510
+ }
511
+ }, modal);
512
+
513
+ // Créer les boutons PayPal (tous les financements)
514
+ const buttons = paypal.Buttons({
515
+ fundingSource: undefined, // Affiche tous les moyens
516
+
517
+ style: orderData.style || {
518
+ layout: 'vertical',
519
+ color: 'black',
520
+ shape: 'rect',
521
+ label: 'checkout',
522
+ height: 45
523
+ },
524
+
525
+ createOrder: async (data, actions) => {
526
+ return this.createOrder(orderData);
527
+ },
528
+
529
+ onApprove: async (data, actions) => {
530
+ const details = await actions.order.capture();
531
+
532
+ if (this.onPaymentSuccess) {
533
+ this.onPaymentSuccess(details);
534
+ }
535
+
536
+ setTimeout(() => {
537
+ this.destroyTemporaryModal();
538
+ }, 1000);
539
+
540
+ return details;
541
+ },
542
+
543
+ onCancel: (data) => {
544
+ if (this.onPaymentCancel) {
545
+ this.onPaymentCancel(data);
546
+ }
547
+ },
548
+
549
+ onError: (err) => {
550
+ if (this.onPaymentError) {
551
+ this.onPaymentError(err);
552
+ }
553
+ }
554
+ });
555
+
556
+ await buttons.render(`#${this.temporaryElements.buttonContainer.id}`);
557
+ this.buttons.push(buttons);
558
+
559
+ console.log('✅ Formulaire de paiement alternatif créé');
560
+ return buttons;
561
+
562
+ } catch (error) {
563
+ console.error('❌ Erreur création formulaire:', error);
564
+ this.cleanupTemporaryElements();
565
+ throw error;
566
+ }
567
+ }
568
+
569
+ /**
570
+ * Méthode utilitaire pour créer une commande
571
+ */
572
+ async createOrder(orderData) {
573
+ if (this.onPaymentProcessing) {
574
+ this.onPaymentProcessing();
575
+ }
576
+
577
+ if (orderData.serverUrl) {
578
+ const response = await fetch(orderData.serverUrl, {
579
+ method: 'POST',
580
+ headers: { 'Content-Type': 'application/json' },
581
+ body: JSON.stringify({
582
+ amount: orderData.amount,
583
+ currency: orderData.currency || this.currency,
584
+ description: orderData.description || '',
585
+ items: orderData.items || []
586
+ })
587
+ });
588
+
589
+ const data = await response.json();
590
+ return data.orderID;
591
+ }
592
+
593
+ // Création côté client par défaut
594
+ return paypal.rest.payment.create({
595
+ transactions: [{
596
+ amount: {
597
+ total: orderData.amount,
598
+ currency: orderData.currency || this.currency
599
+ }
600
+ }]
601
+ });
602
+ }
603
+
604
+ /**
605
+ * Lancer un paiement PayPal simple
606
+ */
607
+ async startPaymentFlow(orderData) {
608
+ try {
609
+ // Initialiser PayPal
610
+ await this.initialize();
611
+
612
+ // Créer le bouton dans une modal
613
+ await this.createTemporaryButton(orderData);
614
+
615
+ return {
616
+ success: true,
617
+ message: 'Interface de paiement PayPal prête'
618
+ };
619
+
620
+ } catch (error) {
621
+ console.error('❌ Erreur démarrage paiement:', error);
622
+ this.cleanupTemporaryElements();
623
+ throw error;
624
+ }
625
+ }
626
+
627
+ /**
628
+ * Fermer le flux de paiement manuellement
629
+ */
630
+ closePaymentFlow() {
631
+ this.destroyTemporaryModal();
632
+ }
633
+
634
+ /**
635
+ * Détruire proprement toutes les ressources
636
+ */
637
+ destroy() {
638
+ // Détruire la modal si elle existe
639
+ this.destroyTemporaryModal();
640
+
641
+ // Nettoyer tous les éléments temporaires
642
+ this.cleanupTemporaryElements();
643
+
644
+ // Réinitialiser l'état
645
+ this.isInitialized = false;
646
+
647
+ console.log('✅ PayPalPayment détruit proprement');
648
+ }
649
+
650
+ /**
651
+ * Détruire avec callback
652
+ */
653
+ destroyWithCallback(onComplete) {
654
+ this.destroy();
655
+ if (typeof onComplete === 'function') {
656
+ onComplete();
657
+ }
658
+ }
659
+ }
660
+
661
+ // Méthodes statiques utilitaires
662
+ PayPalPayment.isPayPalSupported = function() {
663
+ return typeof window !== 'undefined' &&
664
+ typeof document !== 'undefined' &&
665
+ typeof paypal !== 'undefined';
666
+ };
667
+
668
+ PayPalPayment.getSupportedFundingSources = function() {
669
+ if (typeof paypal === 'undefined') return [];
670
+
671
+ return paypal.getFundingSources ?
672
+ paypal.getFundingSources().map(source => ({
673
+ source: source,
674
+ eligible: paypal.isFundingEligible ? paypal.isFundingEligible(source) : true
675
+ })) : [];
676
+ };
677
+
678
+ export default PayPalPayment;