@akinon/projectzero 2.0.43-rc.0 → 2.0.44
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 +8 -4
- package/app-template/.env.example +0 -1
- package/app-template/AGENTS.md +0 -8
- package/app-template/CHANGELOG.md +64 -78
- 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]/orders/checkout/layout.tsx +30 -11
- package/app-template/src/app/[pz]/orders/checkout/page.tsx +31 -18
- package/app-template/src/components/checkbox.tsx +21 -2
- package/app-template/src/hooks/index.ts +0 -2
- package/app-template/src/plugins.js +0 -1
- package/app-template/src/proxy.ts +1 -2
- package/app-template/src/views/checkout/__tests__/part-style-reachability.test.ts +121 -0
- package/app-template/src/views/checkout/step-button.tsx +13 -2
- package/app-template/src/views/checkout/step-list.tsx +5 -1
- package/app-template/src/views/checkout/steps/payment/agreements.tsx +9 -1
- package/app-template/src/views/checkout/steps/payment/index.tsx +10 -2
- package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +2 -0
- package/app-template/src/views/checkout/steps/payment/options/funds-transfer.tsx +2 -0
- package/app-template/src/views/checkout/steps/payment/payment-header.tsx +5 -1
- package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +16 -4
- package/app-template/src/views/checkout/steps/shipping/addresses.tsx +2 -1
- package/app-template/src/views/checkout/steps/shipping/index.tsx +2 -0
- package/app-template/src/views/checkout/steps/shipping/shipping-options.tsx +2 -0
- package/app-template/src/views/checkout/summary.tsx +56 -18
- package/app-template/src/views/checkout/variants/__tests__/registry.test.tsx +82 -0
- package/app-template/src/views/checkout/variants/__tests__/use-resolved-config.test.tsx +208 -0
- package/app-template/src/views/checkout/variants/basket-and-checkout/index.tsx +7 -4
- package/app-template/src/views/checkout/variants/multi-step/index.tsx +7 -2
- package/app-template/src/views/checkout/variants/one-page/accordion-section.tsx +22 -0
- package/app-template/src/views/checkout/variants/one-page/index.tsx +7 -4
- package/app-template/src/views/checkout/variants/one-page/one-page-summary.tsx +80 -18
- package/app-template/src/views/checkout/variants/one-page/payment-options-grid.tsx +34 -22
- package/app-template/src/views/checkout/variants/one-page/sections/address-section.tsx +2 -0
- package/app-template/src/views/checkout/variants/one-page/sections/payment-section.tsx +2 -0
- package/app-template/src/views/checkout/variants/one-page/sections/shipping-section.tsx +2 -0
- package/app-template/src/views/checkout/variants/registry.ts +13 -2
- package/app-template/src/views/checkout/variants/use-resolved-config.ts +77 -5
- package/app-template/src/views/header/search/index.tsx +5 -13
- package/app-template/src/views/product/slider.tsx +38 -85
- 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
|
@@ -23,10 +23,21 @@ export const UNIFIED_VARIANTS: ReadonlySet<string> = new Set([
|
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
25
|
export const getCheckoutVariant = (variant: CheckoutVariantId | undefined) => {
|
|
26
|
-
|
|
26
|
+
// Own-key lookup only (see `isKnownVariant`): an inherited member would
|
|
27
|
+
// resolve to a truthy non-loader and `dynamic()` would never render a flow.
|
|
28
|
+
const loader = isKnownVariant(variant)
|
|
29
|
+
? variantLoaders[variant]
|
|
30
|
+
: variantLoaders['multi-step'];
|
|
27
31
|
|
|
28
32
|
return dynamic(loader, { ssr: false });
|
|
29
33
|
};
|
|
30
34
|
|
|
35
|
+
/**
|
|
36
|
+
* `in` also answers true for inherited Object.prototype members ('toString',
|
|
37
|
+
* 'constructor', '__proto__', …), which would let a saved setting name a
|
|
38
|
+
* "known" variant with no loader — `getCheckoutVariant` would then hand
|
|
39
|
+
* `dynamic()` a non-function and the flow would be lost. Own keys only.
|
|
40
|
+
*/
|
|
31
41
|
export const isKnownVariant = (value: unknown): value is CheckoutVariantId =>
|
|
32
|
-
typeof value === 'string' &&
|
|
42
|
+
typeof value === 'string' &&
|
|
43
|
+
Object.prototype.hasOwnProperty.call(variantLoaders, value);
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
useEffect,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState,
|
|
8
|
+
useSyncExternalStore
|
|
9
|
+
} from 'react';
|
|
4
10
|
import settings from '@theme/settings';
|
|
11
|
+
import {
|
|
12
|
+
getCheckoutFlowVariantOverride,
|
|
13
|
+
isThemeEditorDesigner,
|
|
14
|
+
noFlowVariantOnServer,
|
|
15
|
+
subscribeCheckoutPieces,
|
|
16
|
+
type CheckoutFlowVariantPreference
|
|
17
|
+
} from '@akinon/pz-theme/src/chrome/checkout-pieces';
|
|
5
18
|
import { isKnownVariant } from './registry';
|
|
6
19
|
import type { CheckoutVariantId, CheckoutVariantOptions } from './types';
|
|
7
20
|
|
|
@@ -17,6 +30,21 @@ interface RawCheckoutConfig {
|
|
|
17
30
|
|
|
18
31
|
const FALLBACK_VARIANT: CheckoutVariantId = 'multi-step';
|
|
19
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The only two layouts a composition may steer to, mapped to variant ids. A
|
|
35
|
+
* closed record on purpose: an unknown preference resolves to `undefined` here
|
|
36
|
+
* and the storefront's configured variant survives — and the unified
|
|
37
|
+
* basket-and-checkout family stays unreachable from a composition, so the
|
|
38
|
+
* basket-to-checkout redirect pair can never be broken by a page edit.
|
|
39
|
+
*/
|
|
40
|
+
const COMPOSITION_VARIANT_MAP: Record<
|
|
41
|
+
CheckoutFlowVariantPreference,
|
|
42
|
+
CheckoutVariantId
|
|
43
|
+
> = {
|
|
44
|
+
'one-page': 'one-page',
|
|
45
|
+
'step-by-step': 'multi-step'
|
|
46
|
+
};
|
|
47
|
+
|
|
20
48
|
const coerceOptions = (
|
|
21
49
|
raw: Record<string, unknown> | undefined
|
|
22
50
|
): CheckoutVariantOptions => {
|
|
@@ -56,17 +84,30 @@ const mergeOverride = (
|
|
|
56
84
|
};
|
|
57
85
|
|
|
58
86
|
/**
|
|
59
|
-
* Resolves checkout config from
|
|
87
|
+
* Resolves checkout config from three layers (each optional):
|
|
60
88
|
*
|
|
61
89
|
* 1. settings.js (sync, always available — baseline / fallback)
|
|
62
90
|
* 2. /api/theme-settings (backend theme-config widget — persistent override)
|
|
91
|
+
* 3. the composition's checkout flow outlet (`flowVariant`) — highest
|
|
63
92
|
*
|
|
64
93
|
* Layer 2 falls back silently if backend is unreachable or no config is saved.
|
|
65
94
|
* The editor is OPTIONAL — when no theme-config is saved, the storefront keeps
|
|
66
95
|
* using its built-in settings.js.
|
|
96
|
+
*
|
|
97
|
+
* Layer 3 only ever names a LAYOUT, never options, and only through the
|
|
98
|
+
* closed `COMPOSITION_VARIANT_MAP`. It is read ONCE, in a layout effect, so a
|
|
99
|
+
* composed checkout resolves its variant before first paint (the outlet's own
|
|
100
|
+
* registration layout effect runs in an earlier sibling subtree of the same
|
|
101
|
+
* hydration commit) and never flips mid-session — a late registration, or the
|
|
102
|
+
* theme-settings response landing afterwards, can no longer swap the flow out
|
|
103
|
+
* from under a shopper. On the designer canvas the live value is followed
|
|
104
|
+
* instead, so a `Checkout Type` edit previews immediately.
|
|
105
|
+
*
|
|
106
|
+
* With no outlet registered, nothing overrides: the resolution is exactly the
|
|
107
|
+
* two-layer result the storefront had before the bridge existed.
|
|
67
108
|
*/
|
|
68
109
|
export const useResolvedCheckoutConfig = (): ResolvedCheckoutConfig => {
|
|
69
|
-
const [
|
|
110
|
+
const [merged, setMerged] = useState<ResolvedCheckoutConfig>(readBaseFromSettings);
|
|
70
111
|
|
|
71
112
|
useEffect(() => {
|
|
72
113
|
let cancelled = false;
|
|
@@ -75,7 +116,7 @@ export const useResolvedCheckoutConfig = (): ResolvedCheckoutConfig => {
|
|
|
75
116
|
.then((data: { checkout?: RawCheckoutConfig } | null) => {
|
|
76
117
|
if (cancelled) return;
|
|
77
118
|
if (!data?.checkout) return;
|
|
78
|
-
|
|
119
|
+
setMerged(() => mergeOverride(readBaseFromSettings(), data.checkout));
|
|
79
120
|
})
|
|
80
121
|
.catch(() => {
|
|
81
122
|
// Silent: backend not reachable or widget missing — keep settings.js baseline
|
|
@@ -85,5 +126,36 @@ export const useResolvedCheckoutConfig = (): ResolvedCheckoutConfig => {
|
|
|
85
126
|
};
|
|
86
127
|
}, []);
|
|
87
128
|
|
|
88
|
-
|
|
129
|
+
const liveOverride = useSyncExternalStore(
|
|
130
|
+
subscribeCheckoutPieces,
|
|
131
|
+
getCheckoutFlowVariantOverride,
|
|
132
|
+
noFlowVariantOnServer
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const [latchedOverride, setLatchedOverride] =
|
|
136
|
+
useState<CheckoutFlowVariantPreference | null>(null);
|
|
137
|
+
|
|
138
|
+
useLayoutEffect(() => {
|
|
139
|
+
setLatchedOverride(getCheckoutFlowVariantOverride());
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
// The designer flag rides a postMessage and can arrive after mount; every
|
|
143
|
+
// claim change notifies the pieces store, which re-runs this render, so the
|
|
144
|
+
// canvas picks it up before an outlet edit could matter.
|
|
145
|
+
const composition = isThemeEditorDesigner() ? liveOverride : latchedOverride;
|
|
146
|
+
// Own-key lookup: an inherited member ('constructor', 'toString', …) would
|
|
147
|
+
// otherwise resolve to a truthy non-variant and lose the flow entirely.
|
|
148
|
+
const compositionVariant =
|
|
149
|
+
composition &&
|
|
150
|
+
Object.prototype.hasOwnProperty.call(COMPOSITION_VARIANT_MAP, composition)
|
|
151
|
+
? COMPOSITION_VARIANT_MAP[composition]
|
|
152
|
+
: undefined;
|
|
153
|
+
|
|
154
|
+
return useMemo(
|
|
155
|
+
() =>
|
|
156
|
+
compositionVariant
|
|
157
|
+
? { ...merged, variantId: compositionVariant }
|
|
158
|
+
: merged,
|
|
159
|
+
[merged, compositionVariant]
|
|
160
|
+
);
|
|
89
161
|
};
|
|
@@ -4,7 +4,8 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
4
4
|
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
5
5
|
import { closeSearch } from '@akinon/next/redux/reducers/header';
|
|
6
6
|
import clsx from 'clsx';
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
import { Icon } from '@theme/components';
|
|
8
9
|
import Results from './results';
|
|
9
10
|
import { ROUTES } from '@theme/routes';
|
|
10
11
|
import { useLocalization, useRouter } from '@akinon/next/hooks';
|
|
@@ -41,14 +42,6 @@ export default function Search() {
|
|
|
41
42
|
};
|
|
42
43
|
}, [isSearchOpen, dispatch]);
|
|
43
44
|
|
|
44
|
-
const handleSearchTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
45
|
-
setSearchText(e.target.value);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const handleCloseSearch = () => {
|
|
49
|
-
dispatch(closeSearch());
|
|
50
|
-
};
|
|
51
|
-
|
|
52
45
|
return (
|
|
53
46
|
<>
|
|
54
47
|
<div
|
|
@@ -74,9 +67,9 @@ export default function Search() {
|
|
|
74
67
|
{t('common.search.results_for')}
|
|
75
68
|
</span>
|
|
76
69
|
<div className="flex items-center">
|
|
77
|
-
<
|
|
70
|
+
<input
|
|
78
71
|
value={searchText}
|
|
79
|
-
onChange={
|
|
72
|
+
onChange={(e) => setSearchText(e.target.value)}
|
|
80
73
|
onKeyDown={(e) => {
|
|
81
74
|
if (e.key === 'Enter' && searchText.trim() !== '') {
|
|
82
75
|
router.push(`${ROUTES.LIST}/?search_text=${searchText}`);
|
|
@@ -98,12 +91,11 @@ export default function Search() {
|
|
|
98
91
|
<Icon
|
|
99
92
|
name="close"
|
|
100
93
|
size={14}
|
|
101
|
-
onClick={
|
|
94
|
+
onClick={() => dispatch(closeSearch())}
|
|
102
95
|
className="cursor-pointer"
|
|
103
96
|
/>
|
|
104
97
|
</div>
|
|
105
98
|
</div>
|
|
106
|
-
|
|
107
99
|
<Results searchText={searchText} />
|
|
108
100
|
</div>
|
|
109
101
|
</div>
|
|
@@ -36,100 +36,53 @@ export default function ProductInfoSlider({ product }: ProductSliderItem) {
|
|
|
36
36
|
carouselRef.current?.next();
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
const handleThumbnailClick = (index
|
|
39
|
+
const handleThumbnailClick = (index) => {
|
|
40
40
|
setActiveIndex(index);
|
|
41
41
|
carouselRef.current?.goToSlide(index);
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
return (
|
|
45
|
-
|
|
46
|
-
<div className="lg:
|
|
47
|
-
<div className="
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
disabled={activeIndex === 0}
|
|
56
|
-
>
|
|
57
|
-
<Icon name="chevron-up" size={15} className="fill-[#000000]" />
|
|
58
|
-
</button>
|
|
59
|
-
<div className="hidden flex-col items-center overflow-scroll w-[80px] max-h-[620px] lg:block">
|
|
60
|
-
{product?.productimage_set?.map((item, index) => (
|
|
61
|
-
<Image
|
|
62
|
-
key={index}
|
|
63
|
-
src={item.image}
|
|
64
|
-
alt={`Thumbnail ${index}`}
|
|
65
|
-
width={80}
|
|
66
|
-
height={128}
|
|
67
|
-
aspectRatio={80 / 128}
|
|
68
|
-
className={twMerge('cursor-pointer', [
|
|
69
|
-
activeIndex === index && 'border-2 border-primary'
|
|
70
|
-
])}
|
|
71
|
-
onClick={() => handleThumbnailClick(index)}
|
|
72
|
-
/>
|
|
73
|
-
))}
|
|
74
|
-
</div>
|
|
75
|
-
<button
|
|
76
|
-
onClick={goToNext}
|
|
77
|
-
className={twMerge(
|
|
78
|
-
'hidden justify-center p-2 mt-3 border border-gray-100 rounded-full cursor-pointer lg:block',
|
|
79
|
-
[
|
|
80
|
-
activeIndex === product.productimage_set.length - 1 &&
|
|
81
|
-
'cursor-not-allowed opacity-45'
|
|
82
|
-
]
|
|
83
|
-
)}
|
|
84
|
-
disabled={activeIndex === product.productimage_set.length - 1}
|
|
85
|
-
>
|
|
86
|
-
<Icon name="chevron-down" size={15} className="fill-[#000000]" />
|
|
87
|
-
</button>
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
|
|
91
|
-
<div className="relative lg:col-span-5">
|
|
92
|
-
<FavButton className="absolute right-8 top-6 z-[20] sm:hidden" />
|
|
93
|
-
|
|
94
|
-
<PluginModule
|
|
95
|
-
component={Component.ProductImageSearchFeature}
|
|
96
|
-
props={{
|
|
97
|
-
product,
|
|
98
|
-
activeIndex,
|
|
99
|
-
showResetButton: true
|
|
100
|
-
}}
|
|
101
|
-
/>
|
|
102
|
-
|
|
103
|
-
<CarouselCore
|
|
104
|
-
responsive={{
|
|
105
|
-
all: {
|
|
106
|
-
breakpoint: { max: 5000, min: 0 },
|
|
107
|
-
items: 1
|
|
108
|
-
}
|
|
109
|
-
}}
|
|
110
|
-
arrows={false}
|
|
111
|
-
swipeable={true}
|
|
112
|
-
ref={carouselRef}
|
|
113
|
-
afterChange={(previousSlide, { currentSlide }) => {
|
|
114
|
-
setActiveIndex(currentSlide);
|
|
115
|
-
}}
|
|
116
|
-
containerAspectRatio={{ mobile: 520 / 798, desktop: 484 / 726 }}
|
|
45
|
+
<div className="lg:grid lg:grid-cols-6">
|
|
46
|
+
<div className="lg:col-span-1">
|
|
47
|
+
<div className="flex flex-col items-center justify-center md:mr-[6px]">
|
|
48
|
+
<button
|
|
49
|
+
onClick={goToPrev}
|
|
50
|
+
className={twMerge(
|
|
51
|
+
'hidden justify-center p-2 mb-3 border border-gray-100 rounded-full cursor-pointer lg:block',
|
|
52
|
+
[activeIndex === 0 && 'cursor-not-allowed opacity-45']
|
|
53
|
+
)}
|
|
54
|
+
disabled={activeIndex === 0}
|
|
117
55
|
>
|
|
118
|
-
{
|
|
56
|
+
<Icon name="chevron-up" size={15} className="fill-[#000000]" />
|
|
57
|
+
</button>
|
|
58
|
+
<div className="hidden flex-col items-center overflow-scroll w-[80px] max-h-[620px] lg:block">
|
|
59
|
+
{product?.productimage_set?.map((item, index) => (
|
|
119
60
|
<Image
|
|
120
|
-
key={
|
|
61
|
+
key={index}
|
|
121
62
|
src={item.image}
|
|
122
|
-
alt={
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
fill
|
|
63
|
+
alt={`Thumbnail ${index}`}
|
|
64
|
+
width={80}
|
|
65
|
+
height={128}
|
|
66
|
+
className={twMerge('cursor-pointer', [
|
|
67
|
+
activeIndex === index && 'border-2 border-primary'
|
|
68
|
+
])}
|
|
69
|
+
onClick={() => handleThumbnailClick(index)}
|
|
130
70
|
/>
|
|
131
71
|
))}
|
|
132
|
-
</
|
|
72
|
+
</div>
|
|
73
|
+
<button
|
|
74
|
+
onClick={goToNext}
|
|
75
|
+
className={twMerge(
|
|
76
|
+
'hidden justify-center p-2 mt-3 border border-gray-100 rounded-full cursor-pointer lg:block',
|
|
77
|
+
[
|
|
78
|
+
activeIndex === product.productimage_set.length - 1 &&
|
|
79
|
+
'cursor-not-allowed opacity-45'
|
|
80
|
+
]
|
|
81
|
+
)}
|
|
82
|
+
disabled={activeIndex === product.productimage_set.length - 1}
|
|
83
|
+
>
|
|
84
|
+
<Icon name="chevron-down" size={15} className="fill-[#000000]" />
|
|
85
|
+
</button>
|
|
133
86
|
</div>
|
|
134
87
|
</div>
|
|
135
88
|
|
|
@@ -187,6 +140,6 @@ export default function ProductInfoSlider({ product }: ProductSliderItem) {
|
|
|
187
140
|
))}
|
|
188
141
|
</CarouselCore>
|
|
189
142
|
</div>
|
|
190
|
-
|
|
143
|
+
</div>
|
|
191
144
|
);
|
|
192
145
|
}
|
package/commands/plugins.ts
CHANGED
package/dist/commands/plugins.js
CHANGED
|
@@ -183,10 +183,6 @@ exports.default = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
183
183
|
name: 'Tamara Payment Extension',
|
|
184
184
|
value: 'pz-tamara-extension'
|
|
185
185
|
},
|
|
186
|
-
{
|
|
187
|
-
name: 'Similar Products',
|
|
188
|
-
value: 'pz-similar-products'
|
|
189
|
-
},
|
|
190
186
|
{
|
|
191
187
|
name: 'Hepsipay',
|
|
192
188
|
value: 'pz-hepsipay'
|
package/package.json
CHANGED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useSentryUncaughtErrors } from '@akinon/next/hooks';
|
|
4
|
-
|
|
5
|
-
export default function GlobalError({
|
|
6
|
-
error,
|
|
7
|
-
reset
|
|
8
|
-
}: {
|
|
9
|
-
error: Error & { digest?: string };
|
|
10
|
-
reset: () => void;
|
|
11
|
-
}) {
|
|
12
|
-
useSentryUncaughtErrors(error);
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<html>
|
|
16
|
-
<body>
|
|
17
|
-
<h2>Something went wrong!</h2>
|
|
18
|
-
<button onClick={() => reset()}>Try again</button>
|
|
19
|
-
</body>
|
|
20
|
-
</html>
|
|
21
|
-
);
|
|
22
|
-
}
|