mkfashion-sdk 2.4.6 → 2.4.7
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/index.html +79 -94
- package/mkfashion-sdk-2.4.6.tgz +0 -0
- package/mkfashion-sdk-2.4.7.tgz +0 -0
- package/package.json +1 -1
- package/src/mkfashion.js +364 -5
- package/test-e2e.html +175 -0
package/index.html
CHANGED
|
@@ -1,94 +1,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="
|
|
9
|
-
</head>
|
|
10
|
-
|
|
11
|
-
<body>
|
|
12
|
-
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
<script>
|
|
16
|
-
(function () {
|
|
17
|
-
var projectid = '
|
|
18
|
-
var identifier = '
|
|
19
|
-
|
|
20
|
-
function initMkFashion() {
|
|
21
|
-
mkfashion.addToCart(function (payload) {
|
|
22
|
-
console.log('Adicionando ao carrinho:', payload);
|
|
23
|
-
|
|
24
|
-
var variantSku = payload.selectedIdentifier;
|
|
25
|
-
|
|
26
|
-
fetch('/cart/add.js', {
|
|
27
|
-
method: 'POST',
|
|
28
|
-
headers: {
|
|
29
|
-
'Content-Type': 'application/json',
|
|
30
|
-
},
|
|
31
|
-
body: JSON.stringify({
|
|
32
|
-
items: [{
|
|
33
|
-
quantity: 1,
|
|
34
|
-
id: variantSku
|
|
35
|
-
}]
|
|
36
|
-
})
|
|
37
|
-
})
|
|
38
|
-
.then(function (response) {
|
|
39
|
-
return response.json();
|
|
40
|
-
})
|
|
41
|
-
.then(function (data) {
|
|
42
|
-
console.log('Produto adicionado:', data);
|
|
43
|
-
// document.dispatchEvent(new CustomEvent('cart:updated'));
|
|
44
|
-
})
|
|
45
|
-
.catch(function (error) {
|
|
46
|
-
console.error('Erro carrinho:', error);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
mkfashion.onInteraction((data)
|
|
51
|
-
console.log(data.category, data.action)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
mkfashion.
|
|
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
|
-
|
|
80
|
-
setTimeout(waitForMkFashion, 100);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (document.readyState === "loading") {
|
|
85
|
-
document.addEventListener("DOMContentLoaded", waitForMkFashion);
|
|
86
|
-
} else {
|
|
87
|
-
waitForMkFashion();
|
|
88
|
-
}
|
|
89
|
-
})();
|
|
90
|
-
</script>
|
|
91
|
-
|
|
92
|
-
</body>
|
|
93
|
-
|
|
94
|
-
</html>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="pt-BR">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Provador Virtual</title>
|
|
8
|
+
<script src="./src/mkfashion.js"></script>
|
|
9
|
+
</head>
|
|
10
|
+
|
|
11
|
+
<body>
|
|
12
|
+
|
|
13
|
+
<div id="mkfashion-container"></div>
|
|
14
|
+
|
|
15
|
+
<script>
|
|
16
|
+
(function () {
|
|
17
|
+
var projectid = '69f9e5b0f4eb0c4403319830';
|
|
18
|
+
var identifier = '1881';
|
|
19
|
+
|
|
20
|
+
function initMkFashion() {
|
|
21
|
+
mkfashion.addToCart(function (payload) {
|
|
22
|
+
console.log('Adicionando ao carrinho:', payload);
|
|
23
|
+
|
|
24
|
+
var variantSku = payload.selectedIdentifier;
|
|
25
|
+
|
|
26
|
+
fetch('/cart/add.js', {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
items: [{
|
|
33
|
+
quantity: 1,
|
|
34
|
+
id: variantSku
|
|
35
|
+
}]
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
.then(function (response) {
|
|
39
|
+
return response.json();
|
|
40
|
+
})
|
|
41
|
+
.then(function (data) {
|
|
42
|
+
console.log('Produto adicionado:', data);
|
|
43
|
+
// document.dispatchEvent(new CustomEvent('cart:updated'));
|
|
44
|
+
})
|
|
45
|
+
.catch(function (error) {
|
|
46
|
+
console.error('Erro carrinho:', error);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
mkfashion.onInteraction(function (data) {
|
|
51
|
+
console.log(data.category, data.action);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
mkfashion.init({
|
|
55
|
+
projectId: projectid,
|
|
56
|
+
identifier: identifier,
|
|
57
|
+
target: '#mkfashion-container'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function waitForMkFashion() {
|
|
62
|
+
if (typeof mkfashion !== 'undefined') {
|
|
63
|
+
initMkFashion();
|
|
64
|
+
} else {
|
|
65
|
+
setTimeout(waitForMkFashion, 100);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (document.readyState === "loading") {
|
|
70
|
+
document.addEventListener("DOMContentLoaded", waitForMkFashion);
|
|
71
|
+
} else {
|
|
72
|
+
waitForMkFashion();
|
|
73
|
+
}
|
|
74
|
+
})();
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
</body>
|
|
78
|
+
|
|
79
|
+
</html>
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/src/mkfashion.js
CHANGED
|
@@ -128,11 +128,6 @@ const mkfashion = {
|
|
|
128
128
|
return
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
// Avisa o iframe pra limpar a UI de confirmacao de saida antes de esconder.
|
|
132
|
-
// Sem isso, o modal de "Tem certeza?" fica aberto e reaparece na proxima abertura
|
|
133
|
-
// (porque o iframe nao eh destruido entre open/close — apenas escondido).
|
|
134
|
-
this._postToIframe('dismiss_exit_confirmation')
|
|
135
|
-
|
|
136
131
|
this._hideModal()
|
|
137
132
|
this._triggerCallback('onClose')
|
|
138
133
|
this._isOpen = false
|
|
@@ -395,6 +390,8 @@ const mkfashion = {
|
|
|
395
390
|
}
|
|
396
391
|
},
|
|
397
392
|
|
|
393
|
+
// init: ver seção "BOTÃO TRY-ON" mais abaixo
|
|
394
|
+
|
|
398
395
|
// ============ METODOS PRIVADOS ============
|
|
399
396
|
|
|
400
397
|
_log(message, data = null) {
|
|
@@ -772,6 +769,368 @@ const mkfashion = {
|
|
|
772
769
|
this._messageHandler = null
|
|
773
770
|
this._log('Listener de mensagens removido')
|
|
774
771
|
}
|
|
772
|
+
},
|
|
773
|
+
|
|
774
|
+
// ============================================================
|
|
775
|
+
// ============ BOTÃO TRY-ON ============
|
|
776
|
+
// ============================================================
|
|
777
|
+
// Spec: mk-fashion-cms/docs/tryon-button-spec.md
|
|
778
|
+
//
|
|
779
|
+
// Fluxo:
|
|
780
|
+
// init(opts)
|
|
781
|
+
// → fetch availability + template (paralelo)
|
|
782
|
+
// → _normalizeButtonConfig (escolhe desktop/mobile + aplica defaults)
|
|
783
|
+
// → dispatch via _BUTTON_RENDERERS[config.style] → renderer (gregory-black ou gregory-card)
|
|
784
|
+
// → renderer constrói árvore DOM via _el + helpers de ícone/badge
|
|
785
|
+
// → appendChild no target
|
|
786
|
+
//
|
|
787
|
+
// Sub-seções:
|
|
788
|
+
// 1. Entry point (público): init
|
|
789
|
+
// 2. Configuração visual: constantes e defaults
|
|
790
|
+
// 3. Ícones: SVGs hardcoded
|
|
791
|
+
// 4. Helpers genéricos: _el, ícones, borda, fonte
|
|
792
|
+
// 5. Camada de dados: fetch e normalização do template
|
|
793
|
+
// 6. Preset gregory-black: badge + botão simples
|
|
794
|
+
// 7. Preset gregory-card: card completo com badge + cta
|
|
795
|
+
|
|
796
|
+
// --- 1. Entry point (público) ---
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Inicializa o provador virtual: valida disponibilidade, busca o template visual,
|
|
800
|
+
* renderiza o botão (gregory-black ou gregory-card) e faz wiring do clique → open().
|
|
801
|
+
*
|
|
802
|
+
* @param {Object} options
|
|
803
|
+
* @param {string} options.projectId - ID do projeto (aliases: projectid, store)
|
|
804
|
+
* @param {string} options.identifier - Identificador do produto (alias: sku)
|
|
805
|
+
* @param {string} options.target - Seletor CSS do container (ex: '#produto', '.actions')
|
|
806
|
+
* @returns {Promise<{ok: boolean, reason: string, element: HTMLElement|null}>}
|
|
807
|
+
* - `ok`: true se o botão foi renderizado.
|
|
808
|
+
* - `reason`: 'rendered' (sucesso) | 'unavailable' (produto sem try-on) |
|
|
809
|
+
* 'missing_projectId' | 'missing_identifier' | 'missing_target' |
|
|
810
|
+
* 'target_not_found'.
|
|
811
|
+
* - `element`: o elemento DOM renderizado (ou null em caso de falha).
|
|
812
|
+
*
|
|
813
|
+
* @example
|
|
814
|
+
* const { ok, reason } = await mkfashion.init({ projectId, identifier, target })
|
|
815
|
+
* if (!ok) {
|
|
816
|
+
* if (reason === 'unavailable') alert('Esse produto não tem prova virtual')
|
|
817
|
+
* }
|
|
818
|
+
*/
|
|
819
|
+
async init(options = {}) {
|
|
820
|
+
const fail = reason => ({ ok: false, reason, element: null })
|
|
821
|
+
|
|
822
|
+
const raw = options.projectId || options.projectid || options.store
|
|
823
|
+
const projectId = this._resolveProjectId(raw)
|
|
824
|
+
const identifier = (options.identifier || options.sku || '').trim()
|
|
825
|
+
const target = options.target
|
|
826
|
+
|
|
827
|
+
if (!projectId) {
|
|
828
|
+
console.error('[mKFashion] projectId e obrigatorio')
|
|
829
|
+
return fail('missing_projectId')
|
|
830
|
+
}
|
|
831
|
+
if (!identifier) {
|
|
832
|
+
console.error('[mKFashion] identifier e obrigatorio')
|
|
833
|
+
return fail('missing_identifier')
|
|
834
|
+
}
|
|
835
|
+
if (!target) {
|
|
836
|
+
console.error('[mKFashion] target e obrigatorio')
|
|
837
|
+
return fail('missing_target')
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const container = document.querySelector(target)
|
|
841
|
+
if (!container) {
|
|
842
|
+
console.error(`[mKFashion] Elemento não encontrado: ${target}`)
|
|
843
|
+
return fail('target_not_found')
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const [available, template] = await Promise.all([
|
|
847
|
+
this.isAvailable(projectId, identifier).catch(() => false),
|
|
848
|
+
this._fetchButtonTemplate(projectId)
|
|
849
|
+
])
|
|
850
|
+
|
|
851
|
+
if (!available) {
|
|
852
|
+
this._log('Produto indisponível, botão não criado', { projectId, identifier })
|
|
853
|
+
return fail('unavailable')
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const isMobile = window.matchMedia('(max-width: 767px)').matches
|
|
857
|
+
const config = this._normalizeButtonConfig(template, isMobile)
|
|
858
|
+
const renderer = this._BUTTON_RENDERERS[config.style] || this._BUTTON_RENDERERS['gregory-black']
|
|
859
|
+
|
|
860
|
+
this._ensureJakartaFont()
|
|
861
|
+
const element = renderer.call(this, config, projectId, identifier)
|
|
862
|
+
|
|
863
|
+
container.appendChild(element)
|
|
864
|
+
this._log('Botão criado', { target, projectId, identifier, style: config.style })
|
|
865
|
+
return { ok: true, reason: 'rendered', element }
|
|
866
|
+
},
|
|
867
|
+
|
|
868
|
+
// --- 2. Configuração visual ---
|
|
869
|
+
|
|
870
|
+
_FONT_STACK: "'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
871
|
+
_BUTTON_RADIUS: { none: '0', sm: '4px', md: '8px', pill: '9999px' },
|
|
872
|
+
_CARD_RADIUS: { none: '0', sm: '4px', md: '8px', lg: '16px' },
|
|
873
|
+
|
|
874
|
+
_BUTTON_DEFAULTS: {
|
|
875
|
+
style: 'gregory-black',
|
|
876
|
+
text: 'Provar Virtualmente',
|
|
877
|
+
fontSize: 14,
|
|
878
|
+
subtext: '',
|
|
879
|
+
bgColor: '#1E1E1E',
|
|
880
|
+
textColor: '#FFFFFF',
|
|
881
|
+
badgeBgColor: '#C51A1B',
|
|
882
|
+
badgeTextColor: '#FFFFFF',
|
|
883
|
+
borderRadius: 'md',
|
|
884
|
+
borderColor: '#1E1E1E',
|
|
885
|
+
borderWidth: 0,
|
|
886
|
+
cardTitle: '',
|
|
887
|
+
cardDescription: '',
|
|
888
|
+
cardFooter: '',
|
|
889
|
+
cardBgColor: '#F6F6F6',
|
|
890
|
+
cardTitleColor: '#1E1E1E',
|
|
891
|
+
cardTextColor: '#717171',
|
|
892
|
+
cardBorderRadius: 'md'
|
|
893
|
+
},
|
|
894
|
+
|
|
895
|
+
_BUTTON_RENDERERS: {
|
|
896
|
+
'gregory-black': function (c, p, i) { return this._renderSimpleButton(c, p, i) },
|
|
897
|
+
'gregory-card': function (c, p, i) { return this._renderCardButton(c, p, i) }
|
|
898
|
+
},
|
|
899
|
+
|
|
900
|
+
// --- 3. Ícones (assets visuais hardcoded) ---
|
|
901
|
+
|
|
902
|
+
_CAMERA_ICON_SVG: '<svg viewBox="0 0 17.5263 20.6862" fill="none" xmlns="http://www.w3.org/2000/svg" style="display:block;width:100%;height:100%"><g><path d="M8.49995 12.1862C10.1568 12.1862 11.5 10.8431 11.5 9.18622C11.5 7.52936 10.1568 6.18622 8.49995 6.18622C6.8431 6.18622 5.49995 7.52936 5.49995 9.18622C5.49995 10.8431 6.8431 12.1862 8.49995 12.1862Z" stroke="currentColor" shape-rendering="crispEdges"/><path d="M0.5 9.11152C0.5 6.56953 0.5 5.29894 1.1099 4.38664C1.375 3.9902 1.71424 3.65088 2.10821 3.38809C2.6945 2.9958 3.42899 2.85564 4.55351 2.80588C5.09013 2.80588 5.55183 2.39949 5.65687 1.87284C5.73715 1.48708 5.94572 1.14138 6.24735 0.894155C6.54898 0.646934 6.92516 0.51336 7.31231 0.516006H9.9734C10.7779 0.516006 11.4709 1.08412 11.6288 1.87284C11.7339 2.39949 12.1956 2.80588 12.7322 2.80588C13.8559 2.85564 12.9129 4.67413 13.5 5.06559C13.8949 5.32932 15.1484 3.80355 15.5 4.02647C16.1099 4.93876 16.7857 6.56953 16.7857 9.11152C16.7857 11.6535 16.7857 12.9233 16.1758 13.8364C15.9107 14.2328 15.5715 14.5722 15.1775 14.835C14.2647 15.4445 12.9936 15.4445 10.4522 15.4445H6.83351C4.29213 15.4445 3.02103 15.4445 2.10821 14.835C1.71446 14.5718 1.3755 14.2322 1.11071 13.8356C0.933802 13.5672 0.803519 13.2698 0.725557 12.9564" stroke="currentColor" stroke-linecap="round"/><path d="M12.676 0.607931C12.9446 -0.178095 14.0307 -0.2019 14.3491 0.536515L14.3761 0.60838L14.7385 1.66839C14.8216 1.91149 14.9559 2.13395 15.1322 2.32076C15.3086 2.50757 15.5229 2.65439 15.7608 2.75131L15.8583 2.78769L16.9183 3.14971C17.7044 3.41831 17.7282 4.50437 16.9902 4.82283L16.9183 4.84978L15.8583 5.21225C15.6151 5.29526 15.3926 5.42947 15.2057 5.60582C15.0188 5.78218 14.8719 5.99657 14.7749 6.23453L14.7385 6.33155L14.3765 7.39201C14.1079 8.17803 13.0219 8.20184 12.7039 7.46387L12.676 7.39201L12.314 6.332C12.231 6.08881 12.0968 5.86627 11.9204 5.67938C11.744 5.49249 11.5297 5.3456 11.2917 5.24863L11.1947 5.21225L10.1347 4.85022C9.34817 4.58163 9.32437 3.49557 10.0628 3.17756L10.1347 3.14971L11.1947 2.78769C11.4378 2.70462 11.6602 2.57039 11.847 2.39404C12.0339 2.21769 12.1807 2.00333 12.2776 1.76541L12.314 1.66839L12.676 0.607931Z" fill="currentColor"/></g></svg>',
|
|
903
|
+
|
|
904
|
+
_SPARKLE_ICON_SVG: '<svg viewBox="0 0 11.6122 12.7248" fill="none" xmlns="http://www.w3.org/2000/svg" style="display:block;width:12px;height:12px"><path d="M4.09055 2.01133C4.43939 0.9905 5.84989 0.959583 6.26347 1.91858L6.29847 2.01192L6.76922 3.38858C6.8771 3.7043 7.05144 3.99321 7.28047 4.23583C7.5095 4.47845 7.7879 4.66912 8.09689 4.795L8.22347 4.84225L9.60014 5.31242C10.621 5.66125 10.6519 7.07175 9.69347 7.48533L9.60014 7.52033L8.22347 7.99108C7.90764 8.09889 7.61862 8.2732 7.3759 8.50223C7.13318 8.73127 6.94241 9.0097 6.81647 9.31875L6.76922 9.44475L6.29905 10.822C5.95022 11.8428 4.53972 11.8737 4.12672 10.9153L4.09055 10.822L3.62039 9.44533C3.51258 9.12951 3.33827 8.84048 3.10924 8.59776C2.8802 8.35504 2.60177 8.16427 2.29272 8.03833L2.16672 7.99108L0.790054 7.52092C-0.231363 7.17208 -0.26228 5.76158 0.69672 5.34858L0.790054 5.31242L2.16672 4.84225C2.48244 4.73437 2.77135 4.56003 3.01397 4.331C3.25658 4.10197 3.44726 3.82357 3.57314 3.51458L3.62039 3.38858L4.09055 2.01133ZM9.86147 0C9.9706 0 10.0775 0.0306123 10.1701 0.0883584C10.2627 0.146104 10.3373 0.228668 10.3853 0.326667L10.4133 0.394917L10.6175 0.993417L11.2166 1.19758C11.3259 1.23474 11.4218 1.30353 11.492 1.39523C11.5623 1.48693 11.6037 1.59741 11.611 1.71268C11.6184 1.82795 11.5914 1.94281 11.5334 2.04271C11.4755 2.14261 11.3891 2.22305 11.2854 2.27383L11.2166 2.30183L10.6181 2.506L10.4139 3.10508C10.3767 3.21442 10.3078 3.31024 10.2161 3.38041C10.1244 3.45058 10.0139 3.49194 9.89859 3.49925C9.78333 3.50656 9.66849 3.47949 9.56863 3.42147C9.46876 3.36345 9.38837 3.27709 9.33764 3.17333L9.30964 3.10508L9.10547 2.50658L8.50639 2.30242C8.39702 2.26526 8.30115 2.19647 8.23092 2.10477C8.16069 2.01307 8.11926 1.90259 8.11189 1.78732C8.10452 1.67205 8.13153 1.55719 8.18951 1.45729C8.24749 1.35739 8.33381 1.27695 8.43755 1.22617L8.50639 1.19817L9.10489 0.994L9.30905 0.394917C9.34839 0.279665 9.42281 0.179613 9.52187 0.108791C9.62094 0.0379686 9.73969 -7.30808e-05 9.86147 0Z" fill="currentColor"/></svg>',
|
|
905
|
+
|
|
906
|
+
// --- 4. Helpers genéricos ---
|
|
907
|
+
|
|
908
|
+
/** Cria elemento DOM declarativamente: el('tag', { style, text|html, children, on, attrs }) */
|
|
909
|
+
_el(tag, opts = {}) {
|
|
910
|
+
const node = document.createElement(tag)
|
|
911
|
+
if (opts.style) node.style.cssText = opts.style
|
|
912
|
+
if (opts.text != null) node.textContent = opts.text
|
|
913
|
+
if (opts.html != null) node.innerHTML = opts.html
|
|
914
|
+
if (opts.attrs) for (const k in opts.attrs) node.setAttribute(k, opts.attrs[k])
|
|
915
|
+
if (opts.on) for (const k in opts.on) node.addEventListener(k, opts.on[k])
|
|
916
|
+
if (opts.children) for (const c of opts.children) if (c) node.appendChild(c)
|
|
917
|
+
return node
|
|
918
|
+
},
|
|
919
|
+
|
|
920
|
+
_borderFromConfig(c) {
|
|
921
|
+
return c.borderWidth > 0 && c.borderColor ? `${c.borderWidth}px solid ${c.borderColor}` : 'none'
|
|
922
|
+
},
|
|
923
|
+
|
|
924
|
+
_ensureJakartaFont() {
|
|
925
|
+
if (document.getElementById('mkfashion-jakarta-font')) return
|
|
926
|
+
document.head.appendChild(this._el('link', {
|
|
927
|
+
attrs: {
|
|
928
|
+
id: 'mkfashion-jakarta-font',
|
|
929
|
+
rel: 'stylesheet',
|
|
930
|
+
href: 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700&display=swap'
|
|
931
|
+
}
|
|
932
|
+
}))
|
|
933
|
+
},
|
|
934
|
+
|
|
935
|
+
_renderCameraIcon(color) {
|
|
936
|
+
return this._el('span', {
|
|
937
|
+
style: `width: 24px; height: 24px; padding: 2px; box-sizing: border-box;
|
|
938
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
939
|
+
color: ${color}; flex-shrink: 0;`,
|
|
940
|
+
html: this._CAMERA_ICON_SVG
|
|
941
|
+
})
|
|
942
|
+
},
|
|
943
|
+
|
|
944
|
+
_renderSparkleIcon(color) {
|
|
945
|
+
return this._el('span', {
|
|
946
|
+
style: `width: 12px; height: 12px; display: inline-flex; align-items: center;
|
|
947
|
+
justify-content: center; color: ${color}; flex-shrink: 0;`,
|
|
948
|
+
html: this._SPARKLE_ICON_SVG
|
|
949
|
+
})
|
|
950
|
+
},
|
|
951
|
+
|
|
952
|
+
// --- 5. Camada de dados (API + normalização) ---
|
|
953
|
+
|
|
954
|
+
async _fetchButtonTemplate(projectId) {
|
|
955
|
+
try {
|
|
956
|
+
const response = await fetch(`${this.apiUrl}/projects/${projectId}`)
|
|
957
|
+
if (!response.ok) throw new Error(`Erro na API: ${response.status}`)
|
|
958
|
+
const data = await response.json()
|
|
959
|
+
const tpl = data?.project?.template?.tryOnButton || data?.template?.tryOnButton || null
|
|
960
|
+
this._log('Template recebido', tpl)
|
|
961
|
+
return tpl
|
|
962
|
+
} catch (error) {
|
|
963
|
+
console.error('[mKFashion] Erro ao buscar template do botão:', error)
|
|
964
|
+
return null
|
|
965
|
+
}
|
|
966
|
+
},
|
|
967
|
+
|
|
968
|
+
/** Escolhe config desktop/mobile, força gregory-black em mobile, aplica defaults, pré-resolve px. */
|
|
969
|
+
_normalizeButtonConfig(template, isMobile) {
|
|
970
|
+
const raw = (isMobile ? template?.mobile : template?.desktop)
|
|
971
|
+
|| template?.desktop || template?.mobile || {}
|
|
972
|
+
const merged = { ...this._BUTTON_DEFAULTS, ...raw }
|
|
973
|
+
if (merged.style === 'gregory-card' && isMobile) merged.style = 'gregory-black'
|
|
974
|
+
merged.borderRadiusPx = this._BUTTON_RADIUS[merged.borderRadius] ?? '0'
|
|
975
|
+
merged.cardBorderRadiusPx = this._CARD_RADIUS[merged.cardBorderRadius] ?? '0'
|
|
976
|
+
merged.fontSize = Number(merged.fontSize) || 14
|
|
977
|
+
merged.borderWidth = Number(merged.borderWidth) || 0
|
|
978
|
+
return merged
|
|
979
|
+
},
|
|
980
|
+
|
|
981
|
+
// --- 6. Preset gregory-black (botão simples) ---
|
|
982
|
+
|
|
983
|
+
_renderBlackBadge(c) {
|
|
984
|
+
if (!c.subtext) return null
|
|
985
|
+
return this._el('span', {
|
|
986
|
+
style: `display: flex; align-items: center; justify-content: center;
|
|
987
|
+
padding: 4px; background: ${c.badgeBgColor};
|
|
988
|
+
font-family: ${this._FONT_STACK};
|
|
989
|
+
font-weight: 700; font-size: 16px; text-transform: uppercase;
|
|
990
|
+
color: ${c.badgeTextColor}; line-height: 1; white-space: nowrap;`,
|
|
991
|
+
text: c.subtext
|
|
992
|
+
})
|
|
993
|
+
},
|
|
994
|
+
|
|
995
|
+
_renderSimpleButton(c, projectId, identifier) {
|
|
996
|
+
const content = this._el('span', {
|
|
997
|
+
style: 'display: inline-flex; align-items: center; gap: 4px;',
|
|
998
|
+
children: [
|
|
999
|
+
this._renderCameraIcon(c.textColor),
|
|
1000
|
+
this._el('span', {
|
|
1001
|
+
text: c.text,
|
|
1002
|
+
style: `font-family: ${this._FONT_STACK}; font-weight: 700;
|
|
1003
|
+
font-size: ${c.fontSize}px; text-transform: uppercase;
|
|
1004
|
+
letter-spacing: 2px; color: ${c.textColor};
|
|
1005
|
+
white-space: nowrap; text-align: center; line-height: 1;`
|
|
1006
|
+
})
|
|
1007
|
+
]
|
|
1008
|
+
})
|
|
1009
|
+
|
|
1010
|
+
return this._el('button', {
|
|
1011
|
+
attrs: { type: 'button' },
|
|
1012
|
+
style: `display: inline-flex; align-items: center; justify-content: center;
|
|
1013
|
+
gap: 8px; padding: 8px; width: 238px; box-sizing: border-box;
|
|
1014
|
+
background: ${c.bgColor};
|
|
1015
|
+
border: ${this._borderFromConfig(c)};
|
|
1016
|
+
border-radius: ${c.borderRadiusPx};
|
|
1017
|
+
font-family: ${this._FONT_STACK};
|
|
1018
|
+
cursor: pointer; transition: opacity 0.2s ease; line-height: 1;
|
|
1019
|
+
-webkit-appearance: none; appearance: none;`,
|
|
1020
|
+
children: [content, this._renderBlackBadge(c)],
|
|
1021
|
+
on: {
|
|
1022
|
+
mouseenter: e => { e.currentTarget.style.opacity = '0.85' },
|
|
1023
|
+
mouseleave: e => { e.currentTarget.style.opacity = '1' },
|
|
1024
|
+
click: () => this.open({ projectId, identifier })
|
|
1025
|
+
}
|
|
1026
|
+
})
|
|
1027
|
+
},
|
|
1028
|
+
|
|
1029
|
+
// --- 7. Preset gregory-card (card com title + descrição + cta) ---
|
|
1030
|
+
|
|
1031
|
+
_renderCardBadge(c) {
|
|
1032
|
+
if (!c.subtext) return null
|
|
1033
|
+
return this._el('span', {
|
|
1034
|
+
style: `display: flex; align-items: center; gap: 4px; padding: 4px;
|
|
1035
|
+
background: ${c.badgeBgColor};
|
|
1036
|
+
font-family: ${this._FONT_STACK};
|
|
1037
|
+
font-weight: 600; font-size: 10px; text-transform: uppercase;
|
|
1038
|
+
color: ${c.badgeTextColor}; line-height: 1; white-space: nowrap;`,
|
|
1039
|
+
children: [
|
|
1040
|
+
this._renderSparkleIcon(c.badgeTextColor),
|
|
1041
|
+
this._el('span', { text: c.subtext })
|
|
1042
|
+
]
|
|
1043
|
+
})
|
|
1044
|
+
},
|
|
1045
|
+
|
|
1046
|
+
_renderCardCta(c, projectId, identifier) {
|
|
1047
|
+
return this._el('button', {
|
|
1048
|
+
attrs: { type: 'button' },
|
|
1049
|
+
style: `display: flex; align-items: center; justify-content: center;
|
|
1050
|
+
gap: 8px; padding: 8px 12px 8px 8px;
|
|
1051
|
+
width: 100%; margin-top: auto; box-sizing: border-box;
|
|
1052
|
+
background: ${c.bgColor};
|
|
1053
|
+
border: ${this._borderFromConfig(c)};
|
|
1054
|
+
border-radius: ${c.borderRadiusPx};
|
|
1055
|
+
filter: drop-shadow(0 0 6px rgba(0,0,0,0.08));
|
|
1056
|
+
font-family: ${this._FONT_STACK};
|
|
1057
|
+
cursor: pointer; transition: opacity 0.2s ease; line-height: 1;
|
|
1058
|
+
-webkit-appearance: none; appearance: none;`,
|
|
1059
|
+
children: [
|
|
1060
|
+
this._renderCameraIcon(c.textColor),
|
|
1061
|
+
this._el('span', {
|
|
1062
|
+
text: c.text,
|
|
1063
|
+
style: `font-family: ${this._FONT_STACK}; font-weight: 700;
|
|
1064
|
+
font-size: ${c.fontSize}px; color: ${c.textColor};
|
|
1065
|
+
white-space: nowrap; line-height: 1;`
|
|
1066
|
+
})
|
|
1067
|
+
],
|
|
1068
|
+
on: {
|
|
1069
|
+
mouseenter: e => { e.currentTarget.style.opacity = '0.85' },
|
|
1070
|
+
mouseleave: e => { e.currentTarget.style.opacity = '1' },
|
|
1071
|
+
click: () => this.open({ projectId, identifier })
|
|
1072
|
+
}
|
|
1073
|
+
})
|
|
1074
|
+
},
|
|
1075
|
+
|
|
1076
|
+
_renderCardButton(c, projectId, identifier) {
|
|
1077
|
+
const titleBlock = this._el('div', {
|
|
1078
|
+
style: 'display: flex; flex-direction: column; gap: 4px;',
|
|
1079
|
+
children: [
|
|
1080
|
+
c.cardTitle && this._el('p', {
|
|
1081
|
+
text: c.cardTitle,
|
|
1082
|
+
style: `margin: 0; font-family: ${this._FONT_STACK};
|
|
1083
|
+
font-weight: 700; font-size: 16px; letter-spacing: 1px;
|
|
1084
|
+
text-transform: uppercase; color: ${c.cardTitleColor};
|
|
1085
|
+
line-height: 1.2;`
|
|
1086
|
+
}),
|
|
1087
|
+
c.cardDescription && this._el('p', {
|
|
1088
|
+
html: c.cardDescription,
|
|
1089
|
+
style: `margin: 0; font-family: ${this._FONT_STACK};
|
|
1090
|
+
font-weight: 400; font-size: 12px; max-width: 177px;
|
|
1091
|
+
line-height: 1.2; color: ${c.cardTextColor};`
|
|
1092
|
+
})
|
|
1093
|
+
]
|
|
1094
|
+
})
|
|
1095
|
+
|
|
1096
|
+
const details = this._el('div', {
|
|
1097
|
+
style: `display: flex; flex-direction: column; justify-content: space-between;
|
|
1098
|
+
gap: 16px; flex: 1 1 0; min-width: 0;`,
|
|
1099
|
+
children: [
|
|
1100
|
+
titleBlock,
|
|
1101
|
+
c.cardFooter && this._el('p', {
|
|
1102
|
+
text: c.cardFooter,
|
|
1103
|
+
style: `margin: 0; font-family: ${this._FONT_STACK};
|
|
1104
|
+
font-weight: 400; font-size: 10px; color: ${c.cardTextColor};
|
|
1105
|
+
line-height: 1.2;`
|
|
1106
|
+
})
|
|
1107
|
+
]
|
|
1108
|
+
})
|
|
1109
|
+
|
|
1110
|
+
const right = this._el('div', {
|
|
1111
|
+
style: `display: flex; flex-direction: column; align-items: flex-end;
|
|
1112
|
+
flex: 1 1 0; min-width: 0;`,
|
|
1113
|
+
children: [
|
|
1114
|
+
this._renderCardBadge(c),
|
|
1115
|
+
this._renderCardCta(c, projectId, identifier)
|
|
1116
|
+
]
|
|
1117
|
+
})
|
|
1118
|
+
|
|
1119
|
+
const row = this._el('div', {
|
|
1120
|
+
style: `display: flex; gap: 16px; align-items: stretch;
|
|
1121
|
+
width: 100%; flex: 1; box-sizing: border-box;`,
|
|
1122
|
+
children: [details, right]
|
|
1123
|
+
})
|
|
1124
|
+
|
|
1125
|
+
return this._el('div', {
|
|
1126
|
+
style: `display: flex; flex-direction: column; align-items: flex-start;
|
|
1127
|
+
padding: 12px; width: 402px; min-height: 140px;
|
|
1128
|
+
box-sizing: border-box;
|
|
1129
|
+
background: ${c.cardBgColor};
|
|
1130
|
+
border-radius: ${c.cardBorderRadiusPx};
|
|
1131
|
+
font-family: ${this._FONT_STACK};`,
|
|
1132
|
+
children: [row]
|
|
1133
|
+
})
|
|
775
1134
|
}
|
|
776
1135
|
}
|
|
777
1136
|
|
package/test-e2e.html
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
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>
|