@vipros-org/sdk 3.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/vipros-sdk.js ADDED
@@ -0,0 +1,4190 @@
1
+ /**
2
+ * VIPros SDK v3.0.0-unified - Production Build
3
+ * Generated: 2026-02-10T11:29:53.281Z
4
+ * Environment: Production
5
+ */
6
+ var ViprosSDK = (function () {
7
+ 'use strict';
8
+
9
+ /**
10
+ * TemplateLoader - Gestionnaire de templates HTML modulaires
11
+ *
12
+ * Charge et met en cache les templates HTML externes pour le composant ViprosOfferCard.
13
+ * Utilise un système de templating simple avec remplacement de variables {{variable}}.
14
+ */
15
+ class TemplateLoader {
16
+ constructor(debug = false) {
17
+ this.debug = debug;
18
+ this.templates = new Map();
19
+ this.baseUrl = this.detectBaseUrl();
20
+ }
21
+
22
+ /**
23
+ * Détecte l'URL de base pour les templates
24
+ */
25
+ detectBaseUrl() {
26
+ // Toujours utiliser l'URL API pour les templates
27
+ const baseUrl = window.location.origin;
28
+
29
+ if (window.location.hostname.includes('ddev.site')) {
30
+ return baseUrl + '/api/sdk/components/templates'
31
+ }
32
+
33
+ // Pour production, adapter selon le domaine
34
+ return baseUrl + '/api/sdk/components/templates'
35
+ }
36
+
37
+ /**
38
+ * Charge un template depuis le serveur
39
+ */
40
+ async loadTemplate(templateName) {
41
+ if (this.templates.has(templateName)) {
42
+ return this.templates.get(templateName)
43
+ }
44
+
45
+ try {
46
+ const response = await fetch(`${this.baseUrl}/${templateName}.html`);
47
+ if (!response.ok) {
48
+ throw new Error(`Template ${templateName} not found: ${response.status}`)
49
+ }
50
+
51
+ const html = await response.text();
52
+ this.templates.set(templateName, html);
53
+ return html
54
+ } catch (error) {
55
+ console.error(`[TemplateLoader] Error loading template ${templateName}:`, error);
56
+ return this.getFallbackTemplate(templateName)
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Rend un template avec des données
62
+ */
63
+ async render(templateName, data = {}) {
64
+ const template = await this.loadTemplate(templateName);
65
+ return this.interpolate(template, data)
66
+ }
67
+
68
+ /**
69
+ * Système de templating amélioré - gère {{#if}}, {{else}}, {{/if}}, {{#unless}}
70
+ */
71
+ interpolate(template, data) {
72
+ // D'abord traiter les blocs conditionnels
73
+ let processed = this.processConditionalBlocks(template, data);
74
+
75
+ // Puis remplacer les variables simples
76
+ processed = processed.replace(/\{\{([^}#/][^}]*)\}\}/g, (match, key) => {
77
+ const value = this.resolveNestedKey(data, key.trim());
78
+ return value !== null ? value : ''
79
+ });
80
+
81
+ return processed
82
+ }
83
+
84
+ /**
85
+ * Résout les clés imbriquées comme "cashback.value"
86
+ */
87
+ resolveNestedKey(obj, key) {
88
+ // Les conditions sont maintenant gérées par processConditionalBlocks
89
+ // Ignorer les tokens de condition restants
90
+ if (key.startsWith('#') || key.startsWith('/')) {
91
+ return ''
92
+ }
93
+
94
+ // Résolution de clé normale
95
+ return key.split('.').reduce((current, prop) => {
96
+ return current && current[prop] !== undefined ? current[prop] : null
97
+ }, obj)
98
+ }
99
+
100
+ /**
101
+ * Traite les blocs conditionnels {{#if}}...{{else}}...{{/if}} et {{#unless}}...{{/unless}}
102
+ * Gère les conditions imbriquées de façon récursive
103
+ */
104
+ processConditionalBlocks(template, data) {
105
+ let processed = template;
106
+ let iterations = 0;
107
+ const maxIterations = 10; // Protection contre les boucles infinies
108
+
109
+ // Traiter récursivement jusqu'à ce qu'il n'y ait plus de conditions
110
+ while (processed.includes('{{#if') || processed.includes('{{#unless')) {
111
+ iterations++;
112
+ if (iterations > maxIterations) {
113
+ console.warn('[TemplateLoader] Too many iterations in conditional processing');
114
+ break
115
+ }
116
+
117
+ const before = processed;
118
+
119
+ // Traiter les blocs {{#if}}...{{/if}} (de l'intérieur vers l'extérieur)
120
+ processed = this.processIfBlocks(processed, data);
121
+
122
+ // Traiter les blocs {{#unless}}...{{/unless}}
123
+ processed = this.processUnlessBlocks(processed, data);
124
+
125
+ // Si rien n'a changé, arrêter pour éviter une boucle infinie
126
+ if (processed === before) {
127
+ break
128
+ }
129
+ }
130
+
131
+ return processed
132
+ }
133
+
134
+ /**
135
+ * Traite les blocs {{#if}}...{{/if}} en gérant les conditions imbriquées
136
+ */
137
+ processIfBlocks(template, data) {
138
+ // Trouver le {{#if}} le plus imbriqué (sans autres {{#if}} à l'intérieur)
139
+ const ifPattern = /\{\{#if\s+([^}]+)\}\}((?:(?!\{\{#if)[\s\S])*?)\{\{\/if\}\}/g;
140
+
141
+ return template.replace(ifPattern, (match, condition, content) => {
142
+ const value = this.resolveNestedKey(data, condition.trim());
143
+ const isTrue = Boolean(value);
144
+
145
+ // Chercher {{else}} dans le contenu (mais pas dans des sous-conditions)
146
+ const elseParts = content.split(/\{\{else\}\}/);
147
+
148
+ if (elseParts.length === 2) {
149
+ // Il y a un {{else}}
150
+ return isTrue ? elseParts[0] : elseParts[1]
151
+ } else {
152
+ // Pas de {{else}}, juste la condition
153
+ return isTrue ? content : ''
154
+ }
155
+ })
156
+ }
157
+
158
+ /**
159
+ * Traite les blocs {{#unless}}...{{/unless}}
160
+ */
161
+ processUnlessBlocks(template, data) {
162
+ const unlessPattern = /\{\{#unless\s+([^}]+)\}\}((?:(?!\{\{#unless)[\s\S])*?)\{\{\/unless\}\}/g;
163
+
164
+ return template.replace(unlessPattern, (match, condition, content) => {
165
+ const value = this.resolveNestedKey(data, condition.trim());
166
+ const isFalse = !Boolean(value);
167
+
168
+ return isFalse ? content : ''
169
+ })
170
+ }
171
+
172
+ /**
173
+ * Gestion basique des conditions {{#if}} et {{#unless}} (déprécié - utiliser processConditionalBlocks)
174
+ */
175
+ handleConditional(data, key, isIf) {
176
+ const conditionKey = key.split(' ')[1];
177
+ const value = this.resolveNestedKey(data, conditionKey);
178
+ const condition = Boolean(value);
179
+
180
+ return (isIf ? condition : !condition) ? '' : '<!-- condition-false -->'
181
+ }
182
+
183
+ /**
184
+ * Templates de fallback en cas d'erreur de chargement
185
+ */
186
+ getFallbackTemplate(templateName) {
187
+ const fallbacks = {
188
+ 'CashbackTemplate': `
189
+ <div class="vipros-offer-card cashback-mode">
190
+ <div class="offer-content">
191
+ <div class="main-value">
192
+ <span class="amount">{{cashback.value}}</span>
193
+ <span class="currency">€</span>
194
+ <span class="label">remboursés</span>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ `,
199
+ 'VIPointsTemplate': `
200
+ <div class="vipros-offer-card vipoints-mode">
201
+ <div class="offer-content">
202
+ <div class="main-value">
203
+ <span class="amount">{{vipoints.perTenEuros}}</span>
204
+ <span class="label">VIPoints</span>
205
+ </div>
206
+ </div>
207
+ </div>
208
+ `,
209
+ 'LoadingTemplate': `
210
+ <div class="vipros-offer-card loading-state">
211
+ <div class="loading-text">Recherche...</div>
212
+ </div>
213
+ `,
214
+ 'ErrorTemplate': `
215
+ <div class="vipros-offer-card error-state">
216
+ <div class="error-text">{{message}}</div>
217
+ <div class="debug-notice">Erreur affichée car le mode debug est activé</div>
218
+ </div>
219
+ `
220
+ };
221
+
222
+ return fallbacks[templateName] || '<div class="vipros-offer-card">Template not found</div>'
223
+ }
224
+
225
+ /**
226
+ * Précharge tous les templates pour de meilleures performances
227
+ */
228
+ async preloadTemplates() {
229
+ const templateNames = ['CashbackTemplate', 'VIPointsTemplate', 'LoadingTemplate', 'ErrorTemplate'];
230
+
231
+ const promises = templateNames.map(name =>
232
+ this.loadTemplate(name).catch(error => {
233
+ if (this.debug) console.warn(`[TemplateLoader] Failed to preload ${name}:`, error.message);
234
+ return null
235
+ })
236
+ );
237
+
238
+ await Promise.all(promises);
239
+ if (this.debug) console.log('[TemplateLoader] Templates preloaded');
240
+ }
241
+
242
+ /**
243
+ * Nettoie le cache des templates
244
+ */
245
+ clearCache() {
246
+ this.templates.clear();
247
+ }
248
+ }
249
+
250
+ /**
251
+ * ProductDataTransformer - Transformation des données API vers format composant
252
+ *
253
+ * Centralise toute la logique de transformation des données API
254
+ * vers le format attendu par les templates.
255
+ */
256
+ class ProductDataTransformer {
257
+
258
+ /**
259
+ * Transforme les données API vers le format template
260
+ */
261
+ static transform(apiProduct, price = null) {
262
+ if (!apiProduct) {
263
+ return null
264
+ }
265
+
266
+ const hasCashback = Boolean(apiProduct.cashback?.value > 0);
267
+ const generosityRate = apiProduct.brand?.generosity_rate || 0;
268
+
269
+ // Calculer les VIPoints selon le contexte
270
+ const vipointsData = this.calculateVIPoints(generosityRate, price);
271
+
272
+ return {
273
+ id: apiProduct.id || 'not_found',
274
+ ean: apiProduct.ean,
275
+ name: apiProduct.name || 'Produit inconnu',
276
+ price: price,
277
+
278
+ brand: this.transformBrandData(apiProduct.brand),
279
+ cashback: this.transformCashbackData(apiProduct.cashback),
280
+ vipoints: vipointsData,
281
+
282
+ hasCashback,
283
+ viprosLink: this.buildViprosLink(apiProduct),
284
+
285
+ // URL de l'image VIPoints calculée dynamiquement
286
+ vipointsImageUrl: null // Sera définie par le composant
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Transforme les données de marque
292
+ */
293
+ static transformBrandData(brandData) {
294
+ if (!brandData) {
295
+ return {
296
+ name: 'Marque inconnue',
297
+ displayName: null,
298
+ logoUrl: null,
299
+ generosityRate: 0
300
+ }
301
+ }
302
+
303
+ const displayName = brandData.name && brandData.name !== 'Marque inconnue'
304
+ ? brandData.name
305
+ : null;
306
+
307
+ return {
308
+ name: brandData.name || 'Marque inconnue',
309
+ displayName,
310
+ logoUrl: brandData.logo_url || null,
311
+ generosityRate: brandData.generosity_rate || 0
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Transforme les données de cashback
317
+ */
318
+ static transformCashbackData(cashbackData) {
319
+ if (!cashbackData) {
320
+ return {
321
+ value: 0,
322
+ name: null,
323
+ imageUrl: null,
324
+ startDate: null,
325
+ endDate: null
326
+ }
327
+ }
328
+
329
+ return {
330
+ value: cashbackData.value || 0,
331
+ name: cashbackData.name || null,
332
+ imageUrl: cashbackData.image_url || null,
333
+ startDate: cashbackData.start_date || null,
334
+ endDate: cashbackData.end_date || null
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Calcule les données VIPoints selon le contexte
340
+ */
341
+ static calculateVIPoints(generosityRate, price) {
342
+ let perTenEuros = 0;
343
+ let hasBonus = false;
344
+ let showExact = false;
345
+
346
+ if (generosityRate > 0) {
347
+ hasBonus = true;
348
+
349
+ if (price && price > 0) {
350
+ // Prix renseigné : calculer le nombre exact de VIPoints
351
+ perTenEuros = Math.floor((generosityRate * price) / 10);
352
+ showExact = true;
353
+ } else {
354
+ // Pas de prix : affichage générique "1 VIPoint tous les 10€"
355
+ perTenEuros = 1;
356
+ showExact = false;
357
+ }
358
+ }
359
+
360
+ return {
361
+ perTenEuros,
362
+ generosityRate,
363
+ hasBonus,
364
+ showExact
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Détermine le mode d'affichage selon les données
370
+ */
371
+ static determineDisplayMode(transformedData) {
372
+ if (!transformedData) {
373
+ return 'empty'
374
+ }
375
+
376
+ if (transformedData.hasCashback) {
377
+ return 'cashback'
378
+ }
379
+
380
+ if (transformedData.vipoints.hasBonus) {
381
+ return 'vipoints'
382
+ }
383
+
384
+ return 'empty'
385
+ }
386
+
387
+ /**
388
+ * Valide les données transformées
389
+ */
390
+ static validate(transformedData) {
391
+ if (!transformedData) {
392
+ return { isValid: false, errors: ['No data provided'] }
393
+ }
394
+
395
+ const errors = [];
396
+
397
+ if (!transformedData.ean) {
398
+ errors.push('EAN is required');
399
+ }
400
+
401
+ if (transformedData.hasCashback && !transformedData.cashback.value) {
402
+ errors.push('Cashback value is required when hasCashback is true');
403
+ }
404
+
405
+ if (transformedData.vipoints.hasBonus && !transformedData.vipoints.generosityRate) {
406
+ errors.push('Generosity rate is required when VIPoints bonus is available');
407
+ }
408
+
409
+ return {
410
+ isValid: errors.length === 0,
411
+ errors
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Prépare les données pour le template
417
+ */
418
+ static prepareTemplateData(transformedData, vipointsImageUrl = null) {
419
+ if (!transformedData) {
420
+ return null
421
+ }
422
+
423
+ return {
424
+ ...transformedData,
425
+ vipointsImageUrl: vipointsImageUrl || this.getDefaultVIPointsImageUrl()
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Construit l'URL VIPros complète avec les paramètres origin
431
+ */
432
+ static buildViprosLink(apiProduct) {
433
+ const baseUrl = apiProduct.vipros_link || 'https://vipros.fr';
434
+
435
+ // Récupérer le nom du point de vente depuis les données API
436
+ const origin = this.getSalesPointName(apiProduct);
437
+
438
+ // URL de la page actuelle
439
+ const originUrl = typeof window !== 'undefined' ? window.location.href : '';
440
+
441
+ // Construire l'URL avec les paramètres
442
+ const url = new URL(baseUrl);
443
+ if (origin) {
444
+ url.searchParams.set('origin', origin);
445
+ }
446
+ if (originUrl) {
447
+ url.searchParams.set('origin_url', originUrl);
448
+ }
449
+
450
+ return url.toString()
451
+ }
452
+
453
+ /**
454
+ * Extrait le nom du point de vente depuis les données API
455
+ */
456
+ static getSalesPointName(apiProduct) {
457
+ // Chercher dans les sales_points nested
458
+ if (apiProduct.sales_points && Array.isArray(apiProduct.sales_points)) {
459
+ const salesPoint = apiProduct.sales_points.find(sp => sp.name);
460
+ if (salesPoint) {
461
+ return salesPoint.name
462
+ }
463
+ }
464
+
465
+ // Fallback : utiliser le domaine actuel comme nom du point de vente
466
+ if (typeof window !== 'undefined') {
467
+ const hostname = window.location.hostname;
468
+ // Transformer le domaine en nom plus lisible
469
+ return hostname.replace(/^www\./, '').replace(/\./g, ' ').split(' ')[0]
470
+ }
471
+
472
+ return null
473
+ }
474
+
475
+ /**
476
+ * URL par défaut de l'image VIPoints
477
+ */
478
+ static getDefaultVIPointsImageUrl() {
479
+ // Sera définie dynamiquement par le composant selon son contexte
480
+ return '/api/sdk/assets/image-vipoints.jpg'
481
+ }
482
+ }
483
+
484
+ /**
485
+ * DisplayModeResolver - Résolution du mode d'affichage
486
+ *
487
+ * Détermine comment afficher le composant selon les données produit
488
+ * et les options de configuration utilisateur.
489
+ */
490
+ class DisplayModeResolver {
491
+
492
+ /**
493
+ * Résout le mode d'affichage principal
494
+ */
495
+ static resolveDisplayMode(productData) {
496
+ if (!productData) {
497
+ return 'empty'
498
+ }
499
+
500
+ // Mode cashback prioritaire
501
+ if (productData.hasCashback) {
502
+ return 'cashback'
503
+ }
504
+
505
+ // Mode VIPoints si disponible
506
+ if (productData.vipoints.hasBonus) {
507
+ return 'vipoints'
508
+ }
509
+
510
+ // Aucune offre disponible
511
+ return 'empty'
512
+ }
513
+
514
+ /**
515
+ * Résout le template à utiliser selon le mode
516
+ */
517
+ static resolveTemplateName(displayMode) {
518
+ const templateMap = {
519
+ 'cashback': 'CashbackTemplate',
520
+ 'vipoints': 'VIPointsTemplate',
521
+ 'loading': 'LoadingTemplate',
522
+ 'error': 'ErrorTemplate',
523
+ 'empty': null,
524
+ 'hidden': null
525
+ };
526
+
527
+ return templateMap[displayMode] || null
528
+ }
529
+
530
+ /**
531
+ * Détermine si le composant doit être visible
532
+ */
533
+ static shouldDisplay(displayMode) {
534
+ return !['empty', 'hidden'].includes(displayMode)
535
+ }
536
+
537
+ /**
538
+ * Résout la classe CSS du conteneur
539
+ */
540
+ static resolveContainerClass(displayMode) {
541
+ const classMap = {
542
+ 'cashback': 'cashback-mode',
543
+ 'vipoints': 'vipoints-mode',
544
+ 'loading': 'loading-state',
545
+ 'error': 'error-state',
546
+ 'empty': 'empty-state',
547
+ 'hidden': 'hidden-state'
548
+ };
549
+
550
+ return `vipros-offer-card ${classMap[displayMode] || ''}`
551
+ }
552
+
553
+ /**
554
+ * Validation du mode d'affichage
555
+ */
556
+ static validateDisplayMode(displayMode) {
557
+ const validModes = ['cashback', 'vipoints', 'loading', 'error', 'empty', 'hidden'];
558
+
559
+ if (!validModes.includes(displayMode)) {
560
+ console.warn(`[DisplayModeResolver] Invalid display mode: ${displayMode}`);
561
+ return 'error'
562
+ }
563
+
564
+ return displayMode
565
+ }
566
+
567
+ /**
568
+ * Analyse complète pour déterminer l'affichage
569
+ */
570
+ static analyze(productData, options = {}) {
571
+ const {
572
+ isLoading = false,
573
+ error = null,
574
+ debug = false
575
+ } = options;
576
+
577
+ // États d'erreur et de chargement prioritaires
578
+ if (error) {
579
+ // Afficher les erreurs seulement si debug est activé
580
+ if (debug) {
581
+ return {
582
+ mode: 'error',
583
+ template: 'ErrorTemplate',
584
+ shouldDisplay: true,
585
+ containerClass: this.resolveContainerClass('error'),
586
+ data: {
587
+ message: error.message || 'Une erreur est survenue',
588
+ debugMode: true
589
+ }
590
+ }
591
+ } else {
592
+ // En mode non-debug, ne pas afficher les erreurs (masquer complètement)
593
+ return {
594
+ mode: 'hidden',
595
+ template: null,
596
+ shouldDisplay: false,
597
+ containerClass: this.resolveContainerClass('hidden'),
598
+ data: {}
599
+ }
600
+ }
601
+ }
602
+
603
+ if (isLoading) {
604
+ return {
605
+ mode: 'loading',
606
+ template: 'LoadingTemplate',
607
+ shouldDisplay: true,
608
+ containerClass: this.resolveContainerClass('loading'),
609
+ data: {}
610
+ }
611
+ }
612
+
613
+ // Résolution normale
614
+ const mode = this.resolveDisplayMode(productData);
615
+ const template = this.resolveTemplateName(mode);
616
+ const shouldDisplay = this.shouldDisplay(mode);
617
+ const containerClass = this.resolveContainerClass(mode);
618
+
619
+ return {
620
+ mode: this.validateDisplayMode(mode),
621
+ template,
622
+ shouldDisplay,
623
+ containerClass,
624
+ data: shouldDisplay ? productData : {}
625
+ }
626
+ }
627
+
628
+ /**
629
+ * Vérifie si les données sont suffisantes pour l'affichage
630
+ */
631
+ static hasMinimumRequiredData(productData) {
632
+ if (!productData) {
633
+ return false
634
+ }
635
+
636
+ // Au minimum besoin d'un EAN
637
+ if (!productData.ean) {
638
+ return false
639
+ }
640
+
641
+ // Doit avoir soit cashback soit VIPoints
642
+ return productData.hasCashback || productData.vipoints.hasBonus
643
+ }
644
+
645
+ /**
646
+ * Suggestions d'amélioration des données
647
+ */
648
+ static suggestDataImprovements(productData) {
649
+ if (!productData) {
650
+ return ['Product data is required']
651
+ }
652
+
653
+ const suggestions = [];
654
+
655
+ if (!productData.name || productData.name === 'Produit inconnu') {
656
+ suggestions.push('Product name could be improved');
657
+ }
658
+
659
+ if (productData.hasCashback && !productData.cashback.imageUrl) {
660
+ suggestions.push('Cashback image would improve visual appeal');
661
+ }
662
+
663
+ if (!productData.price && productData.vipoints.hasBonus) {
664
+ suggestions.push('Price would enable exact VIPoints calculation');
665
+ }
666
+
667
+ if (!productData.brand.displayName) {
668
+ suggestions.push('Brand name would improve VIPoints messaging');
669
+ }
670
+
671
+ return suggestions
672
+ }
673
+ }
674
+
675
+ /**
676
+ * EventManager - Gestionnaire d'événements pour ViprosOfferCard
677
+ *
678
+ * Centralise la gestion des événements du composant Web Component.
679
+ * Simplifie l'émission et l'écoute d'événements personnalisés.
680
+ */
681
+ class EventManager {
682
+ constructor(element) {
683
+ this.element = element;
684
+ this.listeners = new Map();
685
+ }
686
+
687
+ /**
688
+ * Émet un événement personnalisé
689
+ */
690
+ emit(eventName, detail = {}) {
691
+ const event = new CustomEvent(eventName, {
692
+ detail,
693
+ bubbles: true,
694
+ cancelable: true
695
+ });
696
+
697
+ this.element.dispatchEvent(event);
698
+
699
+ // Log pour debugging
700
+ if (this.element.getAttribute('debug') === 'true') {
701
+ console.log(`[ViprosOfferCard] Event emitted: ${eventName}`, detail);
702
+ }
703
+ }
704
+
705
+ /**
706
+ * Émet l'événement d'offre chargée
707
+ */
708
+ emitOfferLoaded(productData, ean) {
709
+ this.emit('vipros-offer-loaded', {
710
+ product: productData,
711
+ ean: ean,
712
+ timestamp: Date.now()
713
+ });
714
+ }
715
+
716
+ /**
717
+ * Émet l'événement d'erreur
718
+ */
719
+ emitError(error, ean) {
720
+ this.emit('vipros-offer-error', {
721
+ error: {
722
+ message: error.message || 'Unknown error',
723
+ type: error.constructor.name,
724
+ stack: error.stack
725
+ },
726
+ ean: ean,
727
+ timestamp: Date.now()
728
+ });
729
+ }
730
+
731
+ /**
732
+ * Émet l'événement de clic sur offre
733
+ */
734
+ emitOfferClick(url, productData, ean) {
735
+ this.emit('vipros-offer-click', {
736
+ url,
737
+ product: productData,
738
+ ean: ean,
739
+ timestamp: Date.now()
740
+ });
741
+ }
742
+
743
+ /**
744
+ * Émet l'événement de synchronisation démarrée
745
+ */
746
+ emitSyncStarted(ean, url) {
747
+ this.emit('vipros-sync-started', {
748
+ ean,
749
+ url,
750
+ timestamp: Date.now()
751
+ });
752
+ }
753
+
754
+ /**
755
+ * Émet l'événement de synchronisation réussie
756
+ */
757
+ emitSyncSuccess(ean, result) {
758
+ this.emit('vipros-sync-success', {
759
+ ean,
760
+ result,
761
+ timestamp: Date.now()
762
+ });
763
+ }
764
+
765
+ /**
766
+ * Émet l'événement d'erreur de synchronisation
767
+ */
768
+ emitSyncError(ean, error) {
769
+ this.emit('vipros-sync-error', {
770
+ ean,
771
+ error: {
772
+ message: error.message || 'Sync error',
773
+ type: error.constructor.name
774
+ },
775
+ timestamp: Date.now()
776
+ });
777
+ }
778
+
779
+ /**
780
+ * Ajoute des listeners pour les clics sur les liens
781
+ */
782
+ attachLinkListeners(shadowRoot, productData, ean) {
783
+ const links = shadowRoot.querySelectorAll('a[href]');
784
+
785
+ links.forEach(link => {
786
+ const clickHandler = (event) => {
787
+ this.emitOfferClick(link.href, productData, ean);
788
+
789
+ // Optionnel : tracking analytics
790
+ if (window.gtag) {
791
+ window.gtag('event', 'vipros_offer_click', {
792
+ ean: ean,
793
+ url: link.href,
794
+ product_name: productData?.name
795
+ });
796
+ }
797
+ };
798
+
799
+ link.addEventListener('click', clickHandler);
800
+
801
+ // Stocker la référence pour cleanup
802
+ if (!this.listeners.has('linkClicks')) {
803
+ this.listeners.set('linkClicks', []);
804
+ }
805
+ this.listeners.get('linkClicks').push({ link, handler: clickHandler });
806
+ });
807
+ }
808
+
809
+ /**
810
+ * Supprime tous les listeners attachés
811
+ */
812
+ cleanup() {
813
+ // Nettoyer les listeners de liens
814
+ if (this.listeners.has('linkClicks')) {
815
+ this.listeners.get('linkClicks').forEach(({ link, handler }) => {
816
+ link.removeEventListener('click', handler);
817
+ });
818
+ }
819
+
820
+ this.listeners.clear();
821
+ }
822
+
823
+ /**
824
+ * Configuration des événements globaux pour debugging
825
+ */
826
+ enableGlobalDebugging() {
827
+ const events = [
828
+ 'vipros-offer-loaded',
829
+ 'vipros-offer-error',
830
+ 'vipros-offer-click',
831
+ 'vipros-sync-started',
832
+ 'vipros-sync-success',
833
+ 'vipros-sync-error'
834
+ ];
835
+
836
+ events.forEach(eventName => {
837
+ document.addEventListener(eventName, (event) => {
838
+ console.log(`[ViprosOfferCard] Global event: ${eventName}`, event.detail);
839
+ });
840
+ });
841
+ }
842
+
843
+ /**
844
+ * Méthodes statiques pour les composants sans instance
845
+ */
846
+ static emit(element, eventName, detail = {}) {
847
+ const manager = new EventManager(element);
848
+ manager.emit(eventName, detail);
849
+ }
850
+
851
+ static emitOfferLoaded(element, productData, ean) {
852
+ const manager = new EventManager(element);
853
+ manager.emitOfferLoaded(productData, ean);
854
+ }
855
+
856
+ static emitError(element, error, ean) {
857
+ const manager = new EventManager(element);
858
+ manager.emitError(error, ean);
859
+ }
860
+ }
861
+
862
+ /**
863
+ * ViprosOfferCard - Web Component for displaying VIPros offers
864
+ */
865
+
866
+
867
+ class ViprosOfferCard extends HTMLElement {
868
+ constructor() {
869
+ super();
870
+
871
+ // Shadow DOM for component isolation
872
+ this.attachShadow({ mode: 'open' });
873
+
874
+ this.templateLoader = new TemplateLoader();
875
+ this.eventManager = new EventManager(this);
876
+
877
+ // Component state
878
+ this.state = {
879
+ isLoading: false,
880
+ productData: null,
881
+ error: null,
882
+ apiClient: null
883
+ };
884
+
885
+ // Bind methods
886
+ this.init = this.init.bind(this);
887
+ this.loadProduct = this.loadProduct.bind(this);
888
+ this.render = this.render.bind(this);
889
+ }
890
+
891
+ // Observed attributes
892
+ static get observedAttributes() {
893
+ return [
894
+ 'ean', 'price', 'api-base-url', 'api-key',
895
+ 'primary-color', 'text-color', 'background-color',
896
+ 'card-bonus-bg', 'card-bonus-color'
897
+ ]
898
+ }
899
+
900
+ // Attribute getters
901
+ get ean() {
902
+ return this.getAttribute('ean') || this.getConfigValue('ean')
903
+ }
904
+ get price() {
905
+ const attrPrice = parseFloat(this.getAttribute('price')) || 0;
906
+ if (attrPrice > 0) return attrPrice
907
+
908
+ const configPrice = this.getConfigValue('price');
909
+ if (configPrice && parseFloat(configPrice) > 0) return parseFloat(configPrice)
910
+
911
+ return null
912
+ }
913
+ get apiBaseUrl() {
914
+ return this.getConfigValue('apiBaseUrl') || this.getAttribute('api-base-url') || this.detectFallbackApiBaseUrl()
915
+ }
916
+ get apiKey() {
917
+ return this.getConfigValue('apiKey') || this.getAttribute('api-key')
918
+ }
919
+ get primaryColor() {
920
+ return this.getAttribute('primary-color') || this.getConfigValue('primaryColor')
921
+ }
922
+ get textColor() {
923
+ return this.getAttribute('text-color') || this.getConfigValue('textColor')
924
+ }
925
+ get backgroundColor() {
926
+ return this.getAttribute('background-color') || this.getConfigValue('backgroundColor')
927
+ }
928
+ get cardBonusBg() {
929
+ return this.getAttribute('card-bonus-bg') || this.getConfigValue('cardBonusBg')
930
+ }
931
+ get cardBonusColor() {
932
+ return this.getAttribute('card-bonus-color') || this.getConfigValue('cardBonusColor')
933
+ }
934
+
935
+ // SDK configuration helpers
936
+ getConfigValue(key) {
937
+ const sdk = this.getSDKInstance();
938
+ return sdk?.config?.[key] || null
939
+ }
940
+
941
+ getSDKInstance() {
942
+ return typeof window !== 'undefined' ? window.ViprosSDK : null
943
+ }
944
+
945
+ detectFallbackApiBaseUrl() {
946
+ // Edge case: No window object (SSR or Node.js environment)
947
+ if (typeof window === 'undefined') {
948
+ this.log('[ViprosOfferCard] No window object - defaulting to production API');
949
+ return 'https://msys.vipros.fr/api'
950
+ }
951
+
952
+ try {
953
+ // Edge case: window.location is not available or corrupted
954
+ if (!window.location || typeof window.location.hostname !== 'string') {
955
+ this.log('[ViprosOfferCard] Invalid window.location - defaulting to production API');
956
+ return 'https://msys.vipros.fr/api'
957
+ }
958
+
959
+ const protocol = window.location.protocol || 'https:';
960
+ const hostname = window.location.hostname.toLowerCase();
961
+
962
+ // Edge case: Empty or invalid hostname
963
+ if (!hostname || hostname.length === 0) {
964
+ this.log('[ViprosOfferCard] Empty hostname - defaulting to production API');
965
+ return 'https://msys.vipros.fr/api'
966
+ }
967
+
968
+ // Edge case: Local development or non-standard protocols
969
+ if (protocol === 'file:' || hostname === 'localhost' || hostname.startsWith('127.0.0.1') || hostname.startsWith('0.0.0.0')) {
970
+ this.log('[ViprosOfferCard] Local development detected - using DDEV fallback');
971
+ return 'https://vipros-connect.ddev.site/api'
972
+ }
973
+
974
+ // Correspondance directe des domaines (même logique que ConfigManager)
975
+ if (hostname === 'vipros-connect.ddev.site') {
976
+ return `${protocol}//${hostname}/api`
977
+ }
978
+
979
+ if (hostname === 'msys.preprod.vipros.fr') {
980
+ return `${protocol}//msys.preprod.vipros.fr/api`
981
+ }
982
+
983
+ // Edge case: Subdomain detection for enterprise customers
984
+ if (hostname.endsWith('.vipros.fr')) {
985
+ this.log(`[ViprosOfferCard] VIPros subdomain detected: ${hostname}`);
986
+ return `${protocol}//${hostname}/api`
987
+ }
988
+
989
+ // Par défaut : production (toujours HTTPS pour la sécurité)
990
+ this.log(`[ViprosOfferCard] Unknown domain: ${hostname} - defaulting to production API`);
991
+ return 'https://msys.vipros.fr/api'
992
+
993
+ } catch (error) {
994
+ // Edge case: Exception during detection
995
+ this.log(`[ViprosOfferCard] Error detecting API base URL: ${error.message} - defaulting to production`);
996
+ return 'https://msys.vipros.fr/api'
997
+ }
998
+ }
999
+
1000
+ // Lifecycle methods
1001
+ connectedCallback() {
1002
+ this.log('[ViprosOfferCard] Component connected');
1003
+ this.init();
1004
+ }
1005
+
1006
+ attributeChangedCallback(name, oldValue, newValue) {
1007
+ if (oldValue !== newValue && this.isConnected) {
1008
+ if (name === 'ean') {
1009
+ this.log(`[ViprosOfferCard] EAN changed: ${oldValue} → ${newValue}`);
1010
+ this.loadProduct();
1011
+ } else if ([
1012
+ 'primary-color', 'text-color', 'background-color',
1013
+ 'gift-icon-color', 'gift-card-bonus-bg', 'gift-card-bonus-color',
1014
+ 'vipoints-bonus-bg', 'vipoints-bonus-color'
1015
+ ].includes(name)) {
1016
+ this.log(`[ViprosOfferCard] Color changed: ${name} = ${newValue}`);
1017
+ this.updateCustomColors();
1018
+ }
1019
+ }
1020
+ }
1021
+
1022
+ disconnectedCallback() {
1023
+ this.log('[ViprosOfferCard] Component disconnected');
1024
+ this.cleanup();
1025
+ }
1026
+
1027
+ // Initialization
1028
+ async init() {
1029
+ try {
1030
+ // Load Google Fonts in document head
1031
+ this.loadGoogleFonts();
1032
+
1033
+ // Load CSS styles
1034
+ await this.loadStyles();
1035
+
1036
+ // Apply custom colors if provided
1037
+ this.updateCustomColors();
1038
+
1039
+ // Initialize API client
1040
+ await this.initApiClient();
1041
+
1042
+ // Preload templates
1043
+ await this.templateLoader.preloadTemplates();
1044
+
1045
+ // Load product data
1046
+ if (this.ean) {
1047
+ await this.loadProduct();
1048
+ } else {
1049
+ this.setError(new Error('EAN requis pour afficher une offre VIPros'));
1050
+ }
1051
+ } catch (error) {
1052
+ this.logError('[ViprosOfferCard] Init error:', error);
1053
+ this.setError(error);
1054
+ }
1055
+ }
1056
+
1057
+ // Load Google Fonts in document head
1058
+ loadGoogleFonts() {
1059
+ const fontsToLoad = [
1060
+ 'https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap',
1061
+ 'https://fonts.googleapis.com/css2?family=Encode+Sans+Condensed:wght@100;200;300;400;500;600;700;800;900&display=swap'
1062
+ ];
1063
+
1064
+ fontsToLoad.forEach(fontUrl => {
1065
+ // Check if font link already exists
1066
+ const existingLink = document.querySelector(`link[href="${fontUrl}"]`);
1067
+ if (!existingLink) {
1068
+ const linkElement = document.createElement('link');
1069
+ linkElement.rel = 'stylesheet';
1070
+ linkElement.href = fontUrl;
1071
+ linkElement.crossOrigin = 'anonymous';
1072
+ document.head.appendChild(linkElement);
1073
+ }
1074
+ });
1075
+ }
1076
+
1077
+ // Load CSS styles
1078
+ async loadStyles() {
1079
+ try {
1080
+ const styleUrl = this.templateLoader.baseUrl.replace('/components/templates', '/components/styles/offer-card.css');
1081
+ const response = await fetch(styleUrl);
1082
+
1083
+ if (response.ok) {
1084
+ const css = await response.text();
1085
+ const styleElement = document.createElement('style');
1086
+ styleElement.textContent = css;
1087
+ this.shadowRoot.appendChild(styleElement);
1088
+ } else {
1089
+ // Fallback avec styles minimaux
1090
+ this.injectFallbackStyles();
1091
+ }
1092
+ } catch (error) {
1093
+ this.logError('[ViprosOfferCard] CSS loading failed, using fallback');
1094
+ this.injectFallbackStyles();
1095
+ }
1096
+ }
1097
+
1098
+ // Update custom colors via CSS variables
1099
+ updateCustomColors() {
1100
+ if (!this.shadowRoot) return
1101
+
1102
+ const host = this.shadowRoot.host;
1103
+ const customColors = {};
1104
+
1105
+ // Map attributes to CSS variables
1106
+ if (this.primaryColor) {
1107
+ customColors['--vipros-primary'] = this.primaryColor;
1108
+ customColors['--vipros-text-accent'] = this.primaryColor;
1109
+ }
1110
+ if (this.textColor) {
1111
+ customColors['--vipros-text-main'] = this.textColor;
1112
+ }
1113
+ if (this.backgroundColor) {
1114
+ customColors['--vipros-background'] = this.backgroundColor;
1115
+ }
1116
+ if (this.cardBonusBg) {
1117
+ customColors['--card-bonus-bg'] = this.cardBonusBg;
1118
+ }
1119
+ if (this.cardBonusColor) {
1120
+ customColors['--card-bonus-color'] = this.cardBonusColor;
1121
+ }
1122
+
1123
+ // Apply custom colors to the host element
1124
+ Object.entries(customColors).forEach(([property, value]) => {
1125
+ host.style.setProperty(property, value);
1126
+ });
1127
+
1128
+ this.log('[ViprosOfferCard] Custom colors applied:', customColors);
1129
+ }
1130
+
1131
+ injectFallbackStyles() {
1132
+ const fallbackCSS = `
1133
+ .vipros-offer-card {
1134
+ display: block;
1135
+ padding: 20px;
1136
+ background: #e8ecf8;
1137
+ border-radius: 12px;
1138
+ font-family: sans-serif;
1139
+ }
1140
+ .loading-content, .error-content {
1141
+ text-align: center;
1142
+ padding: 40px;
1143
+ }
1144
+ `;
1145
+ const styleElement = document.createElement('style');
1146
+ styleElement.textContent = fallbackCSS;
1147
+ this.shadowRoot.appendChild(styleElement);
1148
+ }
1149
+
1150
+ // Méthodes de logging respectant le mode debug
1151
+ log(message, ...args) {
1152
+ const sdk = this.getSDKInstance();
1153
+ if (sdk && sdk.config && sdk.config.debug === true) {
1154
+ console.log(message, ...args);
1155
+ }
1156
+ }
1157
+
1158
+ logError(message, ...args) {
1159
+ const sdk = this.getSDKInstance();
1160
+ if (sdk && sdk.config && sdk.config.debug === true) {
1161
+ console.error(message, ...args);
1162
+ }
1163
+ }
1164
+
1165
+ // Initialisation API client
1166
+ async initApiClient() {
1167
+ const sdk = this.getSDKInstance();
1168
+
1169
+ if (sdk && sdk.getApiClient) {
1170
+ this.state.apiClient = sdk.getApiClient();
1171
+ this.log('[ViprosOfferCard] Using shared SDK ApiClient');
1172
+ } else {
1173
+ // Fallback : créer son propre ApiClient
1174
+ const { default: ApiClient } = await Promise.resolve().then(function () { return ApiClient$1; });
1175
+ this.state.apiClient = new ApiClient({
1176
+ apiBaseUrl: this.apiBaseUrl,
1177
+ apiKey: this.apiKey
1178
+ });
1179
+ this.log('[ViprosOfferCard] Created local ApiClient');
1180
+ }
1181
+ }
1182
+
1183
+ // Chargement des données produit
1184
+ async loadProduct() {
1185
+ if (!this.ean || this.state.isLoading) return
1186
+
1187
+ // Attendre que l'apiClient soit prêt
1188
+ if (!this.state.apiClient) {
1189
+ setTimeout(() => this.loadProduct(), 100);
1190
+ return
1191
+ }
1192
+
1193
+ this.setLoading(true);
1194
+
1195
+ try {
1196
+ this.log(`[ViprosOfferCard] Loading product: ${this.ean}`);
1197
+
1198
+ // NOUVEAU Phase 8: Préparer les options pour la sync inline si activée
1199
+ const sdk = this.getSDKInstance();
1200
+ const apiOptions = {
1201
+ timeout: 5000 // 5 secondes pour éviter les timeouts longs sur rate limit
1202
+ };
1203
+
1204
+ // Activer la sync inline si configuré dans le SDK
1205
+ if (sdk?.config?.inlineSyncEnabled) {
1206
+ apiOptions.enableInlineSync = true;
1207
+ apiOptions.productUrl = window.location.href;
1208
+ this.log(`[ViprosOfferCard] Inline sync enabled for EAN ${this.ean}`);
1209
+ }
1210
+
1211
+ // Appel API avec options de sync inline
1212
+ const response = await this.state.apiClient.get('/catalog/items', {
1213
+ ean: this.ean,
1214
+ items_per_page: 1
1215
+ }, apiOptions);
1216
+
1217
+ const bloomMetadata = response?.metadata?.bloomFilter;
1218
+ const debugEnabled = Boolean(this.getConfigValue('debug') || this.getAttribute('debug') === 'true');
1219
+
1220
+ if (bloomMetadata?.skipped) {
1221
+ this.log(`[ViprosOfferCard] Bloom filter skipped API call for EAN ${this.ean}`);
1222
+
1223
+ if (debugEnabled) {
1224
+ const bloomError = new Error('404 | Produit introuvable (Bloom filter)');
1225
+ bloomError.status = 404;
1226
+ bloomError.type = 'BLOOM_FILTER_NOT_FOUND';
1227
+ bloomError.data = {
1228
+ ean: this.ean,
1229
+ reason: bloomMetadata.reason || 'NOT_IN_FILTER'
1230
+ };
1231
+ this.setError(bloomError);
1232
+ } else {
1233
+ this.setProductData(null);
1234
+ }
1235
+
1236
+ return
1237
+ }
1238
+
1239
+ // Log sync metadata if present (Phase 8)
1240
+ const syncMetadata = response?.sdk_metadata?.sync;
1241
+ if (syncMetadata && this.getConfigValue('debug')) {
1242
+ this.log(`[ViprosOfferCard] Sync metadata:`, syncMetadata);
1243
+ }
1244
+
1245
+ if (response.items && response.items.length > 0) {
1246
+ // Transformer les données
1247
+ const productData = ProductDataTransformer.transform(response.items[0], this.price);
1248
+ productData.vipointsImageUrl = this.getVIPointsImageUrl();
1249
+
1250
+ this.setProductData(productData);
1251
+ this.log(`[ViprosOfferCard] Product loaded: ${productData.name}`);
1252
+
1253
+ // Synchronisation en arrière-plan (seulement si inline sync n'a pas été tentée)
1254
+ // On vérifie si sync metadata existe, peu importe si triggered=true ou cooldown
1255
+ const inlineSyncWasAttempted = syncMetadata !== undefined && syncMetadata !== null;
1256
+ if (!inlineSyncWasAttempted) {
1257
+ this.triggerBackgroundSync();
1258
+ } else {
1259
+ this.log(`[ViprosOfferCard] Skipping background sync (inline sync was attempted, status: ${syncMetadata.reason || 'unknown'})`);
1260
+ }
1261
+
1262
+ // Émettre événement
1263
+ this.eventManager.emitOfferLoaded(productData, this.ean);
1264
+ } else {
1265
+ this.log(`[ViprosOfferCard] No product found for EAN: ${this.ean}`);
1266
+ this.setProductData(null);
1267
+ }
1268
+ } catch (error) {
1269
+ this.logError(`[ViprosOfferCard] Error loading EAN ${this.ean}:`, error);
1270
+
1271
+ // Gestion spécifique des erreurs rate limit
1272
+ if (error.status === 429 || error.type === 'RATE_LIMIT_ERROR') {
1273
+ const rateLimitError = new Error('API rate limit exceeded - Trop de requêtes simultanées');
1274
+ rateLimitError.status = 429;
1275
+ rateLimitError.type = 'RATE_LIMIT_ERROR';
1276
+ this.setError(rateLimitError);
1277
+ } else {
1278
+ this.setError(error);
1279
+ }
1280
+ } finally {
1281
+ this.setLoading(false);
1282
+ }
1283
+ }
1284
+
1285
+ // Gestion d'état
1286
+ setLoading(isLoading) {
1287
+ this.state.isLoading = isLoading;
1288
+ // Un seul render() - il gère lui-même l'état loading
1289
+ this.render();
1290
+ }
1291
+
1292
+ setProductData(productData) {
1293
+ this.state.productData = productData;
1294
+ this.state.error = null;
1295
+ // Ne pas render ici, sera fait par setLoading(false)
1296
+ }
1297
+
1298
+ setError(error) {
1299
+ this.state.error = error;
1300
+ this.state.productData = null;
1301
+ this.render();
1302
+ this.eventManager.emitError(error, this.ean);
1303
+ }
1304
+
1305
+ // Rendu principal
1306
+ async render() {
1307
+ // Toujours analyser l'état actuel pour déterminer le template
1308
+ const analysis = DisplayModeResolver.analyze(this.state.productData, {
1309
+ isLoading: this.state.isLoading,
1310
+ error: this.state.error,
1311
+ debug: this.getConfigValue('debug') || false
1312
+ });
1313
+
1314
+ if (!analysis.shouldDisplay) {
1315
+ this.updateShadowContent('<div class="vipros-offer-card hidden"></div>');
1316
+ return
1317
+ }
1318
+
1319
+ if (analysis.template) {
1320
+ const html = await this.templateLoader.render(analysis.template, analysis.data);
1321
+
1322
+ // Mettre à jour le contenu en préservant les styles
1323
+ this.updateShadowContent(html);
1324
+
1325
+ // Attacher les event listeners seulement si pas en loading
1326
+ if (!this.state.isLoading) {
1327
+ this.attachEventListeners();
1328
+ }
1329
+ }
1330
+ }
1331
+
1332
+ // Rendus spécialisés (déprécié - utiliser render() directement)
1333
+ async renderLoading() {
1334
+ // Cette méthode est dépréciée - render() gère maintenant le loading
1335
+ await this.render();
1336
+ }
1337
+
1338
+ // Utilitaire pour mettre à jour le contenu en préservant les styles
1339
+ updateShadowContent(html) {
1340
+ // Collecter tous les styles existants
1341
+ const styles = Array.from(this.shadowRoot.querySelectorAll('style'));
1342
+ const styleTexts = styles.map(style => style.textContent);
1343
+
1344
+ // Destruction complète du contenu
1345
+ this.shadowRoot.replaceChildren();
1346
+
1347
+ // Parser le nouveau HTML et l'ajouter
1348
+ const tempDiv = document.createElement('div');
1349
+ tempDiv.innerHTML = html;
1350
+
1351
+ // Transférer tous les enfants du div temporaire vers shadowRoot
1352
+ while (tempDiv.firstChild) {
1353
+ this.shadowRoot.appendChild(tempDiv.firstChild);
1354
+ }
1355
+
1356
+ // Recréer les styles
1357
+ styleTexts.forEach(styleText => {
1358
+ const styleElement = document.createElement('style');
1359
+ styleElement.textContent = styleText;
1360
+ this.shadowRoot.appendChild(styleElement);
1361
+ });
1362
+ }
1363
+
1364
+ // Event listeners
1365
+ attachEventListeners() {
1366
+ if (this.state.productData) {
1367
+ this.eventManager.attachLinkListeners(
1368
+ this.shadowRoot,
1369
+ this.state.productData,
1370
+ this.ean
1371
+ );
1372
+ }
1373
+ }
1374
+
1375
+ // Utilitaires
1376
+ getVIPointsImageUrl() {
1377
+ const baseUrl = (this.apiBaseUrl || '').replace('/api', '');
1378
+ return `${baseUrl}/api/sdk/assets/image-vipoints.jpg`
1379
+ }
1380
+
1381
+ triggerBackgroundSync() {
1382
+ const sdk = this.getSDKInstance();
1383
+ if (sdk?.syncService) {
1384
+ setTimeout(() => {
1385
+ this.log(`[ViprosOfferCard] Background sync for EAN ${this.ean}`);
1386
+ sdk.syncProductInBackground(this.ean, window.location.href);
1387
+ }, 100);
1388
+ }
1389
+ }
1390
+
1391
+ // Nettoyage
1392
+ cleanup() {
1393
+ this.eventManager.cleanup();
1394
+ this.state.apiClient = null;
1395
+ this.state.productData = null;
1396
+ }
1397
+ }
1398
+
1399
+ // Référence SDK (méthodes statiques de l'ancien composant)
1400
+ ViprosOfferCard.setSDKInstance = function(sdk) {
1401
+ if (sdk?.config?.debug) {
1402
+ console.log('[ViprosOfferCard] SDK instance connected');
1403
+ }
1404
+ };
1405
+
1406
+ class ConfigManager {
1407
+ static create (userConfig = {}) {
1408
+ const defaultConfig = ConfigManager.getDefaultConfig();
1409
+ const mergedConfig = ConfigManager.mergeConfig(defaultConfig, userConfig);
1410
+ const validatedConfig = ConfigManager.validateConfig(mergedConfig);
1411
+
1412
+ return validatedConfig
1413
+ }
1414
+
1415
+ static getDefaultConfig () {
1416
+ return {
1417
+ // API Configuration
1418
+ apiBaseUrl: ConfigManager.detectApiBaseUrl(),
1419
+ apiKey: null, // Clé API obligatoire - doit être fournie explicitement
1420
+ timeout: 10000,
1421
+
1422
+ // EAN Configuration
1423
+ ean: null, // EAN par défaut pour les composants sans attribut ean
1424
+
1425
+ // Global Component Defaults
1426
+ price: null, // Prix par défaut pour les composants sans attribut price
1427
+ primaryColor: null, // Couleur principale par défaut pour les composants sans attribut primary-color
1428
+ textColor: null, // Couleur de texte par défaut pour les composants sans attribut text-color
1429
+ backgroundColor: null, // Couleur de fond par défaut pour les composants sans attribut background-color
1430
+ cardBonusBg: null, // Couleur de fond des badges bonus par défaut pour les composants sans attribut card-bonus-bg
1431
+ cardBonusColor: null, // Couleur de texte/icône des badges bonus par défaut pour les composants sans attribut card-bonus-color
1432
+
1433
+ // Cache Configuration
1434
+ cacheTimeout: 300000, // 5 minutes
1435
+ cachePrefix: 'vipros_sdk_',
1436
+ maxCacheSize: 100,
1437
+
1438
+ // Container Configuration
1439
+ containerSelector: '[data-ean]',
1440
+ eanAttribute: 'data-ean',
1441
+
1442
+ // Rendering Configuration
1443
+ renderPosition: 'replace', // 'before', 'after', 'prepend', 'append', 'replace'
1444
+ excludeSelectors: ['.vipros-offer', '[data-vipros-exclude]'],
1445
+
1446
+ // Template Configuration - Templates par défaut seront utilisés par TemplateEngine
1447
+ templates: {
1448
+ // Ne pas spécifier de valeurs par défaut - TemplateEngine a ses propres templates intégrés
1449
+ },
1450
+
1451
+ // Styling Configuration
1452
+ styling: {
1453
+ theme: 'minimal', // 'minimal', 'branded', 'custom'
1454
+ customCSS: {},
1455
+ inlineStyles: true,
1456
+ cssPrefix: 'vipros-',
1457
+ responsive: true
1458
+ },
1459
+
1460
+ // Performance Configuration
1461
+ batchRequests: true,
1462
+ lazyLoad: true,
1463
+ maxConcurrentRequests: 3,
1464
+ debounceDelay: 300,
1465
+
1466
+ // Behavior Configuration
1467
+ hideOnError: false,
1468
+ retryOnError: true,
1469
+ gracefulDegradetion: true,
1470
+
1471
+ // Sync Configuration
1472
+ syncEnabled: true,
1473
+ syncOptions: {
1474
+ cooldownTime: 24 * 60 * 60 * 1000, // 24 heures - une sync par jour par produit
1475
+ syncDelay: 100, // Délai avant sync en ms
1476
+ maxRetries: 3,
1477
+ retryDelay: 1000 // Délai entre retry en ms
1478
+ },
1479
+
1480
+ // Debug Configuration
1481
+ debug: false,
1482
+ verbose: false,
1483
+ logLevel: 'warn', // 'error', 'warn', 'info', 'debug'
1484
+
1485
+ // Event Callbacks
1486
+ onReady: null,
1487
+ onOfferFound: null,
1488
+ onOfferRendered: null,
1489
+ onOfferLoaded: null, // Nouveau callback quand une offre est complètement chargée et affichée
1490
+ onError: null,
1491
+
1492
+ // Feature Flags
1493
+ features: {
1494
+ analyticsTracking: false,
1495
+ performanceMonitoring: false,
1496
+ a11yEnhancements: true,
1497
+ seoOptimizations: true
1498
+ },
1499
+
1500
+ // Internationalization
1501
+ locale: 'fr-FR',
1502
+ currency: 'EUR',
1503
+ dateFormat: 'DD/MM/YYYY',
1504
+
1505
+ // Version
1506
+ version: '1.0.0'
1507
+ }
1508
+ }
1509
+
1510
+ static mergeConfig (defaultConfig, userConfig) {
1511
+ return ConfigManager.deepMerge(defaultConfig, userConfig)
1512
+ }
1513
+
1514
+ static deepMerge (target, source) {
1515
+ const result = { ...target };
1516
+
1517
+ for (const [key, value] of Object.entries(source)) {
1518
+ if (value === null || value === undefined) {
1519
+ continue
1520
+ }
1521
+
1522
+ if (ConfigManager.isObject(value) && ConfigManager.isObject(result[key])) {
1523
+ result[key] = ConfigManager.deepMerge(result[key], value);
1524
+ } else {
1525
+ result[key] = value;
1526
+ }
1527
+ }
1528
+
1529
+ return result
1530
+ }
1531
+
1532
+ static isObject (item) {
1533
+ return item && typeof item === 'object' && !Array.isArray(item)
1534
+ }
1535
+
1536
+ static validateConfig (config) {
1537
+ const errors = [];
1538
+
1539
+ // Validate API Configuration
1540
+ if (!config.apiBaseUrl || typeof config.apiBaseUrl !== 'string') {
1541
+ errors.push('apiBaseUrl must be a valid string');
1542
+ } else {
1543
+ try {
1544
+ new URL(config.apiBaseUrl);
1545
+ } catch (e) {
1546
+ errors.push('apiBaseUrl must be a valid URL');
1547
+ }
1548
+ }
1549
+
1550
+ if (!config.apiKey) {
1551
+ errors.push('apiKey is required');
1552
+ }
1553
+
1554
+ if (config.apiKey && typeof config.apiKey !== 'string') {
1555
+ errors.push('apiKey must be a string');
1556
+ }
1557
+
1558
+ if (config.timeout && (typeof config.timeout !== 'number' || config.timeout <= 0)) {
1559
+ errors.push('timeout must be a positive number');
1560
+ }
1561
+
1562
+ // Validate Cache Configuration
1563
+ if (config.cacheTimeout && (typeof config.cacheTimeout !== 'number' || config.cacheTimeout < 0)) {
1564
+ errors.push('cacheTimeout must be a non-negative number');
1565
+ }
1566
+
1567
+ if (config.maxCacheSize && (typeof config.maxCacheSize !== 'number' || config.maxCacheSize <= 0)) {
1568
+ errors.push('maxCacheSize must be a positive number');
1569
+ }
1570
+
1571
+ // Validate Container Configuration
1572
+ if (config.containerSelector && typeof config.containerSelector !== 'string') {
1573
+ errors.push('containerSelector must be a string');
1574
+ }
1575
+
1576
+ if (config.eanAttribute && typeof config.eanAttribute !== 'string') {
1577
+ errors.push('eanAttribute must be a string');
1578
+ }
1579
+
1580
+ // Validate Styling
1581
+ if (config.styling) {
1582
+ const validThemes = ['minimal', 'branded', 'custom'];
1583
+ if (config.styling.theme && !validThemes.includes(config.styling.theme)) {
1584
+ errors.push(`styling.theme must be one of: ${validThemes.join(', ')}`);
1585
+ }
1586
+
1587
+ if (config.styling.customCSS && !ConfigManager.isObject(config.styling.customCSS)) {
1588
+ errors.push('styling.customCSS must be an object');
1589
+ }
1590
+ }
1591
+
1592
+ // Validate Performance Configuration
1593
+ if (config.maxConcurrentRequests && (typeof config.maxConcurrentRequests !== 'number' || config.maxConcurrentRequests <= 0)) {
1594
+ errors.push('maxConcurrentRequests must be a positive number');
1595
+ }
1596
+
1597
+ if (config.debounceDelay && (typeof config.debounceDelay !== 'number' || config.debounceDelay < 0)) {
1598
+ errors.push('debounceDelay must be a non-negative number');
1599
+ }
1600
+
1601
+
1602
+ // Validate Log Level
1603
+ const validLogLevels = ['error', 'warn', 'info', 'debug'];
1604
+ if (config.logLevel && !validLogLevels.includes(config.logLevel)) {
1605
+ errors.push(`logLevel must be one of: ${validLogLevels.join(', ')}`);
1606
+ }
1607
+
1608
+ // Validate Event Callbacks
1609
+ const eventCallbacks = ['onReady', 'onOfferFound', 'onOfferRendered', 'onOfferLoaded', 'onError'];
1610
+ for (const callback of eventCallbacks) {
1611
+ if (config[callback] && typeof config[callback] !== 'function') {
1612
+ errors.push(`${callback} must be a function`);
1613
+ }
1614
+ }
1615
+
1616
+ // Validate Locale
1617
+ if (config.locale && typeof config.locale !== 'string') {
1618
+ errors.push('locale must be a string');
1619
+ }
1620
+
1621
+ // Validate Currency
1622
+ if (config.currency && typeof config.currency !== 'string') {
1623
+ errors.push('currency must be a string');
1624
+ }
1625
+
1626
+ if (errors.length > 0) {
1627
+ const error = new Error(`Configuration validation failed:\n${errors.join('\n')}`);
1628
+ error.validationErrors = errors;
1629
+ throw error
1630
+ }
1631
+
1632
+ // Apply post-validation processing
1633
+ return ConfigManager.processConfig(config)
1634
+ }
1635
+
1636
+ static processConfig (config) {
1637
+ const processed = { ...config };
1638
+
1639
+ // Normalize API Base URL
1640
+ processed.apiBaseUrl = processed.apiBaseUrl.replace(/\/$/, '');
1641
+
1642
+ // Ensure containerSelector is properly formatted
1643
+ if (!processed.containerSelector.startsWith('[') && !processed.containerSelector.startsWith('.') && !processed.containerSelector.startsWith('#')) {
1644
+ processed.containerSelector = `[${processed.eanAttribute}]`;
1645
+ }
1646
+
1647
+ // Set debug flags based on logLevel
1648
+ if (processed.logLevel === 'debug') {
1649
+ processed.debug = true;
1650
+ processed.verbose = true;
1651
+ } else if (processed.logLevel === 'info') {
1652
+ processed.debug = true;
1653
+ }
1654
+
1655
+ // Validate and normalize CSS custom properties
1656
+ if (processed.styling.customCSS) {
1657
+ const normalizedCSS = {};
1658
+ for (const [key, value] of Object.entries(processed.styling.customCSS)) {
1659
+ const cssProperty = key.startsWith('--') ? key : `--vipros-${key}`;
1660
+ normalizedCSS[cssProperty] = value;
1661
+ }
1662
+ processed.styling.customCSS = normalizedCSS;
1663
+ }
1664
+
1665
+ // Set cache prefix with version
1666
+ processed.cachePrefix = `${processed.cachePrefix}v${processed.version.replace(/\./g, '_')}_`;
1667
+
1668
+ // Calculate derived values
1669
+ processed.derived = {
1670
+ isProduction: !processed.debug,
1671
+ shouldCache: processed.cacheTimeout > 0,
1672
+ shouldBatch: processed.batchRequests && processed.maxConcurrentRequests > 1,
1673
+ shouldLazyLoad: processed.lazyLoad,
1674
+ hasCustomStyling: processed.styling.theme === 'custom' || Object.keys(processed.styling.customCSS || {}).length > 0
1675
+ };
1676
+
1677
+ return processed
1678
+ }
1679
+
1680
+
1681
+ static createSecureConfig (userConfig, serverConfig = {}) {
1682
+ const clientSafeServerConfig = {
1683
+ apiBaseUrl: serverConfig.apiBaseUrl,
1684
+ allowedFeatures: serverConfig.allowedFeatures,
1685
+ rateLimits: serverConfig.rateLimits,
1686
+ version: serverConfig.version
1687
+ };
1688
+
1689
+ return ConfigManager.create({
1690
+ ...clientSafeServerConfig,
1691
+ ...userConfig
1692
+ })
1693
+ }
1694
+
1695
+ static validateRuntimeConfig (config) {
1696
+ if (!config) {
1697
+ throw new Error('Configuration is required')
1698
+ }
1699
+
1700
+ if (!config.derived) {
1701
+ throw new Error('Configuration has not been processed')
1702
+ }
1703
+
1704
+ return true
1705
+ }
1706
+
1707
+ static getConfigSummary (config) {
1708
+ return {
1709
+ version: config.version,
1710
+ apiBaseUrl: config.apiBaseUrl,
1711
+ theme: config.styling.theme,
1712
+ cacheEnabled: config.derived.shouldCache,
1713
+ debugMode: config.debug,
1714
+ features: Object.keys(config.features).filter(key => config.features[key])
1715
+ }
1716
+ }
1717
+
1718
+ /**
1719
+ * Auto-détection de l'URL d'API selon le domaine
1720
+ * SINGLE SOURCE OF TRUTH pour la détection d'environnement
1721
+ * Enhanced with robust error handling for edge cases
1722
+ */
1723
+ static detectApiBaseUrl(debug = false) {
1724
+ try {
1725
+ // Edge case: No window object (SSR, Node.js, service worker)
1726
+ if (typeof window === 'undefined') {
1727
+ if (debug) console.warn('[ConfigManager] No window object - defaulting to production API');
1728
+ return 'https://msys.vipros.fr/api'
1729
+ }
1730
+
1731
+ // Edge case: window.location is not available or corrupted
1732
+ if (!window.location || typeof window.location.hostname !== 'string') {
1733
+ if (debug) console.warn('[ConfigManager] Invalid window.location - defaulting to production API');
1734
+ return 'https://msys.vipros.fr/api'
1735
+ }
1736
+
1737
+ const protocol = window.location.protocol || 'https:';
1738
+ const hostname = window.location.hostname.toLowerCase();
1739
+
1740
+ // Edge case: Empty or invalid hostname
1741
+ if (!hostname || hostname.length === 0) {
1742
+ if (debug) console.warn('[ConfigManager] Empty hostname - defaulting to production API');
1743
+ return 'https://msys.vipros.fr/api'
1744
+ }
1745
+
1746
+ // Edge case: Local development environments
1747
+ if (protocol === 'file:' || hostname === 'localhost' || hostname.startsWith('127.0.0.1') || hostname.startsWith('0.0.0.0')) {
1748
+ if (debug) console.log('[ConfigManager] Local development detected - using DDEV fallback');
1749
+ return 'https://vipros-connect.ddev.site/api'
1750
+ }
1751
+
1752
+ // Edge case: Non-standard ports for development
1753
+ if (hostname.includes(':') && (hostname.includes('localhost') || hostname.includes('127.0.0.1'))) {
1754
+ if (debug) console.log('[ConfigManager] Local development with port detected - using DDEV fallback');
1755
+ return 'https://vipros-connect.ddev.site/api'
1756
+ }
1757
+
1758
+ // Correspondance directe des domaines
1759
+ if (hostname === 'vipros-connect.ddev.site') {
1760
+ return `${protocol}//${hostname}/api`
1761
+ }
1762
+
1763
+ if (hostname === 'msys.preprod.vipros.fr') {
1764
+ return `${protocol}//msys.preprod.vipros.fr/api`
1765
+ }
1766
+
1767
+ // Edge case: Enterprise subdomains or custom VIPros domains
1768
+ if (hostname.endsWith('.vipros.fr')) {
1769
+ if (debug) console.log(`[ConfigManager] VIPros subdomain detected: ${hostname}`);
1770
+ return `${protocol}//${hostname}/api`
1771
+ }
1772
+
1773
+ // Edge case: IP addresses (development/staging environments)
1774
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
1775
+ if (debug) console.log(`[ConfigManager] IP address detected: ${hostname} - using production API`);
1776
+ return 'https://msys.vipros.fr/api'
1777
+ }
1778
+
1779
+ // Par défaut : production (toujours HTTPS pour la sécurité)
1780
+ if (debug) console.log(`[ConfigManager] Unknown domain: ${hostname} - defaulting to production API`);
1781
+ return 'https://msys.vipros.fr/api'
1782
+
1783
+ } catch (error) {
1784
+ // Edge case: Unexpected error during detection
1785
+ if (debug) console.error(`[ConfigManager] Error during API base URL detection: ${error.message} - defaulting to production`);
1786
+ return 'https://msys.vipros.fr/api'
1787
+ }
1788
+ }
1789
+ }
1790
+
1791
+ class EventEmitter {
1792
+ constructor () {
1793
+ this.events = new Map();
1794
+ this.maxListeners = 10;
1795
+ }
1796
+
1797
+ on (event, callback) {
1798
+ if (typeof callback !== 'function') {
1799
+ throw new TypeError('Callback must be a function')
1800
+ }
1801
+
1802
+ if (!this.events.has(event)) {
1803
+ this.events.set(event, []);
1804
+ }
1805
+
1806
+ const listeners = this.events.get(event);
1807
+
1808
+ if (listeners.length >= this.maxListeners) {
1809
+ console.warn(`Max listeners (${this.maxListeners}) exceeded for event: ${event}`);
1810
+ }
1811
+
1812
+ listeners.push(callback);
1813
+ return this
1814
+ }
1815
+
1816
+ once (event, callback) {
1817
+ if (typeof callback !== 'function') {
1818
+ throw new TypeError('Callback must be a function')
1819
+ }
1820
+
1821
+ const onceWrapper = (...args) => {
1822
+ this.off(event, onceWrapper);
1823
+ callback.apply(this, args);
1824
+ };
1825
+
1826
+ return this.on(event, onceWrapper)
1827
+ }
1828
+
1829
+ off (event, callback) {
1830
+ if (!this.events.has(event)) {
1831
+ return this
1832
+ }
1833
+
1834
+ if (!callback) {
1835
+ this.events.delete(event);
1836
+ return this
1837
+ }
1838
+
1839
+ const listeners = this.events.get(event);
1840
+ const index = listeners.indexOf(callback);
1841
+
1842
+ if (index !== -1) {
1843
+ listeners.splice(index, 1);
1844
+ }
1845
+
1846
+ if (listeners.length === 0) {
1847
+ this.events.delete(event);
1848
+ }
1849
+
1850
+ return this
1851
+ }
1852
+
1853
+ emit (event, ...args) {
1854
+ if (!this.events.has(event)) {
1855
+ return false
1856
+ }
1857
+
1858
+ const listeners = this.events.get(event).slice();
1859
+
1860
+ for (const callback of listeners) {
1861
+ try {
1862
+ callback.apply(this, args);
1863
+ } catch (error) {
1864
+ console.error(`Error in event listener for '${event}':`, error);
1865
+ this.emit('error', error);
1866
+ }
1867
+ }
1868
+
1869
+ return true
1870
+ }
1871
+
1872
+ removeAllListeners (event = null) {
1873
+ if (event) {
1874
+ this.events.delete(event);
1875
+ } else {
1876
+ this.events.clear();
1877
+ }
1878
+ return this
1879
+ }
1880
+
1881
+ listenerCount (event) {
1882
+ return this.events.has(event) ? this.events.get(event).length : 0
1883
+ }
1884
+
1885
+ getMaxListeners () {
1886
+ return this.maxListeners
1887
+ }
1888
+
1889
+ setMaxListeners (max) {
1890
+ if (typeof max !== 'number' || max < 0) {
1891
+ throw new TypeError('Max listeners must be a non-negative number')
1892
+ }
1893
+ this.maxListeners = max;
1894
+ return this
1895
+ }
1896
+
1897
+ eventNames () {
1898
+ return Array.from(this.events.keys())
1899
+ }
1900
+
1901
+ listeners (event) {
1902
+ return this.events.has(event) ? this.events.get(event).slice() : []
1903
+ }
1904
+ }
1905
+
1906
+ // DEFLATE is a complex format; to read this code, you should probably check the RFC first:
1907
+ // https://tools.ietf.org/html/rfc1951
1908
+ // You may also wish to take a look at the guide I made about this program:
1909
+ // https://gist.github.com/101arrowz/253f31eb5abc3d9275ab943003ffecad
1910
+ // Some of the following code is similar to that of UZIP.js:
1911
+ // https://github.com/photopea/UZIP.js
1912
+ // However, the vast majority of the codebase has diverged from UZIP.js to increase performance and reduce bundle size.
1913
+ // Sometimes 0 will appear where -1 would be more appropriate. This is because using a uint
1914
+ // is better for memory in most engines (I *think*).
1915
+
1916
+ // aliases for shorter compressed code (most minifers don't do this)
1917
+ var u8 = Uint8Array, u16 = Uint16Array, i32 = Int32Array;
1918
+ // fixed length extra bits
1919
+ var fleb = new u8([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, /* unused */ 0, 0, /* impossible */ 0]);
1920
+ // fixed distance extra bits
1921
+ var fdeb = new u8([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, /* unused */ 0, 0]);
1922
+ // code length index map
1923
+ var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
1924
+ // get base, reverse index map from extra bits
1925
+ var freb = function (eb, start) {
1926
+ var b = new u16(31);
1927
+ for (var i = 0; i < 31; ++i) {
1928
+ b[i] = start += 1 << eb[i - 1];
1929
+ }
1930
+ // numbers here are at max 18 bits
1931
+ var r = new i32(b[30]);
1932
+ for (var i = 1; i < 30; ++i) {
1933
+ for (var j = b[i]; j < b[i + 1]; ++j) {
1934
+ r[j] = ((j - b[i]) << 5) | i;
1935
+ }
1936
+ }
1937
+ return { b: b, r: r };
1938
+ };
1939
+ var _a = freb(fleb, 2), fl = _a.b, revfl = _a.r;
1940
+ // we can ignore the fact that the other numbers are wrong; they never happen anyway
1941
+ fl[28] = 258, revfl[258] = 28;
1942
+ var _b = freb(fdeb, 0), fd = _b.b;
1943
+ // map of value to reverse (assuming 16 bits)
1944
+ var rev = new u16(32768);
1945
+ for (var i = 0; i < 32768; ++i) {
1946
+ // reverse table algorithm from SO
1947
+ var x = ((i & 0xAAAA) >> 1) | ((i & 0x5555) << 1);
1948
+ x = ((x & 0xCCCC) >> 2) | ((x & 0x3333) << 2);
1949
+ x = ((x & 0xF0F0) >> 4) | ((x & 0x0F0F) << 4);
1950
+ rev[i] = (((x & 0xFF00) >> 8) | ((x & 0x00FF) << 8)) >> 1;
1951
+ }
1952
+ // create huffman tree from u8 "map": index -> code length for code index
1953
+ // mb (max bits) must be at most 15
1954
+ // TODO: optimize/split up?
1955
+ var hMap = (function (cd, mb, r) {
1956
+ var s = cd.length;
1957
+ // index
1958
+ var i = 0;
1959
+ // u16 "map": index -> # of codes with bit length = index
1960
+ var l = new u16(mb);
1961
+ // length of cd must be 288 (total # of codes)
1962
+ for (; i < s; ++i) {
1963
+ if (cd[i])
1964
+ ++l[cd[i] - 1];
1965
+ }
1966
+ // u16 "map": index -> minimum code for bit length = index
1967
+ var le = new u16(mb);
1968
+ for (i = 1; i < mb; ++i) {
1969
+ le[i] = (le[i - 1] + l[i - 1]) << 1;
1970
+ }
1971
+ var co;
1972
+ if (r) {
1973
+ // u16 "map": index -> number of actual bits, symbol for code
1974
+ co = new u16(1 << mb);
1975
+ // bits to remove for reverser
1976
+ var rvb = 15 - mb;
1977
+ for (i = 0; i < s; ++i) {
1978
+ // ignore 0 lengths
1979
+ if (cd[i]) {
1980
+ // num encoding both symbol and bits read
1981
+ var sv = (i << 4) | cd[i];
1982
+ // free bits
1983
+ var r_1 = mb - cd[i];
1984
+ // start value
1985
+ var v = le[cd[i] - 1]++ << r_1;
1986
+ // m is end value
1987
+ for (var m = v | ((1 << r_1) - 1); v <= m; ++v) {
1988
+ // every 16 bit value starting with the code yields the same result
1989
+ co[rev[v] >> rvb] = sv;
1990
+ }
1991
+ }
1992
+ }
1993
+ }
1994
+ else {
1995
+ co = new u16(s);
1996
+ for (i = 0; i < s; ++i) {
1997
+ if (cd[i]) {
1998
+ co[i] = rev[le[cd[i] - 1]++] >> (15 - cd[i]);
1999
+ }
2000
+ }
2001
+ }
2002
+ return co;
2003
+ });
2004
+ // fixed length tree
2005
+ var flt = new u8(288);
2006
+ for (var i = 0; i < 144; ++i)
2007
+ flt[i] = 8;
2008
+ for (var i = 144; i < 256; ++i)
2009
+ flt[i] = 9;
2010
+ for (var i = 256; i < 280; ++i)
2011
+ flt[i] = 7;
2012
+ for (var i = 280; i < 288; ++i)
2013
+ flt[i] = 8;
2014
+ // fixed distance tree
2015
+ var fdt = new u8(32);
2016
+ for (var i = 0; i < 32; ++i)
2017
+ fdt[i] = 5;
2018
+ // fixed length map
2019
+ var flrm = /*#__PURE__*/ hMap(flt, 9, 1);
2020
+ // fixed distance map
2021
+ var fdrm = /*#__PURE__*/ hMap(fdt, 5, 1);
2022
+ // find max of array
2023
+ var max = function (a) {
2024
+ var m = a[0];
2025
+ for (var i = 1; i < a.length; ++i) {
2026
+ if (a[i] > m)
2027
+ m = a[i];
2028
+ }
2029
+ return m;
2030
+ };
2031
+ // read d, starting at bit p and mask with m
2032
+ var bits = function (d, p, m) {
2033
+ var o = (p / 8) | 0;
2034
+ return ((d[o] | (d[o + 1] << 8)) >> (p & 7)) & m;
2035
+ };
2036
+ // read d, starting at bit p continuing for at least 16 bits
2037
+ var bits16 = function (d, p) {
2038
+ var o = (p / 8) | 0;
2039
+ return ((d[o] | (d[o + 1] << 8) | (d[o + 2] << 16)) >> (p & 7));
2040
+ };
2041
+ // get end of byte
2042
+ var shft = function (p) { return ((p + 7) / 8) | 0; };
2043
+ // typed array slice - allows garbage collector to free original reference,
2044
+ // while being more compatible than .slice
2045
+ var slc = function (v, s, e) {
2046
+ if (e == null || e > v.length)
2047
+ e = v.length;
2048
+ // can't use .constructor in case user-supplied
2049
+ return new u8(v.subarray(s, e));
2050
+ };
2051
+ // error codes
2052
+ var ec = [
2053
+ 'unexpected EOF',
2054
+ 'invalid block type',
2055
+ 'invalid length/literal',
2056
+ 'invalid distance',
2057
+ 'stream finished',
2058
+ 'no stream handler',
2059
+ ,
2060
+ 'no callback',
2061
+ 'invalid UTF-8 data',
2062
+ 'extra field too long',
2063
+ 'date not in range 1980-2099',
2064
+ 'filename too long',
2065
+ 'stream finishing',
2066
+ 'invalid zip data'
2067
+ // determined by unknown compression method
2068
+ ];
2069
+ var err = function (ind, msg, nt) {
2070
+ var e = new Error(msg || ec[ind]);
2071
+ e.code = ind;
2072
+ if (Error.captureStackTrace)
2073
+ Error.captureStackTrace(e, err);
2074
+ if (!nt)
2075
+ throw e;
2076
+ return e;
2077
+ };
2078
+ // expands raw DEFLATE data
2079
+ var inflt = function (dat, st, buf, dict) {
2080
+ // source length dict length
2081
+ var sl = dat.length, dl = 0;
2082
+ if (!sl || st.f && !st.l)
2083
+ return buf || new u8(0);
2084
+ var noBuf = !buf;
2085
+ // have to estimate size
2086
+ var resize = noBuf || st.i != 2;
2087
+ // no state
2088
+ var noSt = st.i;
2089
+ // Assumes roughly 33% compression ratio average
2090
+ if (noBuf)
2091
+ buf = new u8(sl * 3);
2092
+ // ensure buffer can fit at least l elements
2093
+ var cbuf = function (l) {
2094
+ var bl = buf.length;
2095
+ // need to increase size to fit
2096
+ if (l > bl) {
2097
+ // Double or set to necessary, whichever is greater
2098
+ var nbuf = new u8(Math.max(bl * 2, l));
2099
+ nbuf.set(buf);
2100
+ buf = nbuf;
2101
+ }
2102
+ };
2103
+ // last chunk bitpos bytes
2104
+ var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n;
2105
+ // total bits
2106
+ var tbts = sl * 8;
2107
+ do {
2108
+ if (!lm) {
2109
+ // BFINAL - this is only 1 when last chunk is next
2110
+ final = bits(dat, pos, 1);
2111
+ // type: 0 = no compression, 1 = fixed huffman, 2 = dynamic huffman
2112
+ var type = bits(dat, pos + 1, 3);
2113
+ pos += 3;
2114
+ if (!type) {
2115
+ // go to end of byte boundary
2116
+ var s = shft(pos) + 4, l = dat[s - 4] | (dat[s - 3] << 8), t = s + l;
2117
+ if (t > sl) {
2118
+ if (noSt)
2119
+ err(0);
2120
+ break;
2121
+ }
2122
+ // ensure size
2123
+ if (resize)
2124
+ cbuf(bt + l);
2125
+ // Copy over uncompressed data
2126
+ buf.set(dat.subarray(s, t), bt);
2127
+ // Get new bitpos, update byte count
2128
+ st.b = bt += l, st.p = pos = t * 8, st.f = final;
2129
+ continue;
2130
+ }
2131
+ else if (type == 1)
2132
+ lm = flrm, dm = fdrm, lbt = 9, dbt = 5;
2133
+ else if (type == 2) {
2134
+ // literal lengths
2135
+ var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4;
2136
+ var tl = hLit + bits(dat, pos + 5, 31) + 1;
2137
+ pos += 14;
2138
+ // length+distance tree
2139
+ var ldt = new u8(tl);
2140
+ // code length tree
2141
+ var clt = new u8(19);
2142
+ for (var i = 0; i < hcLen; ++i) {
2143
+ // use index map to get real code
2144
+ clt[clim[i]] = bits(dat, pos + i * 3, 7);
2145
+ }
2146
+ pos += hcLen * 3;
2147
+ // code lengths bits
2148
+ var clb = max(clt), clbmsk = (1 << clb) - 1;
2149
+ // code lengths map
2150
+ var clm = hMap(clt, clb, 1);
2151
+ for (var i = 0; i < tl;) {
2152
+ var r = clm[bits(dat, pos, clbmsk)];
2153
+ // bits read
2154
+ pos += r & 15;
2155
+ // symbol
2156
+ var s = r >> 4;
2157
+ // code length to copy
2158
+ if (s < 16) {
2159
+ ldt[i++] = s;
2160
+ }
2161
+ else {
2162
+ // copy count
2163
+ var c = 0, n = 0;
2164
+ if (s == 16)
2165
+ n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1];
2166
+ else if (s == 17)
2167
+ n = 3 + bits(dat, pos, 7), pos += 3;
2168
+ else if (s == 18)
2169
+ n = 11 + bits(dat, pos, 127), pos += 7;
2170
+ while (n--)
2171
+ ldt[i++] = c;
2172
+ }
2173
+ }
2174
+ // length tree distance tree
2175
+ var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
2176
+ // max length bits
2177
+ lbt = max(lt);
2178
+ // max dist bits
2179
+ dbt = max(dt);
2180
+ lm = hMap(lt, lbt, 1);
2181
+ dm = hMap(dt, dbt, 1);
2182
+ }
2183
+ else
2184
+ err(1);
2185
+ if (pos > tbts) {
2186
+ if (noSt)
2187
+ err(0);
2188
+ break;
2189
+ }
2190
+ }
2191
+ // Make sure the buffer can hold this + the largest possible addition
2192
+ // Maximum chunk size (practically, theoretically infinite) is 2^17
2193
+ if (resize)
2194
+ cbuf(bt + 131072);
2195
+ var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1;
2196
+ var lpos = pos;
2197
+ for (;; lpos = pos) {
2198
+ // bits read, code
2199
+ var c = lm[bits16(dat, pos) & lms], sym = c >> 4;
2200
+ pos += c & 15;
2201
+ if (pos > tbts) {
2202
+ if (noSt)
2203
+ err(0);
2204
+ break;
2205
+ }
2206
+ if (!c)
2207
+ err(2);
2208
+ if (sym < 256)
2209
+ buf[bt++] = sym;
2210
+ else if (sym == 256) {
2211
+ lpos = pos, lm = null;
2212
+ break;
2213
+ }
2214
+ else {
2215
+ var add = sym - 254;
2216
+ // no extra bits needed if less
2217
+ if (sym > 264) {
2218
+ // index
2219
+ var i = sym - 257, b = fleb[i];
2220
+ add = bits(dat, pos, (1 << b) - 1) + fl[i];
2221
+ pos += b;
2222
+ }
2223
+ // dist
2224
+ var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
2225
+ if (!d)
2226
+ err(3);
2227
+ pos += d & 15;
2228
+ var dt = fd[dsym];
2229
+ if (dsym > 3) {
2230
+ var b = fdeb[dsym];
2231
+ dt += bits16(dat, pos) & (1 << b) - 1, pos += b;
2232
+ }
2233
+ if (pos > tbts) {
2234
+ if (noSt)
2235
+ err(0);
2236
+ break;
2237
+ }
2238
+ if (resize)
2239
+ cbuf(bt + 131072);
2240
+ var end = bt + add;
2241
+ if (bt < dt) {
2242
+ var shift = dl - dt, dend = Math.min(dt, end);
2243
+ if (shift + bt < 0)
2244
+ err(3);
2245
+ for (; bt < dend; ++bt)
2246
+ buf[bt] = dict[shift + bt];
2247
+ }
2248
+ for (; bt < end; ++bt)
2249
+ buf[bt] = buf[bt - dt];
2250
+ }
2251
+ }
2252
+ st.l = lm, st.p = lpos, st.b = bt, st.f = final;
2253
+ if (lm)
2254
+ final = 1, st.m = lbt, st.d = dm, st.n = dbt;
2255
+ } while (!final);
2256
+ // don't reallocate for streams or user buffers
2257
+ return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt);
2258
+ };
2259
+ // empty
2260
+ var et = /*#__PURE__*/ new u8(0);
2261
+ // gzip footer: -8 to -4 = CRC, -4 to -0 is length
2262
+ // gzip start
2263
+ var gzs = function (d) {
2264
+ if (d[0] != 31 || d[1] != 139 || d[2] != 8)
2265
+ err(6, 'invalid gzip data');
2266
+ var flg = d[3];
2267
+ var st = 10;
2268
+ if (flg & 4)
2269
+ st += (d[10] | d[11] << 8) + 2;
2270
+ for (var zs = (flg >> 3 & 1) + (flg >> 4 & 1); zs > 0; zs -= !d[st++])
2271
+ ;
2272
+ return st + (flg & 2);
2273
+ };
2274
+ // gzip length
2275
+ var gzl = function (d) {
2276
+ var l = d.length;
2277
+ return (d[l - 4] | d[l - 3] << 8 | d[l - 2] << 16 | d[l - 1] << 24) >>> 0;
2278
+ };
2279
+ // zlib start
2280
+ var zls = function (d, dict) {
2281
+ if ((d[0] & 15) != 8 || (d[0] >> 4) > 7 || ((d[0] << 8 | d[1]) % 31))
2282
+ err(6, 'invalid zlib data');
2283
+ if ((d[1] >> 5 & 1) == 1)
2284
+ err(6, 'invalid zlib data: ' + (d[1] & 32 ? 'need' : 'unexpected') + ' dictionary');
2285
+ return (d[1] >> 3 & 4) + 2;
2286
+ };
2287
+ /**
2288
+ * Expands GZIP data
2289
+ * @param data The data to decompress
2290
+ * @param opts The decompression options
2291
+ * @returns The decompressed version of the data
2292
+ */
2293
+ function gunzipSync(data, opts) {
2294
+ var st = gzs(data);
2295
+ if (st + 8 > data.length)
2296
+ err(6, 'invalid gzip data');
2297
+ return inflt(data.subarray(st, -8), { i: 2 }, new u8(gzl(data)), opts);
2298
+ }
2299
+ /**
2300
+ * Expands Zlib data
2301
+ * @param data The data to decompress
2302
+ * @param opts The decompression options
2303
+ * @returns The decompressed version of the data
2304
+ */
2305
+ function unzlibSync(data, opts) {
2306
+ return inflt(data.subarray(zls(data), -4), { i: 2 }, opts, opts);
2307
+ }
2308
+ // text decoder
2309
+ var td = typeof TextDecoder != 'undefined' && /*#__PURE__*/ new TextDecoder();
2310
+ // text decoder stream
2311
+ var tds = 0;
2312
+ try {
2313
+ td.decode(et, { stream: true });
2314
+ tds = 1;
2315
+ }
2316
+ catch (e) { }
2317
+
2318
+ /**
2319
+ * Implémentation simple du Bloom Filter pour SDK VIPros
2320
+ * Compatible avec le générateur PHP côté serveur
2321
+ */
2322
+ class SimpleBloomFilter {
2323
+ constructor(compressedData, options = {}) {
2324
+ this.debugEnabled = Boolean(options.debug);
2325
+ try {
2326
+ // Décompresser les données gzip (simulation simple)
2327
+ this.bits = this.decompress(compressedData);
2328
+ this.size = this.bits.length * 8;
2329
+ this.k = 7; // Nombre de fonctions de hachage (même que côté serveur)
2330
+ } catch (error) {
2331
+ console.warn('[BloomFilter] Decompression failed, using raw data', error);
2332
+ this.bits = compressedData;
2333
+ this.size = this.bits.length * 8;
2334
+ this.k = 7;
2335
+ }
2336
+ }
2337
+
2338
+ /**
2339
+ * Teste si un EAN pourrait être présent dans le filtre
2340
+ * @param {string} ean - Code EAN à tester
2341
+ * @returns {boolean} true si l'EAN pourrait être présent, false s'il est définitivement absent
2342
+ */
2343
+ test(ean) {
2344
+ if (!this.isValidEan(ean)) {
2345
+ this.debug('[BloomFilter] Invalid EAN format:', ean);
2346
+ return false
2347
+ }
2348
+
2349
+ this.debug('[BloomFilter] Testing EAN:', ean, 'Size:', this.size, 'Bits length:', this.bits.length);
2350
+
2351
+ for (let i = 0; i < this.k; i++) {
2352
+ const fullHash = this.hash(ean + i);
2353
+ const hash = fullHash % this.size;
2354
+ const byte = Math.floor(hash / 8);
2355
+ const bit = hash % 8;
2356
+ const bitValue = !!(this.bits[byte] & (1 << bit));
2357
+
2358
+ this.debug(`[BloomFilter] Hash ${i}: ${fullHash} % ${this.size} = ${hash}, byte=${byte}, bit=${bit}, value=${bitValue}`);
2359
+
2360
+ if (!bitValue) {
2361
+ this.debug(`[BloomFilter] EAN ${ean} NOT found (hash ${i} failed)`);
2362
+ return false // Définitivement absent
2363
+ }
2364
+ }
2365
+ this.debug(`[BloomFilter] EAN ${ean} FOUND (all ${this.k} hashes passed)`);
2366
+ return true // Peut-être présent
2367
+ }
2368
+
2369
+ /**
2370
+ * Fonction de hachage CRC32 compatible avec PHP
2371
+ * @param {string} str - Chaîne à hacher
2372
+ * @returns {number} Hash absolu
2373
+ */
2374
+ hash(str) {
2375
+ // Implémentation CRC32 compatible avec PHP
2376
+ const crcTable = this.getCrc32Table();
2377
+ let crc = 0xFFFFFFFF;
2378
+
2379
+ for (let i = 0; i < str.length; i++) {
2380
+ const char = str.charCodeAt(i);
2381
+ crc = (crc >>> 8) ^ crcTable[(crc ^ char) & 0xFF];
2382
+ }
2383
+
2384
+ return Math.abs((crc ^ 0xFFFFFFFF) >>> 0)
2385
+ }
2386
+
2387
+ /**
2388
+ * Génère la table CRC32
2389
+ * @returns {Uint32Array} Table CRC32
2390
+ */
2391
+ getCrc32Table() {
2392
+ if (this.crc32Table) {
2393
+ return this.crc32Table
2394
+ }
2395
+
2396
+ // Utiliser une table CRC32 pré-calculée pour éviter les problèmes de stack
2397
+ this.crc32Table = new Uint32Array([
2398
+ 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
2399
+ 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
2400
+ 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
2401
+ 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
2402
+ 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
2403
+ 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
2404
+ 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
2405
+ 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
2406
+ 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
2407
+ 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
2408
+ 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
2409
+ 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
2410
+ 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
2411
+ 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
2412
+ 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
2413
+ 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
2414
+ 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
2415
+ 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
2416
+ 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
2417
+ 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
2418
+ 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
2419
+ 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
2420
+ 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
2421
+ 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
2422
+ 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
2423
+ 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
2424
+ 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
2425
+ 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
2426
+ 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
2427
+ 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
2428
+ 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
2429
+ 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
2430
+ 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
2431
+ 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
2432
+ 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
2433
+ 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
2434
+ 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
2435
+ 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
2436
+ 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
2437
+ 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
2438
+ 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
2439
+ 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
2440
+ 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
2441
+ ]);
2442
+
2443
+ return this.crc32Table
2444
+ }
2445
+
2446
+ /**
2447
+ * Valide le format d'un EAN
2448
+ * @param {string} ean - EAN à valider
2449
+ * @returns {boolean} true si valide
2450
+ */
2451
+ isValidEan(ean) {
2452
+ return typeof ean === 'string' && /^\d{8}$|^\d{12}$|^\d{13}$/.test(ean)
2453
+ }
2454
+
2455
+ /**
2456
+ * Décompresse les données zlib/gzcompress
2457
+ * @param {Uint8Array} data - Données compressées
2458
+ * @returns {Uint8Array} Données décompressées
2459
+ */
2460
+ decompress(data) {
2461
+ try {
2462
+ let buffer = data instanceof Uint8Array ? data : new Uint8Array(data);
2463
+
2464
+ if (buffer.length === 0) {
2465
+ return buffer
2466
+ }
2467
+
2468
+ if (buffer[0] === 0x78) {
2469
+ this.debug('[BloomFilter] Detected zlib/gzcompress data, inflating');
2470
+ try {
2471
+ return unzlibSync(buffer)
2472
+ } catch (inflateError) {
2473
+ this.debug('[BloomFilter] unzlibSync failed, fallback to raw:', inflateError.message);
2474
+ }
2475
+ }
2476
+
2477
+ if (buffer[0] === 0x1f && buffer[1] === 0x8b) {
2478
+ this.debug('[BloomFilter] Detected gzip data, inflating');
2479
+ try {
2480
+ return gunzipSync(buffer)
2481
+ } catch (gunzipError) {
2482
+ this.debug('[BloomFilter] gunzipSync failed, fallback to raw:', gunzipError.message);
2483
+ }
2484
+ }
2485
+ } catch (error) {
2486
+ this.debug('[BloomFilter] Decompression failed:', error.message);
2487
+ }
2488
+
2489
+ const fallbackBuffer = data instanceof Uint8Array ? data : new Uint8Array(data);
2490
+ this.debug('[BloomFilter] Using raw data, size:', fallbackBuffer.length);
2491
+ return fallbackBuffer
2492
+ }
2493
+
2494
+ /**
2495
+ * Retourne les statistiques du filtre
2496
+ * @returns {Object} Statistiques
2497
+ */
2498
+ getStats() {
2499
+ return {
2500
+ size: this.size,
2501
+ bytes: this.bits.length,
2502
+ hashFunctions: this.k,
2503
+ loaded: true
2504
+ }
2505
+ }
2506
+
2507
+ debug(...args) {
2508
+ if (this.debugEnabled) {
2509
+ console.debug(...args);
2510
+ }
2511
+ }
2512
+ }
2513
+
2514
+ class ApiClient extends EventEmitter {
2515
+ constructor (config) {
2516
+ super();
2517
+ this.config = config;
2518
+ this.baseUrl = this.config.apiBaseUrl;
2519
+ this.defaultHeaders = this.buildDefaultHeaders();
2520
+ this.retryConfig = {
2521
+ maxRetries: 3,
2522
+ baseDelay: 1000,
2523
+ maxDelay: 10000
2524
+ };
2525
+ this.requestQueue = [];
2526
+ this.isProcessingQueue = false;
2527
+ this.rateLimitInfo = {
2528
+ remaining: null,
2529
+ resetTime: null,
2530
+ limit: null
2531
+ };
2532
+ // Bloom Filter pour optimisation invisible
2533
+ this.bloomFilter = null;
2534
+ this.bloomLoaded = false;
2535
+ this.bloomLoadPromise = null;
2536
+ }
2537
+
2538
+ buildDefaultHeaders () {
2539
+ const sdkVersion = this.config.version || '3.0.0-unified';
2540
+ const headers = {
2541
+ 'Content-Type': 'application/json',
2542
+ 'Accept': 'application/json',
2543
+ 'User-Agent': `ViprosSDK/${sdkVersion} (JavaScript)`,
2544
+ 'X-SDK-Version': sdkVersion,
2545
+ 'X-SDK-Client': 'JavaScript'
2546
+ };
2547
+
2548
+ if (this.config.apiKey) {
2549
+ headers['x-api-key'] = this.config.apiKey;
2550
+ }
2551
+
2552
+ if (typeof window !== 'undefined' && window.location) {
2553
+ headers['Referer'] = window.location.href;
2554
+ headers['Origin'] = window.location.origin;
2555
+ }
2556
+
2557
+ return headers
2558
+ }
2559
+
2560
+ async getCatalogItems (filters = {}, options = {}) {
2561
+ this.debug('[ViprosSDK] getCatalogItems() called with filters:', filters, 'options:', options);
2562
+
2563
+ const params = new URLSearchParams();
2564
+
2565
+ Object.entries(filters).forEach(([key, value]) => {
2566
+ if (value !== undefined && value !== null && value !== '') {
2567
+ params.append(key, value.toString());
2568
+ }
2569
+ });
2570
+
2571
+ // Support pour la sync inline (nouveau en Phase 8)
2572
+ if (options.enableInlineSync) {
2573
+ params.append('sync', 'auto');
2574
+
2575
+ // Passer l'URL du produit pour la sync
2576
+ if (options.productUrl) {
2577
+ params.append('product_url', options.productUrl);
2578
+ }
2579
+
2580
+ // Force sync si demandé
2581
+ if (options.forceSync) {
2582
+ params.append('force_sync', '1');
2583
+ }
2584
+
2585
+ // Activer debug mode pour obtenir les métadonnées de sync
2586
+ params.append('debug', 'true');
2587
+ }
2588
+
2589
+ // Utiliser l'endpoint SDK sécurisé
2590
+ const url = `${this.baseUrl}/sdk/catalog/items${params.toString() ? '?' + params.toString() : ''}`;
2591
+
2592
+ // Headers additionnels pour la sync inline
2593
+ const headers = {};
2594
+ if (options.enableInlineSync && options.productUrl) {
2595
+ headers['X-Product-Url'] = options.productUrl;
2596
+ }
2597
+
2598
+ return this.request('GET', url, null, { headers })
2599
+ }
2600
+
2601
+ /**
2602
+ * Récupère un produit unique par son EAN (route optimisée)
2603
+ * Plus performante que getCatalogItems() car évite la pagination et le count ES
2604
+ *
2605
+ * @param {string} ean - Code EAN du produit (8-14 chiffres)
2606
+ * @param {Object} options - Options de requête
2607
+ * @param {boolean} options.enableInlineSync - Activer la sync inline
2608
+ * @param {string} options.productUrl - URL du produit pour la sync
2609
+ * @param {boolean} options.forceSync - Forcer la sync même si cooldown actif
2610
+ * @returns {Promise<Object|null>} Produit ou null si non trouvé
2611
+ */
2612
+ async getCatalogItemByEan (ean, options = {}) {
2613
+ this.debug('[ViprosSDK] getCatalogItemByEan() called with ean:', ean, 'options:', options);
2614
+
2615
+ // Vérification Bloom Filter invisible
2616
+ const shouldSkip = await this.shouldSkipEan(ean);
2617
+ if (shouldSkip) {
2618
+ this.debug('[ViprosSDK] getCatalogItemByEan() - EAN not in bloom filter, skipping API call');
2619
+ return null
2620
+ }
2621
+
2622
+ // Construire les paramètres de requête
2623
+ const params = new URLSearchParams();
2624
+
2625
+ // Support pour la sync inline
2626
+ if (options.enableInlineSync) {
2627
+ params.append('sync', 'auto');
2628
+
2629
+ if (options.productUrl) {
2630
+ params.append('product_url', options.productUrl);
2631
+ }
2632
+
2633
+ if (options.forceSync) {
2634
+ params.append('force_sync', '1');
2635
+ }
2636
+
2637
+ params.append('debug', 'true');
2638
+ }
2639
+
2640
+ // Utiliser l'endpoint SDK optimisé avec EAN dans le path
2641
+ const queryString = params.toString();
2642
+ const url = `${this.baseUrl}/sdk/catalog/items/${encodeURIComponent(ean)}${queryString ? '?' + queryString : ''}`;
2643
+
2644
+ // Headers additionnels pour la sync inline
2645
+ const headers = {};
2646
+ if (options.enableInlineSync && options.productUrl) {
2647
+ headers['X-Product-Url'] = options.productUrl;
2648
+ }
2649
+
2650
+ try {
2651
+ const response = await this.request('GET', url, null, { headers });
2652
+
2653
+ // Gérer le cas 404 (produit non trouvé)
2654
+ if (response.code === 404 || !response.item) {
2655
+ this.debug('[ViprosSDK] getCatalogItemByEan() - Product not found for EAN:', ean);
2656
+ return null
2657
+ }
2658
+
2659
+ this.debug('[ViprosSDK] getCatalogItemByEan() - Product found:', response.item?.name);
2660
+ return response.item
2661
+ } catch (error) {
2662
+ this.debug('[ViprosSDK] getCatalogItemByEan() - Error:', error?.message || error);
2663
+ // Graceful degradation - return null instead of throwing
2664
+ return null
2665
+ }
2666
+ }
2667
+
2668
+ async updateProductUrls (updates) {
2669
+ if (!Array.isArray(updates) || updates.length === 0) {
2670
+ throw new Error('Updates must be a non-empty array')
2671
+ }
2672
+
2673
+ const url = `${this.baseUrl}/catalog/stores/update_urls`;
2674
+ return this.request('POST', url, { updates })
2675
+ }
2676
+
2677
+ async getProductUpdates () {
2678
+ const url = `${this.baseUrl}/catalog/stores/get_updates`;
2679
+ return this.request('GET', url)
2680
+ }
2681
+
2682
+ async healthCheck () {
2683
+ const url = `${this.baseUrl}/sdk/health`;
2684
+ return this.request('GET', url)
2685
+ }
2686
+
2687
+ async syncProduct (ean, productUrl, forceSync = false) {
2688
+ if (!ean || !productUrl) {
2689
+ throw new Error('EAN and product URL are required for synchronization')
2690
+ }
2691
+
2692
+ const url = `${this.config.apiBaseUrl}/partners-stores/sync`;
2693
+
2694
+ const headers = {};
2695
+
2696
+ const payload = {
2697
+ ean: ean.toString(),
2698
+ url: productUrl
2699
+ };
2700
+
2701
+ // Ajouter le paramètre force_sync si nécessaire
2702
+ if (forceSync) {
2703
+ payload.force_sync = true;
2704
+ }
2705
+
2706
+ return this.request('POST', url, payload, { headers })
2707
+ }
2708
+
2709
+ /**
2710
+ * Méthode GET simple pour Web Components
2711
+ * @param {string} endpoint - Endpoint (ex: '/catalog/items')
2712
+ * @param {Object} params - Paramètres de requête
2713
+ * @param {Object} options - Options additionnelles (enableInlineSync, productUrl, forceSync)
2714
+ * @returns {Promise<Object>}
2715
+ */
2716
+ async get(endpoint, params = {}, options = {}) {
2717
+ // Rediriger les appels SDK vers les endpoints optimisés
2718
+ if (endpoint === '/catalog/items') {
2719
+ // Si un seul EAN est demandé, utiliser la route optimisée
2720
+ if (params.ean && !params.ean.includes(',')) {
2721
+ this.debug('[ViprosSDK] get() method - using optimized single EAN route');
2722
+ const item = await this.getCatalogItemByEan(params.ean, options);
2723
+
2724
+ // Retourner au format compatible avec l'ancien format items[]
2725
+ if (item) {
2726
+ return {
2727
+ items: [item],
2728
+ code: 200
2729
+ }
2730
+ } else {
2731
+ return {
2732
+ items: [],
2733
+ code: 404,
2734
+ metadata: {
2735
+ ean: params.ean,
2736
+ optimizedRoute: true
2737
+ }
2738
+ }
2739
+ }
2740
+ }
2741
+
2742
+ // Pour les autres cas (pas d'EAN ou plusieurs EANs), utiliser l'ancienne route
2743
+ // Vérification Bloom Filter invisible
2744
+ if (params.ean) {
2745
+ const shouldSkip = await this.shouldSkipEan(params.ean);
2746
+ this.debug(`[ViprosSDK] get() method - shouldSkip: ${shouldSkip}`);
2747
+ if (shouldSkip) {
2748
+ // Retourner réponse vide sans appel API, indiquer le court-circuit Bloom
2749
+ this.debug('[ViprosSDK] get() method - returning empty items, API call skipped (bloom filter)');
2750
+ return {
2751
+ items: [],
2752
+ metadata: {
2753
+ bloomFilter: {
2754
+ skipped: true,
2755
+ reason: 'NOT_IN_FILTER',
2756
+ ean: params.ean
2757
+ }
2758
+ }
2759
+ }
2760
+ }
2761
+ }
2762
+
2763
+ this.debug('[ViprosSDK] get() method - calling getCatalogItems() with options:', options);
2764
+ return this.getCatalogItems(params, options)
2765
+ }
2766
+
2767
+ // Pour les autres endpoints, construire l'URL et faire la requête
2768
+ const url = `${this.baseUrl}${endpoint}`;
2769
+ const searchParams = new URLSearchParams();
2770
+
2771
+ Object.entries(params).forEach(([key, value]) => {
2772
+ if (value !== undefined && value !== null && value !== '') {
2773
+ searchParams.append(key, value.toString());
2774
+ }
2775
+ });
2776
+
2777
+ const fullUrl = searchParams.toString() ? `${url}?${searchParams.toString()}` : url;
2778
+ return this.request('GET', fullUrl)
2779
+ }
2780
+
2781
+ async request (method, url, data = null, options = {}) {
2782
+ return new Promise((resolve, reject) => {
2783
+ const requestConfig = {
2784
+ method,
2785
+ url,
2786
+ data,
2787
+ options: {
2788
+ retry: true,
2789
+ ...options
2790
+ },
2791
+ resolve,
2792
+ reject,
2793
+ attempts: 0
2794
+ };
2795
+
2796
+ this.requestQueue.push(requestConfig);
2797
+ this.processQueue();
2798
+ })
2799
+ }
2800
+
2801
+ async processQueue () {
2802
+ if (this.isProcessingQueue || this.requestQueue.length === 0) {
2803
+ return
2804
+ }
2805
+
2806
+ this.isProcessingQueue = true;
2807
+
2808
+ while (this.requestQueue.length > 0) {
2809
+ const requestConfig = this.requestQueue.shift();
2810
+
2811
+ try {
2812
+ const result = await this.executeRequest(requestConfig);
2813
+ requestConfig.resolve(result);
2814
+ } catch (error) {
2815
+ if (requestConfig.options.retry && this.shouldRetry(error, requestConfig)) {
2816
+ await this.scheduleRetry(requestConfig);
2817
+ } else {
2818
+ requestConfig.reject(error);
2819
+ }
2820
+ }
2821
+
2822
+ if (this.rateLimitInfo.remaining !== null && this.rateLimitInfo.remaining <= 1) {
2823
+ const delay = this.calculateRateLimitDelay();
2824
+ if (delay > 0) {
2825
+ await this.sleep(delay);
2826
+ }
2827
+ }
2828
+ }
2829
+
2830
+ this.isProcessingQueue = false;
2831
+ }
2832
+
2833
+ async executeRequest (requestConfig) {
2834
+ const { method, url, data, options } = requestConfig;
2835
+ requestConfig.attempts++;
2836
+
2837
+
2838
+ const requestOptions = {
2839
+ method,
2840
+ headers: { ...this.defaultHeaders, ...options.headers },
2841
+ credentials: 'omit'
2842
+ };
2843
+
2844
+ if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
2845
+ requestOptions.body = JSON.stringify(data);
2846
+ }
2847
+
2848
+ if (options.timeout) {
2849
+ const controller = new AbortController();
2850
+ requestOptions.signal = controller.signal;
2851
+ setTimeout(() => controller.abort(), options.timeout);
2852
+ }
2853
+
2854
+ const startTime = Date.now();
2855
+
2856
+ try {
2857
+ const response = await fetch(url, requestOptions);
2858
+ const responseTime = Date.now() - startTime;
2859
+
2860
+ this.updateRateLimitInfo(response);
2861
+
2862
+ if (!response.ok) {
2863
+ const error = await this.handleErrorResponse(response);
2864
+ this.emit('error', error);
2865
+ throw error
2866
+ }
2867
+
2868
+ const result = await response.json();
2869
+
2870
+ this.emit('response', {
2871
+ url,
2872
+ method,
2873
+ status: response.status,
2874
+ responseTime,
2875
+ data: result
2876
+ });
2877
+
2878
+ return result
2879
+ } catch (error) {
2880
+
2881
+ if (error.name === 'AbortError') {
2882
+ throw this.createNetworkError('TIMEOUT', `Request timeout after ${options.timeout}ms`, url, method)
2883
+ }
2884
+
2885
+ if (error.name === 'TypeError' && error.message.includes('fetch')) {
2886
+ throw this.createNetworkError('NETWORK_ERROR', 'Network error - please check your connection', url, method, error)
2887
+ }
2888
+
2889
+ throw error
2890
+ }
2891
+ }
2892
+
2893
+ async handleErrorResponse (response) {
2894
+ const contentType = response.headers.get('content-type');
2895
+ let errorData;
2896
+
2897
+ try {
2898
+ if (contentType && contentType.includes('application/json')) {
2899
+ errorData = await response.json();
2900
+ } else {
2901
+ errorData = { message: await response.text() };
2902
+ }
2903
+ } catch (parseError) {
2904
+ errorData = { message: `HTTP ${response.status}` };
2905
+ }
2906
+
2907
+ const error = new Error(errorData.message || `HTTP ${response.status}`);
2908
+ error.status = response.status;
2909
+ error.code = errorData.code || response.status;
2910
+ error.url = response.url;
2911
+ error.data = errorData;
2912
+
2913
+ switch (response.status) {
2914
+ case 400:
2915
+ error.type = 'VALIDATION_ERROR';
2916
+ break
2917
+ case 401:
2918
+ error.type = 'AUTHENTICATION_ERROR';
2919
+ break
2920
+ case 403:
2921
+ error.type = 'AUTHORIZATION_ERROR';
2922
+ break
2923
+ case 404:
2924
+ error.type = 'NOT_FOUND';
2925
+ break
2926
+ case 429:
2927
+ error.type = 'RATE_LIMIT_ERROR';
2928
+ error.retryAfter = parseInt(response.headers.get('Retry-After')) || 60;
2929
+ break
2930
+ case 500:
2931
+ error.type = 'SERVER_ERROR';
2932
+ break
2933
+ case 502:
2934
+ case 503:
2935
+ case 504:
2936
+ error.type = 'SERVICE_UNAVAILABLE';
2937
+ break
2938
+ default:
2939
+ error.type = 'UNKNOWN_ERROR';
2940
+ }
2941
+
2942
+ return error
2943
+ }
2944
+
2945
+ shouldRetry (error, requestConfig) {
2946
+ if (requestConfig.attempts >= this.retryConfig.maxRetries) {
2947
+ return false
2948
+ }
2949
+
2950
+ if (!requestConfig.options.retry) {
2951
+ return false
2952
+ }
2953
+
2954
+ // ❌ NE PAS retry les erreurs rate limit - les faire remonter immédiatement pour affichage
2955
+ if (error.status === 429 || error.type === 'RATE_LIMIT_ERROR') {
2956
+ return false
2957
+ }
2958
+
2959
+ const retryableStatuses = [500, 502, 503, 504];
2960
+ const retryableTypes = ['SERVER_ERROR', 'SERVICE_UNAVAILABLE', 'TIMEOUT', 'NETWORK_ERROR'];
2961
+
2962
+ return retryableStatuses.includes(error.status) ||
2963
+ retryableTypes.includes(error.type) ||
2964
+ retryableTypes.includes(error.code)
2965
+ }
2966
+
2967
+ async scheduleRetry (requestConfig) {
2968
+ const delay = this.calculateRetryDelay(requestConfig);
2969
+
2970
+ await this.sleep(delay);
2971
+ this.requestQueue.unshift(requestConfig);
2972
+ }
2973
+
2974
+ calculateRetryDelay (requestConfig) {
2975
+ const baseDelay = this.retryConfig.baseDelay;
2976
+ const exponentialDelay = baseDelay * Math.pow(2, requestConfig.attempts - 1);
2977
+ const jitter = Math.random() * 1000;
2978
+
2979
+ return Math.min(exponentialDelay + jitter, this.retryConfig.maxDelay)
2980
+ }
2981
+
2982
+ calculateRateLimitDelay () {
2983
+ if (!this.rateLimitInfo.resetTime) {
2984
+ return 0
2985
+ }
2986
+
2987
+ const now = Date.now();
2988
+ const resetTime = this.rateLimitInfo.resetTime * 1000;
2989
+
2990
+ return Math.max(0, resetTime - now + 1000)
2991
+ }
2992
+
2993
+ updateRateLimitInfo (response) {
2994
+ const limit = response.headers.get('X-RateLimit-Limit');
2995
+ const remaining = response.headers.get('X-RateLimit-Remaining');
2996
+ const reset = response.headers.get('X-RateLimit-Reset');
2997
+
2998
+ if (limit) this.rateLimitInfo.limit = parseInt(limit);
2999
+ if (remaining) this.rateLimitInfo.remaining = parseInt(remaining);
3000
+ if (reset) this.rateLimitInfo.resetTime = parseInt(reset);
3001
+ }
3002
+
3003
+ sleep (ms) {
3004
+ return new Promise(resolve => setTimeout(resolve, ms))
3005
+ }
3006
+
3007
+ getRateLimitInfo () {
3008
+ return { ...this.rateLimitInfo }
3009
+ }
3010
+
3011
+ getQueueStatus () {
3012
+ return {
3013
+ queueLength: this.requestQueue.length,
3014
+ isProcessing: this.isProcessingQueue
3015
+ }
3016
+ }
3017
+
3018
+ clearQueue () {
3019
+ this.requestQueue.forEach(req => {
3020
+ req.reject(new Error('Request queue cleared'));
3021
+ });
3022
+ this.requestQueue = [];
3023
+ this.isProcessingQueue = false;
3024
+ }
3025
+
3026
+ createNetworkError (code, message, url, method, originalError = null) {
3027
+ const error = new Error(message);
3028
+ error.code = code;
3029
+ error.url = url;
3030
+ error.method = method;
3031
+ if (originalError) {
3032
+ error.originalError = originalError;
3033
+ }
3034
+ return error
3035
+ }
3036
+
3037
+ destroy () {
3038
+ this.clearQueue();
3039
+ this.removeAllListeners();
3040
+ }
3041
+
3042
+ // Méthodes Bloom Filter pour optimisation invisible
3043
+ async shouldSkipEan(ean) {
3044
+ try {
3045
+ this.debug('[ViprosSDK] Bloom filter check for EAN:', ean);
3046
+
3047
+ await this.loadBloomFilter();
3048
+
3049
+ if (!this.bloomFilter) {
3050
+ this.debug('[ViprosSDK] No bloom filter available, allowing API call');
3051
+ return false
3052
+ }
3053
+
3054
+ const isPresent = this.bloomFilter.test(ean);
3055
+ const shouldSkip = !isPresent;
3056
+ this.debug(`[ViprosSDK] Bloom filter test for ${ean}: ${isPresent}, shouldSkip: ${shouldSkip}`);
3057
+
3058
+ return shouldSkip
3059
+ } catch (error) {
3060
+ this.debug('[ViprosSDK] Bloom filter error:', error?.message || error);
3061
+ return false
3062
+ }
3063
+ }
3064
+
3065
+ async loadBloomFilter() {
3066
+ if (this.bloomLoaded) {
3067
+ return this.bloomFilter
3068
+ }
3069
+
3070
+ if (this.bloomLoadPromise) {
3071
+ return this.bloomLoadPromise
3072
+ }
3073
+
3074
+ this.bloomLoadPromise = (async () => {
3075
+ try {
3076
+ const storage = this.getStorage();
3077
+ if (storage) {
3078
+ const cached = storage.getItem('vipros_bloom');
3079
+ const cacheTime = storage.getItem('vipros_bloom_time');
3080
+
3081
+ if (cached && cacheTime && (Date.now() - parseInt(cacheTime, 10)) < 86400000) {
3082
+ this.bloomFilter = new SimpleBloomFilter(this.base64ToUint8Array(cached), { debug: this.isDebugEnabled() });
3083
+ this.debug('[ViprosSDK] Bloom filter loaded from cache');
3084
+ this.bloomLoaded = true;
3085
+ return this.bloomFilter
3086
+ }
3087
+ }
3088
+
3089
+ const response = await fetch(`${this.baseUrl}/sdk/bloom`, {
3090
+ headers: {
3091
+ 'Accept': 'application/octet-stream',
3092
+ 'x-api-key': this.config.apiKey
3093
+ }
3094
+ });
3095
+
3096
+ if (!response.ok) {
3097
+ this.debug('[ViprosSDK] Unable to download bloom filter, status:', response.status);
3098
+ this.bloomFilter = null;
3099
+ this.bloomLoaded = true;
3100
+ return this.bloomFilter
3101
+ }
3102
+
3103
+ const buffer = await response.arrayBuffer();
3104
+ const data = new Uint8Array(buffer);
3105
+
3106
+ this.bloomFilter = new SimpleBloomFilter(data, { debug: this.isDebugEnabled() });
3107
+
3108
+ if (storage) {
3109
+ storage.setItem('vipros_bloom', this.uint8ArrayToBase64(data));
3110
+ storage.setItem('vipros_bloom_time', Date.now().toString());
3111
+ }
3112
+
3113
+ this.debug('[ViprosSDK] Bloom filter downloaded and cached');
3114
+ this.bloomLoaded = true;
3115
+ return this.bloomFilter
3116
+ } catch (error) {
3117
+ this.debug('[ViprosSDK] Bloom filter unavailable:', error?.message || error);
3118
+ this.bloomFilter = null;
3119
+ this.bloomLoaded = true;
3120
+ return this.bloomFilter
3121
+ } finally {
3122
+ this.bloomLoadPromise = null;
3123
+ }
3124
+ })();
3125
+
3126
+ return this.bloomLoadPromise
3127
+ }
3128
+
3129
+ uint8ArrayToBase64(uint8Array) {
3130
+ if (typeof globalThis !== 'undefined' && globalThis.Buffer) {
3131
+ return globalThis.Buffer.from(uint8Array).toString('base64')
3132
+ }
3133
+
3134
+ let binary = '';
3135
+ const chunkSize = 0x8000;
3136
+
3137
+ for (let i = 0; i < uint8Array.length; i += chunkSize) {
3138
+ const chunk = uint8Array.subarray(i, i + chunkSize);
3139
+ let chunkText = '';
3140
+ for (let j = 0; j < chunk.length; j++) {
3141
+ chunkText += String.fromCharCode(chunk[j]);
3142
+ }
3143
+ binary += chunkText;
3144
+ }
3145
+
3146
+ return btoa(binary)
3147
+ }
3148
+
3149
+ base64ToUint8Array(base64) {
3150
+ if (typeof globalThis !== 'undefined' && globalThis.Buffer) {
3151
+ return new Uint8Array(globalThis.Buffer.from(base64, 'base64'))
3152
+ }
3153
+
3154
+ const binary = atob(base64);
3155
+ const length = binary.length;
3156
+ const bytes = new Uint8Array(length);
3157
+
3158
+ for (let i = 0; i < length; i++) {
3159
+ bytes[i] = binary.charCodeAt(i);
3160
+ }
3161
+
3162
+ return bytes
3163
+ }
3164
+
3165
+ getStorage () {
3166
+ if (typeof window === 'undefined' || !window.localStorage) {
3167
+ return null
3168
+ }
3169
+
3170
+ try {
3171
+ // Accessing localStorage can throw in some browsers (privacy mode)
3172
+ const testKey = '__vipros_storage_test__';
3173
+ window.localStorage.setItem(testKey, '1');
3174
+ window.localStorage.removeItem(testKey);
3175
+ return window.localStorage
3176
+ } catch (error) {
3177
+ this.debug('[ViprosSDK] localStorage unavailable:', error?.message || error);
3178
+ return null
3179
+ }
3180
+ }
3181
+
3182
+ debug(...args) {
3183
+ if (this.isDebugEnabled()) {
3184
+ console.debug(...args);
3185
+ }
3186
+ }
3187
+
3188
+ isDebugEnabled () {
3189
+ return Boolean(this.config && this.config.debug)
3190
+ }
3191
+ }
3192
+
3193
+ var ApiClient$1 = /*#__PURE__*/Object.freeze({
3194
+ __proto__: null,
3195
+ default: ApiClient
3196
+ });
3197
+
3198
+ /**
3199
+ * CacheService Simplifié - Cache Mémoire Uniquement
3200
+ *
3201
+ * Version optimisée qui utilise uniquement la mémoire.
3202
+ * La persistance est gérée côté serveur avec cooldown 24h.
3203
+ */
3204
+ class CacheService extends EventEmitter {
3205
+ constructor (cacheTimeout = 300000) {
3206
+ super();
3207
+ // Validation: s'assurer que cacheTimeout est un nombre
3208
+ if (typeof cacheTimeout !== 'number' || isNaN(cacheTimeout) || cacheTimeout < 0) {
3209
+ console.warn('[CacheService] Invalid cacheTimeout, using default 300000ms');
3210
+ cacheTimeout = 300000;
3211
+ }
3212
+ this.cacheTimeout = cacheTimeout;
3213
+ this.memoryCache = new Map();
3214
+
3215
+ this.setupCleanupInterval();
3216
+ }
3217
+
3218
+ get (key, useStorage = true) {
3219
+ const fullKey = this.buildKey(key);
3220
+
3221
+ let cached = this.memoryCache.get(fullKey);
3222
+ if (cached && this.isValidCacheEntry(cached)) {
3223
+ cached.hits = (cached.hits || 0) + 1;
3224
+ cached.lastAccessTime = Date.now();
3225
+ this.emit('hit', { key, source: 'memory' });
3226
+ return cached.data
3227
+ }
3228
+
3229
+ this.emit('miss', { key });
3230
+ return null
3231
+ }
3232
+
3233
+ set (key, data, options = {}) {
3234
+ const fullKey = this.buildKey(key);
3235
+ const entry = this.createCacheEntry(data, options);
3236
+
3237
+ this.memoryCache.set(fullKey, entry);
3238
+
3239
+ this.emit('set', { key, size: this.estimateSize(data) });
3240
+ this.enforceCacheLimits();
3241
+ }
3242
+
3243
+ createCacheEntry (data, options = {}) {
3244
+ const now = Date.now();
3245
+ const ttl = options.ttl || this.cacheTimeout;
3246
+
3247
+ return {
3248
+ data,
3249
+ timestamp: now,
3250
+ expires: ttl > 0 ? now + ttl : null,
3251
+ hits: 0,
3252
+ lastAccessTime: now,
3253
+ size: this.estimateSize(data),
3254
+ tags: options.tags || [],
3255
+ version: options.version || '3.0.0'
3256
+ }
3257
+ }
3258
+
3259
+ isValidCacheEntry (entry) {
3260
+ if (!entry || typeof entry !== 'object') {
3261
+ return false
3262
+ }
3263
+
3264
+ if (!entry.timestamp || !entry.data) {
3265
+ return false
3266
+ }
3267
+
3268
+ if (entry.expires && Date.now() > entry.expires) {
3269
+ return false
3270
+ }
3271
+
3272
+ return true
3273
+ }
3274
+
3275
+ estimateSize (data) {
3276
+ try {
3277
+ return JSON.stringify(data).length
3278
+ } catch (error) {
3279
+ return 0
3280
+ }
3281
+ }
3282
+
3283
+ buildKey (key) {
3284
+ return `vipros_sdk_${key}`
3285
+ }
3286
+
3287
+ has (key) {
3288
+ const fullKey = this.buildKey(key);
3289
+
3290
+ if (this.memoryCache.has(fullKey)) {
3291
+ const entry = this.memoryCache.get(fullKey);
3292
+ return this.isValidCacheEntry(entry)
3293
+ }
3294
+
3295
+ return false
3296
+ }
3297
+
3298
+ delete (key) {
3299
+ const fullKey = this.buildKey(key);
3300
+ const deleted = this.memoryCache.delete(fullKey);
3301
+
3302
+ if (deleted) {
3303
+ this.emit('delete', { key });
3304
+ }
3305
+
3306
+ return deleted
3307
+ }
3308
+
3309
+ clear () {
3310
+ const memorySize = this.memoryCache.size;
3311
+ this.memoryCache.clear();
3312
+
3313
+ this.emit('clear', { memorySize });
3314
+ }
3315
+
3316
+ size () {
3317
+ return this.memoryCache.size
3318
+ }
3319
+
3320
+ getStats () {
3321
+ const memoryStats = {
3322
+ entries: this.memoryCache.size,
3323
+ totalSize: 0,
3324
+ avgSize: 0
3325
+ };
3326
+
3327
+ for (const [, entry] of this.memoryCache) {
3328
+ memoryStats.totalSize += entry.size || 0;
3329
+ }
3330
+
3331
+ if (memoryStats.entries > 0) {
3332
+ memoryStats.avgSize = Math.round(memoryStats.totalSize / memoryStats.entries);
3333
+ }
3334
+
3335
+ return { memory: memoryStats }
3336
+ }
3337
+
3338
+ enforceCacheLimits () {
3339
+ const maxMemoryEntries = 100;
3340
+ const maxMemorySize = 10 * 1024 * 1024; // 10MB
3341
+
3342
+ if (this.memoryCache.size > maxMemoryEntries) {
3343
+ this.evictLeastRecentlyUsed(this.memoryCache.size - maxMemoryEntries);
3344
+ }
3345
+
3346
+ let totalSize = 0;
3347
+ for (const [, entry] of this.memoryCache) {
3348
+ totalSize += entry.size || 0;
3349
+ }
3350
+
3351
+ if (totalSize > maxMemorySize) {
3352
+ this.evictLargestEntries(totalSize - maxMemorySize);
3353
+ }
3354
+ }
3355
+
3356
+ evictLeastRecentlyUsed (count) {
3357
+ const entries = Array.from(this.memoryCache.entries())
3358
+ .map(([key, entry]) => ({ key, lastAccess: entry.lastAccessTime || 0 }))
3359
+ .sort((a, b) => a.lastAccess - b.lastAccess)
3360
+ .slice(0, count);
3361
+
3362
+ entries.forEach(({ key }) => {
3363
+ this.memoryCache.delete(key);
3364
+ });
3365
+
3366
+ this.emit('lru_eviction', { count });
3367
+ }
3368
+
3369
+ evictLargestEntries (targetSize) {
3370
+ const entries = Array.from(this.memoryCache.entries())
3371
+ .map(([key, entry]) => ({ key, entry }))
3372
+ .sort((a, b) => (b.entry.size || 0) - (a.entry.size || 0));
3373
+
3374
+ let freedSize = 0;
3375
+ let evicted = 0;
3376
+
3377
+ for (const { key, entry } of entries) {
3378
+ if (freedSize >= targetSize) {
3379
+ break
3380
+ }
3381
+
3382
+ this.memoryCache.delete(key);
3383
+ freedSize += entry.size || 0;
3384
+ evicted++;
3385
+ }
3386
+
3387
+ this.emit('size_eviction', { evicted, freedSize });
3388
+ }
3389
+
3390
+ setupCleanupInterval () {
3391
+ if (typeof window !== 'undefined') {
3392
+ setInterval(() => {
3393
+ this.cleanupExpiredEntries();
3394
+ }, 60000);
3395
+ }
3396
+ }
3397
+
3398
+ cleanupExpiredEntries () {
3399
+ let cleaned = 0;
3400
+ const now = Date.now();
3401
+
3402
+ for (const [key, entry] of this.memoryCache) {
3403
+ if (entry.expires && now > entry.expires) {
3404
+ this.memoryCache.delete(key);
3405
+ cleaned++;
3406
+ }
3407
+ }
3408
+
3409
+ if (cleaned > 0) {
3410
+ this.emit('cleanup', { cleaned });
3411
+ }
3412
+ }
3413
+
3414
+ destroy () {
3415
+ this.clear();
3416
+ this.removeAllListeners();
3417
+ }
3418
+ }
3419
+
3420
+ /**
3421
+ * Service de synchronisation EAN avec l'API VIPros (version simplifiée)
3422
+ *
3423
+ * La logique de cooldown est maintenant gérée côté serveur Laravel.
3424
+ * Ce service se contente d'appeler l'API et de gérer les réponses.
3425
+ */
3426
+ class EanSyncService extends EventEmitter {
3427
+ constructor (apiClient, cacheService, config = {}) {
3428
+ super();
3429
+ this.apiClient = apiClient;
3430
+ this.cacheService = cacheService;
3431
+
3432
+ // Configuration simplifiée
3433
+ this.config = {
3434
+ debug: config.debug || false,
3435
+ retryDelayMs: 5000, // 5 secondes pour retry réseau
3436
+ maxRetries: 2, // Retry limité aux erreurs réseau uniquement
3437
+ ...config
3438
+ };
3439
+
3440
+ // Statistiques simplifiées
3441
+ this.stats = {
3442
+ attempted: 0,
3443
+ successful: 0,
3444
+ failed: 0,
3445
+ skipped: 0, // Skipped par le serveur (cooldown 202)
3446
+ rateLimited: 0,
3447
+ errors: []
3448
+ };
3449
+ }
3450
+
3451
+ /**
3452
+ * Lance une synchronisation en arrière-plan (fire & forget)
3453
+ * Version simplifiée - délègue la logique de cooldown au serveur
3454
+ * @param {string} ean - Code EAN du produit
3455
+ * @param {string} productUrl - URL de la page produit (généralement window.location.href)
3456
+ * @returns {void} - Ne retourne rien (fire & forget)
3457
+ */
3458
+ syncProductInBackground (ean, productUrl) {
3459
+ if (!ean || !productUrl) {
3460
+ if (this.config.debug) {
3461
+ console.debug('[ViprosSDK] Sync skipped: missing EAN or URL');
3462
+ }
3463
+ return
3464
+ }
3465
+
3466
+ // Exécution simple - Fire & forget
3467
+ this.performSync(ean, productUrl).catch(error => {
3468
+ // Log silencieux en mode debug uniquement
3469
+ if (this.config.debug) {
3470
+ console.debug(`[ViprosSDK] Sync error for ${ean}:`, error.message);
3471
+ }
3472
+ });
3473
+ }
3474
+
3475
+ /**
3476
+ * Effectue la synchronisation réelle avec l'API (version simplifiée)
3477
+ * @private
3478
+ */
3479
+ async performSync (ean, productUrl, retryCount = 0) {
3480
+ // Démarrer la synchronisation
3481
+ this.stats.attempted++;
3482
+
3483
+ try {
3484
+ // Appel API de synchronisation - délègue toute la logique au serveur
3485
+ const result = await this.apiClient.syncProduct(ean, productUrl);
3486
+
3487
+ // Traiter la réponse en fonction du code de statut
3488
+ if (result.code === 200) {
3489
+ // Succès - synchronisation effectuée
3490
+ this.stats.successful++;
3491
+ this.emit('syncSuccess', {
3492
+ ean,
3493
+ productUrl,
3494
+ result,
3495
+ timestamp: new Date()
3496
+ });
3497
+
3498
+ if (this.config.debug) {
3499
+ console.debug(`[ViprosSDK] Sync successful for ${ean}`);
3500
+ }
3501
+
3502
+ return { status: 'success', result }
3503
+
3504
+ } else if (result.code === 202) {
3505
+ // Cooldown actif côté serveur
3506
+ this.stats.skipped++;
3507
+ this.emit('syncSkipped', {
3508
+ ean,
3509
+ reason: 'server_cooldown',
3510
+ remaining_hours: result.remaining_hours,
3511
+ next_sync_at: result.next_sync_at
3512
+ });
3513
+
3514
+ if (this.config.debug) {
3515
+ console.debug(`[ViprosSDK] Sync skipped for ${ean}: server cooldown (${result.remaining_hours}h remaining)`);
3516
+ }
3517
+
3518
+ return { status: 'skipped', reason: 'server_cooldown' }
3519
+
3520
+ } else {
3521
+ // Autre code de statut - traiter comme erreur
3522
+ throw new Error(`API returned status ${result.code}: ${result.message || 'Unknown error'}`)
3523
+ }
3524
+
3525
+ } catch (error) {
3526
+ return this.handleSyncError(ean, productUrl, error, retryCount)
3527
+ } finally {
3528
+ // Cleanup if needed
3529
+ }
3530
+ }
3531
+
3532
+
3533
+ /**
3534
+ * Gère les erreurs de synchronisation (version simplifiée)
3535
+ * @private
3536
+ */
3537
+ handleSyncError (ean, productUrl, error, retryCount) {
3538
+ this.stats.failed++;
3539
+
3540
+ // Analyser le type d'erreur
3541
+ const errorType = this.categorizeError(error);
3542
+
3543
+ // Gestion spéciale du rate limiting
3544
+ if (errorType === 'RATE_LIMIT') {
3545
+ this.stats.rateLimited++;
3546
+ this.emit('syncRateLimited', {
3547
+ ean,
3548
+ productUrl,
3549
+ error,
3550
+ retryAfter: error.retryAfter || 60
3551
+ });
3552
+
3553
+ if (this.config.debug) {
3554
+ console.debug(`[ViprosSDK] Sync rate limited for ${ean}`);
3555
+ }
3556
+
3557
+ return { status: 'rate_limited', error: error.message }
3558
+ }
3559
+
3560
+ // Pour les erreurs réseau, on peut retry une fois
3561
+ if (errorType === 'NETWORK_ERROR' && retryCount < this.config.maxRetries) {
3562
+ if (this.config.debug) {
3563
+ console.debug(`[ViprosSDK] Retrying sync for ${ean} (attempt ${retryCount + 1})`);
3564
+ }
3565
+
3566
+ // Retry après délai
3567
+ setTimeout(() => {
3568
+ this.performSync(ean, productUrl, retryCount + 1).catch(() => {
3569
+ // Ignore retry errors - already handled
3570
+ });
3571
+ }, this.config.retryDelayMs);
3572
+
3573
+ return { status: 'retrying', attempt: retryCount + 1 }
3574
+ }
3575
+
3576
+ // Enregistrer l'erreur
3577
+ this.logError(ean, error, errorType);
3578
+
3579
+ // Émettre l'événement d'erreur
3580
+ this.emit('syncError', {
3581
+ ean,
3582
+ productUrl,
3583
+ error,
3584
+ errorType,
3585
+ retryCount
3586
+ });
3587
+
3588
+ // Log pour debug
3589
+ if (this.config.debug) {
3590
+ console.error(`[ViprosSDK] Sync failed for ${ean}:`, error);
3591
+ }
3592
+
3593
+ return { status: 'error', error: error.message, errorType }
3594
+ }
3595
+
3596
+ /**
3597
+ * Catégorise une erreur pour déterminer l'action appropriée
3598
+ * @private
3599
+ */
3600
+ categorizeError (error) {
3601
+ if (error.status === 429 || error.type === 'RATE_LIMIT_ERROR') {
3602
+ return 'RATE_LIMIT'
3603
+ }
3604
+ if (error.status >= 500 || error.type === 'SERVER_ERROR') {
3605
+ return 'SERVER_ERROR'
3606
+ }
3607
+ if (error.code === 'NETWORK_ERROR' || error.name === 'NetworkError') {
3608
+ return 'NETWORK_ERROR'
3609
+ }
3610
+ if (error.status === 400 || error.type === 'VALIDATION_ERROR') {
3611
+ return 'VALIDATION_ERROR'
3612
+ }
3613
+ return 'UNKNOWN'
3614
+ }
3615
+
3616
+
3617
+ /**
3618
+ * Enregistre une erreur pour analyse
3619
+ * @private
3620
+ */
3621
+ logError (ean, error, type) {
3622
+ this.stats.errors.push({
3623
+ ean,
3624
+ error: error.message,
3625
+ type,
3626
+ timestamp: Date.now()
3627
+ });
3628
+
3629
+ // Limiter la taille du log d'erreurs
3630
+ if (this.stats.errors.length > 100) {
3631
+ this.stats.errors = this.stats.errors.slice(-50);
3632
+ }
3633
+ }
3634
+
3635
+ /**
3636
+ * Retourne les statistiques de synchronisation (simplifiées)
3637
+ */
3638
+ getSyncStats () {
3639
+ return {
3640
+ ...this.stats,
3641
+ successRate: this.stats.attempted > 0
3642
+ ? (this.stats.successful / this.stats.attempted * 100).toFixed(2) + '%'
3643
+ : '0%'
3644
+ }
3645
+ }
3646
+
3647
+ /**
3648
+ * Réinitialise les statistiques de synchronisation
3649
+ */
3650
+ clearSyncStats () {
3651
+ this.stats = {
3652
+ attempted: 0,
3653
+ successful: 0,
3654
+ failed: 0,
3655
+ skipped: 0,
3656
+ rateLimited: 0,
3657
+ errors: []
3658
+ };
3659
+ }
3660
+
3661
+ /**
3662
+ * Force une synchronisation immédiate (version simplifiée)
3663
+ * Utilise le paramètre force_sync côté serveur
3664
+ */
3665
+ async forceSyncNow (ean, productUrl) {
3666
+ // Effectuer la sync avec force_sync = true
3667
+ return this.performSyncWithForce(ean, productUrl)
3668
+ }
3669
+
3670
+ /**
3671
+ * Effectue une synchronisation forcée
3672
+ * @private
3673
+ */
3674
+ async performSyncWithForce(ean, productUrl) {
3675
+ this.stats.attempted++;
3676
+
3677
+ try {
3678
+ // Appel API avec force_sync = true
3679
+ const result = await this.apiClient.syncProduct(ean, productUrl, true);
3680
+
3681
+ if (result.code === 200) {
3682
+ this.stats.successful++;
3683
+ this.emit('syncSuccess', {
3684
+ ean,
3685
+ productUrl,
3686
+ result,
3687
+ forced: true,
3688
+ timestamp: new Date()
3689
+ });
3690
+
3691
+ return { status: 'success', result, forced: true }
3692
+ } else {
3693
+ throw new Error(`API returned status ${result.code}: ${result.message || 'Unknown error'}`)
3694
+ }
3695
+
3696
+ } catch (error) {
3697
+ this.stats.failed++;
3698
+ this.logError(ean, error, 'forced_sync');
3699
+
3700
+ this.emit('syncError', {
3701
+ ean,
3702
+ productUrl,
3703
+ error,
3704
+ forced: true
3705
+ });
3706
+
3707
+ return { status: 'error', error: error.message, forced: true }
3708
+ } finally {
3709
+ // Cleanup if needed
3710
+ }
3711
+ }
3712
+
3713
+ /**
3714
+ * Nettoie et détruit le service (version simplifiée)
3715
+ */
3716
+ destroy () {
3717
+ // Nettoyer les statistiques
3718
+ this.clearSyncStats();
3719
+
3720
+ // Retirer tous les listeners
3721
+ this.removeAllListeners();
3722
+ }
3723
+ }
3724
+
3725
+ /**
3726
+ * VIPros SDK Unifié
3727
+ *
3728
+ * SDK moderne basé sur les Web Components avec rétrocompatibilité pour l'ancien système.
3729
+ * Combine les meilleures fonctionnalités des deux approches précédentes.
3730
+ *
3731
+ * Utilisation moderne (recommandée):
3732
+ * <vipros-offer-card ean="4007123684731"></vipros-offer-card>
3733
+ *
3734
+ * Utilisation legacy (rétrocompatibilité):
3735
+ * <div data-ean="4007123684731"></div> + ViprosSDK.init()
3736
+ */
3737
+ class ViprosSDK extends EventEmitter {
3738
+ constructor() {
3739
+ super();
3740
+ this.version = '3.0.0-unified';
3741
+ this.isInitialized = false;
3742
+
3743
+ // Configuration par défaut non validée - sera validée au moment de init()
3744
+ this.config = {
3745
+ apiKey: null,
3746
+ debug: false,
3747
+ syncEnabled: true,
3748
+ inlineSyncEnabled: true, // Phase 8: Active la sync inline par défaut (workflow à 1 seul appel)
3749
+ disableBackgroundSync: true, // Phase 8: Désactive les appels POST /sync explicites par défaut
3750
+ containerSelector: '[data-ean]',
3751
+ autoScan: true
3752
+ };
3753
+
3754
+ // Services principaux - Lazy initialization for better performance
3755
+ this._apiClient = null;
3756
+ this._cacheService = null;
3757
+ this._syncService = null; // Service de synchronisation
3758
+ this._servicesInitialized = false;
3759
+
3760
+ // Statistiques globales
3761
+ this.stats = {
3762
+ version: this.version,
3763
+ componentsRegistered: 0,
3764
+ componentsActive: 0,
3765
+ offersLoaded: 0,
3766
+ errors: 0,
3767
+ cacheHits: 0,
3768
+ apiCalls: 0,
3769
+ syncAttempts: 0,
3770
+ syncSuccess: 0
3771
+ };
3772
+
3773
+ // Cache mémoire simple (rétrocompatibilité)
3774
+ this.cache = new Map();
3775
+
3776
+ // Event listeners pour les événements DOM personnalisés
3777
+ this.eventListeners = new Map();
3778
+
3779
+ // Singleton pattern
3780
+ if (ViprosSDK.instance) {
3781
+ return ViprosSDK.instance
3782
+ }
3783
+ ViprosSDK.instance = this;
3784
+ }
3785
+
3786
+ /**
3787
+ * Méthode statique pour obtenir l'instance singleton
3788
+ */
3789
+ static getInstance() {
3790
+ if (!ViprosSDK.instance) {
3791
+ ViprosSDK.instance = new ViprosSDK();
3792
+ }
3793
+ return ViprosSDK.instance
3794
+ }
3795
+
3796
+
3797
+ /**
3798
+ * Initialise le SDK avec configuration optionnelle
3799
+ */
3800
+ init(userConfig = {}) {
3801
+ if (this.isInitialized) {
3802
+ this.debugLog('SDK déjà initialisé');
3803
+ return this
3804
+ }
3805
+
3806
+ // Fusionner avec la nouvelle configuration via ConfigManager
3807
+ this.config = ConfigManager.create({
3808
+ ...this.config,
3809
+ ...userConfig
3810
+ });
3811
+
3812
+ this.debugLog(`🚀 Initialisation VIPros SDK v${this.version}`);
3813
+ this.debugLog('Configuration:', this.config);
3814
+
3815
+ // Enregistrer le composant Web Components immédiatement
3816
+ this.registerWebComponents();
3817
+
3818
+ // Auto-scan pour rétrocompatibilité si demandé
3819
+ if (this.config.autoScan) {
3820
+ this.scanAndConvert();
3821
+ }
3822
+
3823
+ this.isInitialized = true;
3824
+ this.emitEvent('sdk-ready', { version: this.version, config: this.config });
3825
+
3826
+ this.debugLog('🎯 SDK initialisé avec lazy loading - services créés à la demande');
3827
+
3828
+ return this
3829
+ }
3830
+
3831
+ /**
3832
+ * Lazy getter for ApiClient - created on first access
3833
+ */
3834
+ get apiClient() {
3835
+ if (!this._apiClient) {
3836
+ this.debugLog('📡 Création lazy d\'ApiClient');
3837
+ this._apiClient = new ApiClient(this.config);
3838
+ }
3839
+ return this._apiClient
3840
+ }
3841
+
3842
+ /**
3843
+ * Lazy getter for CacheService - created on first access
3844
+ */
3845
+ get cacheService() {
3846
+ if (!this._cacheService) {
3847
+ this.debugLog('💾 Création lazy de CacheService');
3848
+ this._cacheService = new CacheService(this.config.cacheTimeout || 300000);
3849
+ }
3850
+ return this._cacheService
3851
+ }
3852
+
3853
+ /**
3854
+ * Lazy getter for SyncService - created on first access
3855
+ */
3856
+ get syncService() {
3857
+ if (!this._syncService && this.config.syncEnabled) {
3858
+ this.debugLog('🔄 Création lazy d\'EanSyncService');
3859
+ this._syncService = new EanSyncService(this.apiClient, this.cacheService, this.config);
3860
+
3861
+ // Connecter les événements de synchronisation lors de la première création
3862
+ this.setupSyncEvents();
3863
+ }
3864
+ return this._syncService
3865
+ }
3866
+
3867
+ /**
3868
+ * Initialize all services immediately (for cases requiring eager loading)
3869
+ */
3870
+ initializeServicesEager() {
3871
+ this.debugLog('🚀 Initialisation eager de tous les services...');
3872
+
3873
+ // Access getters to trigger lazy initialization
3874
+ const api = this.apiClient;
3875
+ const cache = this.cacheService;
3876
+ const sync = this.syncService;
3877
+
3878
+ this._servicesInitialized = true;
3879
+ this.debugLog('✅ Tous les services initialisés (eager)');
3880
+
3881
+ return { api, cache, sync }
3882
+ }
3883
+
3884
+ /**
3885
+ * Configure les événements de synchronisation
3886
+ */
3887
+ setupSyncEvents() {
3888
+ if (!this.syncService) return
3889
+
3890
+ // Rediriger les événements du syncService vers le SDK principal
3891
+ this.syncService.on('syncSuccess', (data) => {
3892
+ this.stats.syncSuccess++;
3893
+ this.emit('syncSuccess', data);
3894
+ });
3895
+
3896
+ this.syncService.on('syncSkipped', (data) => {
3897
+ this.emit('syncSkipped', data);
3898
+ });
3899
+
3900
+ this.syncService.on('syncError', (data) => {
3901
+ this.emit('syncError', data);
3902
+ });
3903
+
3904
+ this.syncService.on('syncRateLimited', (data) => {
3905
+ this.emit('syncRateLimited', data);
3906
+ });
3907
+
3908
+ this.debugLog('Événements de synchronisation connectés');
3909
+ }
3910
+
3911
+ /**
3912
+ * Accès à CacheService pour les Web Components
3913
+ */
3914
+ getCacheService() {
3915
+ return this.cacheService
3916
+ }
3917
+
3918
+ /**
3919
+ * Déclenche une synchronisation en arrière-plan
3920
+ * Peut être désactivé via config.disableBackgroundSync (Phase 8)
3921
+ */
3922
+ syncProductInBackground(ean, productUrl) {
3923
+ // Vérifier si la sync en arrière-plan est désactivée (Phase 8)
3924
+ if (this.config.disableBackgroundSync) {
3925
+ this.debugLog('Sync en arrière-plan désactivée (disableBackgroundSync=true)');
3926
+ return
3927
+ }
3928
+
3929
+ if (!this.syncService) {
3930
+ this.debugLog('Sync demandée mais service non disponible');
3931
+ return
3932
+ }
3933
+
3934
+ this.stats.syncAttempts++;
3935
+ this.debugLog(`Déclenchement sync pour EAN ${ean}`);
3936
+ this.syncService.syncProductInBackground(ean, productUrl);
3937
+ }
3938
+
3939
+ /**
3940
+ * Enregistre les Web Components
3941
+ */
3942
+ registerWebComponents() {
3943
+ if (!customElements.get('vipros-offer-card')) {
3944
+ // Passer le SDK aux composants pour qu'ils puissent accéder à la config et au cache
3945
+ ViprosOfferCard.setSDKInstance(this);
3946
+ customElements.define('vipros-offer-card', ViprosOfferCard);
3947
+ this.stats.componentsRegistered++;
3948
+ this.debugLog('✅ vipros-offer-card registered');
3949
+ }
3950
+ }
3951
+
3952
+ /**
3953
+ * Scanne et convertit les anciens containers data-ean (rétrocompatibilité)
3954
+ */
3955
+ scanAndConvert() {
3956
+ const containers = document.querySelectorAll(this.config.containerSelector);
3957
+ this.debugLog(`🔍 Scan: ${containers.length} containers data-ean trouvés`);
3958
+
3959
+ containers.forEach(container => {
3960
+ const ean = container.getAttribute('data-ean');
3961
+ const price = container.getAttribute('data-price');
3962
+
3963
+ if (ean) {
3964
+ // Créer un Web Component à la place
3965
+ const offerCard = document.createElement('vipros-offer-card');
3966
+ offerCard.setAttribute('ean', ean);
3967
+ if (price) offerCard.setAttribute('price', price);
3968
+
3969
+ // Remplacer le container
3970
+ container.parentNode.replaceChild(offerCard, container);
3971
+ this.debugLog(`🔄 Converti data-ean="${ean}" vers Web Component`);
3972
+ }
3973
+ });
3974
+ }
3975
+
3976
+ /**
3977
+ * Obtenir l'ApiClient partagé (lazy initialization)
3978
+ * Now uses lazy getter for better performance
3979
+ */
3980
+ getApiClient() {
3981
+ return this.apiClient // Uses lazy getter
3982
+ }
3983
+
3984
+ /**
3985
+ * Check if services are initialized (performance monitoring)
3986
+ */
3987
+ getServicesStatus() {
3988
+ return {
3989
+ apiClientInitialized: !!this._apiClient,
3990
+ cacheServiceInitialized: !!this._cacheService,
3991
+ syncServiceInitialized: !!this._syncService,
3992
+ allServicesEager: this._servicesInitialized
3993
+ }
3994
+ }
3995
+
3996
+ /**
3997
+ * API publique pour récupérer des données produit
3998
+ * Utilise la route optimisée /catalog/items/{ean} via ApiClient
3999
+ */
4000
+ async getProductData(ean) {
4001
+ const cacheKey = `product_${ean}`;
4002
+
4003
+ // Vérifier le cache mémoire local
4004
+ if (this.cache.has(cacheKey)) {
4005
+ const cached = this.cache.get(cacheKey);
4006
+ if (Date.now() - cached.timestamp < this.config.cacheTimeout) {
4007
+ this.stats.cacheHits++;
4008
+ this.debugLog(`💾 Cache hit for EAN ${ean}`);
4009
+ return cached.data
4010
+ }
4011
+ }
4012
+
4013
+ try {
4014
+ this.stats.apiCalls++;
4015
+
4016
+ // Utiliser ApiClient avec la route optimisée /sdk/catalog/items/{ean}
4017
+ // Cela bénéficie du Bloom Filter, rate limiting, et route optimisée
4018
+ const item = await this.apiClient.getCatalogItemByEan(ean);
4019
+
4020
+ // Construire la réponse
4021
+ const data = item ? {
4022
+ code: 200,
4023
+ item: item
4024
+ } : {
4025
+ code: 404,
4026
+ item: null
4027
+ };
4028
+
4029
+ // Mise en cache (seulement si produit trouvé)
4030
+ if (item) {
4031
+ this.cache.set(cacheKey, {
4032
+ data: data,
4033
+ timestamp: Date.now()
4034
+ });
4035
+ }
4036
+
4037
+ this.debugLog(`📡 API data loaded for EAN ${ean} (optimized route)`);
4038
+ return data
4039
+
4040
+ } catch (error) {
4041
+ this.stats.errors++;
4042
+ this.debugLog(`❌ Erreur API pour EAN ${ean}:`, error);
4043
+ throw error
4044
+ }
4045
+ }
4046
+
4047
+ /**
4048
+ * Émet un événement personnalisé
4049
+ */
4050
+ emitEvent(type, detail = {}) {
4051
+ const eventName = `vipros-${type}`;
4052
+ const event = new CustomEvent(eventName, {
4053
+ detail: detail,
4054
+ bubbles: true,
4055
+ cancelable: true
4056
+ });
4057
+ document.dispatchEvent(event);
4058
+ this.debugLog(`📤 Event: ${eventName}`, detail);
4059
+ }
4060
+
4061
+ /**
4062
+ * Écoute un événement SDK
4063
+ */
4064
+ on(eventType, callback) {
4065
+ const eventName = `vipros-${eventType}`;
4066
+ document.addEventListener(eventName, callback);
4067
+
4068
+ if (!this.eventListeners.has(eventName)) {
4069
+ this.eventListeners.set(eventName, []);
4070
+ }
4071
+ this.eventListeners.get(eventName).push(callback);
4072
+ }
4073
+
4074
+ /**
4075
+ * Supprime un listener d'événement
4076
+ */
4077
+ off(eventType, callback) {
4078
+ const eventName = `vipros-${eventType}`;
4079
+ document.removeEventListener(eventName, callback);
4080
+
4081
+ if (this.eventListeners.has(eventName)) {
4082
+ const listeners = this.eventListeners.get(eventName);
4083
+ const index = listeners.indexOf(callback);
4084
+ if (index > -1) {
4085
+ listeners.splice(index, 1);
4086
+ }
4087
+ }
4088
+ }
4089
+
4090
+ /**
4091
+ * Vide le cache
4092
+ */
4093
+ clearCache() {
4094
+ this.cache.clear();
4095
+ this.debugLog('🗑️ Cache vidé');
4096
+ }
4097
+
4098
+ /**
4099
+ * Force le rechargement de tous les composants
4100
+ */
4101
+ refreshAll() {
4102
+ const components = document.querySelectorAll('vipros-offer-card');
4103
+ components.forEach(comp => {
4104
+ if (comp.refresh && typeof comp.refresh === 'function') {
4105
+ comp.refresh();
4106
+ }
4107
+ });
4108
+ this.debugLog(`🔄 Refresh de ${components.length} composants`);
4109
+ }
4110
+
4111
+ /**
4112
+ * Récupère les statistiques
4113
+ */
4114
+ getStats() {
4115
+ this.stats.componentsActive = document.querySelectorAll('vipros-offer-card').length;
4116
+ return { ...this.stats }
4117
+ }
4118
+
4119
+ /**
4120
+ * Log de debug conditionnel
4121
+ */
4122
+ debugLog(message, data = null) {
4123
+ if (this.config.debug) {
4124
+ if (data) {
4125
+ console.log(`[ViprosSDK] ${message}`, data);
4126
+ } else {
4127
+ console.log(`[ViprosSDK] ${message}`);
4128
+ }
4129
+ }
4130
+ }
4131
+
4132
+ /**
4133
+ * Détruit l'instance SDK (cleanup)
4134
+ * Enhanced to handle lazy-loaded services
4135
+ */
4136
+ destroy() {
4137
+ // Supprimer tous les event listeners
4138
+ this.eventListeners.forEach((listeners, eventName) => {
4139
+ listeners.forEach(callback => {
4140
+ document.removeEventListener(eventName, callback);
4141
+ });
4142
+ });
4143
+
4144
+ this.eventListeners.clear();
4145
+ this.cache.clear();
4146
+
4147
+ // Cleanup lazy-loaded services
4148
+ if (this._apiClient) {
4149
+ this.debugLog('🧹 Nettoyage ApiClient');
4150
+ this._apiClient = null;
4151
+ }
4152
+
4153
+ if (this._cacheService) {
4154
+ this.debugLog('🧹 Nettoyage CacheService');
4155
+ this._cacheService = null;
4156
+ }
4157
+
4158
+ if (this._syncService) {
4159
+ this.debugLog('🧹 Nettoyage SyncService');
4160
+ this._syncService = null;
4161
+ }
4162
+
4163
+ this._servicesInitialized = false;
4164
+ this.isInitialized = false;
4165
+
4166
+ this.debugLog('🔥 SDK détruit avec nettoyage des services lazy');
4167
+ }
4168
+ }
4169
+
4170
+ /**
4171
+ * VIPros SDK - Point d'entrée principal
4172
+ *
4173
+ * Version unifiée remplaçant ViprosSDK.js et ViprosWebComponents.js
4174
+ * Compatible avec les deux modes d'utilisation
4175
+ */
4176
+
4177
+
4178
+ // Create and return an instance directly
4179
+ const ViprosSDKInstance = new ViprosSDK();
4180
+
4181
+ // Pour les builds navigateur : exposer directement l'instance
4182
+ if (typeof window !== 'undefined') {
4183
+ // Assigner l'instance directement à window
4184
+ // Note: Pour les builds minifiés, cette assignation est aussi gérée par le footer du rollup
4185
+ window.ViprosSDK = ViprosSDKInstance;
4186
+ }
4187
+
4188
+ return ViprosSDKInstance;
4189
+
4190
+ })();