mkfashion-sdk 2.4.9 → 2.7.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/gtm-snippet.js ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * mKFashion SDK — Injection Snippet (versão compacta)
3
+ *
4
+ * Auto-detecta o SKU do produto na PDP, carrega o SDK e renderiza o botão.
5
+ * Quando o cliente adiciona ao carrinho dentro do provador, faz POST
6
+ * pra /cart/add.js com tamanho/cor se disponíveis.
7
+ *
8
+ * USO: cole o conteúdo no GTM (Custom HTML tag), no console DevTools de
9
+ * uma PDP, ou converta pra bookmarklet.
10
+ *
11
+ * CONFIG: troque SEU_PROJECT_ID pelo projectId real do mKFashion.
12
+ */
13
+ (function () {
14
+ // Pode ser sobrescrito setando window.MKFASHION_PROJECT_ID antes deste script
15
+ // (útil pra GTM Variables ou ambientes de teste).
16
+ var PROJECT_ID = window.MKFASHION_PROJECT_ID || 'SEU_PROJECT_ID';
17
+
18
+ if (window.mkfashion) return go();
19
+ var s = document.createElement('script');
20
+ s.src = 'https://unpkg.com/mkfashion-sdk/src/mkfashion.js';
21
+ s.async = true;
22
+ s.onload = go;
23
+ document.head.appendChild(s);
24
+
25
+ // Detecta SKU do produto na PDP, em ordem de prioridade.
26
+ function detect() {
27
+ // 1. Override manual via ?sku= ou ?identifier=
28
+ var qs = new URLSearchParams(location.search);
29
+ if (qs.get('sku')) return qs.get('sku');
30
+ if (qs.get('identifier')) return qs.get('identifier');
31
+
32
+ // 2. Konfidency (Marisa, Renner e outras marcas BR usam esse serviço de reviews)
33
+ if (window.konfidencyData && window.konfidencyData.sku) {
34
+ return String(window.konfidencyData.sku);
35
+ }
36
+
37
+ // 3. dataLayer (GA4 / Universal Analytics Enhanced Ecommerce)
38
+ if (window.dataLayer) {
39
+ for (var i = window.dataLayer.length - 1; i >= 0; i--) {
40
+ var x = (window.dataLayer[i] || {}).ecommerce;
41
+ if (!x) continue;
42
+ var p = (x.detail && x.detail.products && x.detail.products[0]) ||
43
+ (x.items && x.items[0]);
44
+ if (p) return String(p.id || p.item_id);
45
+ }
46
+ }
47
+
48
+ // 4. JSON-LD Schema.org Product
49
+ var lds = document.querySelectorAll('script[type="application/ld+json"]');
50
+ for (var j = 0; j < lds.length; j++) {
51
+ try {
52
+ var items = [].concat(JSON.parse(lds[j].textContent));
53
+ for (var k = 0; k < items.length; k++) {
54
+ var pr = items[k]['@type'] === 'Product' ? items[k] :
55
+ (items[k]['@graph'] || []).find(function (g) { return g['@type'] === 'Product'; });
56
+ if (pr && (pr.sku || pr.productID)) return String(pr.sku || pr.productID);
57
+ }
58
+ } catch (e) {}
59
+ }
60
+
61
+ return null;
62
+ }
63
+
64
+ function go() {
65
+ var sku = detect();
66
+ if (!sku) {
67
+ console.error('[mKFashion] SKU não detectado. Tente passar ?sku=SEU_SKU na URL.');
68
+ return;
69
+ }
70
+ console.log('[mKFashion] SKU:', sku);
71
+
72
+ // Cria container se ainda não existe (overlay fixo no canto como fallback)
73
+ if (!document.getElementById('mkfashion-container')) {
74
+ var c = document.createElement('div');
75
+ c.id = 'mkfashion-container';
76
+ c.style.cssText = 'position:fixed;top:90px;right:20px;z-index:99999;background:#fff;padding:8px;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,.12)';
77
+ document.body.appendChild(c);
78
+ }
79
+
80
+ // Adiciona ao carrinho da plataforma com tamanho/cor se disponíveis
81
+ mkfashion.addToCart(function (p) {
82
+ console.log('[mKFashion] addToCart:', p);
83
+ var item = { id: p.selectedIdentifier, quantity: 1 };
84
+ var props = {};
85
+ if (p.selectedSize) props['Tamanho'] = p.selectedSize;
86
+ if (p.selectedColor) props['Cor'] = p.selectedColor;
87
+ if (Object.keys(props).length) item.properties = props;
88
+
89
+ // Endpoint Shopify-style. Para VTEX/Wake/Magento, ajuste a URL e o body.
90
+ fetch('/cart/add.js', {
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/json' },
93
+ body: JSON.stringify({ items: [item] })
94
+ })
95
+ .then(function (r) { return r.json(); })
96
+ .then(function (d) { console.log('[mKFashion] no carrinho:', d); })
97
+ .catch(function (e) { console.error('[mKFashion] erro cart:', e); });
98
+ });
99
+
100
+ mkfashion.init({
101
+ projectId: PROJECT_ID,
102
+ identifier: sku,
103
+ target: '#mkfashion-container'
104
+ }).then(function (r) {
105
+ // Workaround: depois do init() já validar availability, evita re-fetch no
106
+ // click handler. Sem isso, em sites com monitor SPA (New Relic, etc.) o
107
+ // click intercepta o documento e quebra o fetch com "global scope shutting
108
+ // down". Redundante na SDK 2.5.x+ (cache nativo) — mas inofensivo.
109
+ if (r && r.ok) {
110
+ mkfashion.getAvailability = function () {
111
+ return Promise.resolve({ available: true });
112
+ };
113
+ }
114
+ });
115
+ }
116
+ })();
package/index.html CHANGED
@@ -1,79 +1,178 @@
1
- <!DOCTYPE html>
2
- <html lang="pt-BR">
3
-
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>Provador Virtual</title>
8
- <script src="./src/mkfashion.js"></script>
9
- </head>
10
-
11
- <body>
12
-
13
- <div id="mkfashion-container"></div>
14
-
15
- <script>
16
- (function () {
17
- var projectid = '69f9e5b0f4eb0c4403319830';
18
- var identifier = '1881';
19
-
20
- function initMkFashion() {
21
- mkfashion.addToCart(function (payload) {
22
- console.log('Adicionando ao carrinho:', payload);
23
-
24
- var variantSku = payload.selectedIdentifier;
25
-
26
- fetch('/cart/add.js', {
27
- method: 'POST',
28
- headers: {
29
- 'Content-Type': 'application/json',
30
- },
31
- body: JSON.stringify({
32
- items: [{
33
- quantity: 1,
34
- id: variantSku
35
- }]
36
- })
37
- })
38
- .then(function (response) {
39
- return response.json();
40
- })
41
- .then(function (data) {
42
- console.log('Produto adicionado:', data);
43
- // document.dispatchEvent(new CustomEvent('cart:updated'));
44
- })
45
- .catch(function (error) {
46
- console.error('Erro carrinho:', error);
47
- });
48
- });
49
-
50
- mkfashion.onInteraction(function (data) {
51
- console.log(data.category, data.action);
52
- });
53
-
54
- mkfashion.init({
55
- projectId: projectid,
56
- identifier: identifier,
57
- target: '#mkfashion-container'
58
- });
59
- }
60
-
61
- function waitForMkFashion() {
62
- if (typeof mkfashion !== 'undefined') {
63
- initMkFashion();
64
- } else {
65
- setTimeout(waitForMkFashion, 100);
66
- }
67
- }
68
-
69
- if (document.readyState === "loading") {
70
- document.addEventListener("DOMContentLoaded", waitForMkFashion);
71
- } else {
72
- waitForMkFashion();
73
- }
74
- })();
75
- </script>
76
-
77
- </body>
78
-
79
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="pt-BR">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Provador Virtual</title>
8
+ <script src="./src/mkfashion.js"></script>
9
+ </head>
10
+
11
+ <body>
12
+
13
+ <div id="mkfashion-container"></div>
14
+
15
+ <script>
16
+ (function () {
17
+ var projectid = '69f9e5b0f4eb0c4403319830';
18
+
19
+ // ============================================================
20
+ // Detecta o identifier (SKU) da PDP automaticamente.
21
+ // Estratégias em ordem de prioridade — retorna a primeira que casar.
22
+ // ============================================================
23
+ function detectIdentifier() {
24
+ // 1. Query string (?sku=... ou ?identifier=...) — útil pra teste / override
25
+ var qs = new URLSearchParams(location.search);
26
+ var fromUrl = qs.get('sku') || qs.get('identifier');
27
+ if (fromUrl) return { value: fromUrl, source: 'url-param' };
28
+
29
+ // 2. JSON-LD (Schema.org Product) — presente em PDPs com SEO decente
30
+ var scripts = document.querySelectorAll('script[type="application/ld+json"]');
31
+ for (var i = 0; i < scripts.length; i++) {
32
+ try {
33
+ var data = JSON.parse(scripts[i].textContent);
34
+ var items = Array.isArray(data) ? data : [data];
35
+ for (var j = 0; j < items.length; j++) {
36
+ var node = items[j];
37
+ var product = node['@type'] === 'Product' ? node :
38
+ (node['@graph'] && node['@graph'].find(function (g) { return g['@type'] === 'Product'; }));
39
+ if (product) {
40
+ var sku = product.sku || product.mpn || product.productID;
41
+ if (sku) return { value: String(sku), source: 'json-ld' };
42
+ }
43
+ }
44
+ } catch (e) { /* json inválido — ignora */ }
45
+ }
46
+
47
+ // 3. Meta tags
48
+ var meta = document.querySelector(
49
+ 'meta[itemprop="sku"], meta[property="product:retailer_item_id"], meta[name="sku"]'
50
+ );
51
+ if (meta && meta.content) return { value: meta.content, source: 'meta' };
52
+
53
+ // 4. Microdata em qualquer elemento
54
+ var micro = document.querySelector('[itemprop="sku"]');
55
+ if (micro) {
56
+ var v = micro.getAttribute('content') || (micro.textContent || '').trim();
57
+ if (v) return { value: v, source: 'microdata' };
58
+ }
59
+
60
+ // 5. dataLayer (GA4 / Enhanced Ecommerce)
61
+ if (window.dataLayer && window.dataLayer.length) {
62
+ for (var k = window.dataLayer.length - 1; k >= 0; k--) {
63
+ var e = window.dataLayer[k] || {};
64
+ // GA4
65
+ if (e.ecommerce && e.ecommerce.items && e.ecommerce.items[0]) {
66
+ var ga4 = e.ecommerce.items[0].item_id || e.ecommerce.items[0].id;
67
+ if (ga4) return { value: String(ga4), source: 'dataLayer/ga4' };
68
+ }
69
+ // Universal Analytics Enhanced Ecommerce
70
+ if (e.ecommerce && e.ecommerce.detail && e.ecommerce.detail.products && e.ecommerce.detail.products[0]) {
71
+ var ee = e.ecommerce.detail.products[0].id;
72
+ if (ee) return { value: String(ee), source: 'dataLayer/ee' };
73
+ }
74
+ }
75
+ }
76
+
77
+ // 6. Globais de plataforma
78
+ if (window.ShopifyAnalytics && window.ShopifyAnalytics.meta && window.ShopifyAnalytics.meta.product) {
79
+ return { value: String(window.ShopifyAnalytics.meta.product.id), source: 'shopify' };
80
+ }
81
+ // VTEX — várias formas de expor a SKU
82
+ if (window.vtxctx && window.vtxctx.skus && window.vtxctx.skus[0]) {
83
+ return { value: String(window.vtxctx.skus[0]), source: 'vtex/vtxctx' };
84
+ }
85
+ if (window.skuJson_0 && window.skuJson_0.skus && window.skuJson_0.skus[0]) {
86
+ return { value: String(window.skuJson_0.skus[0].sku), source: 'vtex/skuJson' };
87
+ }
88
+ // Konfidency (serviço de reviews usado por várias marcas BR — Marisa, etc.)
89
+ var kd = window.konfidencyData;
90
+ if (kd && kd.product && kd.product.variants && kd.product.variants[0]) {
91
+ return { value: String(kd.product.variants[0]), source: 'konfidency/variant' };
92
+ }
93
+
94
+ // Nota: NÃO usamos padrão de URL como fallback (ex: /p/{id} na VTEX
95
+ // retorna o productId, não o SKU). É melhor falhar do que dar dado
96
+ // errado — quem precisar de override deve passar ?sku= na URL.
97
+
98
+ // 7. Fallback hardcoded — útil pra rodar index.html localmente sem
99
+ // estrutura de PDP. Em produção, remova ou ajuste pro seu cenário.
100
+ return { value: '1881', source: 'fallback-hardcoded' };
101
+ }
102
+
103
+ // ============================================================
104
+ // Inicialização
105
+ // ============================================================
106
+ function initMkFashion() {
107
+ var detected = detectIdentifier();
108
+ console.log('[mKFashion] SKU detectado:', detected.value, '(via ' + detected.source + ')');
109
+
110
+ // Callback de carrinho — recebe o produto escolhido no provador
111
+ // e adiciona ao carrinho da plataforma (exemplo: Shopify).
112
+ mkfashion.addToCart(function (payload) {
113
+ console.log('[mKFashion] addToCart payload:', payload);
114
+
115
+ var variantSku = payload.selectedIdentifier;
116
+ if (!variantSku) {
117
+ console.warn('[mKFashion] produto sem variantSku — pulando adição ao carrinho');
118
+ return;
119
+ }
120
+
121
+ // Properties opcionais — passa tamanho/cor se vieram no payload.
122
+ // No Shopify aparecem no carrinho como propriedades de line item.
123
+ var properties = {};
124
+ if (payload.selectedSize) properties['Tamanho'] = payload.selectedSize;
125
+ if (payload.selectedColor) properties['Cor'] = payload.selectedColor;
126
+
127
+ var item = { id: variantSku, quantity: 1 };
128
+ if (Object.keys(properties).length) item.properties = properties;
129
+
130
+ // Endpoint Shopify-style. Ajuste pra sua plataforma:
131
+ // VTEX: POST /api/checkout/pub/orderForm/{id}/items
132
+ // Wake: API própria
133
+ // Magento: rest/V1/carts/mine/items
134
+ fetch('/cart/add.js', {
135
+ method: 'POST',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify({ items: [item] })
138
+ })
139
+ .then(function (response) { return response.json(); })
140
+ .then(function (data) {
141
+ console.log('[mKFashion] produto adicionado ao carrinho:', data);
142
+ // document.dispatchEvent(new CustomEvent('cart:updated'));
143
+ })
144
+ .catch(function (error) {
145
+ console.error('[mKFashion] erro ao adicionar ao carrinho:', error);
146
+ });
147
+ });
148
+
149
+ mkfashion.onInteraction(function (data) {
150
+ console.log(data.category, data.action);
151
+ });
152
+
153
+ mkfashion.init({
154
+ projectId: projectid,
155
+ identifier: detected.value,
156
+ target: '#mkfashion-container'
157
+ });
158
+ }
159
+
160
+ function waitForMkFashion() {
161
+ if (typeof mkfashion !== 'undefined') {
162
+ initMkFashion();
163
+ } else {
164
+ setTimeout(waitForMkFashion, 100);
165
+ }
166
+ }
167
+
168
+ if (document.readyState === "loading") {
169
+ document.addEventListener("DOMContentLoaded", waitForMkFashion);
170
+ } else {
171
+ waitForMkFashion();
172
+ }
173
+ })();
174
+ </script>
175
+
176
+ </body>
177
+
178
+ </html>
Binary file
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mkfashion-sdk",
3
- "version": "2.4.9",
4
- "description": "SDK para integrar o provador virtual mKFashion com suporte a Wake Commerce",
3
+ "version": "2.7.0",
4
+ "description": "SDK para integrar o provador virtual mKFashion com suporte a Wake Commerce e tracking analytics completo",
5
5
  "main": "src/mkfashion.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Abra test.html no navegador\""
