mk-sdk-git 0.1.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.
@@ -0,0 +1,73 @@
1
+ // sanitize.js — remove PII e campos pesados antes de enviar pro collector.
2
+
3
+ // Campos que NUNCA devem sair da página (LGPD). Match case-insensitive parcial.
4
+ const PII_PATTERNS = [
5
+ /email/i, /e-?mail/i, /cpf/i, /cnpj/i, /phone/i, /telefone/i, /celular/i,
6
+ /address/i, /endereco/i, /cep/i, /zip/i, /card/i, /cartao/i, /cvv/i,
7
+ /password/i, /senha/i, /token/i, /\bnome\b/i, /fullname/i, /firstname/i,
8
+ /lastname/i, /sobrenome/i, /birth/i, /nascimento/i, /rg\b/i
9
+ ];
10
+
11
+ // Campos de imagem/binário pesados (não têm valor analítico).
12
+ const HEAVY_KEYS = { imageUrl: 1, image: 1, thumbnail: 1, photo: 1, imageBase64: 1, src: 1 };
13
+ const MAX_STRING_LEN = 2000;
14
+
15
+ function isPiiKey(key) {
16
+ for (let i = 0; i < PII_PATTERNS.length; i++) {
17
+ if (PII_PATTERNS[i].test(key)) return true;
18
+ }
19
+ return false;
20
+ }
21
+
22
+ /**
23
+ * Sanitiza um objeto de params:
24
+ * - Remove chaves PII completamente
25
+ * - Troca campos de imagem por <key>Present: boolean
26
+ * - Trunca strings > 2KB
27
+ * - Percorre 1 nível de profundidade (objetos aninhados rasos)
28
+ */
29
+ export function sanitize(data) {
30
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
31
+ if (typeof data === 'string' && data.length > MAX_STRING_LEN) {
32
+ return data.slice(0, 200) + '...[truncated ' + data.length + 'B]';
33
+ }
34
+ return data;
35
+ }
36
+ const out = {};
37
+ for (const k in data) {
38
+ if (!Object.prototype.hasOwnProperty.call(data, k)) continue;
39
+ if (isPiiKey(k)) continue; // descarta PII
40
+ const v = data[k];
41
+ if (HEAVY_KEYS[k]) {
42
+ out[k + 'Present'] = !!v;
43
+ continue;
44
+ }
45
+ if (typeof v === 'string' && v.length > MAX_STRING_LEN) {
46
+ out[k] = v.slice(0, 200) + '...[truncated ' + v.length + 'B]';
47
+ continue;
48
+ }
49
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
50
+ // 1 nível de recursão pra objetos aninhados (ex: product, ecommerce)
51
+ out[k] = sanitizeShallow(v);
52
+ continue;
53
+ }
54
+ out[k] = v;
55
+ }
56
+ return out;
57
+ }
58
+
59
+ function sanitizeShallow(obj) {
60
+ const out = {};
61
+ for (const k in obj) {
62
+ if (!Object.prototype.hasOwnProperty.call(obj, k)) continue;
63
+ if (isPiiKey(k)) continue;
64
+ const v = obj[k];
65
+ if (HEAVY_KEYS[k]) { out[k + 'Present'] = !!v; continue; }
66
+ if (typeof v === 'string' && v.length > MAX_STRING_LEN) {
67
+ out[k] = v.slice(0, 200) + '...[truncated]';
68
+ continue;
69
+ }
70
+ out[k] = v; // não recursa mais fundo (evita payload gigante)
71
+ }
72
+ return out;
73
+ }
@@ -0,0 +1,124 @@
1
+ // transport.js — fila de eventos com batch, flush por timer/tamanho, sendBeacon.
2
+
3
+ import { getConfig, log } from './config.js';
4
+ import { getVisitorId, getSessionId } from './identity.js';
5
+ import { sanitize } from './sanitize.js';
6
+
7
+ let queue = [];
8
+ let flushTimer = null;
9
+ let siteContext = null;
10
+
11
+ /**
12
+ * Define o contexto do site (uma vez por pageview). Injetado em todo batch.
13
+ */
14
+ export function setSiteContext(ctx) {
15
+ siteContext = ctx;
16
+ }
17
+
18
+ /**
19
+ * Enfileira um evento. name é o nome cru (vira "page_<name>").
20
+ * Aplica sanitize automaticamente.
21
+ */
22
+ export function track(name, params, tsOverride) {
23
+ const cfg = getConfig();
24
+ if (!cfg.projectId) {
25
+ // Sem projectId não dá pra enviar — enfileira em pré-buffer pequeno.
26
+ (track._pre = track._pre || []).push({ name, params, ts: tsOverride || Date.now() });
27
+ if (track._pre.length > 50) track._pre.shift();
28
+ return;
29
+ }
30
+
31
+ // Drena pré-buffer se houver
32
+ if (track._pre && track._pre.length) {
33
+ const pending = track._pre;
34
+ track._pre = [];
35
+ for (const p of pending) _enqueue(p.name, p.params, p.ts);
36
+ }
37
+
38
+ _enqueue(name, params, tsOverride);
39
+ }
40
+
41
+ function _enqueue(name, params, tsOverride) {
42
+ const cfg = getConfig();
43
+ const cleanParams = sanitize(params || {});
44
+ const finalName = 'page_' + normalizeName(name);
45
+ queue.push({
46
+ name: finalName,
47
+ ts: tsOverride || Date.now(),
48
+ params: cleanParams
49
+ });
50
+ log('queued', name, cleanParams);
51
+
52
+ // Canal de observação (extensão de QA). Inócuo em produção (observe=false).
53
+ if (cfg.observe) {
54
+ try {
55
+ window.postMessage({
56
+ __mkpixel: true,
57
+ event: { name: finalName, params: cleanParams },
58
+ site: siteContext ? { pageType: siteContext.pageType, platform: siteContext.platform, url: siteContext.url } : null
59
+ }, '*');
60
+ } catch (_) {}
61
+ }
62
+
63
+ if (queue.length >= cfg.batchMaxSize) {
64
+ flush();
65
+ return;
66
+ }
67
+ if (!flushTimer) {
68
+ flushTimer = setTimeout(() => { flushTimer = null; flush(); }, cfg.batchIntervalMs);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Normaliza nome: tira 'on' prefix, camelCase -> snake_case, evita prefixo page_ duplo.
74
+ */
75
+ function normalizeName(name) {
76
+ if (typeof name !== 'string' || !name) return 'unknown';
77
+ let n = name.replace(/^on/, '');
78
+ n = n.replace(/[A-Z]/g, (m, i) => (i > 0 ? '_' : '') + m.toLowerCase());
79
+ if (n.indexOf('page_') === 0) n = n.slice(5); // evita page_page_view
80
+ return n;
81
+ }
82
+
83
+ export function flush(useBeacon) {
84
+ if (queue.length === 0) return;
85
+ const cfg = getConfig();
86
+ if (!cfg.projectId || !cfg.collectorUrl) return;
87
+
88
+ const events = queue.splice(0, 50);
89
+ const payload = JSON.stringify({
90
+ product: cfg.product || undefined,
91
+ visitorId: getVisitorId(),
92
+ sessionId: getSessionId(),
93
+ events,
94
+ site: siteContext || undefined
95
+ });
96
+
97
+ const url = cfg.collectorUrl + '/v1/track';
98
+
99
+ if (useBeacon && navigator.sendBeacon) {
100
+ try {
101
+ const blob = new Blob([payload], { type: 'application/json' });
102
+ navigator.sendBeacon(url + '?projectId=' + encodeURIComponent(cfg.projectId), blob);
103
+ return;
104
+ } catch (_) {}
105
+ }
106
+
107
+ fetch(url, {
108
+ method: 'POST',
109
+ keepalive: true,
110
+ headers: { 'Content-Type': 'application/json', 'X-MK-Project-Id': cfg.projectId },
111
+ body: payload
112
+ }).catch(() => {});
113
+ }
114
+
115
+ // Flush final no unload pra não perder eventos pendentes.
116
+ export function initUnloadFlush() {
117
+ try {
118
+ window.addEventListener('pagehide', () => flush(true));
119
+ window.addEventListener('beforeunload', () => flush(true));
120
+ document.addEventListener('visibilitychange', () => {
121
+ if (document.hidden) flush(true);
122
+ });
123
+ } catch (_) {}
124
+ }
@@ -0,0 +1,144 @@
1
+ // extractors.js — extração de produto/pedido/lista por sinais nativos genéricos.
2
+ // O SKU por plataforma vive em ../platforms/ (resolveSku é re-exportado daqui só
3
+ // por compatibilidade com quem já importava deste módulo). JSON-LD em ./jsonld.js.
4
+
5
+ import { readJsonLd } from './jsonld.js';
6
+ import { resolveSku } from '../platforms/index.js';
7
+
8
+ export { readJsonLd }; // re-export (compat)
9
+ export { resolveSku }; // re-export (compat) — a lógica por plataforma está em ../platforms/
10
+
11
+ /**
12
+ * Extrai produto da PDP. Combina o resolveSku (por plataforma) com nome/preço/marca
13
+ * de JSON-LD, microdata ou Open Graph.
14
+ */
15
+ export function extractProduct() {
16
+ const skuRes = resolveSku();
17
+ const products = readJsonLd(['Product']);
18
+
19
+ if (products.length > 0) {
20
+ const p = products[0];
21
+ const offer = Array.isArray(p.offers) ? p.offers[0] : p.offers;
22
+ return {
23
+ sku: skuRes ? skuRes.value : (p.sku || p.mpn || p.gtin13 || p.productID || null),
24
+ skuSource: skuRes ? skuRes.source : null,
25
+ name: typeof p.name === 'string' ? p.name : null,
26
+ price: offer ? parseFloat(offer.price || offer.lowPrice) || null : null,
27
+ currency: offer ? offer.priceCurrency : null,
28
+ brand: p.brand ? (typeof p.brand === 'string' ? p.brand : p.brand.name) : null,
29
+ category: p.category || null
30
+ };
31
+ }
32
+
33
+ const og = (prop) => {
34
+ const m = document.querySelector('meta[property="og:' + prop + '"], meta[property="product:' + prop + '"]');
35
+ return m ? m.getAttribute('content') : null;
36
+ };
37
+ const micro = (prop) => {
38
+ const e = document.querySelector('[itemprop="' + prop + '"]');
39
+ return e ? (e.getAttribute('content') || (e.textContent || '').trim()) : null;
40
+ };
41
+
42
+ if (skuRes || og('type') === 'product' || og('price:amount') || micro('name')) {
43
+ const priceRaw = micro('price') || og('price:amount');
44
+ return {
45
+ sku: skuRes ? skuRes.value : null,
46
+ skuSource: skuRes ? skuRes.source : null,
47
+ name: micro('name') || og('title') || (document.title || null),
48
+ price: priceRaw ? parseFloat(String(priceRaw).replace(',', '.')) || null : null,
49
+ currency: micro('priceCurrency') || og('price:currency') || null,
50
+ brand: micro('brand') || null,
51
+ category: null
52
+ };
53
+ }
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ * Extrai dados de pedido/compra (página de confirmação).
59
+ * Cascata: JSON-LD Order > dataLayer (bônus) > meta > DOM scraping.
60
+ */
61
+ export function extractOrder() {
62
+ const orders = readJsonLd(['Order', 'Invoice']);
63
+ if (orders.length > 0) {
64
+ const o = orders[0];
65
+ const total = o.totalPaymentDue ? parseFloat(o.totalPaymentDue.price) : parseFloat(o.price || o.total);
66
+ return {
67
+ orderId: o.orderNumber || o.confirmationNumber || o.identifier || null,
68
+ revenue: Number.isFinite(total) ? total : null,
69
+ currency: (o.totalPaymentDue && o.totalPaymentDue.priceCurrency) || o.priceCurrency || null,
70
+ source: 'jsonld'
71
+ };
72
+ }
73
+ try {
74
+ if (Array.isArray(window.dataLayer)) {
75
+ for (let i = window.dataLayer.length - 1; i >= 0; i--) {
76
+ const item = window.dataLayer[i];
77
+ if (item && item.event === 'purchase' && item.ecommerce) {
78
+ const ec = item.ecommerce;
79
+ return {
80
+ orderId: ec.transaction_id || ec.transactionId || null,
81
+ revenue: parseFloat(ec.value || ec.revenue) || null,
82
+ currency: ec.currency || ec.currencyCode || null,
83
+ itemCount: (ec.items || ec.products || []).length || null,
84
+ source: 'datalayer'
85
+ };
86
+ }
87
+ }
88
+ }
89
+ } catch (_) {}
90
+ const metaTotal = document.querySelector('meta[property="order:total"], meta[name="order-total"]');
91
+ if (metaTotal && metaTotal.content) {
92
+ return { orderId: null, revenue: parseFloat(metaTotal.content) || null, source: 'meta' };
93
+ }
94
+ const dom = scrapeOrderFromDom();
95
+ if (dom) return dom;
96
+ return null;
97
+ }
98
+
99
+ function scrapeOrderFromDom() {
100
+ try {
101
+ const totalSelectors = ['.order-total', '#order-total', '.total-price', '.order__total', '[data-order-total]', '.summary-total', '.grand-total'];
102
+ for (const sel of totalSelectors) {
103
+ const el = document.querySelector(sel);
104
+ if (el) {
105
+ const val = parseMoney(el.textContent);
106
+ if (val) return { orderId: scrapeOrderId(), revenue: val, source: 'dom' };
107
+ }
108
+ }
109
+ return null;
110
+ } catch (_) { return null; }
111
+ }
112
+
113
+ function scrapeOrderId() {
114
+ try {
115
+ const text = document.body.innerText || '';
116
+ const m = text.match(/(?:pedido|order|n[º°o]\.?)\s*#?\s*([A-Z0-9-]{4,})/i);
117
+ return m ? m[1] : null;
118
+ } catch (_) { return null; }
119
+ }
120
+
121
+ function parseMoney(str) {
122
+ if (!str) return null;
123
+ const cleaned = String(str).replace(/[^\d.,]/g, '');
124
+ if (!cleaned) return null;
125
+ let normalized;
126
+ if (cleaned.indexOf(',') > cleaned.indexOf('.')) normalized = cleaned.replace(/\./g, '').replace(',', '.');
127
+ else normalized = cleaned.replace(/,/g, '');
128
+ const n = parseFloat(normalized);
129
+ return Number.isFinite(n) && n > 0 ? n : null;
130
+ }
131
+
132
+ /** Lê item list de categoria/busca (JSON-LD ItemList). */
133
+ export function extractItemList() {
134
+ const lists = readJsonLd(['ItemList', 'CollectionPage']);
135
+ if (lists.length === 0) return null;
136
+ const list = lists[0];
137
+ const elements = list.itemListElement || (list.mainEntity && list.mainEntity.itemListElement) || [];
138
+ const skus = [];
139
+ for (const el of elements.slice(0, 50)) {
140
+ const item = el.item || el;
141
+ if (item && (item.sku || item.productID || item.name)) skus.push(item.sku || item.productID || item.name);
142
+ }
143
+ return skus.length > 0 ? { count: skus.length, skus: skus.slice(0, 20) } : null;
144
+ }
@@ -0,0 +1,131 @@
1
+ // funnel.js — emite eventos do funil de e-commerce reagindo ao pageType
2
+ // detectado, + heurística de add_to_cart por clique. Sinais nativos, sem GA.
3
+
4
+ import { track } from '../core/transport.js';
5
+ import { log } from '../core/config.js';
6
+ import { extractProduct, extractOrder, extractItemList } from './extractors.js';
7
+
8
+ let purchaseEmittedThisLoad = false;
9
+
10
+ // ─── Heurística de add_to_cart por texto de botão (mesma do collector) ───
11
+ const ADD_TO_CART_PATTERNS = [
12
+ /\b(adicionar|adic\.?|colocar|incluir|jogar)\b.{0,30}\b(carrinho|sacola|sacolinha|bag)\b/i,
13
+ /\bcomprar\b(?!\s+(em|por|sem))/i, // só BUTTON (checado no listener)
14
+ /\bcomprar\s*$/i,
15
+ /^add\s+to\s+(cart|bag)\b/i,
16
+ /^buy\s+(now|it|this)\b/i,
17
+ /^(sacolinha|carrinho|sacola)$/i
18
+ ];
19
+ const PURCHASE_BTN_PATTERNS = [
20
+ /^finalizar(\s+(compra|pedido|pagamento))?\b/i,
21
+ /^(fechar|confirmar)\s+(compra|pedido|pagamento)\b/i,
22
+ /^pagar(\s+(agora|pedido))?\b/i,
23
+ /^(complete|place)\s+(purchase|order)\b/i,
24
+ /^checkout$/i
25
+ ];
26
+
27
+ function matchAny(patterns, text) {
28
+ for (let i = 0; i < patterns.length; i++) if (patterns[i].test(text)) return true;
29
+ return false;
30
+ }
31
+
32
+ /**
33
+ * Chamado pelo autoTrack a cada pageview (incl. SPA). Emite o evento de funil
34
+ * correspondente ao tipo de página.
35
+ */
36
+ export function onPageView(pageType) {
37
+ switch (pageType) {
38
+ case 'pdp': {
39
+ const product = extractProduct();
40
+ track('view_item', product || { sku: null });
41
+ break;
42
+ }
43
+ case 'category':
44
+ case 'search': {
45
+ const list = extractItemList();
46
+ track('view_item_list', list || { count: 0 });
47
+ break;
48
+ }
49
+ case 'cart':
50
+ track('view_cart', {});
51
+ break;
52
+ case 'checkout':
53
+ track('begin_checkout', {});
54
+ break;
55
+ case 'purchase':
56
+ emitPurchase();
57
+ break;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Emite purchase com dedup. Conversão é contada mesmo sem revenue.
63
+ */
64
+ function emitPurchase() {
65
+ const order = extractOrder();
66
+
67
+ // Dedup: por orderId (cross-reload) ou por load (sem orderId)
68
+ if (order && order.orderId) {
69
+ const key = '_mk_purch_' + order.orderId;
70
+ try {
71
+ if (sessionStorage.getItem(key)) { log('purchase já contado', order.orderId); return; }
72
+ sessionStorage.setItem(key, '1');
73
+ } catch (_) {}
74
+ } else {
75
+ if (purchaseEmittedThisLoad) return;
76
+ }
77
+ purchaseEmittedThisLoad = true;
78
+
79
+ track('purchase', order || { revenue: null, _note: 'no_order_data_extracted' });
80
+ log('purchase emitido', order);
81
+ }
82
+
83
+ /**
84
+ * Listener dedicado pra detectar add_to_cart / begin_checkout por clique.
85
+ * Complementa os eventos de pageType (que não pegam o momento do clique).
86
+ */
87
+ export function initFunnelClickTracking() {
88
+ if (!document.body) return;
89
+ let lastFire = 0;
90
+ document.body.addEventListener('click', (e) => {
91
+ try {
92
+ const now = Date.now();
93
+ if (now - lastFire < 150) return;
94
+ let el = e.target;
95
+ for (let i = 0; i < 5 && el && el !== document.body; i++) {
96
+ if (el.tagName === 'BUTTON' || el.tagName === 'A' ||
97
+ (el.getAttribute && el.getAttribute('role') === 'button')) break;
98
+ el = el.parentElement;
99
+ }
100
+ if (!el) return;
101
+ const isButton = el.tagName !== 'A';
102
+ const text = (el.textContent || '').replace(/\s+/g, ' ').trim();
103
+ if (!text || text.length > 80) return;
104
+
105
+ // add_to_cart
106
+ let isAdd = false;
107
+ if (matchAny([ADD_TO_CART_PATTERNS[0]], text)) isAdd = true; // adicionar+carrinho (qualquer tag)
108
+ else if (isButton && matchAny([ADD_TO_CART_PATTERNS[1]], text)) isAdd = true; // comprar (só button)
109
+ else if (isButton && matchAny([ADD_TO_CART_PATTERNS[2]], text)) isAdd = true; // termina em comprar
110
+ else if (matchAny(ADD_TO_CART_PATTERNS.slice(3), text)) isAdd = true; // en + palavra única
111
+
112
+ if (isAdd) {
113
+ lastFire = now;
114
+ const product = extractProduct();
115
+ track('add_to_cart', {
116
+ via: 'click',
117
+ buttonText: text,
118
+ sku: product ? product.sku : null,
119
+ price: product ? product.price : null
120
+ });
121
+ return;
122
+ }
123
+
124
+ // begin_checkout (intenção)
125
+ if (isButton && matchAny(PURCHASE_BTN_PATTERNS, text)) {
126
+ lastFire = now;
127
+ track('begin_checkout', { via: 'click', buttonText: text });
128
+ }
129
+ } catch (_) {}
130
+ }, { capture: true, passive: true });
131
+ }
@@ -0,0 +1,23 @@
1
+ // jsonld.js — leitura de dados estruturados JSON-LD. Helper genérico, sem deps
2
+ // (usado por extractors, pageContext e platforms/generic — evita dependência circular).
3
+
4
+ export function readJsonLd(types) {
5
+ const results = [];
6
+ try {
7
+ const nodes = document.querySelectorAll('script[type="application/ld+json"]');
8
+ for (const node of nodes) {
9
+ let parsed;
10
+ try { parsed = JSON.parse(node.textContent); } catch (_) { continue; }
11
+ const items = [];
12
+ if (Array.isArray(parsed)) items.push(...parsed);
13
+ else if (parsed['@graph']) items.push(...parsed['@graph']);
14
+ else items.push(parsed);
15
+ for (const item of items) {
16
+ if (!item || !item['@type']) continue;
17
+ const t = Array.isArray(item['@type']) ? item['@type'] : [item['@type']];
18
+ if (!types || types.some((wanted) => t.includes(wanted))) results.push(item);
19
+ }
20
+ }
21
+ } catch (_) {}
22
+ return results;
23
+ }
package/src/index.js ADDED
@@ -0,0 +1,67 @@
1
+ // index.js — entrypoint do mk-sdk. Inicia core tracking em todas as páginas
2
+ // e expõe a API de try-on em window.mk.
3
+
4
+ import { resolveConfig, getConfig, log } from './core/config.js';
5
+ import { startAutoTrack } from './core/autoTrack.js';
6
+ import { initUnloadFlush } from './core/transport.js';
7
+ import { onPageView, initFunnelClickTracking } from './ecommerce/funnel.js';
8
+ import { maybeMountButton, initPickerBridge } from './tryon/automount.js';
9
+ import * as tryon from './tryon/bridge.js';
10
+
11
+ (function boot() {
12
+ 'use strict';
13
+ try {
14
+ const cfg = resolveConfig();
15
+
16
+ // ─── Core tracking: roda em TODA página ───────────────────────────────
17
+ function init() {
18
+ try {
19
+ initUnloadFlush();
20
+ initPickerBridge(); // canal de preview pro picker de posição do addon
21
+ tryon.initListener(); // escuta o provador (add_to_cart → cart-adapter)
22
+ if (cfg.autoTrack !== false) {
23
+ // funnel + auto-mount do botão reagem ao pageType a cada pageview
24
+ startAutoTrack((pageType, ctx) => {
25
+ onPageView(pageType, ctx);
26
+ maybeMountButton(pageType);
27
+ });
28
+ initFunnelClickTracking();
29
+ }
30
+ log('mk-sdk iniciado', { projectId: cfg.projectId, collectorUrl: cfg.collectorUrl });
31
+ } catch (e) {
32
+ if (cfg.debug) console.error('[mk-sdk] init erro', e);
33
+ }
34
+ }
35
+
36
+ if (document.readyState === 'loading') {
37
+ document.addEventListener('DOMContentLoaded', init);
38
+ } else {
39
+ init();
40
+ }
41
+
42
+ // ─── API pública ──────────────────────────────────────────────────────
43
+ const api = {
44
+ // try-on
45
+ open: tryon.open,
46
+ close: tryon.close,
47
+ isAvailable: tryon.isAvailable,
48
+ getAvailability: tryon.getAvailability,
49
+ onReady: tryon.onReady,
50
+ onClose: tryon.onClose,
51
+ onAddToCart: tryon.onAddToCart,
52
+ onProductLoaded: tryon.onProductLoaded,
53
+ onError: tryon.onError,
54
+ onInteraction: tryon.onInteraction,
55
+ onGenerationComplete: tryon.onGenerationComplete,
56
+ // meta
57
+ _version: '0.1.0',
58
+ _isMkSdk: true
59
+ };
60
+
61
+ if (typeof window !== 'undefined') {
62
+ window.mk = api; // global único e canônico do mk-sdk
63
+ }
64
+ } catch (e) {
65
+ if (typeof console !== 'undefined') console.error('[mk-sdk] boot fatal', e);
66
+ }
67
+ })();
@@ -0,0 +1,41 @@
1
+ // generic.js — fallback pra lojas sem plataforma com adapter dedicado.
2
+ // SKU por sinais nativos (JSON-LD, microdata, Konfidency); cart = clique nativo.
3
+
4
+ import { readJsonLd } from '../ecommerce/jsonld.js';
5
+
6
+ export const id = 'generic';
7
+ export function detect() { return true; } // sempre casa (é o último recurso)
8
+
9
+ export function resolveSku() {
10
+ const products = readJsonLd(['Product']);
11
+ if (products[0]) {
12
+ const p = products[0];
13
+ const s = p.sku || p.mpn || p.gtin13 || p.productID;
14
+ if (s) return { value: String(s), source: 'jsonld' };
15
+ }
16
+ try {
17
+ const el = document.querySelector('[itemprop="sku"]');
18
+ if (el) { const v = el.getAttribute('content') || (el.textContent || '').trim(); if (v) return { value: String(v), source: 'microdata' }; }
19
+ } catch (_) {}
20
+ try {
21
+ const kd = window.konfidencyData;
22
+ if (kd && kd.product && kd.product.variants && kd.product.variants[0])
23
+ return { value: String(kd.product.variants[0]), source: 'konfidency' };
24
+ } catch (_) {}
25
+ return null;
26
+ }
27
+
28
+ const ANCHOR = /(adicionar\s*(à|a|ao)?\s*(sacola|carrinho|cesta|bolsa))|adicionar ao carrinho|add to (cart|bag)|comprar agora/i;
29
+ const ANCHOR_NEG = /continuar|como comprar|lista|desejos|favorito/i;
30
+
31
+ // fallback universal: clica o botão de carrinho nativo (a loja resolve o resto)
32
+ export async function addToCart() {
33
+ const els = document.querySelectorAll('button,[role=button],input[type=submit]');
34
+ for (const el of els) {
35
+ const t = (el.textContent || el.value || '').replace(/\s+/g, ' ').trim();
36
+ if (t && t.length < 40 && ANCHOR.test(t) && !ANCHOR_NEG.test(t)) {
37
+ try { el.click(); return { platform: 'native-click', clicked: t }; } catch (_) {}
38
+ }
39
+ }
40
+ return { platform: 'none', error: 'botão de carrinho nativo não encontrado' };
41
+ }
@@ -0,0 +1,44 @@
1
+ // hybris.js — TUDO da plataforma SAP Commerce / Hybris (ex: Marisa): detecção,
2
+ // SKU (productCode da URL) e carrinho. Validado ao vivo na Marisa.
3
+
4
+ export const id = 'hybris';
5
+ export function detect() { return !!(window.ACC && window.ACC.config); }
6
+
7
+ // o productCode da URL (/p/{code}) é o identificador do catálogo. O [itemprop=sku]
8
+ // do Hybris é um código interno diferente — por isso o SKU vem da URL aqui.
9
+ export function resolveSku() {
10
+ const m = (location.pathname || '').match(/\/p\/([^/?#]+)/);
11
+ if (m && m[1]) return { value: decodeURIComponent(m[1]), source: 'hybris/url' };
12
+ return null;
13
+ }
14
+
15
+ const norm = (s) => String(s == null ? '' : s).trim().toLowerCase();
16
+
17
+ export async function addToCart({ size, quantity }) {
18
+ // cada tamanho carrega o productCode da variante em data-code
19
+ const els = [...document.querySelectorAll('.js-product-select-size, a[data-code], [class*="select-size"]')]
20
+ .filter((e) => e.getAttribute && e.getAttribute('data-code'));
21
+ let el = size ? els.find((e) => norm(e.textContent) === norm(size)) : null;
22
+ if (!el) el = els.find((e) => e.offsetParent !== null) || els[0];
23
+ const code = el && el.getAttribute('data-code');
24
+ if (!code) throw new Error('hybris: variante (data-code) não encontrada');
25
+
26
+ const form = document.getElementById('product--add-to-cart-form') || document.querySelector('form[action*="/cart/add"]');
27
+ const csrfEl = form && form.querySelector('[name="CSRFToken"]');
28
+ const csrf = csrfEl && csrfEl.value;
29
+ const action = (form && form.getAttribute('action')) || '/cart/add';
30
+ if (!csrf) throw new Error('hybris: CSRFToken não encontrado');
31
+
32
+ const body = new URLSearchParams();
33
+ body.set('productCodePost', code);
34
+ body.set('qty', String(quantity || 1));
35
+ body.set('CSRFToken', csrf);
36
+
37
+ const resp = await fetch(action, {
38
+ method: 'POST',
39
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' },
40
+ body: body.toString()
41
+ });
42
+ if (!resp.ok) throw new Error('hybris: ' + action + ' HTTP ' + resp.status);
43
+ return { platform: 'hybris', productCode: code, size: el.textContent.trim() };
44
+ }