Viewer – Comunicare bidirecțională prin postMessage

Introducere

Instanțele Viewer încorporate comunică cu pagina gazdă prin postMessage. Mediul de execuție actual începe cu un handshake nonce, apoi solicită un token de încorporare prin ALTER_TOOL_INIT_SESSION.

Configurare:

  1. În aplicația Alter Product, deschide panoul Setări e-commerce – Încorporare.
  2. Adaugă domeniul în care va fi încorporat Viewer, de exemplu https://yourwebsite.com sau http://localhost:3000, și salvează.

Flux:

  1. Site-ul tău încarcă iframe-ul Viewer.
  2. Iframe-ul trimite ALTER_CHILD_HELLO; pagina răspunde cu ALTER_PARENT_ACK și același nonce.
  3. Iframe-ul solicită un token prin ALTER_TOOL_INIT_SESSION, cu payload.tool setat la viewer.
  4. Backendul creează tokenul prin Alter Product Public API, iar pagina răspunde cu ALTER_TOOL_SESSION_READY.
  5. Cu add_to_cart=1, Viewer poate emite ALTER_VIEWER_ADD_TO_CART. Gazda poate solicita și payloadul curent cu ALTER_VIEWER_GET_PRODUCT_DATA și poate primi ALTER_VIEWER_DATA_RESPONSE.

Payloadul include metadatele variantelor definite în Configurarea produsului.

Mesaje trimise de Viewer

TipDescriere
ALTER_CHILD_HELLOInițializarea handshake-ului (conține un nonce)
ALTER_TOOL_INIT_SESSIONSolicită un token de sesiune de la site-ul gazdă
ALTER_VIEWER_ADD_TO_CARTUtilizatorul a apăsat „Adaugă în coș” într-un Viewer încorporat cu add_to_cart=1
ALTER_VIEWER_DATA_RESPONSERăspuns la solicitarea datelor produsului
ALTER_WP_RUNTIME_CONTEXT_REQUESTSolicitare de context de execuție WordPress/WooCommerce
ALTER_WP_LOCAL_DESIGN_REQUESTSolicitare de inițializare a designului local WordPress când wp_local_design=1

Mesaje primite de Viewer

TipDescriere
ALTER_PARENT_ACKConfirmarea handshake-ului (trebuie să includă nonce-ul)
ALTER_TOOL_SESSION_READYFurnizează tokenul de sesiune pentru autorizarea Viewer
ALTER_TOOL_SESSION_ERRORSolicitarea tokenului a eșuat (eroare pe gazdă)
ALTER_VIEWER_GET_PRODUCT_DATASolicită datele produsului de la Viewer
ALTER_WP_RUNTIME_CONTEXT_RESPONSERăspuns cu contextul de execuție WordPress/WooCommerce
ALTER_WP_LOCAL_DESIGN_RESPONSERăspuns de inițializare a designului local WordPress

Exemplu complet (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>

Exemplu de payload

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