@akinon/projectzero 2.0.43 → 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 +10 -0
- package/app-template/CHANGELOG.md +33 -1
- package/app-template/package.json +29 -29
- 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/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/package.json +1 -1
|
@@ -5,6 +5,7 @@ import { RootState } from '@theme/redux/store';
|
|
|
5
5
|
import { useSetAddressesMutation } from '@akinon/next/data/client/checkout';
|
|
6
6
|
import { CheckoutAddressType } from '@akinon/next/types';
|
|
7
7
|
import { Checkbox, Icon, Modal } from '@theme/components';
|
|
8
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
8
9
|
import AddressBox from './address-box';
|
|
9
10
|
import { useAddAddressMutation } from '@akinon/next/data/client/address';
|
|
10
11
|
import { AddressForm } from '@theme/views/account/address-form';
|
|
@@ -102,7 +103,7 @@ const Addresses = () => {
|
|
|
102
103
|
.map((addressType) => (
|
|
103
104
|
<div key={addressType.label}>
|
|
104
105
|
<div className="flex items-center justify-between border-b border-gray-400 px-8 py-4">
|
|
105
|
-
<h2 className="text-2xl">
|
|
106
|
+
<h2 className="text-2xl" {...partAttrs('section-title')}>
|
|
106
107
|
{addressType.text} {t('checkout.address.address')}
|
|
107
108
|
</h2>
|
|
108
109
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
2
|
import { useAppSelector } from '@akinon/next/redux/hooks';
|
|
3
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
3
4
|
import { RootState } from '@theme/redux/store';
|
|
4
5
|
import Addresses from './addresses';
|
|
5
6
|
import ShippingOptions from './shipping-options';
|
|
@@ -17,6 +18,7 @@ const ShippingStep = () => {
|
|
|
17
18
|
'pointer-events-none opacity-30': isShippingStepBusy
|
|
18
19
|
}
|
|
19
20
|
)}
|
|
21
|
+
{...partAttrs('section')}
|
|
20
22
|
>
|
|
21
23
|
<Addresses />
|
|
22
24
|
<ShippingOptions />
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
useSetDataSourceShippingOptionsMutation
|
|
12
12
|
} from '@akinon/next/data/client/checkout';
|
|
13
13
|
import { Price, Button, Radio } from '@theme/components';
|
|
14
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
14
15
|
import { CheckoutStep } from '@akinon/next/types';
|
|
15
16
|
import { useLocalization } from '@akinon/next/hooks';
|
|
16
17
|
|
|
@@ -267,6 +268,7 @@ const ShippingOptions: React.FC = () => {
|
|
|
267
268
|
disabled={!steps.shipping.completed}
|
|
268
269
|
onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
|
|
269
270
|
data-testid="checkout-shipping-save"
|
|
271
|
+
{...partAttrs('continue-button')}
|
|
270
272
|
>
|
|
271
273
|
{t('checkout.address.shipping.button')}
|
|
272
274
|
</Button>
|
|
@@ -2,6 +2,7 @@ import { setCurrentStep } from '@akinon/next/redux/reducers/checkout';
|
|
|
2
2
|
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
3
3
|
import { RootState } from '@theme/redux/store';
|
|
4
4
|
import { Price, Link } from '@theme/components';
|
|
5
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
5
6
|
import { CheckoutStep } from '@akinon/next/types';
|
|
6
7
|
import { useLocalization } from '@akinon/next/hooks';
|
|
7
8
|
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
@@ -40,9 +41,18 @@ export const Summary = () => {
|
|
|
40
41
|
}}
|
|
41
42
|
/>
|
|
42
43
|
<StoreCredits />
|
|
43
|
-
<div
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
<div
|
|
45
|
+
className="flex flex-col w-full border border-solid border-gray-400"
|
|
46
|
+
{...partAttrs('summary')}
|
|
47
|
+
>
|
|
48
|
+
<div
|
|
49
|
+
className="flex justify-between items-center flex-row border-b border-solid border-gray-400 px-4 py-2 sm:px-5 sm:py-4 sm:min-h-15"
|
|
50
|
+
{...partAttrs('summary-header')}
|
|
51
|
+
>
|
|
52
|
+
<span
|
|
53
|
+
className="text-black-800 text-xl font-light sm:text-2xl"
|
|
54
|
+
{...partAttrs('summary-title')}
|
|
55
|
+
>
|
|
46
56
|
{t('checkout.summary.title')}
|
|
47
57
|
</span>
|
|
48
58
|
<span className="text-gray-950 text-xs">
|
|
@@ -55,6 +65,7 @@ export const Summary = () => {
|
|
|
55
65
|
<div
|
|
56
66
|
key={`summary-basketitem-${index}`}
|
|
57
67
|
className="flex flex-row border-b border-solid border-gray-400 py-3 px-4 last:border-b-0 sm:px-5"
|
|
68
|
+
{...partAttrs('summary-item')}
|
|
58
69
|
>
|
|
59
70
|
<Link
|
|
60
71
|
href={'#'}
|
|
@@ -104,42 +115,68 @@ export const Summary = () => {
|
|
|
104
115
|
))}
|
|
105
116
|
</div>
|
|
106
117
|
<div className="pt-3">
|
|
107
|
-
<div
|
|
108
|
-
|
|
118
|
+
<div
|
|
119
|
+
className="flex items-center justify-between w-full text-xs text-black-800 py-1 px-4 sm:px-5"
|
|
120
|
+
{...partAttrs('summary-row')}
|
|
121
|
+
>
|
|
122
|
+
<span {...partAttrs('summary-row-label')}>
|
|
109
123
|
{t('checkout.summary.subtotal')} (
|
|
110
124
|
{preOrder.basket.basketitem_set.length}{' '}
|
|
111
125
|
{t('checkout.summary.items')})
|
|
112
126
|
</span>
|
|
113
|
-
<span>
|
|
127
|
+
<span {...partAttrs('summary-row-value')}>
|
|
114
128
|
<Price value={preOrder?.basket?.total_amount} />
|
|
115
129
|
</span>
|
|
116
130
|
</div>
|
|
117
|
-
<div
|
|
118
|
-
|
|
119
|
-
|
|
131
|
+
<div
|
|
132
|
+
className="flex items-center justify-between w-full text-xs text-black-800 py-1 px-4 sm:px-5"
|
|
133
|
+
{...partAttrs('summary-row')}
|
|
134
|
+
>
|
|
135
|
+
<span {...partAttrs('summary-row-label')}>
|
|
136
|
+
{t('checkout.summary.shipping')}
|
|
137
|
+
</span>
|
|
138
|
+
<span {...partAttrs('summary-row-value')}>
|
|
120
139
|
<Price value={preOrder?.shipping_amount} />
|
|
121
140
|
</span>
|
|
122
141
|
</div>
|
|
123
142
|
{parseFloat(preOrder?.loyalty_money) > 0 && (
|
|
124
|
-
<div
|
|
125
|
-
|
|
126
|
-
|
|
143
|
+
<div
|
|
144
|
+
className="flex items-center justify-between w-full text-xs text-black-800 py-1 px-4 sm:px-5"
|
|
145
|
+
{...partAttrs('summary-row')}
|
|
146
|
+
>
|
|
147
|
+
<span {...partAttrs('summary-row-label')}>
|
|
148
|
+
{t('checkout.summary.loyalty_money_total')}
|
|
149
|
+
</span>
|
|
150
|
+
<span {...partAttrs('summary-row-value')}>
|
|
127
151
|
<Price value={preOrder?.loyalty_money} />
|
|
128
152
|
</span>
|
|
129
153
|
</div>
|
|
130
154
|
)}
|
|
131
|
-
<div
|
|
132
|
-
|
|
133
|
-
|
|
155
|
+
<div
|
|
156
|
+
className="flex items-center justify-between w-full text-xs text-black-800 py-1 px-4 sm:px-5"
|
|
157
|
+
{...partAttrs('summary-row')}
|
|
158
|
+
>
|
|
159
|
+
<span {...partAttrs('summary-row-label')}>
|
|
160
|
+
{t('checkout.summary.discounts_total')}
|
|
161
|
+
</span>
|
|
162
|
+
<span {...partAttrs('summary-row-value')}>
|
|
134
163
|
<Price
|
|
135
164
|
value={preOrder?.basket?.total_discount_amount}
|
|
136
165
|
useNegative
|
|
137
166
|
/>
|
|
138
167
|
</span>
|
|
139
168
|
</div>
|
|
140
|
-
<div
|
|
141
|
-
|
|
142
|
-
|
|
169
|
+
<div
|
|
170
|
+
className="flex items-center justify-between w-full text-black-800 px-4 sm:px-5 text-lg border-t border-solid border-gray-400 py-2 mt-3 sm:text-xl sm:py-3"
|
|
171
|
+
{...partAttrs('summary-total')}
|
|
172
|
+
>
|
|
173
|
+
<span className="font-light" {...partAttrs('summary-total-label')}>
|
|
174
|
+
{t('checkout.summary.total')}
|
|
175
|
+
</span>
|
|
176
|
+
<span
|
|
177
|
+
className="min-w-max pl-4"
|
|
178
|
+
{...partAttrs('summary-total-value')}
|
|
179
|
+
>
|
|
143
180
|
<Price value={preOrder?.unpaid_amount} />
|
|
144
181
|
</span>
|
|
145
182
|
</div>
|
|
@@ -154,6 +191,7 @@ export const Summary = () => {
|
|
|
154
191
|
<div
|
|
155
192
|
className="text-xs text-black-800 italic cursor-pointer underline transition-all hover:text-secondary"
|
|
156
193
|
onClick={() => dispatch(setCurrentStep(CheckoutStep.Shipping))}
|
|
194
|
+
{...partAttrs('summary-edit')}
|
|
157
195
|
>
|
|
158
196
|
{t('checkout.summary.change')}
|
|
159
197
|
</div>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { getCheckoutVariant, isKnownVariant } from '../registry';
|
|
3
|
+
|
|
4
|
+
jest.mock('../multi-step', () => ({
|
|
5
|
+
__esModule: true,
|
|
6
|
+
default: () => <div data-testid="variant-multi-step" />
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
jest.mock('../one-page', () => ({
|
|
10
|
+
__esModule: true,
|
|
11
|
+
default: () => <div data-testid="variant-one-page" />
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
jest.mock('../basket-and-checkout', () => ({
|
|
15
|
+
__esModule: true,
|
|
16
|
+
default: () => <div data-testid="variant-basket-and-checkout" />
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The variant registry is the last hop before the checkout flow renders, and
|
|
21
|
+
* the value it receives is untrusted: it comes from settings.js, from the
|
|
22
|
+
* backend theme-settings widget, or (now) from a composition. A lookup that
|
|
23
|
+
* accepts inherited Object members would answer "known" for 'constructor' and
|
|
24
|
+
* then hand `dynamic()` something that is not a loader — the flow would never
|
|
25
|
+
* render and ordering would be impossible (I2). Every corpus entry must still
|
|
26
|
+
* end at a real variant component.
|
|
27
|
+
*/
|
|
28
|
+
const UNTRUSTED_CORPUS: unknown[] = [
|
|
29
|
+
'toString',
|
|
30
|
+
'constructor',
|
|
31
|
+
'valueOf',
|
|
32
|
+
'hasOwnProperty',
|
|
33
|
+
'__proto__',
|
|
34
|
+
'',
|
|
35
|
+
undefined,
|
|
36
|
+
42,
|
|
37
|
+
'multi-step',
|
|
38
|
+
'one-page'
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
describe('isKnownVariant', () => {
|
|
42
|
+
it('accepts only own keys of the loader map', () => {
|
|
43
|
+
expect(isKnownVariant('multi-step')).toBe(true);
|
|
44
|
+
expect(isKnownVariant('one-page')).toBe(true);
|
|
45
|
+
expect(isKnownVariant('accordion')).toBe(true);
|
|
46
|
+
expect(isKnownVariant('basket-and-checkout')).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it.each(
|
|
50
|
+
UNTRUSTED_CORPUS.filter(
|
|
51
|
+
(value) => value !== 'multi-step' && value !== 'one-page'
|
|
52
|
+
)
|
|
53
|
+
)('rejects %p', (value) => {
|
|
54
|
+
expect(isKnownVariant(value)).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('getCheckoutVariant', () => {
|
|
59
|
+
it.each(UNTRUSTED_CORPUS)('renders a real variant for %p', async (value) => {
|
|
60
|
+
const Variant = getCheckoutVariant(value as never);
|
|
61
|
+
|
|
62
|
+
render(<Variant />);
|
|
63
|
+
|
|
64
|
+
const expected =
|
|
65
|
+
value === 'one-page' ? 'variant-one-page' : 'variant-multi-step';
|
|
66
|
+
|
|
67
|
+
expect(await screen.findByTestId(expected)).toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('maps every known variant id to its component', async () => {
|
|
71
|
+
const Accordion = getCheckoutVariant('accordion');
|
|
72
|
+
render(<Accordion />);
|
|
73
|
+
// 'accordion' is an alias of the one-page layout.
|
|
74
|
+
expect(await screen.findByTestId('variant-one-page')).toBeInTheDocument();
|
|
75
|
+
|
|
76
|
+
const Unified = getCheckoutVariant('basket-and-checkout');
|
|
77
|
+
render(<Unified />);
|
|
78
|
+
expect(
|
|
79
|
+
await screen.findByTestId('variant-basket-and-checkout')
|
|
80
|
+
).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useCallback } from 'react';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
5
|
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
6
|
+
import { CheckoutPiece } from '@akinon/pz-theme/src/chrome/checkout-piece';
|
|
6
7
|
import ContactSection from '../one-page/sections/contact-section';
|
|
7
8
|
import AddressSection from '../one-page/sections/address-section';
|
|
8
9
|
import ShippingSection from '../one-page/sections/shipping-section';
|
|
@@ -104,10 +105,12 @@ const BasketAndCheckout = ({ options }: CheckoutVariantProps) => {
|
|
|
104
105
|
data-testid="basket-and-checkout-summary"
|
|
105
106
|
>
|
|
106
107
|
<div className="sticky top-6">
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
<CheckoutPiece piece="summary">
|
|
109
|
+
<OnePageSummary
|
|
110
|
+
onEditSection={openSection}
|
|
111
|
+
showReview={showOrderReview}
|
|
112
|
+
/>
|
|
113
|
+
</CheckoutPiece>
|
|
111
114
|
</div>
|
|
112
115
|
</aside>
|
|
113
116
|
</div>
|
|
@@ -6,6 +6,7 @@ import { setCurrentStep } from '@akinon/next/redux/reducers/checkout';
|
|
|
6
6
|
import { CheckoutStep } from '@akinon/next/types';
|
|
7
7
|
import { RootState } from '@theme/redux/store';
|
|
8
8
|
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
9
|
+
import { CheckoutPiece } from '@akinon/pz-theme/src/chrome/checkout-piece';
|
|
9
10
|
import { CheckoutStepList } from '@theme/views/checkout/step-list';
|
|
10
11
|
import { Summary } from '@theme/views/checkout/summary';
|
|
11
12
|
import ShippingStep from '@theme/views/checkout/steps/shipping';
|
|
@@ -53,7 +54,9 @@ const MultiStepCheckout = (_props: CheckoutVariantProps) => {
|
|
|
53
54
|
<PluginModule component={Component.MasterpassLinkModal} />
|
|
54
55
|
|
|
55
56
|
<div className="container flex flex-col flex-wrap w-full px-4 md:px-0">
|
|
56
|
-
<
|
|
57
|
+
<CheckoutPiece piece="steps">
|
|
58
|
+
<CheckoutStepList />
|
|
59
|
+
</CheckoutPiece>
|
|
57
60
|
|
|
58
61
|
<div className="w-full flex flex-wrap">
|
|
59
62
|
<div className="w-full h-fit-content lg:w-2/3">
|
|
@@ -62,7 +65,9 @@ const MultiStepCheckout = (_props: CheckoutVariantProps) => {
|
|
|
62
65
|
</div>
|
|
63
66
|
|
|
64
67
|
<div className="w-full h-fit-content mt-6 lg:w-1/3 lg:pl-8 lg:mt-0">
|
|
65
|
-
<
|
|
68
|
+
<CheckoutPiece piece="summary">
|
|
69
|
+
<Summary />
|
|
70
|
+
</CheckoutPiece>
|
|
66
71
|
</div>
|
|
67
72
|
</div>
|
|
68
73
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ReactNode, useEffect, useRef, useId } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useLocalization } from '@akinon/next/hooks';
|
|
4
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
4
5
|
import { Icon } from '@theme/components';
|
|
5
6
|
|
|
6
7
|
export type SectionStatus = 'locked' | 'active' | 'completed';
|
|
@@ -61,6 +62,11 @@ export const AccordionSection = ({
|
|
|
61
62
|
data-testid={dataTestId}
|
|
62
63
|
data-status={status}
|
|
63
64
|
aria-labelledby={headerId}
|
|
65
|
+
{...partAttrs('section', {
|
|
66
|
+
open: isOpen,
|
|
67
|
+
completed: isCompleted,
|
|
68
|
+
disabled: isLocked
|
|
69
|
+
})}
|
|
64
70
|
>
|
|
65
71
|
<header
|
|
66
72
|
id={headerId}
|
|
@@ -68,6 +74,11 @@ export const AccordionSection = ({
|
|
|
68
74
|
'flex items-center justify-between gap-3 px-6 py-5 sm:px-7',
|
|
69
75
|
isOpen && 'border-b border-gray-100'
|
|
70
76
|
)}
|
|
77
|
+
{...partAttrs('section-header', {
|
|
78
|
+
open: isOpen,
|
|
79
|
+
completed: isCompleted,
|
|
80
|
+
disabled: isLocked
|
|
81
|
+
})}
|
|
71
82
|
>
|
|
72
83
|
<div className="flex items-center gap-3.5">
|
|
73
84
|
<span
|
|
@@ -78,6 +89,11 @@ export const AccordionSection = ({
|
|
|
78
89
|
isLocked && 'bg-gray-100 text-gray-400'
|
|
79
90
|
)}
|
|
80
91
|
aria-hidden="true"
|
|
92
|
+
{...partAttrs('section-number', {
|
|
93
|
+
open: isOpen,
|
|
94
|
+
completed: isCompleted,
|
|
95
|
+
disabled: isLocked
|
|
96
|
+
})}
|
|
81
97
|
>
|
|
82
98
|
{isCompleted ? (
|
|
83
99
|
<Icon name="check" size={14} className="fill-white" />
|
|
@@ -90,6 +106,11 @@ export const AccordionSection = ({
|
|
|
90
106
|
'text-lg font-semibold leading-tight tracking-tight',
|
|
91
107
|
isOpen || isCompleted ? 'text-black-800' : 'text-gray-500'
|
|
92
108
|
)}
|
|
109
|
+
{...partAttrs('section-title', {
|
|
110
|
+
open: isOpen,
|
|
111
|
+
completed: isCompleted,
|
|
112
|
+
disabled: isLocked
|
|
113
|
+
})}
|
|
93
114
|
>
|
|
94
115
|
{title}
|
|
95
116
|
</h2>
|
|
@@ -107,6 +128,7 @@ export const AccordionSection = ({
|
|
|
107
128
|
)}
|
|
108
129
|
data-testid={`${dataTestId}-edit`}
|
|
109
130
|
aria-label={`${t('checkout.one_page.edit')} ${title}`}
|
|
131
|
+
{...partAttrs('edit-button')}
|
|
110
132
|
>
|
|
111
133
|
<span>{t('checkout.one_page.edit')}</span>
|
|
112
134
|
</button>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useCallback } from 'react';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
5
|
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
6
|
+
import { CheckoutPiece } from '@akinon/pz-theme/src/chrome/checkout-piece';
|
|
6
7
|
import ContactSection from './sections/contact-section';
|
|
7
8
|
import AddressSection from './sections/address-section';
|
|
8
9
|
import ShippingSection from './sections/shipping-section';
|
|
@@ -97,10 +98,12 @@ const OnePageCheckout = ({ options }: CheckoutVariantProps) => {
|
|
|
97
98
|
data-testid="one-page-summary"
|
|
98
99
|
>
|
|
99
100
|
<div className="sticky top-6">
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
<CheckoutPiece piece="summary">
|
|
102
|
+
<OnePageSummary
|
|
103
|
+
onEditSection={openSection}
|
|
104
|
+
showReview={showOrderReview}
|
|
105
|
+
/>
|
|
106
|
+
</CheckoutPiece>
|
|
104
107
|
</div>
|
|
105
108
|
</aside>
|
|
106
109
|
</div>
|