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 +116 -0
- package/index.html +178 -79
- package/mkfashion-sdk-2.5.0.tgz +0 -0
- package/mkfashion-sdk-2.7.0.tgz +0 -0
- package/package.json +2 -2
- package/src/mkfashion.js +516 -11
- package/mkfashion-sdk-2.4.4.tgz +0 -0
- package/test-e2e.html +0 -175
- package/test-responsive.html +0 -45
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
"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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
package/mkfashion-sdk-2.4.4.tgz
DELETED
|
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>
|
package/test-responsive.html
DELETED
|
@@ -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>
|