@akinon/projectzero 2.0.79-rc.0 → 2.0.79
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 +6 -4
- package/app-template/.env.example +10 -1
- package/app-template/AGENTS.md +0 -8
- package/app-template/CHANGELOG.md +38 -81
- package/app-template/README.md +1 -25
- package/app-template/next.config.mjs +1 -7
- package/app-template/package.json +41 -41
- package/app-template/src/app/[pz]/layout.tsx +20 -0
- package/app-template/src/app/api/preview/link/route.ts +115 -0
- package/app-template/src/app/api/preview/route.ts +96 -0
- package/app-template/src/hooks/index.ts +0 -2
- package/app-template/src/hooks/use-fav-button.tsx +13 -35
- package/app-template/src/plugins.js +0 -1
- package/app-template/src/proxy.ts +47 -4
- package/app-template/src/settings.js +17 -0
- package/app-template/src/utils/__tests__/preview-contract.test.ts +223 -0
- package/app-template/src/views/category/category-info.tsx +17 -25
- package/app-template/src/views/header/search/index.tsx +5 -13
- package/app-template/src/views/product/slider.tsx +38 -85
- package/app-template/src/views/product-item/index.tsx +1 -1
- package/app-template/tailwind.config.js +2 -0
- package/commands/plugins.ts +0 -4
- package/dist/commands/plugins.js +0 -4
- package/package.json +1 -1
- package/app-template/src/app/api/client-env/route.ts +0 -5
- package/app-template/src/app/global-error.tsx +0 -22
- package/app-template/src/hooks/favourite-product-ids-context.tsx +0 -104
- package/app-template/src/utils/__tests__/favourite-product-ids-fallback.test.ts +0 -42
- package/app-template/src/utils/favourite-product-ids-fallback.ts +0 -51
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# @akinon/projectzero
|
|
2
2
|
|
|
3
|
-
## 2.0.79
|
|
3
|
+
## 2.0.79
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
- 945e153d: ZERO-4669: Keep the designer action toolbar reachable at the top of the canvas.
|
|
8
|
+
|
|
9
|
+
The toolbar is portaled to `<body>` (ZERO-4651), so nothing can clip it any more — but hanging above its host still draws it at a negative y when the host sits at the viewport top: the first section at scroll 0, and any section in a chrome slot pinned with `position: sticky; top: 0`, which reports the same box at every scroll position and so was unreachable permanently. The toolbar now flips inside its host when there is not enough room above it on screen, while the portal frame stays pinned to the host's box.
|
|
10
|
+
|
|
11
|
+
Also adds the `@akinon/pz-theme` content globs to the app's Tailwind config: without them the toolbar's own utilities (`-top-0.5`, `top-1`, `-translate-y-full`, `bg-[#4482ff]`) are never generated, so the designer chrome rendered unstyled. The app template carries the same two lines, so scaffolded brand apps are not shipping unstyled designer chrome either.
|
|
10
12
|
|
|
11
13
|
## 2.0.78
|
|
12
14
|
|
|
@@ -7,7 +7,6 @@ NEXT_PUBLIC_URL=http://localhost:3000
|
|
|
7
7
|
SERVICE_BACKEND_URL=https://02fde10fee4440269e695aa10707dfaf.lb.akinoncloud.com
|
|
8
8
|
SITEMAP_S3_BUCKET_NAME=0fb534
|
|
9
9
|
NEXT_PUBLIC_VIRTUAL_TRY_ON_API_URL=https://d2a26507c7094f359aba349b96a96881.lb.akinoncloud.com
|
|
10
|
-
NEXT_PUBLIC_ENABLE_IMAGE_SEARCH=true
|
|
11
10
|
|
|
12
11
|
# PZ Dashboard dataLayer observability (unset = observer disabled)
|
|
13
12
|
# NEXT_PUBLIC_PZ_DASHBOARD_URL=http://localhost:3005
|
|
@@ -22,3 +21,13 @@ NEXT_PUBLIC_ENABLE_IMAGE_SEARCH=true
|
|
|
22
21
|
# CACHE_KEY_PREFIX=
|
|
23
22
|
|
|
24
23
|
# BF_CACHE=false
|
|
24
|
+
|
|
25
|
+
# Widget preview (theme editor named previews).
|
|
26
|
+
# PREVIEW_SECRET signs the expiring preview links the editor requests from
|
|
27
|
+
# /api/preview/link; it never travels in a url. Unset = preview disabled.
|
|
28
|
+
# OMNITRON_URL is the Omnitron the editor is signed into — the link endpoint
|
|
29
|
+
# verifies the editor's session token there before minting a link.
|
|
30
|
+
# PREVIEW_LINK_TTL_SECONDS: how long a link stays valid (default 86400 = 24h).
|
|
31
|
+
# PREVIEW_SECRET=
|
|
32
|
+
# OMNITRON_URL=https://<omnitron-host>
|
|
33
|
+
# PREVIEW_LINK_TTL_SECONDS=86400
|
package/app-template/AGENTS.md
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
<!-- BEGIN:nextjs-agent-rules -->
|
|
2
|
-
|
|
3
|
-
# Next.js: ALWAYS read docs before coding
|
|
4
|
-
|
|
5
|
-
Before any Next.js work, find and read the relevant doc in `node_modules/next/dist/docs/`. Your training data is outdated — the docs are the source of truth.
|
|
6
|
-
|
|
7
|
-
<!-- END:nextjs-agent-rules -->
|
|
8
|
-
|
|
9
1
|
# Project Zero Next.js - Developer Instructions
|
|
10
2
|
|
|
11
3
|
## 📚 Available Instruction Files
|
|
@@ -1,86 +1,43 @@
|
|
|
1
1
|
# projectzeronext
|
|
2
2
|
|
|
3
|
-
## 2.0.79
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
- Updated dependencies [9db81a714]
|
|
42
|
-
- Updated dependencies [591e345e1]
|
|
43
|
-
- Updated dependencies [0df6bc1f8]
|
|
44
|
-
- Updated dependencies [4de5303c5]
|
|
45
|
-
- Updated dependencies [63a720003]
|
|
46
|
-
- Updated dependencies [95b139dc1]
|
|
47
|
-
- Updated dependencies [1d00f2d06]
|
|
48
|
-
- Updated dependencies [4ac7b2a1e]
|
|
49
|
-
- Updated dependencies [e9598c71]
|
|
50
|
-
- Updated dependencies [4998a9631]
|
|
51
|
-
- Updated dependencies [804d2bd6c]
|
|
52
|
-
- Updated dependencies [c0228aff0]
|
|
53
|
-
- Updated dependencies [3909d3224]
|
|
54
|
-
- Updated dependencies [6a3d8a631]
|
|
55
|
-
- Updated dependencies [e18836b20]
|
|
56
|
-
- Updated dependencies [b1111d7b]
|
|
57
|
-
- @akinon/pz-tabby-extension@2.0.79-rc.0
|
|
58
|
-
- @akinon/next@2.0.79-rc.0
|
|
59
|
-
- @akinon/pz-checkout-gift-pack@2.0.79-rc.0
|
|
60
|
-
- @akinon/pz-masterpass-rest@2.0.79-rc.0
|
|
61
|
-
- @akinon/pz-one-click-checkout@2.0.79-rc.0
|
|
62
|
-
- @akinon/pz-basket-gift-pack@2.0.79-rc.0
|
|
63
|
-
- @akinon/pz-pay-on-delivery@2.0.79-rc.0
|
|
64
|
-
- @akinon/pz-credit-payment@2.0.79-rc.0
|
|
65
|
-
- @akinon/pz-click-collect@2.0.79-rc.0
|
|
66
|
-
- @akinon/pz-multi-basket@2.0.79-rc.0
|
|
67
|
-
- @akinon/pz-masterpass@2.0.79-rc.0
|
|
68
|
-
- @akinon/pz-gpay@2.0.79-rc.0
|
|
69
|
-
- @akinon/pz-b2b@2.0.79-rc.0
|
|
70
|
-
- @akinon/pz-bkm@2.0.79-rc.0
|
|
71
|
-
- @akinon/pz-otp@2.0.79-rc.0
|
|
72
|
-
- @akinon/pz-similar-products@2.0.79-rc.0
|
|
73
|
-
- @akinon/pz-virtual-try-on@2.0.79-rc.0
|
|
74
|
-
- @akinon/pz-theme@2.0.79-rc.0
|
|
75
|
-
- @akinon/pz-akifast@2.0.79-rc.0
|
|
76
|
-
- @akinon/pz-apple-pay@2.0.79-rc.0
|
|
77
|
-
- @akinon/pz-cybersource-uc@2.0.79-rc.0
|
|
78
|
-
- @akinon/pz-flow-payment@2.0.79-rc.0
|
|
79
|
-
- @akinon/pz-google-pay@2.0.79-rc.0
|
|
80
|
-
- @akinon/pz-haso@2.0.79-rc.0
|
|
81
|
-
- @akinon/pz-hepsipay@2.0.79-rc.0
|
|
82
|
-
- @akinon/pz-saved-card@2.0.79-rc.0
|
|
83
|
-
- @akinon/pz-tamara-extension@2.0.79-rc.0
|
|
3
|
+
## 2.0.79
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 945e153d: ZERO-4669: Keep the designer action toolbar reachable at the top of the canvas.
|
|
8
|
+
|
|
9
|
+
The toolbar is portaled to `<body>` (ZERO-4651), so nothing can clip it any more — but hanging above its host still draws it at a negative y when the host sits at the viewport top: the first section at scroll 0, and any section in a chrome slot pinned with `position: sticky; top: 0`, which reports the same box at every scroll position and so was unreachable permanently. The toolbar now flips inside its host when there is not enough room above it on screen, while the portal frame stays pinned to the host's box.
|
|
10
|
+
|
|
11
|
+
Also adds the `@akinon/pz-theme` content globs to the app's Tailwind config: without them the toolbar's own utilities (`-top-0.5`, `top-1`, `-translate-y-full`, `bg-[#4482ff]`) are never generated, so the designer chrome rendered unstyled. The app template carries the same two lines, so scaffolded brand apps are not shipping unstyled designer chrome either.
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [945e153d]
|
|
14
|
+
- @akinon/pz-theme@2.0.79
|
|
15
|
+
- @akinon/next@2.0.79
|
|
16
|
+
- @akinon/pz-akifast@2.0.79
|
|
17
|
+
- @akinon/pz-apple-pay@2.0.79
|
|
18
|
+
- @akinon/pz-b2b@2.0.79
|
|
19
|
+
- @akinon/pz-basket-gift-pack@2.0.79
|
|
20
|
+
- @akinon/pz-bkm@2.0.79
|
|
21
|
+
- @akinon/pz-checkout-gift-pack@2.0.79
|
|
22
|
+
- @akinon/pz-click-collect@2.0.79
|
|
23
|
+
- @akinon/pz-credit-payment@2.0.79
|
|
24
|
+
- @akinon/pz-cybersource-uc@2.0.79
|
|
25
|
+
- @akinon/pz-flow-payment@2.0.79
|
|
26
|
+
- @akinon/pz-google-pay@2.0.79
|
|
27
|
+
- @akinon/pz-gpay@2.0.79
|
|
28
|
+
- @akinon/pz-haso@2.0.79
|
|
29
|
+
- @akinon/pz-hepsipay@2.0.79
|
|
30
|
+
- @akinon/pz-masterpass@2.0.79
|
|
31
|
+
- @akinon/pz-masterpass-rest@2.0.79
|
|
32
|
+
- @akinon/pz-multi-basket@2.0.79
|
|
33
|
+
- @akinon/pz-one-click-checkout@2.0.79
|
|
34
|
+
- @akinon/pz-otp@2.0.79
|
|
35
|
+
- @akinon/pz-pay-on-delivery@2.0.79
|
|
36
|
+
- @akinon/pz-saved-card@2.0.79
|
|
37
|
+
- @akinon/pz-similar-products@2.0.79
|
|
38
|
+
- @akinon/pz-tabby-extension@2.0.79
|
|
39
|
+
- @akinon/pz-tamara-extension@2.0.79
|
|
40
|
+
- @akinon/pz-virtual-try-on@2.0.79
|
|
84
41
|
|
|
85
42
|
## 2.0.78
|
|
86
43
|
|
package/app-template/README.md
CHANGED
|
@@ -12,30 +12,6 @@ Run `cp .env.example .env && yarn` command at project root and all dependencies
|
|
|
12
12
|
|
|
13
13
|
- `SITEMAP_S3_BUCKET_NAME`: S3 bucket name for XML sitemaps (e.g., "0fb534"). This is required for the sitemap route to function correctly.
|
|
14
14
|
|
|
15
|
-
### Optional Environment Variables
|
|
16
|
-
|
|
17
|
-
- `NEXT_PUBLIC_ENABLE_IMAGE_SEARCH`: Set to `true` to enable image search feature for all users. If not set or `false`, the feature will only be visible to users who have enabled it via localStorage.
|
|
18
|
-
|
|
19
|
-
## Image Search Feature
|
|
20
|
-
|
|
21
|
-
The image search functionality allows users to search for similar products by uploading images or cropping existing product images.
|
|
22
|
-
|
|
23
|
-
### Feature Visibility Control
|
|
24
|
-
|
|
25
|
-
The image search feature can be controlled in two ways:
|
|
26
|
-
|
|
27
|
-
1. **Environment Variable (Global)**: Set `NEXT_PUBLIC_ENABLE_IMAGE_SEARCH=true` to enable for all users
|
|
28
|
-
2. **localStorage (Individual Users)**: Users can enable it locally using browser developer tools
|
|
29
|
-
|
|
30
|
-
````
|
|
31
|
-
|
|
32
|
-
### Feature Locations
|
|
33
|
-
|
|
34
|
-
When enabled, the image search feature appears in:
|
|
35
|
-
|
|
36
|
-
- **Header Search**: Camera icon next to the search input
|
|
37
|
-
- **Product Detail Page**: "View Similar Styles" button on product images
|
|
38
|
-
|
|
39
15
|
### Build
|
|
40
16
|
|
|
41
17
|
To build the app, run the following command:
|
|
@@ -43,7 +19,7 @@ To build the app, run the following command:
|
|
|
43
19
|
```bash
|
|
44
20
|
cd projectzeronext # Project root
|
|
45
21
|
yarn build
|
|
46
|
-
|
|
22
|
+
```
|
|
47
23
|
|
|
48
24
|
### Develop
|
|
49
25
|
|
|
@@ -38,9 +38,6 @@ const nextConfig = {
|
|
|
38
38
|
]
|
|
39
39
|
}
|
|
40
40
|
];
|
|
41
|
-
},
|
|
42
|
-
logging: {
|
|
43
|
-
browserToTerminal: 'error'
|
|
44
41
|
}
|
|
45
42
|
};
|
|
46
43
|
|
|
@@ -53,12 +50,9 @@ const withSerwist = withSerwistInit({
|
|
|
53
50
|
const sentryConfig = {
|
|
54
51
|
silent: true,
|
|
55
52
|
dryRun: !process.env.SENTRY_DSN,
|
|
56
|
-
org: 'akinon'
|
|
53
|
+
org: 'akinon'
|
|
57
54
|
// project: 'enter_your_project_name_here',
|
|
58
55
|
// authToken: 'enter_your_auth_token_here'
|
|
59
|
-
sourcemaps: {
|
|
60
|
-
deleteSourcemapsAfterUpload: true
|
|
61
|
-
}
|
|
62
56
|
};
|
|
63
57
|
|
|
64
58
|
const enhancedConfig = withPzConfig(nextConfig);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectzeronext",
|
|
3
|
-
"version": "2.0.79
|
|
3
|
+
"version": "2.0.79",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"scripts": {
|
|
@@ -24,33 +24,33 @@
|
|
|
24
24
|
"test:middleware": "jest middleware-matcher.test.ts --bail"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@akinon/next": "2.0.79
|
|
28
|
-
"@akinon/pz-akifast": "2.0.79
|
|
29
|
-
"@akinon/pz-apple-pay": "2.0.79
|
|
30
|
-
"@akinon/pz-b2b": "2.0.79
|
|
31
|
-
"@akinon/pz-basket-gift-pack": "2.0.79
|
|
32
|
-
"@akinon/pz-bkm": "2.0.79
|
|
33
|
-
"@akinon/pz-checkout-gift-pack": "2.0.79
|
|
34
|
-
"@akinon/pz-click-collect": "2.0.79
|
|
35
|
-
"@akinon/pz-credit-payment": "2.0.79
|
|
36
|
-
"@akinon/pz-cybersource-uc": "2.0.79
|
|
37
|
-
"@akinon/pz-flow-payment": "2.0.79
|
|
38
|
-
"@akinon/pz-google-pay": "2.0.79
|
|
39
|
-
"@akinon/pz-gpay": "2.0.79
|
|
40
|
-
"@akinon/pz-haso": "2.0.79
|
|
41
|
-
"@akinon/pz-hepsipay": "2.0.79
|
|
42
|
-
"@akinon/pz-masterpass": "2.0.79
|
|
43
|
-
"@akinon/pz-masterpass-rest": "2.0.79
|
|
44
|
-
"@akinon/pz-multi-basket": "2.0.79
|
|
45
|
-
"@akinon/pz-one-click-checkout": "2.0.79
|
|
46
|
-
"@akinon/pz-otp": "2.0.79
|
|
47
|
-
"@akinon/pz-pay-on-delivery": "2.0.79
|
|
48
|
-
"@akinon/pz-saved-card": "2.0.79
|
|
49
|
-
"@akinon/pz-similar-products": "2.0.79
|
|
50
|
-
"@akinon/pz-tabby-extension": "2.0.79
|
|
51
|
-
"@akinon/pz-tamara-extension": "2.0.79
|
|
52
|
-
"@akinon/pz-theme": "2.0.79
|
|
53
|
-
"@akinon/pz-virtual-try-on": "2.0.79
|
|
27
|
+
"@akinon/next": "2.0.79",
|
|
28
|
+
"@akinon/pz-akifast": "2.0.79",
|
|
29
|
+
"@akinon/pz-apple-pay": "2.0.79",
|
|
30
|
+
"@akinon/pz-b2b": "2.0.79",
|
|
31
|
+
"@akinon/pz-basket-gift-pack": "2.0.79",
|
|
32
|
+
"@akinon/pz-bkm": "2.0.79",
|
|
33
|
+
"@akinon/pz-checkout-gift-pack": "2.0.79",
|
|
34
|
+
"@akinon/pz-click-collect": "2.0.79",
|
|
35
|
+
"@akinon/pz-credit-payment": "2.0.79",
|
|
36
|
+
"@akinon/pz-cybersource-uc": "2.0.79",
|
|
37
|
+
"@akinon/pz-flow-payment": "2.0.79",
|
|
38
|
+
"@akinon/pz-google-pay": "2.0.79",
|
|
39
|
+
"@akinon/pz-gpay": "2.0.79",
|
|
40
|
+
"@akinon/pz-haso": "2.0.79",
|
|
41
|
+
"@akinon/pz-hepsipay": "2.0.79",
|
|
42
|
+
"@akinon/pz-masterpass": "2.0.79",
|
|
43
|
+
"@akinon/pz-masterpass-rest": "2.0.79",
|
|
44
|
+
"@akinon/pz-multi-basket": "2.0.79",
|
|
45
|
+
"@akinon/pz-one-click-checkout": "2.0.79",
|
|
46
|
+
"@akinon/pz-otp": "2.0.79",
|
|
47
|
+
"@akinon/pz-pay-on-delivery": "2.0.79",
|
|
48
|
+
"@akinon/pz-saved-card": "2.0.79",
|
|
49
|
+
"@akinon/pz-similar-products": "2.0.79",
|
|
50
|
+
"@akinon/pz-tabby-extension": "2.0.79",
|
|
51
|
+
"@akinon/pz-tamara-extension": "2.0.79",
|
|
52
|
+
"@akinon/pz-theme": "2.0.79",
|
|
53
|
+
"@akinon/pz-virtual-try-on": "2.0.79",
|
|
54
54
|
"@hookform/resolvers": "2.9.0",
|
|
55
55
|
"@next/third-parties": "16.2.6",
|
|
56
56
|
"@react-google-maps/api": "2.17.1",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"next": "16.2.6",
|
|
60
60
|
"@serwist/next": "9.5.11",
|
|
61
61
|
"next-auth": "5.0.0-beta.30",
|
|
62
|
-
"pino": "8.
|
|
62
|
+
"pino": "8.11.0",
|
|
63
63
|
"serwist": "9.5.11",
|
|
64
64
|
"postcss": "8.4.49",
|
|
65
65
|
"react": "19.2.5",
|
|
@@ -67,14 +67,14 @@
|
|
|
67
67
|
"react-google-recaptcha": "2.1.0",
|
|
68
68
|
"react-hook-form": "7.71.2",
|
|
69
69
|
"react-intersection-observer": "9.4.0",
|
|
70
|
-
"react-multi-carousel": "2.8.
|
|
71
|
-
"react-string-replace": "1.1.
|
|
72
|
-
"start-server-and-test": "2.0.
|
|
73
|
-
"tailwind-merge": "1.
|
|
70
|
+
"react-multi-carousel": "2.8.4",
|
|
71
|
+
"react-string-replace": "1.1.0",
|
|
72
|
+
"start-server-and-test": "2.0.3",
|
|
73
|
+
"tailwind-merge": "1.8.0",
|
|
74
74
|
"yup": "0.32.11"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@akinon/eslint-plugin-projectzero": "2.0.79
|
|
77
|
+
"@akinon/eslint-plugin-projectzero": "2.0.79",
|
|
78
78
|
"@semantic-release/changelog": "6.0.2",
|
|
79
79
|
"@semantic-release/exec": "6.0.3",
|
|
80
80
|
"@semantic-release/git": "10.0.1",
|
|
@@ -89,25 +89,25 @@
|
|
|
89
89
|
"@types/react": "19.2.14",
|
|
90
90
|
"@types/react-dom": "19.2.3",
|
|
91
91
|
"client-only": "0.0.1",
|
|
92
|
-
"clsx": "1.
|
|
92
|
+
"clsx": "1.1.1",
|
|
93
93
|
"currency-symbol-map": "5.1.0",
|
|
94
94
|
"eslint": "9.39.4",
|
|
95
95
|
"eslint-config-next": "16.2.4",
|
|
96
96
|
"eslint-config-prettier": "10.1.1",
|
|
97
97
|
"husky": "8.0.0",
|
|
98
98
|
"jest": "29.7.0",
|
|
99
|
-
"jest-css-modules-transform": "4.
|
|
99
|
+
"jest-css-modules-transform": "4.3.0",
|
|
100
100
|
"jest-environment-jsdom": "29.7.0",
|
|
101
|
-
"lint-staged": "13.
|
|
102
|
-
"prettier": "2.
|
|
101
|
+
"lint-staged": "13.1.0",
|
|
102
|
+
"prettier": "2.6.2",
|
|
103
103
|
"react-number-format": "5.3.4",
|
|
104
|
-
"sass": "1.
|
|
104
|
+
"sass": "1.49.9",
|
|
105
105
|
"semantic-release": "19.0.5",
|
|
106
106
|
"server-only": "0.0.1",
|
|
107
|
-
"stylelint": "14.
|
|
107
|
+
"stylelint": "14.6.0",
|
|
108
108
|
"stylelint-config-sass-guidelines": "9.0.1",
|
|
109
109
|
"stylelint-config-standard": "25.0.0",
|
|
110
|
-
"stylelint-scss": "4.
|
|
110
|
+
"stylelint-scss": "4.2.0",
|
|
111
111
|
"stylelint-selector-bem-pattern": "2.1.1",
|
|
112
112
|
"tailwindcss": "4.2.0",
|
|
113
113
|
"ts-jest": "29.2.6",
|
|
@@ -14,6 +14,9 @@ import pwaTags from '@theme/components/pwa-tags';
|
|
|
14
14
|
import { OfflineFallback } from '@theme/components/offline-fallback';
|
|
15
15
|
import { InstallPrompt } from '@theme/components/install-prompt';
|
|
16
16
|
import type { Viewport } from 'next';
|
|
17
|
+
import { draftMode } from 'next/headers';
|
|
18
|
+
import { PREVIEW_PZ_SEGMENT } from '@akinon/next/utils/preview';
|
|
19
|
+
import { parsePzParams } from '@akinon/next/utils/pz-segments';
|
|
17
20
|
import settings from '@theme/settings';
|
|
18
21
|
import RouteHandler from '@theme/components/route-handler';
|
|
19
22
|
import { PreviewAuthStateReporter } from '@akinon/pz-theme/src/chrome/preview-auth-state';
|
|
@@ -55,8 +58,15 @@ export async function generateMetadata() {
|
|
|
55
58
|
async function RootLayout({
|
|
56
59
|
locale,
|
|
57
60
|
translations,
|
|
61
|
+
params,
|
|
58
62
|
children
|
|
59
63
|
}: ResolvedRootLayoutProps) {
|
|
64
|
+
// Widget preview indicator — reading draftMode does NOT opt the page into
|
|
65
|
+
// dynamic rendering; it is only true behind the /api/preview bypass cookie.
|
|
66
|
+
const { isEnabled: isPreview } = await draftMode();
|
|
67
|
+
// Which named preview is being viewed, so the banner says so — several can
|
|
68
|
+
// exist side by side and they look identical otherwise.
|
|
69
|
+
const previewId = parsePzParams(params, settings)[PREVIEW_PZ_SEGMENT] ?? '';
|
|
60
70
|
// Reconstructed here, above the error boundary, because the boundary is a
|
|
61
71
|
// client component that can neither fetch nor receive server props — see
|
|
62
72
|
// pz-theme's error-composition.tsx. Costs one memoized widget read until a
|
|
@@ -67,6 +77,16 @@ async function RootLayout({
|
|
|
67
77
|
<html lang={locale.isoCode} {...(locale.rtl ? { dir: 'rtl' } : {})}>
|
|
68
78
|
<head />
|
|
69
79
|
<body className="overflow-x-hidden">
|
|
80
|
+
{isPreview && (
|
|
81
|
+
<div className="fixed bottom-3 left-1/2 -translate-x-1/2 z-[9999] flex items-center gap-2 rounded bg-black/80 px-3 py-1.5 text-xs text-white">
|
|
82
|
+
<span>{previewId ? `Preview: ${previewId}` : 'Preview'}</span>
|
|
83
|
+
{/* Relative query-only href: exits preview and returns to the
|
|
84
|
+
SAME page via the middleware ?preview=exit interception. */}
|
|
85
|
+
<a href="?preview=exit" className="underline">
|
|
86
|
+
Exit
|
|
87
|
+
</a>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
70
90
|
<PzRoot translations={translations} locale={locale.value}>
|
|
71
91
|
<ClientRoot>
|
|
72
92
|
<div className="overflow-x-clip">
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { isValidPreviewId, PREVIEW_ID_QUERY_PARAM } from '@akinon/next/utils/preview';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_PREVIEW_LINK_TTL_SECONDS,
|
|
4
|
+
signPreviewToken
|
|
5
|
+
} from '@akinon/next/utils/preview-token';
|
|
6
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Mints a signed, expiring preview entry token for the theme editor.
|
|
10
|
+
*
|
|
11
|
+
* The editor is a static SPA on another origin and must never hold the
|
|
12
|
+
* storefront's PREVIEW_SECRET. Instead it proves it is an editor user by
|
|
13
|
+
* forwarding its Omnitron session token; this handler verifies that token
|
|
14
|
+
* against Omnitron (`OMNITRON_URL`) and answers with a token the editor puts
|
|
15
|
+
* on the link as `?preview=<token>`. The link opens exactly one preview and
|
|
16
|
+
* stops working after PREVIEW_LINK_TTL_SECONDS (default 24h).
|
|
17
|
+
*
|
|
18
|
+
* CORS is open on purpose: the request carries no cookies, only the caller's
|
|
19
|
+
* Omnitron token, and the response is worthless without one.
|
|
20
|
+
*/
|
|
21
|
+
const CORS_HEADERS = {
|
|
22
|
+
'Access-Control-Allow-Origin': '*',
|
|
23
|
+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
24
|
+
'Access-Control-Allow-Headers': 'Authorization, Content-Type',
|
|
25
|
+
'Access-Control-Max-Age': '600',
|
|
26
|
+
'Cache-Control': 'no-store'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const json = (body: Record<string, unknown>, status: number) =>
|
|
30
|
+
NextResponse.json(body, { status, headers: CORS_HEADERS });
|
|
31
|
+
|
|
32
|
+
export async function OPTIONS() {
|
|
33
|
+
return new NextResponse(null, { status: 204, headers: CORS_HEADERS });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Bounded wait for Omnitron — an unreachable host must fail fast, not hang the editor. */
|
|
37
|
+
const OMNITRON_TIMEOUT_MS = 8_000;
|
|
38
|
+
|
|
39
|
+
/** True when Omnitron accepts the token as a signed-in user. */
|
|
40
|
+
const isEditorUser = async (
|
|
41
|
+
omnitronUrl: string,
|
|
42
|
+
authorization: string
|
|
43
|
+
): Promise<boolean> => {
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetch(
|
|
46
|
+
`${omnitronUrl.replace(/\/+$/, '')}/api/v1/active_user/`,
|
|
47
|
+
{
|
|
48
|
+
headers: { Authorization: authorization, Accept: 'application/json' },
|
|
49
|
+
cache: 'no-store',
|
|
50
|
+
signal: AbortSignal.timeout(OMNITRON_TIMEOUT_MS)
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
return response.status === 200;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export async function GET(request: NextRequest) {
|
|
60
|
+
const secret = process.env.PREVIEW_SECRET;
|
|
61
|
+
const omnitronUrl = process.env.OMNITRON_URL;
|
|
62
|
+
if (!secret || !omnitronUrl) {
|
|
63
|
+
return json(
|
|
64
|
+
{
|
|
65
|
+
error: 'preview-disabled',
|
|
66
|
+
message:
|
|
67
|
+
'Preview links are disabled: PREVIEW_SECRET and OMNITRON_URL must both be configured on the storefront.'
|
|
68
|
+
},
|
|
69
|
+
503
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const previewId =
|
|
74
|
+
request.nextUrl.searchParams.get(PREVIEW_ID_QUERY_PARAM) ?? '';
|
|
75
|
+
if (!isValidPreviewId(previewId)) {
|
|
76
|
+
return json(
|
|
77
|
+
{ error: 'invalid-preview-id', message: 'A valid preview_id is required.' },
|
|
78
|
+
400
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const authorization = request.headers.get('authorization') ?? '';
|
|
83
|
+
if (!/^Token \S+$/i.test(authorization)) {
|
|
84
|
+
return json(
|
|
85
|
+
{ error: 'unauthorized', message: 'An Omnitron token is required.' },
|
|
86
|
+
401
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (!(await isEditorUser(omnitronUrl, authorization))) {
|
|
90
|
+
return json(
|
|
91
|
+
{
|
|
92
|
+
error: 'unauthorized',
|
|
93
|
+
message: `Omnitron at ${omnitronUrl} did not accept the token (or did not answer).`
|
|
94
|
+
},
|
|
95
|
+
401
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const ttl =
|
|
100
|
+
Number(process.env.PREVIEW_LINK_TTL_SECONDS) ||
|
|
101
|
+
DEFAULT_PREVIEW_LINK_TTL_SECONDS;
|
|
102
|
+
const expiresAt = Math.floor(Date.now() / 1000) + ttl;
|
|
103
|
+
const token = await signPreviewToken({ previewId, expiresAt }, secret);
|
|
104
|
+
|
|
105
|
+
return json(
|
|
106
|
+
{
|
|
107
|
+
token,
|
|
108
|
+
previewId,
|
|
109
|
+
expiresAt: new Date(expiresAt * 1000).toISOString(),
|
|
110
|
+
// Ready to append to any page url of this storefront.
|
|
111
|
+
query: `preview=${encodeURIComponent(token)}`
|
|
112
|
+
},
|
|
113
|
+
200
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { PREVIEW_ID_COOKIE, PREVIEW_ID_QUERY_PARAM } from '@akinon/next/utils/preview';
|
|
2
|
+
import { verifyPreviewToken } from '@akinon/next/utils/preview-token';
|
|
3
|
+
import { cookies, draftMode } from 'next/headers';
|
|
4
|
+
import { redirect } from 'next/navigation';
|
|
5
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Enables/disables Next.js Draft Mode for widget previews, and records WHICH
|
|
9
|
+
* named preview is being viewed.
|
|
10
|
+
*
|
|
11
|
+
* Visitors never call this directly — any page url carrying `?preview` is
|
|
12
|
+
* redirected here by the middleware (see src/proxy.ts), and redirected back
|
|
13
|
+
* to the same page once the cookies are set. With draft mode on, the widget
|
|
14
|
+
* fetch layer serves `<slug>-preview-<id>` copies (falling back to live).
|
|
15
|
+
*
|
|
16
|
+
* The preview id rides in its own cookie rather than the url: pages keep
|
|
17
|
+
* their clean addresses and links followed afterwards stay inside the same
|
|
18
|
+
* preview. The cookie is folded into the pz route segment by the middleware
|
|
19
|
+
* (see settings.js `pzSegments`) because the widget pages are force-static,
|
|
20
|
+
* where Next.js blanks `cookies()` inside the render.
|
|
21
|
+
*
|
|
22
|
+
* Deny-by-default: entering preview ALWAYS requires `?preview=<token>` — a
|
|
23
|
+
* signed, expiring token minted by /api/preview/link for an editor user (see
|
|
24
|
+
* utils/preview-token.ts); PREVIEW_SECRET is the signing key and never
|
|
25
|
+
* travels in a url. Environments without the env have preview disabled
|
|
26
|
+
* entirely. Exiting never requires a token.
|
|
27
|
+
*/
|
|
28
|
+
export async function GET(request: NextRequest) {
|
|
29
|
+
const { searchParams } = request.nextUrl;
|
|
30
|
+
|
|
31
|
+
// Only ever redirect within the site — the target comes from the query.
|
|
32
|
+
const target = searchParams.get('redirect') ?? '/';
|
|
33
|
+
let safeTarget =
|
|
34
|
+
target.startsWith('/') && !target.startsWith('//') ? target : '/';
|
|
35
|
+
// Never bounce back to a url that still carries ?preview — that would
|
|
36
|
+
// re-enter the middleware redirect and loop.
|
|
37
|
+
const targetUrl = new URL(safeTarget, request.nextUrl.origin);
|
|
38
|
+
targetUrl.searchParams.delete('preview');
|
|
39
|
+
targetUrl.searchParams.delete(PREVIEW_ID_QUERY_PARAM);
|
|
40
|
+
safeTarget = `${targetUrl.pathname}${targetUrl.search}`;
|
|
41
|
+
// URL normalization can reintroduce a protocol-relative target the first
|
|
42
|
+
// guard already rejected (e.g. '/..//evil.com' normalizes to pathname
|
|
43
|
+
// '//evil.com', which browsers resolve to https://evil.com) — re-check the
|
|
44
|
+
// FINAL string, not just the raw input.
|
|
45
|
+
if (!safeTarget.startsWith('/') || safeTarget.startsWith('//')) {
|
|
46
|
+
safeTarget = '/';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const draft = await draftMode();
|
|
50
|
+
const cookieStore = await cookies();
|
|
51
|
+
|
|
52
|
+
if (searchParams.has('exit')) {
|
|
53
|
+
draft.disable();
|
|
54
|
+
cookieStore.delete(PREVIEW_ID_COOKIE);
|
|
55
|
+
redirect(safeTarget);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const secret = process.env.PREVIEW_SECRET;
|
|
59
|
+
if (!secret) {
|
|
60
|
+
return new NextResponse(
|
|
61
|
+
'Preview is disabled: PREVIEW_SECRET is not configured.',
|
|
62
|
+
{ status: 401 }
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const verdict = await verifyPreviewToken(
|
|
67
|
+
searchParams.get('token') ?? '',
|
|
68
|
+
secret
|
|
69
|
+
);
|
|
70
|
+
if (verdict.ok !== true) {
|
|
71
|
+
const { reason } = verdict;
|
|
72
|
+
return new NextResponse(
|
|
73
|
+
reason === 'expired'
|
|
74
|
+
? 'This preview link has expired — open the page from the editor again to get a fresh one.'
|
|
75
|
+
: 'Preview link is invalid or missing. Preview pages are opened from the theme editor.',
|
|
76
|
+
{ status: 401 }
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const { previewId } = verdict.claims;
|
|
80
|
+
|
|
81
|
+
draft.enable();
|
|
82
|
+
// Same flags Next gives its own draft cookie (SameSite=None; Secure outside
|
|
83
|
+
// development): the storefront may be embedded cross-site (editor,
|
|
84
|
+
// Omnitron), and a Lax id cookie would be dropped there while the draft
|
|
85
|
+
// cookie survives — draft mode on, no id, live content under a "Preview"
|
|
86
|
+
// banner. `nextUrl.protocol` is not consulted: behind a TLS-terminating
|
|
87
|
+
// proxy it reads http even in production.
|
|
88
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
89
|
+
cookieStore.set(PREVIEW_ID_COOKIE, previewId, {
|
|
90
|
+
path: '/',
|
|
91
|
+
httpOnly: true,
|
|
92
|
+
sameSite: isDevelopment ? 'lax' : 'none',
|
|
93
|
+
secure: !isDevelopment
|
|
94
|
+
});
|
|
95
|
+
redirect(safeTarget);
|
|
96
|
+
}
|