@akinon/pz-theme 2.0.129-rc.0 → 2.0.129

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,40 +1,12 @@
1
1
  # @akinon/pz-theme
2
2
 
3
- ## 2.0.129-rc.0
4
-
5
- ### Patch Changes
6
-
7
- - Updated dependencies [86187b1c2]
8
- - Updated dependencies [0cf9ea239]
9
- - Updated dependencies [324f97d55]
10
- - Updated dependencies [51ea06888]
11
- - Updated dependencies
12
- - Updated dependencies [2fea41430]
13
- - Updated dependencies [b55acb768]
14
- - Updated dependencies [760258c1c]
15
- - Updated dependencies [143be2b9d]
16
- - Updated dependencies [7889b08fe]
17
- - Updated dependencies [9f8cd3bc5]
18
- - Updated dependencies [bfafa3f49]
19
- - Updated dependencies [57d7eb305]
20
- - Updated dependencies [d99a6a7d5]
21
- - Updated dependencies [9db81a714]
22
- - Updated dependencies [591e345e1]
23
- - Updated dependencies [4de5303c5]
24
- - Updated dependencies [95b139dc1]
25
- - Updated dependencies [1d00f2d06]
26
- - Updated dependencies [4ac7b2a1e]
27
- - Updated dependencies [e9598c71d]
28
- - Updated dependencies [4998a9631]
29
- - Updated dependencies [804d2bd6c]
30
- - Updated dependencies [3909d3224]
31
- - Updated dependencies [6a3d8a631]
32
- - Updated dependencies [e18836b20]
33
- - Updated dependencies [b1111d7b1]
34
- - Updated dependencies [1595d458]
35
- - Updated dependencies [dca12811]
36
- - @akinon/next@2.0.129-rc.0
37
- - @akinon/theme-contract@2.0.129-rc.0
3
+ ## 2.0.129
4
+
5
+ ### Patch Changes
6
+
7
+ - 68666c4: ZERO-5159: Stop commerce events firing while the theme editor is attached
8
+ - @akinon/next@2.0.129
9
+ - @akinon/theme-contract@2.0.129
38
10
 
39
11
  ## 2.0.128
40
12
 
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@akinon/pz-theme",
3
3
  "description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
4
- "version": "2.0.129-rc.0",
4
+ "version": "2.0.129",
5
5
  "license": "MIT",
6
6
  "main": "src/index.ts",
7
7
  "scripts": {
8
8
  "test": "jest"
9
9
  },
10
10
  "peerDependencies": {
11
- "@akinon/next": "2.0.129-rc.0",
11
+ "@akinon/next": "2.0.129",
12
12
  "react": "^18.0.0 || ^19.0.0",
13
13
  "react-dom": "^18.0.0 || ^19.0.0"
14
14
  },
15
15
  "dependencies": {
16
- "@akinon/theme-contract": "2.0.129-rc.0",
16
+ "@akinon/theme-contract": "2.0.129",
17
17
  "clsx": "2.1.1",
18
18
  "lottie-web": "^5.13.0",
19
19
  "react-multi-carousel": "2.8.4",
@@ -21,7 +21,7 @@
21
21
  "zod": "4.4.1"
22
22
  },
