@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 +7 -35
- package/package.json +4 -4
- package/src/utils/__tests__/analytics.test.ts +73 -0
- package/src/utils/analytics.ts +30 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,40 +1,12 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.129
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
});
|
package/src/utils/analytics.ts
CHANGED
|
@@ -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(
|