mkfashion-sdk 2.4.8 → 2.5.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
@@ -15,35 +15,134 @@
15
15
  <script>
16
16
  (function () {
17
17
  var projectid = '69f9e5b0f4eb0c4403319830';
18
- var identifier = '1881';
19
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
+ // ============================================================
20
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).
21
112
  mkfashion.addToCart(function (payload) {
22
- console.log('Adicionando ao carrinho:', payload);
113
+ console.log('[mKFashion] addToCart payload:', payload);
23
114
 
24
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;
25
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
26
134
  fetch('/cart/add.js', {
27
135
  method: 'POST',
28
- headers: {
29
- 'Content-Type': 'application/json',
30
- },
31
- body: JSON.stringify({
32
- items: [{
33
- quantity: 1,
34
- id: variantSku
35
- }]
36
- })
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify({ items: [item] })
37
138
  })
38
- .then(function (response) {
39
- return response.json();
40
- })
139
+ .then(function (response) { return response.json(); })
41
140
  .then(function (data) {
42
- console.log('Produto adicionado:', data);
141
+ console.log('[mKFashion] produto adicionado ao carrinho:', data);
43
142
  // document.dispatchEvent(new CustomEvent('cart:updated'));
44
143
  })
45
144
  .catch(function (error) {
46
- console.error('Erro carrinho:', error);
145
+ console.error('[mKFashion] erro ao adicionar ao carrinho:', error);
47
146
  });
48
147
  });
49
148
 
@@ -53,7 +152,7 @@
53
152
 
54
153
  mkfashion.init({
55
154
  projectId: projectid,
56
- identifier: identifier,
155
+ identifier: detected.value,
57
156
  target: '#mkfashion-container'
58
157
  });
59
158
  }
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mkfashion-sdk",
3
- "version": "2.4.8",
4
- "description": "SDK para integrar o provador virtual mKFashion com suporte a Wake Commerce",
3
+ "version": "2.5.0",
4
+ "description": "SDK para integrar o provador virtual mKFashion com suporte a Wake Commerce e tracking analytics",
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,10 +303,32 @@ const mkfashion = {
279
303
 
280
304
  const data = await response.json()
281
305
  this._log('Disponibilidade recebida', data)
306
+
307
+ // Tracking: cada checagem de availability é um sinal de "impressão"
308
+ // do produto na vitrine. Funciona mesmo se o usuário não clicar.
309
+ try {
310
+ if (!this._config) this._config = { projectId: resolved, identifier }
311
+ this._emitToCollector('availability_checked', {
312
+ projectId: resolved,
313
+ identifier,
314
+ available: data.available === true,
315
+ reason: data.reason || data.message || null
316
+ })
317
+ } catch (_) {}
318
+
282
319
  return data
283
320
 
284
321
  } catch (error) {
285
322
  console.error('[mKFashion] Erro ao verificar disponibilidade:', error)
323
+ // Tracking: também emite erros de availability — útil pra detectar 404/500 em prod.
324
+ try {
325
+ if (!this._config) this._config = { projectId: resolved, identifier }
326
+ this._emitToCollector('availability_error', {
327
+ projectId: resolved,
328
+ identifier,
329
+ error: String(error.message || error).slice(0, 200)
330
+ })
331
+ } catch (_) {}
286
332
  throw error
287
333
  }
288
334
  },
@@ -420,6 +466,9 @@ const mkfashion = {
420
466
  },
421
467
 
