@akinon/projectzero 1.126.19 → 1.126.20
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 +2 -0
- package/app-template/.env.example +6 -0
- package/app-template/CHANGELOG.md +40 -0
- package/app-template/package.json +29 -28
- package/app-template/public/locales/en/checkout.json +50 -0
- package/app-template/public/locales/tr/checkout.json +50 -0
- package/app-template/src/app/[pz]/account/(dashboard)/layout.tsx +20 -0
- package/app-template/src/app/[pz]/account/account-page-slot.tsx +35 -0
- package/app-template/src/app/[pz]/account/address/layout.tsx +18 -0
- package/app-template/src/app/[pz]/account/change-email/layout.tsx +18 -0
- package/app-template/src/app/[pz]/account/change-password/layout.tsx +18 -0
- package/app-template/src/app/[pz]/account/contact/layout.tsx +18 -0
- package/app-template/src/app/[pz]/account/coupons/layout.tsx +18 -0
- package/app-template/src/app/[pz]/account/faq/layout.tsx +14 -0
- package/app-template/src/app/[pz]/account/favourite-products/layout.tsx +21 -0
- package/app-template/src/app/[pz]/account/layout.tsx +36 -3
- package/app-template/src/app/[pz]/account/orders/(list)/layout.tsx +20 -0
- package/app-template/src/app/[pz]/account/orders/[id]/(detail)/layout.tsx +40 -0
- package/app-template/src/app/[pz]/account/profile/layout.tsx +18 -0
- package/app-template/src/app/[pz]/auth/(login)/layout.tsx +37 -0
- package/app-template/src/app/[pz]/basket/layout.tsx +25 -0
- package/app-template/src/app/[pz]/client-root.tsx +2 -0
- package/app-template/src/app/[pz]/configure-page-context.ts +17 -0
- package/app-template/src/app/[pz]/layout.tsx +22 -3
- package/app-template/src/app/[pz]/orders/checkout/layout.tsx +47 -0
- package/app-template/src/app/[pz]/orders/checkout/page.tsx +75 -97
- package/app-template/src/app/[pz]/orders/completed/[token]/layout.tsx +24 -2
- package/app-template/src/app/[pz]/users/password/reset/layout.tsx +21 -0
- package/app-template/src/app/[pz]/users/reset/[[...id]]/layout.tsx +30 -0
- package/app-template/src/components/checkbox.tsx +22 -3
- package/app-template/src/components/route-handler.tsx +48 -0
- package/app-template/src/routes/index.ts +6 -6
- package/app-template/src/views/checkout/__tests__/part-style-reachability.test.ts +119 -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/basket-section.tsx +150 -0
- package/app-template/src/views/checkout/variants/basket-and-checkout/index.tsx +135 -0
- package/app-template/src/views/checkout/variants/multi-step/index.tsx +78 -0
- package/app-template/src/views/checkout/variants/one-page/accordion-section.tsx +165 -0
- package/app-template/src/views/checkout/variants/one-page/hooks/use-checkout-funnel-gtm.ts +60 -0
- package/app-template/src/views/checkout/variants/one-page/hooks/use-section-state.ts +103 -0
- package/app-template/src/views/checkout/variants/one-page/index.tsx +125 -0
- package/app-template/src/views/checkout/variants/one-page/mobile-summary-sheet.tsx +109 -0
- package/app-template/src/views/checkout/variants/one-page/one-page-summary.tsx +301 -0
- package/app-template/src/views/checkout/variants/one-page/payment-options-grid.tsx +220 -0
- package/app-template/src/views/checkout/variants/one-page/sections/address-section.tsx +320 -0
- package/app-template/src/views/checkout/variants/one-page/sections/contact-section.tsx +62 -0
- package/app-template/src/views/checkout/variants/one-page/sections/payment-section.tsx +73 -0
- package/app-template/src/views/checkout/variants/one-page/sections/shipping-section.tsx +328 -0
- package/app-template/src/views/checkout/variants/one-page/skeletons/address-skeleton.tsx +25 -0
- package/app-template/src/views/checkout/variants/one-page/skeletons/checkout-skeleton.tsx +27 -0
- package/app-template/src/views/checkout/variants/one-page/skeletons/index.ts +6 -0
- package/app-template/src/views/checkout/variants/one-page/skeletons/payment-skeleton.tsx +23 -0
- package/app-template/src/views/checkout/variants/one-page/skeletons/section-skeleton.tsx +47 -0
- package/app-template/src/views/checkout/variants/one-page/skeletons/shipping-skeleton.tsx +20 -0
- package/app-template/src/views/checkout/variants/one-page/skeletons/summary-skeleton.tsx +38 -0
- package/app-template/src/views/checkout/variants/one-page/trust-badges.tsx +35 -0
- package/app-template/src/views/checkout/variants/registry.ts +43 -0
- package/app-template/src/views/checkout/variants/types.ts +30 -0
- package/app-template/src/views/checkout/variants/use-resolved-config.ts +168 -0
- package/app-template/tailwind.config.js +3 -1
- package/dist/commands/plugins.js +23 -2
- package/package.json +1 -1
- package/app-template/src/app/[pz]/account/orders/[id]/layout.tsx +0 -5
- /package/app-template/src/app/[pz]/account/{page.tsx → (dashboard)/page.tsx} +0 -0
- /package/app-template/src/app/[pz]/account/orders/{page.tsx → (list)/page.tsx} +0 -0
- /package/app-template/src/app/[pz]/account/orders/[id]/{page.tsx → (detail)/page.tsx} +0 -0
- /package/app-template/src/app/[pz]/auth/{page.tsx → (login)/page.tsx} +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import {
|
|
3
|
+
registerCheckoutOutlet,
|
|
4
|
+
resetCheckoutPiecesForTests
|
|
5
|
+
} from '@akinon/pz-theme/src/chrome/checkout-pieces';
|
|
6
|
+
import settingsModule from '../../../../settings';
|
|
7
|
+
import { useResolvedCheckoutConfig } from '../use-resolved-config';
|
|
8
|
+
|
|
9
|
+
// Same module the hook imports as `@theme/settings` — jest keys mocks by
|
|
10
|
+
// resolved path, and the `@theme/*` alias is not resolvable from a test file.
|
|
11
|
+
// Mocking it keeps the suite off the real storefront settings (and its
|
|
12
|
+
// runtime-only requires) while letting each case state its own baseline.
|
|
13
|
+
jest.mock('../../../../settings', () => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: {}
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const mockSettings = settingsModule as { checkout?: Record<string, unknown> };
|
|
19
|
+
|
|
20
|
+
const themeSettingsResponse = (body: unknown) => {
|
|
21
|
+
(global.fetch as jest.Mock).mockResolvedValue({
|
|
22
|
+
ok: true,
|
|
23
|
+
json: async () => body
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const registerFlowOutlet = (flowVariant?: unknown, id = 'outlet-1') => {
|
|
28
|
+
registerCheckoutOutlet('flow', {
|
|
29
|
+
id,
|
|
30
|
+
node: document.createElement('div'),
|
|
31
|
+
flowVariant: flowVariant as never
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Three layers resolve the checkout layout — settings.js, the backend
|
|
37
|
+
* theme-settings widget, and the composition's flow outlet — and the order
|
|
38
|
+
* matters in both directions: a composition must be able to say "this page is
|
|
39
|
+
* the one-page checkout", and nothing may be able to say "no checkout at all".
|
|
40
|
+
* These tests pin the precedence, the closed mapping (an unrecognised
|
|
41
|
+
* preference always falls through to the inherited variant), and the live
|
|
42
|
+
* latch that keeps a shopper's flow from swapping mid-session.
|
|
43
|
+
*/
|
|
44
|
+
describe('useResolvedCheckoutConfig', () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
resetCheckoutPiecesForTests();
|
|
47
|
+
delete mockSettings.checkout;
|
|
48
|
+
delete window.__themeEditorDesigner;
|
|
49
|
+
global.fetch = jest
|
|
50
|
+
.fn()
|
|
51
|
+
.mockResolvedValue({ ok: true, json: async () => ({}) });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
jest.resetAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('falls back to the step-by-step flow when nothing is configured', async () => {
|
|
59
|
+
const { result } = renderHook(() => useResolvedCheckoutConfig());
|
|
60
|
+
|
|
61
|
+
expect(result.current.variantId).toBe('multi-step');
|
|
62
|
+
expect(result.current.options).toEqual({});
|
|
63
|
+
await act(async () => undefined);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('uses the settings.js baseline when the backend has no override', async () => {
|
|
67
|
+
mockSettings.checkout = {
|
|
68
|
+
variant: 'one-page',
|
|
69
|
+
options: { showTrustBadges: false }
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const { result } = renderHook(() => useResolvedCheckoutConfig());
|
|
73
|
+
|
|
74
|
+
expect(result.current.variantId).toBe('one-page');
|
|
75
|
+
expect(result.current.options).toEqual({ showTrustBadges: false });
|
|
76
|
+
await act(async () => undefined);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('lets the theme-settings response override settings.js', async () => {
|
|
80
|
+
mockSettings.checkout = { variant: 'multi-step' };
|
|
81
|
+
themeSettingsResponse({ checkout: { variant: 'one-page' } });
|
|
82
|
+
|
|
83
|
+
const { result } = renderHook(() => useResolvedCheckoutConfig());
|
|
84
|
+
|
|
85
|
+
await waitFor(() => expect(result.current.variantId).toBe('one-page'));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('coerces options from both layers, dropping unknown and mistyped keys', async () => {
|
|
89
|
+
mockSettings.checkout = {
|
|
90
|
+
options: { autoAdvance: false, compactMode: true }
|
|
91
|
+
};
|
|
92
|
+
themeSettingsResponse({
|
|
93
|
+
checkout: {
|
|
94
|
+
options: { compactMode: 'yes', gtmEnabled: false, madeUpKey: 'x' }
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const { result } = renderHook(() => useResolvedCheckoutConfig());
|
|
99
|
+
|
|
100
|
+
await waitFor(() =>
|
|
101
|
+
expect(result.current.options).toEqual({
|
|
102
|
+
autoAdvance: false,
|
|
103
|
+
compactMode: true,
|
|
104
|
+
gtmEnabled: false
|
|
105
|
+
})
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('lets a composed flow outlet beat settings.js and the backend', async () => {
|
|
110
|
+
mockSettings.checkout = { variant: 'multi-step' };
|
|
111
|
+
themeSettingsResponse({ checkout: { variant: 'multi-step' } });
|
|
112
|
+
registerFlowOutlet('one-page');
|
|
113
|
+
|
|
114
|
+
const { result } = renderHook(() => useResolvedCheckoutConfig());
|
|
115
|
+
|
|
116
|
+
expect(result.current.variantId).toBe('one-page');
|
|
117
|
+
// The later theme-settings arrival must not displace the latched layout.
|
|
118
|
+
await act(async () => undefined);
|
|
119
|
+
expect(result.current.variantId).toBe('one-page');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('maps step-by-step to the multi-step variant', async () => {
|
|
123
|
+
mockSettings.checkout = { variant: 'one-page' };
|
|
124
|
+
registerFlowOutlet('step-by-step');
|
|
125
|
+
|
|
126
|
+
const { result } = renderHook(() => useResolvedCheckoutConfig());
|
|
127
|
+
|
|
128
|
+
expect(result.current.variantId).toBe('multi-step');
|
|
129
|
+
await act(async () => undefined);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it.each([
|
|
133
|
+
'accordion',
|
|
134
|
+
'basket-and-checkout',
|
|
135
|
+
'constructor',
|
|
136
|
+
'toString',
|
|
137
|
+
'',
|
|
138
|
+
42,
|
|
139
|
+
null
|
|
140
|
+
])(
|
|
141
|
+
'falls through to the inherited variant for the preference %p',
|
|
142
|
+
async (preference) => {
|
|
143
|
+
mockSettings.checkout = { variant: 'one-page' };
|
|
144
|
+
registerFlowOutlet(preference);
|
|
145
|
+
|
|
146
|
+
const { result } = renderHook(() => useResolvedCheckoutConfig());
|
|
147
|
+
|
|
148
|
+
expect(result.current.variantId).toBe('one-page');
|
|
149
|
+
await act(async () => undefined);
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
it('never lets a composition add options of its own', async () => {
|
|
154
|
+
mockSettings.checkout = { options: { stickyMobileCta: false } };
|
|
155
|
+
registerFlowOutlet('one-page');
|
|
156
|
+
|
|
157
|
+
const { result } = renderHook(() => useResolvedCheckoutConfig());
|
|
158
|
+
|
|
159
|
+
expect(result.current.options).toEqual({ stickyMobileCta: false });
|
|
160
|
+
await act(async () => undefined);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('ignores an outlet that registers after the first commit', async () => {
|
|
164
|
+
mockSettings.checkout = { variant: 'multi-step' };
|
|
165
|
+
|
|
166
|
+
const { result } = renderHook(() => useResolvedCheckoutConfig());
|
|
167
|
+
|
|
168
|
+
expect(result.current.variantId).toBe('multi-step');
|
|
169
|
+
|
|
170
|
+
act(() => registerFlowOutlet('one-page'));
|
|
171
|
+
|
|
172
|
+
expect(result.current.variantId).toBe('multi-step');
|
|
173
|
+
await act(async () => undefined);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('follows the live claim on the designer canvas', async () => {
|
|
177
|
+
window.__themeEditorDesigner = true;
|
|
178
|
+
mockSettings.checkout = { variant: 'multi-step' };
|
|
179
|
+
|
|
180
|
+
const { result } = renderHook(() => useResolvedCheckoutConfig());
|
|
181
|
+
|
|
182
|
+
expect(result.current.variantId).toBe('multi-step');
|
|
183
|
+
|
|
184
|
+
act(() => registerFlowOutlet('one-page'));
|
|
185
|
+
|
|
186
|
+
expect(result.current.variantId).toBe('one-page');
|
|
187
|
+
await act(async () => undefined);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('resolves exactly like the two-layer storefront when nothing registers', async () => {
|
|
191
|
+
mockSettings.checkout = {
|
|
192
|
+
variant: 'multi-step',
|
|
193
|
+
options: { showOrderReview: false }
|
|
194
|
+
};
|
|
195
|
+
themeSettingsResponse({
|
|
196
|
+
checkout: { variant: 'one-page', options: { enableEditMode: false } }
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const { result } = renderHook(() => useResolvedCheckoutConfig());
|
|
200
|
+
|
|
201
|
+
await waitFor(() =>
|
|
202
|
+
expect(result.current).toEqual({
|
|
203
|
+
variantId: 'one-page',
|
|
204
|
+
options: { showOrderReview: false, enableEditMode: false }
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useGetBasketQuery } from '@akinon/next/data/client/basket';
|
|
3
|
+
import { checkoutApi } from '@akinon/next/data/client/checkout';
|
|
4
|
+
import { useAppDispatch } from '@akinon/next/redux/hooks';
|
|
5
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
6
|
+
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
7
|
+
import { BasketItem } from '@theme/views/basket';
|
|
8
|
+
import { Icon, Link, Button, LoaderSpinner } from '@theme/components';
|
|
9
|
+
import { ROUTES } from '@theme/routes';
|
|
10
|
+
import settings from '@theme/settings';
|
|
11
|
+
|
|
12
|
+
const BasketSection = () => {
|
|
13
|
+
const { t } = useLocalization();
|
|
14
|
+
const dispatch = useAppDispatch();
|
|
15
|
+
const { data: basket, isLoading, isSuccess } = useGetBasketQuery();
|
|
16
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
17
|
+
|
|
18
|
+
const multiBasket =
|
|
19
|
+
(settings as { plugins?: { multiBasket?: boolean } })?.plugins
|
|
20
|
+
?.multiBasket ?? false;
|
|
21
|
+
|
|
22
|
+
const totalQuantity = basket?.total_quantity ?? 0;
|
|
23
|
+
const totalAmount = basket?.total_amount ?? '0';
|
|
24
|
+
const initialSnapshotRef = useRef<{
|
|
25
|
+
qty: number;
|
|
26
|
+
amount: string;
|
|
27
|
+
} | null>(null);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!isSuccess) return;
|
|
31
|
+
if (initialSnapshotRef.current === null) {
|
|
32
|
+
initialSnapshotRef.current = { qty: totalQuantity, amount: totalAmount };
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const prev = initialSnapshotRef.current;
|
|
36
|
+
if (prev.qty !== totalQuantity || prev.amount !== totalAmount) {
|
|
37
|
+
initialSnapshotRef.current = { qty: totalQuantity, amount: totalAmount };
|
|
38
|
+
dispatch(checkoutApi.util.invalidateTags(['Checkout']));
|
|
39
|
+
}
|
|
40
|
+
}, [isSuccess, totalQuantity, totalAmount, dispatch]);
|
|
41
|
+
|
|
42
|
+
if (isLoading) {
|
|
43
|
+
return (
|
|
44
|
+
<section
|
|
45
|
+
className="mb-5 rounded-2xl bg-white py-10 shadow-[0_2px_8px_-4px_rgba(0,0,0,0.06)] ring-1 ring-black/[0.04]"
|
|
46
|
+
data-testid="basket-section-loading"
|
|
47
|
+
>
|
|
48
|
+
<div className="flex justify-center">
|
|
49
|
+
<LoaderSpinner />
|
|
50
|
+
</div>
|
|
51
|
+
</section>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
isSuccess &&
|
|
57
|
+
(!basket?.basketitem_set || basket.basketitem_set.length === 0)
|
|
58
|
+
) {
|
|
59
|
+
return (
|
|
60
|
+
<section
|
|
61
|
+
className="mb-5 rounded-2xl border-2 border-dashed border-gray-200 bg-white px-6 py-12 text-center"
|
|
62
|
+
data-testid="basket-section-empty"
|
|
63
|
+
>
|
|
64
|
+
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-gray-100">
|
|
65
|
+
<Icon
|
|
66
|
+
name="basket"
|
|
67
|
+
size={24}
|
|
68
|
+
className="fill-gray-500"
|
|
69
|
+
aria-hidden="true"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
<h2 className="mb-2 text-lg font-semibold text-black-800">
|
|
73
|
+
{t('basket.empty.title')}
|
|
74
|
+
</h2>
|
|
75
|
+
<p className="mb-5 text-sm text-gray-600">
|
|
76
|
+
{t('basket.empty.content_first')}
|
|
77
|
+
</p>
|
|
78
|
+
<Link href={ROUTES.HOME} passHref>
|
|
79
|
+
<Button
|
|
80
|
+
appearance="filled"
|
|
81
|
+
className="px-6"
|
|
82
|
+
data-testid="basket-section-empty-cta"
|
|
83
|
+
>
|
|
84
|
+
{t('basket.empty.button')}
|
|
85
|
+
</Button>
|
|
86
|
+
</Link>
|
|
87
|
+
</section>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!basket) return null;
|
|
92
|
+
|
|
93
|
+
const itemCount = basket.basketitem_set.length;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<section
|
|
97
|
+
className="mb-5 overflow-hidden rounded-2xl bg-white shadow-[0_2px_8px_-4px_rgba(0,0,0,0.06)] ring-1 ring-black/[0.04]"
|
|
98
|
+
data-testid="basket-section"
|
|
99
|
+
>
|
|
100
|
+
<header className="flex items-center justify-between gap-3 px-6 py-5 sm:px-7">
|
|
101
|
+
<div className="flex items-baseline gap-3">
|
|
102
|
+
<h2 className="text-base font-semibold leading-tight text-black-800 sm:text-lg">
|
|
103
|
+
{t('checkout.one_page.basket.title')}
|
|
104
|
+
</h2>
|
|
105
|
+
<span className="text-[11px] font-medium uppercase tracking-wider text-gray-500">
|
|
106
|
+
{itemCount} {t('checkout.summary.items')}
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
onClick={() => setCollapsed((prev) => !prev)}
|
|
112
|
+
aria-expanded={!collapsed}
|
|
113
|
+
aria-controls="basket-section-content"
|
|
114
|
+
className="rounded-lg px-3 py-1.5 text-xs font-medium text-gray-700 transition-all hover:bg-gray-100 hover:text-black-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-secondary"
|
|
115
|
+
data-testid="basket-section-toggle"
|
|
116
|
+
>
|
|
117
|
+
{collapsed
|
|
118
|
+
? t('checkout.one_page.basket.show')
|
|
119
|
+
: t('checkout.one_page.basket.hide')}
|
|
120
|
+
</button>
|
|
121
|
+
</header>
|
|
122
|
+
|
|
123
|
+
{!collapsed && (
|
|
124
|
+
<div
|
|
125
|
+
id="basket-section-content"
|
|
126
|
+
className="max-h-[28rem] overflow-y-auto border-t border-gray-100 px-6 sm:px-7"
|
|
127
|
+
>
|
|
128
|
+
<ul>
|
|
129
|
+
{multiBasket ? (
|
|
130
|
+
<PluginModule
|
|
131
|
+
component={Component.MultiBasket}
|
|
132
|
+
props={{ BasketItem }}
|
|
133
|
+
/>
|
|
134
|
+
) : (
|
|
135
|
+
basket.basketitem_set.map((basketItem, index) => (
|
|
136
|
+
<BasketItem
|
|
137
|
+
basketItem={basketItem}
|
|
138
|
+
namespace={basket.namespace}
|
|
139
|
+
key={`${basketItem.id}-${index}`}
|
|
140
|
+
/>
|
|
141
|
+
))
|
|
142
|
+
)}
|
|
143
|
+
</ul>
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</section>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export default BasketSection;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
import clsx from 'clsx';
|
|
5
|
+
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
6
|
+
import { CheckoutPiece } from '@akinon/pz-theme/src/chrome/checkout-piece';
|
|
7
|
+
import ContactSection from '../one-page/sections/contact-section';
|
|
8
|
+
import AddressSection from '../one-page/sections/address-section';
|
|
9
|
+
import ShippingSection from '../one-page/sections/shipping-section';
|
|
10
|
+
import PaymentSection from '../one-page/sections/payment-section';
|
|
11
|
+
import OnePageSummary from '../one-page/one-page-summary';
|
|
12
|
+
import MobileSummarySheet from '../one-page/mobile-summary-sheet';
|
|
13
|
+
import TrustBadges from '../one-page/trust-badges';
|
|
14
|
+
import {
|
|
15
|
+
useSectionState,
|
|
16
|
+
type SectionId
|
|
17
|
+
} from '../one-page/hooks/use-section-state';
|
|
18
|
+
import { useCheckoutFunnelGtm } from '../one-page/hooks/use-checkout-funnel-gtm';
|
|
19
|
+
import BasketSection from './basket-section';
|
|
20
|
+
import type { CheckoutVariantProps } from '../types';
|
|
21
|
+
|
|
22
|
+
const BasketAndCheckout = ({ options }: CheckoutVariantProps) => {
|
|
23
|
+
const autoAdvance = options?.autoAdvance ?? true;
|
|
24
|
+
const stickyMobileCta = options?.stickyMobileCta ?? true;
|
|
25
|
+
const enableEditMode = options?.enableEditMode ?? true;
|
|
26
|
+
const showOrderReview = options?.showOrderReview ?? true;
|
|
27
|
+
const showTrustBadges = options?.showTrustBadges ?? true;
|
|
28
|
+
const gtmEnabled = options?.gtmEnabled ?? true;
|
|
29
|
+
const compactMode = options?.compactMode ?? false;
|
|
30
|
+
|
|
31
|
+
const { sections, activeSection, openSection } = useSectionState({
|
|
32
|
+
autoAdvance
|
|
33
|
+
});
|
|
34
|
+
useCheckoutFunnelGtm(activeSection, { enabled: gtmEnabled });
|
|
35
|
+
|
|
36
|
+
const sectionStatus = useCallback(
|
|
37
|
+
(id: SectionId) =>
|
|
38
|
+
sections.find((section) => section.id === id)?.status ?? 'locked',
|
|
39
|
+
[sections]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const goTo = useCallback(
|
|
43
|
+
(id: SectionId) => () => openSection(id),
|
|
44
|
+
[openSection]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<PluginModule component={Component.MasterpassProvider}>
|
|
49
|
+
<PluginModule component={Component.MasterpassDeleteConfirmationModal} />
|
|
50
|
+
<PluginModule component={Component.MasterpassOtpModal} />
|
|
51
|
+
<PluginModule component={Component.MasterpassLinkModal} />
|
|
52
|
+
|
|
53
|
+
<div
|
|
54
|
+
className={clsx(
|
|
55
|
+
'min-h-screen bg-[#fafafa]',
|
|
56
|
+
compactMode ? 'pb-12 md:pb-8' : 'pb-20 md:pb-16'
|
|
57
|
+
)}
|
|
58
|
+
data-variant="basket-and-checkout"
|
|
59
|
+
data-compact={compactMode || undefined}
|
|
60
|
+
>
|
|
61
|
+
<div
|
|
62
|
+
className={clsx(
|
|
63
|
+
'container w-full px-4 md:px-6 lg:px-8',
|
|
64
|
+
compactMode ? 'pt-4' : 'pt-6 md:pt-10'
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
<div
|
|
68
|
+
className={clsx(
|
|
69
|
+
'flex flex-col lg:flex-row lg:items-start',
|
|
70
|
+
compactMode ? 'gap-4 lg:gap-6' : 'gap-6 lg:gap-8'
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
<div
|
|
74
|
+
className="w-full lg:w-2/3"
|
|
75
|
+
data-testid="basket-and-checkout-sections"
|
|
76
|
+
>
|
|
77
|
+
<BasketSection />
|
|
78
|
+
|
|
79
|
+
<ContactSection
|
|
80
|
+
status={sectionStatus('contact')}
|
|
81
|
+
onEdit={goTo('contact')}
|
|
82
|
+
/>
|
|
83
|
+
<AddressSection
|
|
84
|
+
status={sectionStatus('address')}
|
|
85
|
+
onEdit={goTo('address')}
|
|
86
|
+
onComplete={goTo('shipping')}
|
|
87
|
+
editable={enableEditMode}
|
|
88
|
+
/>
|
|
89
|
+
<ShippingSection
|
|
90
|
+
status={sectionStatus('shipping')}
|
|
91
|
+
onEdit={goTo('shipping')}
|
|
92
|
+
onComplete={goTo('payment')}
|
|
93
|
+
editable={enableEditMode}
|
|
94
|
+
/>
|
|
95
|
+
<PaymentSection
|
|
96
|
+
status={sectionStatus('payment')}
|
|
97
|
+
onEdit={goTo('payment')}
|
|
98
|
+
/>
|
|
99
|
+
|
|
100
|
+
{showTrustBadges && <TrustBadges />}
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<aside
|
|
104
|
+
className="hidden w-full lg:block lg:w-1/3"
|
|
105
|
+
data-testid="basket-and-checkout-summary"
|
|
106
|
+
>
|
|
107
|
+
<div className="sticky top-6">
|
|
108
|
+
<CheckoutPiece piece="summary">
|
|
109
|
+
<OnePageSummary
|
|
110
|
+
onEditSection={openSection}
|
|
111
|
+
showReview={showOrderReview}
|
|
112
|
+
/>
|
|
113
|
+
</CheckoutPiece>
|
|
114
|
+
</div>
|
|
115
|
+
</aside>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{stickyMobileCta && (
|
|
120
|
+
<div
|
|
121
|
+
className="fixed inset-x-0 bottom-0 z-30 lg:hidden"
|
|
122
|
+
data-testid="basket-and-checkout-mobile-cta"
|
|
123
|
+
>
|
|
124
|
+
<MobileSummarySheet
|
|
125
|
+
onEditSection={openSection}
|
|
126
|
+
showReview={showOrderReview}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
</PluginModule>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export default BasketAndCheckout;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
5
|
+
import { setCurrentStep } from '@akinon/next/redux/reducers/checkout';
|
|
6
|
+
import { CheckoutStep } from '@akinon/next/types';
|
|
7
|
+
import { RootState } from '@theme/redux/store';
|
|
8
|
+
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
9
|
+
import { CheckoutPiece } from '@akinon/pz-theme/src/chrome/checkout-piece';
|
|
10
|
+
import { CheckoutStepList } from '@theme/views/checkout/step-list';
|
|
11
|
+
import { Summary } from '@theme/views/checkout/summary';
|
|
12
|
+
import ShippingStep from '@theme/views/checkout/steps/shipping';
|
|
13
|
+
import PaymentStep from '@theme/views/checkout/steps/payment';
|
|
14
|
+
import { pushAddPaymentInfo, pushAddShippingInfo } from '@theme/utils/gtm';
|
|
15
|
+
import type { CheckoutVariantProps } from '../types';
|
|
16
|
+
|
|
17
|
+
const MultiStepCheckout = (_props: CheckoutVariantProps) => {
|
|
18
|
+
const { steps, preOrder } = useAppSelector(
|
|
19
|
+
(state: RootState) => state.checkout
|
|
20
|
+
);
|
|
21
|
+
const dispatch = useAppDispatch();
|
|
22
|
+
const initialStepChanged = useRef<boolean>(false);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (steps.shipping.completed && !initialStepChanged.current) {
|
|
26
|
+
dispatch(setCurrentStep(CheckoutStep.Payment));
|
|
27
|
+
initialStepChanged.current = true;
|
|
28
|
+
}
|
|
29
|
+
}, [steps.shipping.completed]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (preOrder && !preOrder.shipping_option) {
|
|
33
|
+
initialStepChanged.current = true;
|
|
34
|
+
}
|
|
35
|
+
}, [preOrder]);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!preOrder?.basket?.basketitem_set) return;
|
|
39
|
+
const products = preOrder.basket.basketitem_set.map((item) => ({
|
|
40
|
+
...item.product
|
|
41
|
+
}));
|
|
42
|
+
if (steps.current === CheckoutStep.Shipping) {
|
|
43
|
+
pushAddShippingInfo(products);
|
|
44
|
+
}
|
|
45
|
+
if (steps.current === CheckoutStep.Payment) {
|
|
46
|
+
pushAddPaymentInfo(products, String(preOrder?.payment_option?.name));
|
|
47
|
+
}
|
|
48
|
+
}, [steps.current, preOrder?.payment_option?.name]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<PluginModule component={Component.MasterpassProvider}>
|
|
52
|
+
<PluginModule component={Component.MasterpassDeleteConfirmationModal} />
|
|
53
|
+
<PluginModule component={Component.MasterpassOtpModal} />
|
|
54
|
+
<PluginModule component={Component.MasterpassLinkModal} />
|
|
55
|
+
|
|
56
|
+
<div className="container flex flex-col flex-wrap w-full px-4 md:px-0">
|
|
57
|
+
<CheckoutPiece piece="steps">
|
|
58
|
+
<CheckoutStepList />
|
|
59
|
+
</CheckoutPiece>
|
|
60
|
+
|
|
61
|
+
<div className="w-full flex flex-wrap">
|
|
62
|
+
<div className="w-full h-fit-content lg:w-2/3">
|
|
63
|
+
{steps.current === CheckoutStep.Shipping && <ShippingStep />}
|
|
64
|
+
{steps.current === CheckoutStep.Payment && <PaymentStep />}
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="w-full h-fit-content mt-6 lg:w-1/3 lg:pl-8 lg:mt-0">
|
|
68
|
+
<CheckoutPiece piece="summary">
|
|
69
|
+
<Summary />
|
|
70
|
+
</CheckoutPiece>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</PluginModule>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default MultiStepCheckout;
|