1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8" />
5 <title>Alter Product Configurator Communication (Handshake + Token + postMessage)</title>
6 <meta name="viewport" content="width=device-width, initial-scale=1" />
7 <style>
8 body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 24px; }
9 iframe { width: 100%; height: 600px; border: 0; display: block; border-radius: 12px; }
10 .row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
11 button { padding: 10px 14px; border-radius: 10px; border: 1px solid #ddd; background: #fff; cursor: pointer; }
12 button:disabled { opacity: 0.5; cursor: not-allowed; }
13 pre { background: #0b1020; color: #d7e1ff; padding: 12px; border-radius: 12px; overflow:auto; }
14 </style>
15</head>
16<body>
17 <h1>Configurator — Two-Way Communication via postMessage</h1>
18
19 <div class="row">
20 <button id="getDataBtn" disabled>Get Configured Product</button>
21 </div>
22
23 <iframe
24 id="configuratorWidget"
25 src="https://alterproduct.com/app/configurator/1?nav=0&add_to_cart=1"
26 title="Alter Product Configurator"
27 allowfullscreen>
28 </iframe>
29
30 <h3>Logs</h3>
31 <pre id="log"></pre>
32
33 <script>
34 const IFRAME_ORIGIN = 'https://alterproduct.com';
35
36 const iframe = document.getElementById('configuratorWidget');
37 const getDataBtn = document.getElementById('getDataBtn');
38 const logEl = document.getElementById('log');
39
40 let handshakeOk = false;
41 let toolReady = false;
42
43 function log(...args) {
44 const line = args.map(a => (typeof a === 'string' ? a : JSON.stringify(a, null, 2))).join(' ');
45 logEl.textContent += line + '\n';
46 }
47
48 function postToIframe(payload) {
49 iframe.contentWindow.postMessage(payload, IFRAME_ORIGIN);
50 }
51
52 5556
57 async function getSessionTokenFromYourBackend({ tool, mode, uiDesignId }) {
58 const url = new URL('/api/alter/session-token', window.location.origin);
59 url.searchParams.set('tool', tool); // viewer | configurator | customizer
60 url.searchParams.set('mode', mode || 'design');
61 url.searchParams.set('uiDesignId', String(uiDesignId || 0));
62
63 const res = await fetch(url.toString(), { method: 'GET' });
64 if (!res.ok) throw new Error('Failed to get session token');
65 const data = await res.json();
66 if (!data || !data.sessionToken) throw new Error('Missing sessionToken');
67 return data.sessionToken;
68 }
69
70 // UI: request configured product data (only after tool is ready)
71 getDataBtn.addEventListener('click', () => {
72 if (!toolReady) return;
73 log('[Host] -> Configurator: ALTER_CONFIGURATOR_GET_PRODUCT_DATA');
74 postToIframe({ type: 'ALTER_CONFIGURATOR_GET_PRODUCT_DATA' });
75 });
76
77 window.addEventListener('message', async (event) => {
78 // 1) Validate origin
79 if (event.origin !== IFRAME_ORIGIN) return;
80
81 // 2) Validate source (must be the embedded iframe)
82 if (event.source !== iframe.contentWindow) return;
83
84 const msg = event.data || {};
85 if (!msg.type || typeof msg.type !== 'string') return;
86
87 // -----------------------
88 // A) HANDSHAKE
89 // Configurator -> Host: ALTER_CHILD_HELLO { nonce }
90 // Host -> Configurator: ALTER_PARENT_ACK { nonce }
91 // -----------------------
92 if (msg.type === 'ALTER_CHILD_HELLO') {
93 const nonce = msg.nonce;
94 if (!nonce || typeof nonce !== 'string') return;
95
96 handshakeOk = true;
97 log('[Configurator] -> Host: ALTER_CHILD_HELLO', { nonce });
98
99 log('[Host] -> Configurator: ALTER_PARENT_ACK');
100 postToIframe({ type: 'ALTER_PARENT_ACK', nonce });
101
102 return;
103 }
104
105 // Ignore everything until handshake is done
106 if (!handshakeOk) return;
107
108 // -----------------------
109 // B) TOKEN INIT (on-demand)
110 // Configurator -> Host: ALTER_TOOL_INIT_SESSION { payload: { tool, mode, uiDesignId } }
111 // Host -> Configurator: ALTER_TOOL_SESSION_READY { token }
112 // -----------------------
113 if (msg.type === 'ALTER_TOOL_INIT_SESSION') {
114 try {
115 const payload = msg.payload || {};
116 const tool = String(payload.tool || 'configurator').toLowerCase();
117 const mode = String(payload.mode || 'design');
118 const uiDesignId = Number(payload.uiDesignId || 0);
119
120 log('[Configurator] -> Host: ALTER_TOOL_INIT_SESSION', { tool, mode, uiDesignId });
121
122 const token = await getSessionTokenFromYourBackend({ tool, mode, uiDesignId });
123
124 log('[Host] -> Configurator: ALTER_TOOL_SESSION_READY');
125 postToIframe({ type: 'ALTER_TOOL_SESSION_READY', token });
126 } catch (e) {
127 log('[Host] -> Configurator: ALTER_TOOL_SESSION_ERROR', String(e && e.message ? e.message : e));
128 postToIframe({ type: 'ALTER_TOOL_SESSION_ERROR', error: String(e && e.message ? e.message : e) });
129 }
130 return;
131 }
132
133 // -----------------------
134 // C) Configurator lifecycle & events
135 // -----------------------
136 if (msg.type === 'ALTER_TOOL_READY') {
137 toolReady = true;
138 getDataBtn.disabled = false;
139 log('[Configurator] -> Host: ALTER_TOOL_READY', msg);
140 return;
141 }
142
143 if (msg.type === 'ALTER_CONFIGURATOR_DATA_RESPONSE') {
144 log('[Configurator] -> Host: ALTER_CONFIGURATOR_DATA_RESPONSE', msg.payload);
145 return;
146 }
147
148 if (msg.type === 'ALTER_CONFIGURATOR_ADD_TO_CART') {
149 log('[Configurator] -> Host: ALTER_CONFIGURATOR_ADD_TO_CART', msg.payload);
150 return;
151 }
152
153 if (msg.type === 'ALTER_TOOL_ERROR') {
154 log('[Configurator] -> Host: ALTER_TOOL_ERROR', msg);
155 return;
156 }
157 });
158 </script>
159</body>
160</html>
{
"userDesignId": 10,
"designName": "Mug 450ml (15oz)",
"totalPrice": {
"value": 0,
"currency": "EUR"
},
"productGroup": {
"id": 1,
"name": {
"pl": "Kubek 450ml (15oz)",
"en": "Mug 450ml (15oz)"
}
},
"productItems": [
{
"model3d": {
"id": 3
},
"size": {
"id": 9,
"name": {
"pl": "450ml (15oz)",
"en": "450ml (15oz)"
},
"measureSize": {
"D": 8.65,
"H": 11.95
},
"externalMapping": {
"attribute": {
"internalId": 123,
"slug": "pa_size",
"label": "Size"
},
"term": {
"internalId": 456,
"slug": "450ml-15oz",
"label": "450ml (15oz)"
}
}
},
"material": {
"id": 3,
"name": {
"pl": "Ceramika",
"en": "Ceramic"
},
"externalMapping": {
"attribute": {
"internalId": 124,
"slug": "pa_material",
"label": "Material"
},
"term": {
"internalId": 457,
"slug": "ceramic",
"label": "Ceramic"
}
}
},
"printMethod": {
"id": 5,
"name": {
"pl": "Sublimacja",
"en": "Sublimation"
},
"externalMapping": {
"attribute": {
"internalId": 125,
"slug": "pa_printing-method",
"label": "Printing method"
},
"term": {
"internalId": 458,
"slug": "sublimation",
"label": "Sublimation"
}
}
},
"color": {
"id": 11,
"name": {
"pl": "Domyślny",
"en": "Default"
},
"hex": "#FFFFFF",
"externalMapping": {
"attribute": {
"internalId": 126,
"slug": "pa_color",
"label": "Color"
},
"term": {
"internalId": 459,
"slug": "default",
"label": "Default"
}
}
},
"variant": {
"id": 11,
"productGroupId": 1,
"productModel3dId": 3,
"sizeId": 9,
"materialId": 3,
"printTypeId": 5,
"colorId": 11,
"metadata": null,
"minOrderQuantity": null,
"processingTime": null,
"stockQuantity": null,
"volume": null,
"weight": null
},
"unitPrice": {
"value": 0,
"currency": "EUR"
},
"totalPrice": {
"value": 0,
"currency": "EUR"
},
"quantity": 1
}
]
}