422
468
  _triggerCallback(name, data = null) {
469
+ // Emite pro collector ANTES do callback do cliente (não bloqueia, fire-and-forget).
470
+ try { this._emitToCollector(name, data) } catch (_) {}
471
+
423
472
  if (this._callbacks && typeof this._callbacks[name] === 'function') {
424
473
  try {
425
474
  this._callbacks[name](data)
@@ -430,6 +479,184 @@ const mkfashion = {
430
479
  }
431
480
  },
432
481
 
482
+ // ============ TRACKING (mk-collector-api) ============
483
+
484
+ _visitorId: null,
485
+ _sessionId: null,
486
+ _siteContext: null,
487
+
488
+ _getVisitorId() {
489
+ if (this._visitorId) return this._visitorId
490
+ try {
491
+ let v = localStorage.getItem('_mk_sdk_vid')
492
+ if (!v) {
493
+ v = 'v_' + (typeof crypto !== 'undefined' && crypto.randomUUID
494
+ ? crypto.randomUUID()
495
+ : Math.random().toString(36).slice(2) + Date.now().toString(36))
496
+ localStorage.setItem('_mk_sdk_vid', v)
497
+ }
498
+ this._visitorId = v
499
+ return v
500
+ } catch (_) {
501
+ this._visitorId = 'v_' + Math.random().toString(36).slice(2)
502
+ return this._visitorId
503
+ }
504
+ },
505
+
506
+ _getSessionId() {
507
+ if (this._sessionId) return this._sessionId
508
+ try {
509
+ let s = sessionStorage.getItem('_mk_sdk_sid')
510
+ if (!s) {
511
+ s = 's_' + (typeof crypto !== 'undefined' && crypto.randomUUID
512
+ ? crypto.randomUUID()
513
+ : Math.random().toString(36).slice(2) + Date.now().toString(36))
514
+ sessionStorage.setItem('_mk_sdk_sid', s)
515
+ }
516
+ this._sessionId = s
517
+ return s
518
+ } catch (_) {
519
+ this._sessionId = 's_' + Math.random().toString(36).slice(2)
520
+ return this._sessionId
521
+ }
522
+ },
523
+
524
+ _parseUtm() {
525
+ try {
526
+ const p = new URLSearchParams(location.search)
527
+ const out = {}
528
+ const keys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']
529
+ for (const k of keys) {
530
+ const v = p.get(k)
531
+ if (v) out[k.replace('utm_', '')] = v
532
+ }
533
+ return Object.keys(out).length > 0 ? out : null
534
+ } catch (_) { return null }
535
+ },
536
+
537
+ _detectPlatform() {
538
+ try {
539
+ if (window.Shopify || window.ShopifyAnalytics) return 'shopify'
540
+ if (window.vtex || window.vtxctx) return 'vtex'
541
+ if (window.wakeCommerce || window.__WAKE__) return 'wake'
542
+ if (window.Konfidency || window.konfidency) return 'konfidency'
543
+ if (window.wc_add_to_cart_params || window.woocommerce_params) return 'woocommerce'
544
+ if (window.dataLayer && Array.isArray(window.dataLayer) && window.dataLayer[0]) {
545
+ const p = window.dataLayer[0].platform
546
+ if (p) return String(p).toLowerCase()
547
+ }
548
+ } catch (_) {}
549
+ return null
550
+ },
551
+
552
+ _detectCurrentProduct() {
553
+ try {
554
+ // 1. dataLayer (GA4/UA padrão)
555
+ if (window.dataLayer && Array.isArray(window.dataLayer)) {
556
+ for (let i = window.dataLayer.length - 1; i >= 0; i--) {
557
+ const item = window.dataLayer[i]
558
+ if (!item) continue
559
+ if (item.product) {
560
+ return {
561
+ sku: item.product.sku || item.product.id || null,
562
+ name: item.product.name || null,
563
+ price: item.product.price || null
564
+ }
565
+ }
566
+ if (item.ecommerce && item.ecommerce.items && item.ecommerce.items[0]) {
567
+ const p = item.ecommerce.items[0]
568
+ return { sku: p.item_id || p.id, name: p.item_name || p.name, price: p.price }
569
+ }
570
+ }
571
+ }
572
+ // 2. JSON-LD schema.org/Product
573
+ const lds = document.querySelectorAll('script[type="application/ld+json"]')
574
+ for (const ld of lds) {
575
+ try {
576
+ const j = JSON.parse(ld.textContent || '{}')
577
+ const arr = Array.isArray(j) ? j : [j]
578
+ for (const item of arr) {
579
+ if (item && (item['@type'] === 'Product' || (Array.isArray(item['@type']) && item['@type'].includes('Product')))) {
580
+ return {
581
+ sku: item.sku || item.mpn || item.gtin || null,
582
+ name: item.name || null,
583
+ price: (item.offers && (item.offers.price || (item.offers[0] && item.offers[0].price))) || null
584
+ }
585
+ }
586
+ }
587
+ } catch (_) {}
588
+ }
589
+ // 3. Open Graph (og:type=product)
590
+ const og = (n) => {
591
+ const m = document.querySelector('meta[property="og:' + n + '"]')
592
+ return m && m.content || null
593
+ }
594
+ if (og('type') === 'product') {
595
+ return { sku: null, name: og('title'), price: og('price:amount') }
596
+ }
597
+ } catch (_) {}
598
+ return null
599
+ },
600
+
601
+ _detectSiteContext() {
602
+ if (this._siteContext) return this._siteContext
603
+ try {
604
+ this._siteContext = {
605
+ url: location.href,
606
+ pathname: location.pathname,
607
+ referrer: document.referrer || null,
608
+ title: document.title || null,
609
+ utm: this._parseUtm(),
610
+ userAgent: navigator.userAgent,
611
+ language: navigator.language,
612
+ viewport: window.innerWidth + 'x' + window.innerHeight,
613
+ devicePixelRatio: window.devicePixelRatio || 1,
614
+ platform: this._detectPlatform(),
615
+ detectedProduct: this._detectCurrentProduct()
616
+ }
617
+ } catch (_) {
618
+ this._siteContext = {}
619
+ }
620
+ return this._siteContext
621
+ },
622
+
623
+ /**
624
+ * Envia evento pro mk-collector-api. Fire-and-forget — nunca atrapalha UX.
625
+ * Identificação: X-MK-Project-Id (o próprio projectId do mkfashion).
626
+ */
627
+ _emitToCollector(eventName, data) {
628
+ try {
629
+ if (!this._config || !this._config.projectId) return
630
+ if (!this.collectorUrl) return
631
+
632
+ const params = { identifier: this._config.identifier || null }
633
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
634
+ Object.assign(params, data)
635
+ } else if (data !== null && data !== undefined) {
636
+ params.value = data
637
+ }
638
+
639
+ const payload = JSON.stringify({
640
+ visitorId: this._getVisitorId(),
641
+ sessionId: this._getSessionId(),
642
+ events: [{ name: 'sdk_' + eventName, ts: Date.now(), params: params }],
643
+ site: this._detectSiteContext()
644
+ })
645
+
646
+ fetch(this.collectorUrl + '/v1/track/sdk', {
647
+ method: 'POST',
648
+ keepalive: true,
649
+ headers: {
650
+ 'Content-Type': 'application/json',
651
+ 'X-MK-Project-Id': this._config.projectId
652
+ },
653
+ body: payload
654
+ }).catch(() => {})
655
+ } catch (e) {
656
+ if (this.debug) console.warn('[mKFashion tracking]', e.message)
657
+ }
658
+ },
659
+
433
660
  _buildUrl() {
434
661
  const encoded = encodeURIComponent(this._config.identifier).replace(/\./g, '%2E')
435
662
  const url = `${this.appUrl}/${this._config.projectId}/${encoded}`
@@ -729,6 +956,7 @@ const mkfashion = {
729
956
  price: product.price || null,
730
957
  sizePrice: product.sizePrice || null,
731
958
  selectedSize: data?.size || null,
959
+ selectedColor: data?.color || product.color || null,
732
960
  produtoVarianteId: product.produtoVarianteId || null,
733
961
  productUrl: product.productUrl || null,
734
962
  tryonImageUrl: product.tryonImageUrl || null
@@ -850,9 +1078,17 @@ const mkfashion = {
850
1078
 
851
1079
  if (!available) {
852
1080
  this._log('Produto indisponível, botão não criado', { projectId, identifier })
1081
+ try {
1082
+ if (!this._config) this._config = { projectId, identifier }
1083
+ this._emitToCollector('button_unavailable', { projectId, identifier })
1084
+ } catch (_) {}
853
1085
  return fail('unavailable')
854
1086
  }
855
1087
 
1088
+ // Marca como validado pra open() pular o fetch redundante no click handler.
1089
+ // (Importante em sites com monitores SPA tipo New Relic que interceptam clicks.)
1090
+ this._availabilityCheckedFor = projectId + ':' + identifier
1091
+
856
1092
  const isMobile = window.matchMedia('(max-width: 767px)').matches
857
1093
  const config = this._normalizeButtonConfig(template, isMobile)
858
1094
  const renderer = this._BUTTON_RENDERERS[config.style] || this._BUTTON_RENDERERS['gregory-black']
@@ -862,6 +1098,12 @@ const mkfashion = {
862
1098
 
863
1099
  container.appendChild(element)
864
1100
  this._log('Botão criado', { target, projectId, identifier, style: config.style })
1101
+ try {
1102
+ if (!this._config) this._config = { projectId, identifier }
1103
+ this._emitToCollector('button_rendered', {
1104
+ projectId, identifier, style: config.style
1105
+ })
1106
+ } catch (_) {}
865
1107
  return { ok: true, reason: 'rendered', element }
866
1108
  },
867
1109
 
Binary file
Binary file
Binary file
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>