Customizer – komunikacja przez postMessage

Wprowadzenie

Aby włączyć komunikację między Customizerem a stroną hostującą:

  1. Otwórz zakładkę Ustawienia e-commerce w aplikacji Alter Product.
  2. W sekcji Integracja ustaw domenę, na której zostanie osadzony iframe (np. https://twojastrona.pl lub http://localhost:3000 ).

Możesz także dodać klucze metadanych do każdego wariantu w projekcie — zostaną one uwzględnione w produkcie w zakładce Konfiguracja produktu.

Wiadomości wysyłane przez Customizer

TypOpis
ALTER_CUSTOMIZER_ADD_TO_CARTKliknięcie przycisku „Dodaj do koszyka” wysyłane po pomyślnym zapisaniu projektu — zintegrowane z Twoim koszykiem/sklepem

Gdy klient kliknie „Dodaj do koszyka” w Customizerze, tworzony jest nowy rekord zamówienia ze statusem domyślnym: pending, co pozwala klientowi kontynuować edycję projektu (np. poprzez link: https://alterproduct.com/app/customizer/{customizerId}/{orderId}). Gdy status zmieni się na inny niż „pending”, edycja zostaje zablokowana — klient widzi zamrożoną wersję projektu.

Wiadomości odbierane przez Customizer

TypOpis
ALTER_PARENT_ACKPotwierdzenie handshake, które musi zawierać nonce
ALTER_TOOL_SESSION_READYPrzekazuje token sesji używany do autoryzacji Customizera
ALTER_TOOL_SESSION_ERRORŻądanie tokenu nie powiodło się po stronie hosta

Pełny przykład (HTML + JS)

1<!DOCTYPE html>
2<html lang="en">
3<head>
4  <meta charset="UTF-8" />
5  <meta name="viewport" content="width=device-width, initial-scale=1" />
6  <title>Alter Product Customizer — postMessage (Handshake + Token)</title>
7  <style>
8    body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 24px; }
9    iframe { width: 100%; height: 720px; border: 0; display: block; border-radius: 12px; }
10    pre { background: #0b1020; color: #d7e1ff; padding: 12px; border-radius: 12px; overflow:auto; }
11  </style>
12</head>
13<body>
14  <h1>My E-commerce</h1>
15
16  <iframe
17    id="customizerWidget"
18    src="https://alterproduct.com/app/customizer/1"
19    title="Alter Product Customizer"
20    allowfullscreen>
21  </iframe>
22
23  <h3>Logs</h3>
24  <pre id="log"></pre>
25
26  <script>
27    const IFRAME_ORIGIN = 'https://alterproduct.com';
28    const iframe = document.getElementById('customizerWidget');
29    const logEl = document.getElementById('log');
30
31    let handshakeOk = false;
32
33    function log(...args) {
34      const line = args.map(a => (typeof a === 'string' ? a : JSON.stringify(a, null, 2))).join(' ');
35      logEl.textContent += line + '\n';
36    }
37
38    function postToIframe(payload) {
39      iframe.contentWindow.postMessage(payload, IFRAME_ORIGIN);
40    }
41
42    /**
43     * Your backend MUST request the session token (recommended).
44     * This endpoint is owned by YOU and should call Alter Product API using server-side credentials.
45     * Expected response: { sessionToken: "..." }
46     */
47    async function getSessionTokenFromYourBackend({ tool, mode, uiDesignId }) {
48      const url = new URL('/api/alter/session-token', window.location.origin);
49      url.searchParams.set('tool', tool);           // viewer | configurator | customizer
50      url.searchParams.set('mode', mode || 'design');
51      url.searchParams.set('uiDesignId', String(uiDesignId || 0));
52
53      const res = await fetch(url.toString(), { method: 'GET' });
54      if (!res.ok) throw new Error('Failed to get session token');
55      const data = await res.json();
56      if (!data || !data.sessionToken) throw new Error('Missing sessionToken');
57      return data.sessionToken;
58    }
59
60    window.addEventListener('message', async (event) => {
61      // ✅ 1) Validate origin
62      if (event.origin !== IFRAME_ORIGIN) return;
63
64      // ✅ 2) Validate source
65      if (event.source !== iframe.contentWindow) return;
66
67      const msg = event.data || {};
68      if (!msg.type || typeof msg.type !== 'string') return;
69
70      // -----------------------
71      // A) HANDSHAKE
72      // Customizer -> Host: ALTER_CHILD_HELLO { nonce }
73      // Host       -> Customizer: ALTER_PARENT_ACK { nonce }
74      // -----------------------
75      if (msg.type === 'ALTER_CHILD_HELLO') {
76        const nonce = msg.nonce;
77        if (!nonce || typeof nonce !== 'string') return;
78
79        handshakeOk = true;
80        log('[Customizer] -> Host: ALTER_CHILD_HELLO', { nonce });
81
82        log('[Host] -> Customizer: ALTER_PARENT_ACK');
83        postToIframe({ type: 'ALTER_PARENT_ACK', nonce });
84        return;
85      }
86
87      // Ignore everything until handshake is done
88      if (!handshakeOk) return;
89
90      // -----------------------
91      // B) TOKEN INIT (on-demand)
92      // Customizer -> Host: ALTER_TOOL_INIT_SESSION { payload: { tool, mode, uiDesignId } }
93      // Host       -> Customizer: ALTER_TOOL_SESSION_READY { token }
94      // -----------------------
95      if (msg.type === 'ALTER_TOOL_INIT_SESSION') {
96        try {
97          const payload = msg.payload || {};
98          const tool = String(payload.tool || 'customizer').toLowerCase();
99          const mode = String(payload.mode || 'design');
100          const uiDesignId = Number(payload.uiDesignId || 0);
101
102          log('[Customizer] -> Host: ALTER_TOOL_INIT_SESSION', { tool, mode, uiDesignId });
103
104          const token = await getSessionTokenFromYourBackend({ tool, mode, uiDesignId });
105
106          log('[Host] -> Customizer: ALTER_TOOL_SESSION_READY');
107          postToIframe({ type: 'ALTER_TOOL_SESSION_READY', token });
108        } catch (e) {
109          log('[Host] -> Customizer: ALTER_TOOL_SESSION_ERROR', String(e && e.message ? e.message : e));
110          postToIframe({ type: 'ALTER_TOOL_SESSION_ERROR', error: String(e && e.message ? e.message : e) });
111        }
112        return;
113      }
114
115      // -----------------------
116      // C) Customizer events
117      // -----------------------
118      if (msg.type === 'ALTER_TOOL_READY') {
119        log('[Customizer] -> Host: ALTER_TOOL_READY', msg);
120        return;
121      }
122
123      if (msg.type === 'ALTER_CUSTOMIZER_ADD_TO_CART') {
124        log('[Customizer] -> Host: ALTER_CUSTOMIZER_ADD_TO_CART', msg.payload);
125        // Send payload to your backend and add item to cart in your e-commerce system
126        return;
127      }
128
129      if (msg.type === 'ALTER_TOOL_ERROR') {
130        log('[Customizer] -> Host: ALTER_TOOL_ERROR', msg);
131        return;
132      }
133    });
134  </script>
135</body>
136</html>

Przykładowy payload

{
  "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
    }
  ]
}