package/src/mkfashion.js CHANGED
@@ -19,11 +19,15 @@ const mkfashion = {
19
19
 
20
20
  appUrl: 'https://mkfashion.mk3dlabs.com/visualizer',
21
21
  apiUrl: 'https://mkfashion-new-api.mk3dlabs.com',
22
+ // BETA: collector apontando para tunnel temporario (Cloudflare quick tunnel).
23
+ // Trocar para 'https://collector.metakosmos.com.br' quando o deploy formal subir.
24
+ collectorUrl: 'https://something-perl-parker-radios.trycloudflare.com',
22
25
  debug: false,
23
26
 
24
27
  // DEV - Descomentar para desenvolvimento local
25
28
  //appUrl: 'http://localhost:5174/visualizer',
26
29
  //apiUrl: 'http://localhost:3007',
30
+ //collectorUrl: 'http://localhost:3030',
27
31
 
28
32
  // ============ ESTADO INTERNO ============
29
33
 
@@ -80,17 +84,25 @@ const mkfashion = {
80
84
  return
81
85
  }
82
86
 
83
- try {
84
- const availability = await this.getAvailability(projectId, identifier)
85
- if (!availability.available) {
86
- console.error('[mKFashion] Produto nao disponivel para try-on:', availability.message)
87
- alert('Este produto não está disponível para prova virtual.')
87
+ // Pula re-validação se init() já validou esse projeto+SKU (caso comum:
88
+ // botão renderizado pelo init e clicado depois). Evita fetch redundante
89
+ // no click handler — agente de monitoramento SPA (New Relic etc.) pode
90
+ // interceptar o click e quebrar fetches durante interceptação.
91
+ var availabilityCacheKey = projectId + ':' + identifier
92
+ if (this._availabilityCheckedFor !== availabilityCacheKey) {
93
+ try {
94
+ const availability = await this.getAvailability(projectId, identifier)
95
+ if (!availability.available) {
96
+ console.error('[mKFashion] Produto nao disponivel para try-on:', availability.message)
97
+ alert('Este produto não está disponível para prova virtual.')
98
+ return
99
+ }
100
+ } catch (error) {
101
+ console.error('[mKFashion] Erro ao verificar disponibilidade:', error)
102
+ alert('Produto não encontrado ou indisponível.')
88
103
  return
89
104
  }
90
- } catch (error) {
91
- console.error('[mKFashion] Erro ao verificar disponibilidade:', error)
92
- alert('Produto não encontrado ou indisponível.')
93
- return
105
+ this._availabilityCheckedFor = availabilityCacheKey
94
106
  }
95
107
 
96
108
  this._config = {
@@ -119,6 +131,10 @@ const mkfashion = {
119
131
 
120
132
  this._isOpen = true
121
133
  this._log('Aberto', this._config)
134
+
135
+ // Emite modal_opened — não passa pelo _triggerCallback porque é um evento
136
+ // só de tracking (não tem callback público equivalente).
137
+ try { this._emitToCollector('modal_opened', { projectId, identifier }) } catch (_) {}
122
138
  },
123
139
 
124
140
  /** Fecha o modal do provador virtual (esconde sem destruir a sessao) */
@@ -133,13 +149,21 @@ const mkfashion = {
133
149
  this._isOpen = false
134
150
  this._confirmingExit = false
135
151
  this._log('Fechado')
152
+
153
+ // Emite modal_closed (complementa o onClose que já passou por _triggerCallback).
154
+ try {
155
+ this._emitToCollector('modal_closed', {
156
+ projectId: this._config && this._config.projectId,
157
+ identifier: this._config && this._config.identifier
158
+ })
159
+ } catch (_) {}
136
160
  },
137
161
 
138
162
  /**
139
163
  * Registra callback de adicionar ao carrinho.
140
164
  *
141
165
  * Payload simplificado (não-Gregory):
142
- * { mainIdentifier, selectedIdentifier, name, price, sizePrice, selectedSize, productUrl, tryonImageUrl }
166
+ * { mainIdentifier, selectedIdentifier, name, price, sizePrice, selectedSize, selectedColor, productUrl, tryonImageUrl }
143
167
  *
144
168
  * Projetos Gregory mantêm estrutura legada completa.
145
169
  */
@@ -279,6 +303,16 @@ const mkfashion = {
279
303
 
280
304
  const data = await response.json()
281
305
  this._log('Disponibilidade recebida', data)
306
+
307
+ // NOTA: availability check é decisão interna (mostra botão ou não) e
308
+ // NÃO é emitido pro collector — não traz valor analítico. O sinal de
309
+ // "impressão" vem do auto-tracking (page_view), e o sinal de "produto
310
+ // tem try-on" vem do page_button_rendered no init().
311
+ // Apenas inicializa _config pra que o auto-tracking saiba quem é o projeto.
312
+ try {
313
+ if (!this._config) this._config = { projectId: resolved, identifier }
314
+ } catch (_) {}
315
+
282
316
  return data
283
317
 
284
318
  } catch (error) {
@@ -420,6 +454,9 @@ const mkfashion = {
420
454
  },
421
455
 
422
456
  _triggerCallback(name, data = null) {
457
+ // Emite pro collector ANTES do callback do cliente (não bloqueia, fire-and-forget).
458
+ try { this._emitToCollector(name, data) } catch (_) {}
459
+
423
460
  if (this._callbacks && typeof this._callbacks[name] === 'function') {
424
461
  try {
425
462
  this._callbacks[name](data)
@@ -430,6 +467,459 @@ const mkfashion = {
430
467
  }
431
468
  },
432
469
 
470
+ // ============ TRACKING (mk-collector-api) ============
471
+
472
+ _visitorId: null,
473
+ _sessionId: null,
474
+ _siteContext: null,
475
+
476
+ _getVisitorId() {
477
+ if (this._visitorId) return this._visitorId
478
+ try {
479
+ let v = localStorage.getItem('_mk_sdk_vid')
480
+ if (!v) {
481
+ v = 'v_' + (typeof crypto !== 'undefined' && crypto.randomUUID
482
+ ? crypto.randomUUID()
483
+ : Math.random().toString(36).slice(2) + Date.now().toString(36))
484
+ localStorage.setItem('_mk_sdk_vid', v)
485
+ }
486
+ this._visitorId = v
487
+ return v
488
+ } catch (_) {
489
+ this._visitorId = 'v_' + Math.random().toString(36).slice(2)
490
+ return this._visitorId
491
+ }
492
+ },
493
+
494
+ _getSessionId() {
495
+ if (this._sessionId) return this._sessionId
496
+ try {
497
+ let s = sessionStorage.getItem('_mk_sdk_sid')
498
+ if (!s) {
499
+ s = 's_' + (typeof crypto !== 'undefined' && crypto.randomUUID
500
+ ? crypto.randomUUID()
501
+ : Math.random().toString(36).slice(2) + Date.now().toString(36))
502
+ sessionStorage.setItem('_mk_sdk_sid', s)
503
+ }
504
+ this._sessionId = s
505
+ return s
506
+ } catch (_) {
507
+ this._sessionId = 's_' + Math.random().toString(36).slice(2)
508
+ return this._sessionId
509
+ }
510
+ },
511
+
512
+ _parseUtm() {
513
+ try {
514
+ const p = new URLSearchParams(location.search)
515
+ const out = {}
516
+ const keys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']
517
+ for (const k of keys) {
518
+ const v = p.get(k)
519
+ if (v) out[k.replace('utm_', '')] = v
520
+ }
521
+ return Object.keys(out).length > 0 ? out : null
522
+ } catch (_) { return null }
523
+ },
524
+
525
+ _detectPlatform() {
526
+ try {
527
+ if (window.Shopify || window.ShopifyAnalytics) return 'shopify'
528
+ if (window.vtex || window.vtxctx) return 'vtex'
529
+ if (window.wakeCommerce || window.__WAKE__) return 'wake'
530
+ if (window.Konfidency || window.konfidency) return 'konfidency'
531
+ if (window.wc_add_to_cart_params || window.woocommerce_params) return 'woocommerce'
532
+ if (window.dataLayer && Array.isArray(window.dataLayer) && window.dataLayer[0]) {
533
+ const p = window.dataLayer[0].platform
534
+ if (p) return String(p).toLowerCase()
535
+ }
536
+ } catch (_) {}
537
+ return null
538
+ },
539
+
540
+ _detectCurrentProduct() {
541
+ try {
542
+ // 1. dataLayer (GA4/UA padrão)
543
+ if (window.dataLayer && Array.isArray(window.dataLayer)) {
544
+ for (let i = window.dataLayer.length - 1; i >= 0; i--) {
545
+ const item = window.dataLayer[i]
546
+ if (!item) continue
547
+ if (item.product) {
548
+ return {
549
+ sku: item.product.sku || item.product.id || null,
550
+ name: item.product.name || null,
551
+ price: item.product.price || null
552
+ }
553
+ }
554
+ if (item.ecommerce && item.ecommerce.items && item.ecommerce.items[0]) {
555
+ const p = item.ecommerce.items[0]
556
+ return { sku: p.item_id || p.id, name: p.item_name || p.name, price: p.price }
557
+ }
558
+ }
559
+ }
560
+ // 2. JSON-LD schema.org/Product
561
+ const lds = document.querySelectorAll('script[type="application/ld+json"]')
562
+ for (const ld of lds) {
563
+ try {
564
+ const j = JSON.parse(ld.textContent || '{}')
565
+ const arr = Array.isArray(j) ? j : [j]
566
+ for (const item of arr) {
567
+ if (item && (item['@type'] === 'Product' || (Array.isArray(item['@type']) && item['@type'].includes('Product')))) {
568
+ return {
569
+ sku: item.sku || item.mpn || item.gtin || null,
570
+ name: item.name || null,
571
+ price: (item.offers && (item.offers.price || (item.offers[0] && item.offers[0].price))) || null
572
+ }
573
+ }
574
+ }
575
+ } catch (_) {}
576
+ }
577
+ // 3. Open Graph (og:type=product)
578
+ const og = (n) => {
579
+ const m = document.querySelector('meta[property="og:' + n + '"]')
580
+ return m && m.content || null
581
+ }
582
+ if (og('type') === 'product') {
583
+ return { sku: null, name: og('title'), price: og('price:amount') }
584
+ }
585
+ } catch (_) {}
586
+ return null
587
+ },
588
+
589
+ _detectSiteContext() {
590
+ if (this._siteContext) return this._siteContext
591
+ try {
592
+ this._siteContext = {
593
+ url: location.href,
594
+ pathname: location.pathname,
595
+ referrer: document.referrer || null,
596
+ title: document.title || null,
597
+ utm: this._parseUtm(),
598
+ userAgent: navigator.userAgent,
599
+ language: navigator.language,
600
+ viewport: window.innerWidth + 'x' + window.innerHeight,
601
+ devicePixelRatio: window.devicePixelRatio || 1,
602
+ platform: this._detectPlatform(),
603
+ detectedProduct: this._detectCurrentProduct()
604
+ }
605
+ } catch (_) {
606
+ this._siteContext = {}
607
+ }
608
+ return this._siteContext
609
+ },
610
+
611
+ /**
612
+ * Enfileira evento no batch buffer. O flush real acontece em _flushQueue.
613
+ * Fire-and-forget — nunca atrapalha UX.
614
+ */
615
+ _emitToCollector(eventName, data) {
616
+ try {
617
+ if (!this._config || !this._config.projectId) {
618
+ // Antes do projectId estar setado, enfileira pra flush quando estiver.
619
+ this._preConfigQueue = this._preConfigQueue || []
620
+ if (this._preConfigQueue.length < 50) {
621
+ this._preConfigQueue.push({ eventName, data, ts: Date.now() })
622
+ }
623
+ return
624
+ }
625
+ if (!this.collectorUrl) return
626
+
627
+ // Drena qualquer evento que foi enfileirado antes do config existir.
628
+ if (this._preConfigQueue && this._preConfigQueue.length > 0) {
629
+ const pending = this._preConfigQueue
630
+ this._preConfigQueue = []
631
+ for (const p of pending) this._queueEvent(p.eventName, p.data, p.ts)
632
+ }
633
+
634
+ this._queueEvent(eventName, data)
635
+
636
+ // Auto-start tracking de site no primeiro emit com config.
637
+ if (!this._autoTrackInitialized && this.autoTrack !== false) {
638
+ this._initAutoTrack()
639
+ }
640
+ } catch (e) {
641
+ if (this.debug) console.warn('[mKFashion tracking]', e.message)
642
+ }
643
+ },
644
+
645
+ /**
646
+ * Normaliza nome de evento — tira prefixo "on" e converte camelCase pra snake_case.
647
+ * Ex: 'onAddToCart' → 'add_to_cart'; 'modal_opened' → 'modal_opened' (já snake).
648
+ */
649
+ _normalizeEventName(name) {
650
+ if (typeof name !== 'string' || !name) return 'unknown'
651
+ var n = name.replace(/^on/, '')
652
+ n = n.replace(/[A-Z]/g, function (m, i) { return (i > 0 ? '_' : '') + m.toLowerCase() })
653
+ if (n.charAt(0) >= 'A' && n.charAt(0) <= 'Z') n = n.toLowerCase()
654
+ return n
655
+ },
656
+
657
+ /**
658
+ * Adiciona ao buffer de batch. Flush é disparado por timer ou tamanho.
659
+ * Todos os eventos recebem prefixo 'page_'.
660
+ */
661
+ _queueEvent(eventName, data, tsOverride) {
662
+ this._eventQueue = this._eventQueue || []
663
+ const params = { identifier: this._config.identifier || null }
664
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
665
+ Object.assign(params, data)
666
+ } else if (data !== null && data !== undefined) {
667
+ params.value = data
668
+ }
669
+ this._eventQueue.push({
670
+ name: 'page_' + this._normalizeEventName(eventName),
671
+ ts: tsOverride || Date.now(),
672
+ params: params
673
+ })
674
+
675
+ // Flush sincrono se atingiu limite de tamanho.
676
+ if (this._eventQueue.length >= 20) {
677
+ this._flushQueue()
678
+ return
679
+ }
680
+ // Senão, agenda flush em 2s.
681
+ if (!this._flushTimer) {
682
+ this._flushTimer = setTimeout(() => { this._flushTimer = null; this._flushQueue() }, 2000)
683
+ }
684
+ },
685
+
686
+ _flushQueue(useBeacon) {
687
+ if (!this._eventQueue || this._eventQueue.length === 0) return
688
+ if (!this._config || !this._config.projectId) return
689
+ if (!this.collectorUrl) return
690
+
691
+ const events = this._eventQueue.splice(0, 50) // máximo 50 por POST
692
+ const payload = JSON.stringify({
693
+ visitorId: this._getVisitorId(),
694
+ sessionId: this._getSessionId(),
695
+ events: events,
696
+ site: this._detectSiteContext()
697
+ })
698
+
699
+ const url = this.collectorUrl + '/v1/track/sdk'
700
+ const headers = {
701
+ 'Content-Type': 'application/json',
702
+ 'X-MK-Project-Id': this._config.projectId
703
+ }
704
+
705
+ // sendBeacon no pagehide pra garantir envio durante unload.
706
+ if (useBeacon && navigator.sendBeacon) {
707
+ try {
708
+ const blob = new Blob([payload], { type: 'application/json' })
709
+ // sendBeacon nao suporta headers custom — usamos query param.
710
+ navigator.sendBeacon(url + '?projectId=' + encodeURIComponent(this._config.projectId), blob)
711
+ return
712
+ } catch (_) {}
713
+ }
714
+
715
+ fetch(url, { method: 'POST', keepalive: true, headers: headers, body: payload })
716
+ .catch(() => {})
717
+ },
718
+
719
+ // ============ AUTO-TRACK (pageview/click/scroll/form/engagement) ============
720
+
721
+ _autoTrackInitialized: false,
722
+ _scrollDepthSeen: null,
723
+ _engagementTimer: null,
724
+ _engagementLastTick: 0,
725
+
726
+ /**
727
+ * Inicia captura automática de comportamento no site host. Chamado quando
728
+ * _config recebe um projectId, OU manualmente via mkfashion.startTracking().
729
+ * Setar mkfashion.autoTrack = false antes do init pra desabilitar.
730
+ */
731
+ _initAutoTrack() {
732
+ if (this._autoTrackInitialized) return
733
+ this._autoTrackInitialized = true
734
+ try {
735
+ this._trackPageview()
736
+ this._initClickTracking()
737
+ this._initScrollTracking()
738
+ this._initFormTracking()
739
+ this._initEngagementTracking()
740
+ this._initSpaNavigation()
741
+ this._initFlushOnUnload()
742
+ this._log('Auto-tracking iniciado')
743
+ } catch (e) {
744
+ if (this.debug) console.warn('[mKFashion auto-track init]', e.message)
745
+ }
746
+ },
747
+
748
+ /**
749
+ * API pública pra ativar auto-tracking sem precisar abrir o modal.
750
+ * Útil em sites onde a SDK só renderiza botão (sem getAvailability) ou
751
+ * em landing pages sem produto definido.
752
+ */
753
+ startTracking(projectId, identifier) {
754
+ if (!projectId) {
755
+ console.error('[mKFashion] startTracking: projectId obrigatorio')
756
+ return
757
+ }
758
+ const resolved = this._resolveProjectId(projectId)
759
+ if (!this._config) this._config = { projectId: resolved, identifier: identifier || null }
760
+ this._initAutoTrack()
761
+ },
762
+
763
+ _trackPageview() {
764
+ this._scrollDepthSeen = {}
765
+ // 'view' aqui vira 'page_view' (prefix 'page_' é adicionado em _queueEvent).
766
+ this._emitToCollector('view', {
767
+ url: location.href,
768
+ pathname: location.pathname,
769
+ referrer: document.referrer || null,
770
+ title: document.title || null
771
+ })
772
+ },
773
+
774
+ _initClickTracking() {
775
+ if (!document.body) return
776
+ const INTERACTIVE = { A: 1, BUTTON: 1, INPUT: 1, SELECT: 1, TEXTAREA: 1 }
777
+ let lastClick = 0
778
+ document.body.addEventListener('click', (e) => {
779
+ try {
780
+ const now = Date.now()
781
+ if (now - lastClick < 100) return
782
+ lastClick = now
783
+ let el = e.target
784
+ for (let i = 0; i < 5 && el && el !== document.body; i++) {
785
+ if (INTERACTIVE[el.tagName] || (el.getAttribute && el.getAttribute('role') === 'button')) break
786
+ el = el.parentElement
787
+ }
788
+ if (!el) return
789
+ if (!INTERACTIVE[el.tagName] && !(el.getAttribute && el.getAttribute('role') === 'button')) return
790
+
791
+ const data = { tag: el.tagName }
792
+ const t = el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' ? el.value : el.textContent
793
+ data.text = (t || '').replace(/\s+/g, ' ').trim().substring(0, 100)
794
+ if (el.href) data.href = String(el.href).substring(0, 500)
795
+ if (el.id) data.id = el.id
796
+ if (el.className && typeof el.className === 'string') {
797
+ data.classes = el.className.trim().substring(0, 200)
798
+ }
799
+ this._emitToCollector('click', data)
800
+ } catch (_) {}
801
+ }, { capture: true, passive: true })
802
+ },
803
+
804
+ _initScrollTracking() {
805
+ const TH = [25, 50, 75, 100]
806
+ let scrollTimer = null
807
+ const check = () => {
808
+ try {
809
+ const sy = window.scrollY || document.documentElement.scrollTop || 0
810
+ const dh = Math.max(
811
+ document.body && document.body.scrollHeight || 0,
812
+ document.documentElement && document.documentElement.scrollHeight || 0
813
+ )
814
+ if (dh <= 0) return
815
+ const p = Math.min(100, Math.floor((sy + window.innerHeight) / dh * 100))
816
+ if (!this._scrollDepthSeen) this._scrollDepthSeen = {}
817
+ for (const t of TH) {
818
+ if (p >= t && !this._scrollDepthSeen[t]) {
819
+ this._scrollDepthSeen[t] = true
820
+ this._emitToCollector('scroll_depth', { depth: t })
821
+ }
822
+ }
823
+ } catch (_) {}
824
+ }
825
+ check()
826
+ window.addEventListener('scroll', () => {
827
+ if (scrollTimer) return
828
+ scrollTimer = setTimeout(() => { scrollTimer = null; check() }, 250)
829
+ }, { passive: true })
830
+ },
831
+
832
+ _initFormTracking() {
833
+ if (!document.body) return
834
+ const CC = ['wpcf7-form', 'wpforms-form', 'gform_wrapper', 'formidable']
835
+ const NP = ['subscribe', 'newsletter', 'mailchimp', 'convertkit']
836
+ document.body.addEventListener('submit', (e) => {
837
+ try {
838
+ const f = e.target
839
+ if (!f || f.tagName !== 'FORM') return
840
+ // Skip WooCommerce checkout (capturado por outro caminho)
841
+ if (f.classList && (f.classList.contains('woocommerce-checkout') ||
842
+ f.classList.contains('wc-block-checkout__form'))) return
843
+
844
+ let ft = 'other'
845
+ const cls = f.className || ''
846
+ const act = (f.action || '').toLowerCase()
847
+ if (f.querySelector('input[type="search"]') || f.querySelector('input[name="s"]')) ft = 'search'
848
+ else {
849
+ for (const c of CC) if (cls.indexOf(c) !== -1) { ft = 'contact'; break }
850
+ if (ft === 'other') for (const n of NP) if (act.indexOf(n) !== -1) { ft = 'newsletter'; break }
851
+ }
852
+ this._emitToCollector('form_submit', {
853
+ form_id: f.id || '',
854
+ form_action: (f.action || '').substring(0, 300),
855
+ form_type: ft
856
+ })
857
+ } catch (_) {}
858
+ }, { capture: true })
859
+ },
860
+
861
+ _initEngagementTracking() {
862
+ const INTERVAL = 15000
863
+ this._engagementLastTick = Date.now()
864
+ this._engagementTimer = setInterval(() => {
865
+ if (document.hidden) return
866
+ const now = Date.now()
867
+ const elapsedSec = Math.round((now - this._engagementLastTick) / 1000)
868
+ this._engagementLastTick = now
869
+ if (elapsedSec > 0 && elapsedSec < 60) {
870
+ this._emitToCollector('engagement', { url: location.pathname, dwellSec: elapsedSec })
871
+ }
872
+ }, INTERVAL)
873
+
874
+ document.addEventListener('visibilitychange', () => {
875
+ if (document.hidden) {
876
+ const elapsedSec = Math.round((Date.now() - this._engagementLastTick) / 1000)
877
+ if (elapsedSec > 0 && elapsedSec < 600) {
878
+ this._emitToCollector('engagement', { url: location.pathname, dwellSec: elapsedSec })
879
+ }
880
+ this._engagementLastTick = Date.now()
881
+ } else {
882
+ this._engagementLastTick = Date.now()
883
+ }
884
+ })
885
+ },
886
+
887
+ _initSpaNavigation() {
888
+ // Captura SPA navigation (React/Vue/Next/etc) emitindo page_view a cada
889
+ // mudança de URL via history API.
890
+ let lastUrl = location.href
891
+ const self = this
892
+ const onChange = () => {
893
+ if (location.href !== lastUrl) {
894
+ lastUrl = location.href
895
+ self._trackPageview()
896
+ }
897
+ }
898
+ try {
899
+ const origPush = history.pushState
900
+ const origReplace = history.replaceState
901
+ history.pushState = function () { origPush.apply(this, arguments); setTimeout(onChange, 0) }
902
+ history.replaceState = function () { origReplace.apply(this, arguments); setTimeout(onChange, 0) }
903
+ window.addEventListener('popstate', onChange)
904
+ } catch (_) {}
905
+ },
906
+
907
+ _initFlushOnUnload() {
908
+ // Flush sincrono no pagehide pra não perder engagement final + qualquer evento pendente.
909
+ window.addEventListener('pagehide', () => {
910
+ try {
911
+ const elapsedSec = Math.round((Date.now() - this._engagementLastTick) / 1000)
912
+ if (elapsedSec > 0 && elapsedSec < 600) {
913
+ this._emitToCollector('engagement', { url: location.pathname, dwellSec: elapsedSec })
914
+ }
915
+ this._flushQueue(true)
916
+ } catch (_) {}
917
+ })
918
+ window.addEventListener('beforeunload', () => {
919
+ try { this._flushQueue(true) } catch (_) {}
920
+ })
921
+ },
922
+
433
923
  _buildUrl() {
434
924
  const encoded = encodeURIComponent(this._config.identifier).replace(/\./g, '%2E')
435
925
  const url = `${this.appUrl}/${this._config.projectId}/${encoded}`
@@ -642,7 +1132,7 @@ const mkfashion = {
642
1132
  iframe.style.width = typeof width === 'number' ? `${width}px` : width
643
1133
  iframe.style.height = typeof height === 'number' ? `${height}px` : height
644
1134
  iframe.style.border = 'none'
645
- iframe.setAttribute('allow', 'camera *; microphone *; web-share *')
1135
+ iframe.setAttribute('allow', 'camera *; microphone *')
646
1136
  iframe.setAttribute('allowfullscreen', 'true')
647
1137
  return iframe
648
1138
  },
@@ -729,6 +1219,7 @@ const mkfashion = {
729
1219
  price: product.price || null,
730
1220
  sizePrice: product.sizePrice || null,
731
1221
  selectedSize: data?.size || null,
1222
+ selectedColor: data?.color || product.color || null,
732
1223
  produtoVarianteId: product.produtoVarianteId || null,
733
1224
  productUrl: product.productUrl || null,
734
1225
  tryonImageUrl: product.tryonImageUrl || null
@@ -850,9 +1341,17 @@ const mkfashion = {
850
1341
 
851
1342
  if (!available) {
852
1343
  this._log('Produto indisponível, botão não criado', { projectId, identifier })
1344
+ try {
1345
+ if (!this._config) this._config = { projectId, identifier }
1346
+ this._emitToCollector('button_unavailable', { projectId, identifier })
1347
+ } catch (_) {}
853
1348
  return fail('unavailable')
854
1349
  }
855
1350
 
1351
+ // Marca como validado pra open() pular o fetch redundante no click handler.
1352
+ // (Importante em sites com monitores SPA tipo New Relic que interceptam clicks.)
1353
+ this._availabilityCheckedFor = projectId + ':' + identifier
1354
+
856
1355
  const isMobile = window.matchMedia('(max-width: 767px)').matches
857
1356
  const config = this._normalizeButtonConfig(template, isMobile)
858
1357
  const renderer = this._BUTTON_RENDERERS[config.style] || this._BUTTON_RENDERERS['gregory-black']
@@ -862,6 +1361,12 @@ const mkfashion = {
862
1361
 
863
1362
  container.appendChild(element)
864
1363
  this._log('Botão criado', { target, projectId, identifier, style: config.style })
1364
+ try {
1365
+ if (!this._config) this._config = { projectId, identifier }
1366
+ this._emitToCollector('button_rendered', {
1367
+ projectId, identifier, style: config.style
1368
+ })
1369
+ } catch (_) {}
865
1370
  return { ok: true, reason: 'rendered', element }
866
1371
  },
867
1372
 
Binary file
package/test-e2e.html DELETED
@@ -1,175 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="pt-BR">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>mKFashion SDK — E2E Test</title>
6
- <script src="./src/mkfashion.js"></script>
7
- <style>
8
- body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; margin: 24px; max-width: 900px; }
9
- .test { margin-bottom: 16px; padding: 12px; background: #f5f5f5; border-radius: 6px; border-left: 4px solid #999; }
10
- .test.pass { border-color: #16a34a; background: #ecfdf5; }
11
- .test.fail { border-color: #dc2626; background: #fef2f2; }
12
- .test h3 { margin: 0 0 6px; font-size: 14px; }
13
- .test pre { margin: 0; font-size: 11px; white-space: pre-wrap; }
14
- .target { display: inline-block; margin: 0 8px; padding: 4px 8px; background: white; border: 1px dashed #ccc; }
15
- </style>
16
- </head>
17
- <body>
18
- <h2>mKFashion SDK — E2E Tests</h2>
19
-
20
- <div>
21
- <span>Container 1 (válido):</span><span class="target" id="target-valid"></span>
22
- <span>Container 2 (válido):</span><span class="target" id="target-2"></span>
23
- </div>
24
-
25
- <div id="output" style="margin-top: 24px;"></div>
26
-
27
- <script>
28
- const projectId = '69c56d5373ecdf64df48e330'
29
- const validSku = '000756397001'
30
- const invalidSku = '99999999999999'
31
- const output = document.getElementById('output')
32
-
33
- function log(name, passed, details) {
34
- const div = document.createElement('div')
35
- div.className = 'test ' + (passed ? 'pass' : 'fail')
36
- div.innerHTML = `<h3>${passed ? 'PASS' : 'FAIL'} — ${name}</h3><pre>${details}</pre>`
37
- output.appendChild(div)
38
- }
39
-
40
- function fmt(obj) {
41
- return JSON.stringify(obj, (k, v) => v instanceof HTMLElement ? `<${v.tagName.toLowerCase()}>` : v, 2)
42
- }
43
-
44
- async function run() {
45
- // ---- TEST 1: init com params completos e SKU válido ----
46
- const r1 = await mkfashion.init({
47
- projectId,
48
- identifier: validSku,
49
- target: '#target-valid'
50
- })
51
- log(
52
- 'init() com SKU válido → ok: true, reason: rendered',
53
- r1.ok === true && r1.reason === 'rendered' && r1.element instanceof HTMLElement,
54
- fmt(r1)
55
- )
56
-
57
- // ---- TEST 2: init sem target ----
58
- const r2 = await mkfashion.init({ projectId, identifier: validSku })
59
- log(
60
- 'init() sem target → ok: false, reason: missing_target',
61
- r2.ok === false && r2.reason === 'missing_target' && r2.element === null,
62
- fmt(r2)
63
- )
64
-
65
- // ---- TEST 3 (skipped): init sem projectId ----
66
- // Comportamento atual: _resolveProjectId silenciosamente faz fallback pro
67
- // projeto Gregory quando raw é falsy. Conhecido como débito técnico,
68
- // não testamos como "missing_projectId" aqui.
69
-
70
- // ---- TEST 4: init sem identifier ----
71
- const r4 = await mkfashion.init({ projectId, target: '#target-2' })
72
- log(
73
- 'init() sem identifier → ok: false, reason: missing_identifier',
74
- r4.ok === false && r4.reason === 'missing_identifier' && r4.element === null,
75
- fmt(r4)
76
- )
77
-
78
- // ---- TEST 5: init com target inexistente ----
79
- const r5 = await mkfashion.init({
80
- projectId,
81
- identifier: validSku,
82
- target: '#nao-existe'
83
- })
84
- log(
85
- 'init() com target inexistente → ok: false, reason: target_not_found',
86
- r5.ok === false && r5.reason === 'target_not_found' && r5.element === null,
87
- fmt(r5)
88
- )
89
-
90
- // ---- TEST 6: init com SKU indisponível ----
91
- const r6 = await mkfashion.init({
92
- projectId,
93
- identifier: invalidSku,
94
- target: '#target-2'
95
- })
96
- log(
97
- 'init() com SKU indisponível → ok: false, reason: unavailable',
98
- r6.ok === false && r6.reason === 'unavailable' && r6.element === null,
99
- fmt(r6)
100
- )
101
-
102
- // ---- TEST 7: addToCart callback é disparado via postMessage ----
103
- let cartPayload = null
104
- mkfashion.addToCart(payload => { cartPayload = payload })
105
-
106
- // Abre o modal pra acionar o setup do message listener
107
- await mkfashion.open({ projectId, identifier: validSku })
108
- // Fecha imediatamente (listener fica armado)
109
- mkfashion.close()
110
-
111
- // Simula iframe enviando add_to_cart
112
- const fakePayload = {
113
- size: 'M',
114
- product: {
115
- name: 'Afrik - Óculos de Sol',
116
- price: 299.90,
117
- variantSku: 'SKU-VAR-M',
118
- produtoVarianteId: '12345',
119
- productUrl: 'https://example.com/produto/afrik',
120
- tryonImageUrl: 'https://cdn.example.com/tryon/abc.jpg'
121
- }
122
- }
123
- window.postMessage({
124
- source: 'mkfashion-app',
125
- action: 'add_to_cart',
126
- data: fakePayload
127
- }, '*')
128
-
129
- // Aguarda o handler async processar
130
- await new Promise(r => setTimeout(r, 100))
131
-
132
- log(
133
- 'addToCart callback dispara com payload simplificado (não-Gregory)',
134
- cartPayload !== null
135
- && cartPayload.mainIdentifier === validSku
136
- && cartPayload.selectedIdentifier === 'SKU-VAR-M'
137
- && cartPayload.name === 'Afrik - Óculos de Sol'
138
- && cartPayload.price === 299.90
139
- && cartPayload.selectedSize === 'M'
140
- && cartPayload.productUrl === 'https://example.com/produto/afrik',
141
- fmt(cartPayload)
142
- )
143
-
144
- // ---- TEST 8: onInteraction callback ----
145
- let interactionData = null
146
- mkfashion.onInteraction(data => { interactionData = data })
147
-
148
- window.postMessage({
149
- source: 'mkfashion-app',
150
- action: 'interaction',
151
- data: { category: 'try_on', action: 'photo_uploaded' }
152
- }, '*')
153
-
154
- await new Promise(r => setTimeout(r, 100))
155
-
156
- log(
157
- 'onInteraction callback dispara com category + action',
158
- interactionData !== null
159
- && interactionData.category === 'try_on'
160
- && interactionData.action === 'photo_uploaded',
161
- fmt(interactionData)
162
- )
163
-
164
- // ---- Marker pro screenshot saber que terminou ----
165
- document.title = 'DONE — ' + output.querySelectorAll('.test').length + ' tests'
166
- }
167
-
168
- if (typeof mkfashion !== 'undefined') {
169
- run().catch(e => log('Exception não tratada', false, e.message + '\n' + e.stack))
170
- } else {
171
- document.addEventListener('DOMContentLoaded', run)
172
- }
173
- </script>
174
- </body>
175
- </html>
@@ -1,45 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="pt-BR">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>Test — Responsividade do botão</title>
6
- <script src="./src/mkfashion.js"></script>
7
- <style>
8
- body { font-family: system-ui, sans-serif; margin: 24px; }
9
- h3 { margin: 20px 0 8px; font-size: 13px; color: #444; }
10
- .box { background: #fef3c7; padding: 8px; margin-bottom: 8px; }
11
- </style>
12
- </head>
13
- <body>
14
-
15
- <h3>Container 800px (mais largo que o max-width)</h3>
16
- <div class="box" style="width: 800px;">
17
- <div id="c1"></div>
18
- </div>
19
-
20
- <h3>Container 300px (menor que 402px do card)</h3>
21
- <div class="box" style="width: 300px;">
22
- <div id="c2"></div>
23
- </div>
24
-
25
- <h3>Container 180px (menor que 238px do simples)</h3>
26
- <div class="box" style="width: 180px;">
27
- <div id="c3"></div>
28
- </div>
29
-
30
- <h3>Container sem width (herda do pai)</h3>
31
- <div class="box">
32
- <div id="c4"></div>
33
- </div>
34
-
35
- <script>
36
- var pid = '69c56d5373ecdf64df48e330'
37
- var sku = '000756397001'
38
- // Configura os 4 inits — todos pra mesma config (gregory-card no momento)
39
- mkfashion.init({ projectId: pid, identifier: sku, target: '#c1' })
40
- mkfashion.init({ projectId: pid, identifier: sku, target: '#c2' })
41
- mkfashion.init({ projectId: pid, identifier: sku, target: '#c3' })
42
- mkfashion.init({ projectId: pid, identifier: sku, target: '#c4' })
43
- </script>
44
- </body>
45
- </html>