Viewer — двосторонній обмін через postMessage

Вступ

Вбудований Viewer обмінюється даними зі сторінкою-хостом через postMessage. Поточне середовище виконання починає роботу з узгодження nonce, а потім запитує токен вбудовування через ALTER_TOOL_INIT_SESSION.

Налаштування:

  1. У застосунку Alter Product відкрийте панель Налаштування електронної комерції — Вбудовування.
  2. Додайте домен, де буде вбудовано Viewer (наприклад, https://yourwebsite.com або http://localhost:3000), і збережіть.

Послідовність:

  1. Ваш сайт завантажує iframe Viewer.
  2. iframe надсилає ALTER_CHILD_HELLO; сторінка відповідає ALTER_PARENT_ACK із тим самим nonce.
  3. iframe запитує токен через ALTER_TOOL_INIT_SESSION, де payload.tool має значення viewer.
  4. Ваш сервер створює токен через Alter Product Public API, а сторінка відповідає ALTER_TOOL_SESSION_READY.
  5. За add_to_cart=1 Viewer може надсилати ALTER_VIEWER_ADD_TO_CART. Хост також може запитати поточні дані через ALTER_VIEWER_GET_PRODUCT_DATA й отримати ALTER_VIEWER_DATA_RESPONSE.

До даних включено метадані варіантів, задані в конфігурації товару.

Повідомлення від Viewer

ТипОпис
ALTER_CHILD_HELLOПочаток узгодження (містить nonce)
ALTER_TOOL_INIT_SESSIONЗапитує токен сеансу в сайту-хоста
ALTER_VIEWER_ADD_TO_CARTКористувач натиснув «До кошика» у вбудованому Viewer із add_to_cart=1
ALTER_VIEWER_DATA_RESPONSEВідповідь на запит даних товару
ALTER_WP_RUNTIME_CONTEXT_REQUESTЗапит контексту середовища виконання WordPress/WooCommerce
ALTER_WP_LOCAL_DESIGN_REQUESTЗапит початкових даних локального дизайну WordPress за wp_local_design=1

Повідомлення до Viewer

ТипОпис
ALTER_PARENT_ACKПідтвердження узгодження (має містити nonce)
ALTER_TOOL_SESSION_READYНадає токен сеансу для авторизації Viewer
ALTER_TOOL_SESSION_ERRORНе вдалося отримати токен (помилка на стороні хоста)
ALTER_VIEWER_GET_PRODUCT_DATAЗапитує дані товару у Viewer
ALTER_WP_RUNTIME_CONTEXT_RESPONSEВідповідь із контекстом середовища виконання WordPress/WooCommerce
ALTER_WP_LOCAL_DESIGN_RESPONSEВідповідь із початковими даними локального дизайну WordPress

Повний приклад (HTML + JS)

1<!DOCTYPE html>
2<html lang="en">
3<head>
4  <meta charset="UTF-8" />
5  <title>Alter Product Viewer 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>Viewer — Two-Way Communication via postMessage</h1>
18
19  <div class="row">
20    <button id="btnGetData" disabled>Request Current Payload</button>
21  </div>
22
23  <iframe
24    id="viewerWidget"
25    src="https://alterproduct.com/app/viewer/1?nav=0&add_to_cart=1"
26    title="Alter Product Viewer"
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    const iframe = document.getElementById('viewerWidget');
36    const logEl = document.getElementById('log');
37    const btnGetData = document.getElementById('btnGetData');
38
39    let handshakeOk = false;
40    let tokenReady = false;
41
42    function log(...args) {
43      const line = args.map(a => (typeof a === 'string' ? a : JSON.stringify(a, null, 2))).join(' ');
44      logEl.textContent += line + '\n';
45    }
46
47    function postToIframe(payload) {
48      iframe.contentWindow.postMessage(payload, IFRAME_ORIGIN);
49    }
50
51    /**
52     * Your backend MUST request a session token (recommended).
53     * This endpoint is owned by YOU and should call Alter Product API using server-side credentials.
54     * Expected response: { sessionToken: "..." }
55     */
56    async function getSessionTokenFromYourBackend({ tool, mode, uiDesignId }) {
57      const url = new URL('/api/alter/session-token', window.location.origin);
58      url.searchParams.set('tool', tool);
59      url.searchParams.set('mode', mode || 'design');
60      url.searchParams.set('uiDesignId', String(uiDesignId || 0));
61
62      const res = await fetch(url.toString(), { method: 'GET' });
63      if (!res.ok) throw new Error('Failed to get session token');
64      const data = await res.json();
65      if (!data || !data.sessionToken) throw new Error('Missing sessionToken');
66      return data.sessionToken;
67    }
68
69    // UI: request product data after the iframe has received a session token.
70    btnGetData.addEventListener('click', () => {
71      if (!tokenReady) return;
72      log('[Host] -> Viewer: ALTER_VIEWER_GET_PRODUCT_DATA');
73      postToIframe({ type: 'ALTER_VIEWER_GET_PRODUCT_DATA' });
74    });
75
76    // Handle messages from the Viewer (iframe)
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      // Viewer -> Host: ALTER_CHILD_HELLO { nonce }
90      // Host   -> Viewer: 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('[Viewer] -> Host: ALTER_CHILD_HELLO', { nonce });
98
99        log('[Host] -> Viewer: 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      // Viewer -> Host: ALTER_TOOL_INIT_SESSION { payload: { tool, mode, uiDesignId } }
111      // Host   -> Viewer: 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 || 'viewer').toLowerCase();
117          const mode = String(payload.mode || 'design');
118          const uiDesignId = Number(payload.uiDesignId || 0);
119
120          log('[Viewer] -> Host: ALTER_TOOL_INIT_SESSION', { tool, mode, uiDesignId });
121
122          const token = await getSessionTokenFromYourBackend({ tool, mode, uiDesignId });
123
124          log('[Host] -> Viewer: ALTER_TOOL_SESSION_READY');
125          postToIframe({ type: 'ALTER_TOOL_SESSION_READY', tool, token });
126          tokenReady = true;
127          btnGetData.disabled = false;
128        } catch (e) {
129          log('[Host] -> Viewer: ALTER_TOOL_SESSION_ERROR', String(e && e.message ? e.message : e));
130          postToIframe({ type: 'ALTER_TOOL_SESSION_ERROR', message: String(e && e.message ? e.message : e) });
131        }
132        return;
133      }
134
135      // -----------------------
136      // C) Viewer events
137      // -----------------------
138      if (msg.type === 'ALTER_VIEWER_DATA_RESPONSE') {
139        log('[Viewer] -> Host: ALTER_VIEWER_DATA_RESPONSE', msg.payload);
140        return;
141      }
142
143      if (msg.type === 'ALTER_VIEWER_ADD_TO_CART') {
144        log('[Viewer] -> Host: ALTER_VIEWER_ADD_TO_CART', msg.payload);
145        return;
146      }
147    });
148  </script>
149</body>
150</html>

Приклад даних

{
  "userDesignId": 10,
  "designName": "Mug 450ml (15oz)",
  "totalPrice": {
    "value": 24.99,
    "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"
          }
        }
      },
      "printingMethod": {
        "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",
        "customColor": false,
        "pickedColors": {},
        "patternId": null,
        "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,
        "printingMethodId": 5,
        "colorId": 11,
        "metadata": {
          "sku": "MUG-450-WHITE"
        },
        "minOrderQuantity": 1,
        "processingTime": null,
        "stockQuantity": null,
        "volume": null,
        "weight": null
      },
      "unitPrice": {
        "value": 24.99,
        "currency": "EUR"
      },
      "totalPrice": {
        "value": 24.99,
        "currency": "EUR"
      },
      "quantity": 1
    }
  ]
}