23
23
  "devDependencies": {
24
- "@akinon/next": "2.0.129-rc.0",
24
+ "@akinon/next": "2.0.129",
25
25
  "@testing-library/jest-dom": "5.16.4",
26
26
  "@testing-library/react": "16.3.2",
27
27
  "@testing-library/user-event": "14.1.1",
@@ -8,6 +8,7 @@ import {
8
8
 
9
9
  declare global {
10
10
  interface Window {
11
+ __themeEditorDesigner?: boolean;
11
12
  // Must match the augmentation in ../analytics, and the core package's.
12
13
  // eslint-disable-next-line @typescript-eslint/ban-types
13
14
  dataLayer?: Object[];
@@ -266,3 +267,75 @@ describe('default GA4 dataLayer bridge', () => {
266
267
  expect(window.dataLayer).toEqual([]);
267
268
  });
268
269
  });
270
+
271
+ describe('the editor gate (ZERO-5159)', () => {
272
+ let received: Array<Record<string, unknown>>;
273
+ let listener: (event: Event) => void;
274
+
275
+ beforeEach(() => {
276
+ received = [];
277
+ listener = (event) => received.push((event as CustomEvent).detail);
278
+ window.addEventListener(COMMERCE_EVENT_NAME, listener);
279
+ });
280
+
281
+ afterEach(() => {
282
+ window.removeEventListener(COMMERCE_EVENT_NAME, listener);
283
+ delete window.__themeEditorDesigner;
284
+ });
285
+
286
+ it('reports nothing while the theme editor is attached', () => {
287
+ window.__themeEditorDesigner = true;
288
+
289
+ emitCommerceEvent('add_to_cart', {
290
+ items: [buildCommerceItem({ pk: 1, name: 'A' })]
291
+ });
292
+
293
+ expect(received).toEqual([]);
294
+ });
295
+
296
+ it('covers the page-scope moments too, not just the block ones', () => {
297
+ // view_item / view_cart / begin_checkout / purchase all reach the wire
298
+ // through this same emitter.
299
+ window.__themeEditorDesigner = true;
300
+
301
+ emitCommerceEvent('purchase', {
302
+ transactionId: 'ORDER-1',
303
+ value: 199,
304
+ items: [buildCommerceItem({ pk: 1, name: 'A' })]
305
+ });
306
+
307
+ expect(received).toEqual([]);
308
+ });
309
+
310
+ it('is the raw editor flag, so browse mode is silent as well', () => {
311
+ // A block is handed `isDesigner && !isBrowseMode`; in browse mode that
312
+ // prop is false while the editor is still attached, which is the state
313
+ // this gate exists for. Nothing here consults that prop.
314
+ window.__themeEditorDesigner = true;
315
+
316
+ emitCommerceEvent('select_item', {
317
+ items: [buildCommerceItem({ pk: 7, name: 'Shirt' })]
318
+ });
319
+
320
+ expect(received).toEqual([]);
321
+ });
322
+
323
+ it('keeps reporting when no editor is attached', () => {
324
+ window.__themeEditorDesigner = false;
325
+
326
+ emitCommerceEvent('search', { searchTerm: 'elbise' });
327
+
328
+ expect(received).toEqual([{ action: 'search', searchTerm: 'elbise' }]);
329
+ });
330
+
331
+ it('keeps reporting on a storefront that is merely framed', () => {
332
+ // Framing is not evidence of the editor: payment providers and
333
+ // marketplace previews frame a production storefront, and those events
334
+ // belong to real shoppers.
335
+ delete window.__themeEditorDesigner;
336
+
337
+ emitCommerceEvent('search', { searchTerm: 'ceket' });
338
+
339
+ expect(received).toHaveLength(1);
340
+ });
341
+ });
@@ -146,8 +146,7 @@ export const buildCommerceItem = (
146
146
 
147
147
  const baseCode = toText(source.base_code);
148
148
  const sku = toText(source.sku);
149
- const id =
150
- baseCode ?? sku ?? toText(source.productPk) ?? toText(source.pk);
149
+ const id = baseCode ?? sku ?? toText(source.productPk) ?? toText(source.pk);
151
150
  const name = toText(source.name) ?? toText(source.title);
152
151
 
153
152
  if (!id && !name) return null;
@@ -160,9 +159,7 @@ export const buildCommerceItem = (
160
159
  parseAmount(source.retailPriceAmount) ??
161
160
  parseAmount(source.retail_price) ??
162
161
  parseAmount(source.retailPrice) ??
163
- parseAmount(
164
- (source.active_price as Record<string, unknown>)?.retail_price
165
- );
162
+ parseAmount((source.active_price as Record<string, unknown>)?.retail_price);
166
163
  const attributes = readItemAttributes(source);
167
164
 
168
165
  return {
@@ -184,6 +181,31 @@ export const readItemsCurrency = (
184
181
  ): string | undefined =>
185
182
  toText(source?.currency_type) ?? toText(source?.currency) ?? toText(fallback);
186
183
 
184
+ /**
185
+ * True while the theme editor is attached — edit mode AND browse mode.
186
+ *
187
+ * Read inline rather than imported, the way `overlays/theme-modal-mount.tsx`,
188
+ * `blocks/video-block.tsx`, `chrome/checkout-piece.tsx` and
189
+ * `placeholder-registry.ts` read the same flag: this module is reachable from
190
+ * both graphs and must keep its single import.
191
+ *
192
+ * THE RAW FLAG, not the `isDesigner` a block receives. A block is handed
193
+ * `isDesigner && !isBrowseMode`, so the moment a designer switches to browse
194
+ * mode that prop goes false while the editor is still attached — and every
195
+ * block-level emit below fired for real, indistinguishable from a shopper's.
196
+ * The gate belongs here rather than at the fifteen call sites: it cannot be
197
+ * forgotten by the sixteenth, and it needs no change to what `isDesigner`
198
+ * means for sections and blocks, which is what browse mode is built on.
199
+ *
200
+ * Framing is deliberately not part of it. A production storefront is
201
+ * legitimately inside an iframe — payment providers, marketplace previews —
202
+ * and those shoppers' events are real.
203
+ */
204
+ const isThemeEditorAttached = (): boolean =>
205
+ typeof window !== 'undefined' &&
206
+ (window as { __themeEditorDesigner?: boolean }).__themeEditorDesigner ===
207
+ true;
208
+
187
209
  /**
188
210
  * Emit one commerce moment. `items` may carry nulls from the builder; they are
189
211
  * dropped, and an action whose items ALL dropped is not emitted.
@@ -202,6 +224,9 @@ export const emitCommerceEvent = (
202
224
  ): void => {
203
225
  if (typeof window === 'undefined') return;
204
226
 
227
+ // Nothing reported from inside the editor, browse mode included.
228
+ if (isThemeEditorAttached()) return;
229
+
205
230
  try {
206
231
  const { items, value, currency, ...rest } = detail;
207
232
  const presentItems = items?.filter(