Viewer — Αμφίδρομη επικοινωνία μέσω postMessage

Εισαγωγή

Τα ενσωματωμένα Viewer επικοινωνούν με τη σελίδα υποδοχής μέσω postMessage. Το τρέχον περιβάλλον εκτέλεσης ξεκινά με χειραψία nonce και στη συνέχεια ζητά token ενσωμάτωσης μέσω ALTER_TOOL_INIT_SESSION.

Ρύθμιση:

  1. Στην εφαρμογή Alter Product, ανοίξτε τον πίνακα Ρυθμίσεις ηλεκτρονικού εμπορίου — Ενσωμάτωση.
  2. Προσθέστε το domain όπου θα ενσωματωθεί το Viewer, π.χ. https://yourwebsite.com ή http://localhost:3000, και αποθηκεύστε.

Ροή:

  1. Ο ιστότοπός σας φορτώνει το iframe του Viewer.
  2. Το iframe στέλνει ALTER_CHILD_HELLO· η σελίδα απαντά με ALTER_PARENT_ACK και το ίδιο nonce.
  3. Το iframe ζητά token με ALTER_TOOL_INIT_SESSION και payload.tool ορισμένο σε viewer.
  4. Το backend δημιουργεί το token μέσω 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Ζητά token συνεδρίας από τον ιστότοπο υποδοχής
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Παρέχει token συνεδρίας για εξουσιοδότηση του Viewer
ALTER_TOOL_SESSION_ERRORΤο αίτημα token απέτυχε (σφάλμα στον ιστότοπο υποδοχής)
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
    }
  ]
}