@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
|
@@ -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>
|
|
@@ -2,6 +2,7 @@ import { useAppSelector } from '@akinon/next/redux/hooks';
|
|
|
2
2
|
import { useLocalization } from '@akinon/next/hooks';
|
|
3
3
|
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
4
4
|
import { Price, Link } from '@theme/components';
|
|
5
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
5
6
|
import { Image } from '@akinon/next/components/image';
|
|
6
7
|
import { StoreCredits } from '@theme/views/checkout/steps/payment/options/store-credit';
|
|
7
8
|
import type { RootState } from '@theme/redux/store';
|
|
@@ -37,9 +38,18 @@ const OnePageSummary = ({
|
|
|
37
38
|
/>
|
|
38
39
|
<StoreCredits />
|
|
39
40
|
|
|
40
|
-
<section
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
<section
|
|
42
|
+
className="overflow-hidden rounded-2xl bg-white shadow-[0_1px_3px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.04]"
|
|
43
|
+
{...partAttrs('summary')}
|
|
44
|
+
>
|
|
45
|
+
<header
|
|
46
|
+
className="flex items-baseline justify-between px-6 py-5"
|
|
47
|
+
{...partAttrs('summary-header')}
|
|
48
|
+
>
|
|
49
|
+
<span
|
|
50
|
+
className="text-lg font-semibold tracking-tight text-black-800"
|
|
51
|
+
{...partAttrs('summary-title')}
|
|
52
|
+
>
|
|
43
53
|
{t('checkout.summary.title')}
|
|
44
54
|
</span>
|
|
45
55
|
<span className="text-[11px] font-medium uppercase tracking-wider text-gray-500">
|
|
@@ -52,6 +62,7 @@ const OnePageSummary = ({
|
|
|
52
62
|
<li
|
|
53
63
|
key={`one-page-summary-item-${index}`}
|
|
54
64
|
className="flex gap-3 px-6 py-4"
|
|
65
|
+
{...partAttrs('summary-item')}
|
|
55
66
|
>
|
|
56
67
|
<Link
|
|
57
68
|
href={item.product.absolute_url || '#'}
|
|
@@ -100,30 +111,67 @@ const OnePageSummary = ({
|
|
|
100
111
|
</ul>
|
|
101
112
|
|
|
102
113
|
<dl className="space-y-2.5 border-t border-gray-100 px-6 py-5 text-sm text-gray-600">
|
|
103
|
-
<div
|
|
104
|
-
|
|
105
|
-
|
|
114
|
+
<div
|
|
115
|
+
className="flex items-center justify-between"
|
|
116
|
+
{...partAttrs('summary-row')}
|
|
117
|
+
>
|
|
118
|
+
<dt {...partAttrs('summary-row-label')}>
|
|
119
|
+
{t('checkout.summary.subtotal')}
|
|
120
|
+
</dt>
|
|
121
|
+
{/*
|
|
122
|
+
The amount declares its own weight and colour, so a rule on the
|
|
123
|
+
row can never reach it by inheritance — it gets its own part
|
|
124
|
+
instead of leaving the row's Text Color a dead control.
|
|
125
|
+
*/}
|
|
126
|
+
<dd
|
|
127
|
+
className="font-medium tabular-nums text-black-800"
|
|
128
|
+
{...partAttrs('summary-row-value')}
|
|
129
|
+
>
|
|
106
130
|
<Price value={preOrder.basket.total_amount} />
|
|
107
131
|
</dd>
|
|
108
132
|
</div>
|
|
109
|
-
<div
|
|
110
|
-
|
|
111
|
-
|
|
133
|
+
<div
|
|
134
|
+
className="flex items-center justify-between"
|
|
135
|
+
{...partAttrs('summary-row')}
|
|
136
|
+
>
|
|
137
|
+
<dt {...partAttrs('summary-row-label')}>
|
|
138
|
+
{t('checkout.summary.shipping')}
|
|
139
|
+
</dt>
|
|
140
|
+
<dd
|
|
141
|
+
className="font-medium tabular-nums text-black-800"
|
|
142
|
+
{...partAttrs('summary-row-value')}
|
|
143
|
+
>
|
|
112
144
|
<Price value={preOrder.shipping_amount} />
|
|
113
145
|
</dd>
|
|
114
146
|
</div>
|
|
115
147
|
{parseFloat(preOrder.loyalty_money || '0') > 0 && (
|
|
116
|
-
<div
|
|
117
|
-
|
|
118
|
-
|
|
148
|
+
<div
|
|
149
|
+
className="flex items-center justify-between"
|
|
150
|
+
{...partAttrs('summary-row')}
|
|
151
|
+
>
|
|
152
|
+
<dt {...partAttrs('summary-row-label')}>
|
|
153
|
+
{t('checkout.summary.loyalty_money_total')}
|
|
154
|
+
</dt>
|
|
155
|
+
<dd
|
|
156
|
+
className="font-medium tabular-nums text-black-800"
|
|
157
|
+
{...partAttrs('summary-row-value')}
|
|
158
|
+
>
|
|
119
159
|
<Price value={preOrder.loyalty_money} />
|
|
120
160
|
</dd>
|
|
121
161
|
</div>
|
|
122
162
|
)}
|
|
123
163
|
{parseFloat(preOrder.basket.total_discount_amount || '0') > 0 && (
|
|
124
|
-
<div
|
|
125
|
-
|
|
126
|
-
|
|
164
|
+
<div
|
|
165
|
+
className="flex items-center justify-between"
|
|
166
|
+
{...partAttrs('summary-row')}
|
|
167
|
+
>
|
|
168
|
+
<dt {...partAttrs('summary-row-label')}>
|
|
169
|
+
{t('checkout.summary.discounts_total')}
|
|
170
|
+
</dt>
|
|
171
|
+
<dd
|
|
172
|
+
className="font-medium tabular-nums text-black-800"
|
|
173
|
+
{...partAttrs('summary-row-value')}
|
|
174
|
+
>
|
|
127
175
|
<Price
|
|
128
176
|
value={preOrder.basket.total_discount_amount}
|
|
129
177
|
useNegative
|
|
@@ -133,11 +181,22 @@ const OnePageSummary = ({
|
|
|
133
181
|
)}
|
|
134
182
|
</dl>
|
|
135
183
|
|
|
136
|
-
|
|
137
|
-
|
|
184
|
+
{/*
|
|
185
|
+
The row carries the baseline typography that used to live on the
|
|
186
|
+
label span, so the row's own controls have something to change; the
|
|
187
|
+
amount keeps its distinct treatment and answers to its own part.
|
|
188
|
+
*/}
|
|
189
|
+
<div
|
|
190
|
+
className="flex items-baseline justify-between border-t border-gray-100 px-6 py-5 text-sm font-medium uppercase tracking-wider text-gray-700"
|
|
191
|
+
{...partAttrs('summary-total')}
|
|
192
|
+
>
|
|
193
|
+
<span {...partAttrs('summary-total-label')}>
|
|
138
194
|
{t('checkout.summary.total')}
|
|
139
195
|
</span>
|
|
140
|
-
<span
|
|
196
|
+
<span
|
|
197
|
+
className="text-2xl font-bold tracking-tight text-black-800 tabular-nums"
|
|
198
|
+
{...partAttrs('summary-total-value')}
|
|
199
|
+
>
|
|
141
200
|
<Price value={preOrder.unpaid_amount} />
|
|
142
201
|
</span>
|
|
143
202
|
</div>
|
|
@@ -175,6 +234,7 @@ const OnePageSummary = ({
|
|
|
175
234
|
onClick={() => onEditSection('address')}
|
|
176
235
|
className="rounded-md px-2 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-black-800"
|
|
177
236
|
data-testid="one-page-summary-edit-address"
|
|
237
|
+
{...partAttrs('summary-edit')}
|
|
178
238
|
>
|
|
179
239
|
{t('checkout.one_page.edit')}
|
|
180
240
|
</button>
|
|
@@ -201,6 +261,7 @@ const OnePageSummary = ({
|
|
|
201
261
|
onClick={() => onEditSection('shipping')}
|
|
202
262
|
className="rounded-md px-2 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-black-800"
|
|
203
263
|
data-testid="one-page-summary-edit-shipping"
|
|
264
|
+
{...partAttrs('summary-edit')}
|
|
204
265
|
>
|
|
205
266
|
{t('checkout.one_page.edit')}
|
|
206
267
|
</button>
|
|
@@ -222,6 +283,7 @@ const OnePageSummary = ({
|
|
|
222
283
|
onClick={() => onEditSection('payment')}
|
|
223
284
|
className="rounded-md px-2 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-black-800"
|
|
224
285
|
data-testid="one-page-summary-edit-payment"
|
|
286
|
+
{...partAttrs('summary-edit')}
|
|
225
287
|
>
|
|
226
288
|
{t('checkout.one_page.edit')}
|
|
227
289
|
</button>
|
|
@@ -4,6 +4,7 @@ import { useAppSelector } from '@akinon/next/redux/hooks';
|
|
|
4
4
|
import { useSetPaymentOptionMutation } from '@akinon/next/data/client/checkout';
|
|
5
5
|
import { usePaymentOptions } from '@akinon/next/hooks/use-payment-options';
|
|
6
6
|
import { useLocalization } from '@akinon/next/hooks';
|
|
7
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
7
8
|
import { Icon } from '@theme/components';
|
|
8
9
|
import type { RootState } from '@theme/redux/store';
|
|
9
10
|
import type { CheckoutPaymentOption } from '@akinon/next/types';
|
|
@@ -138,6 +139,7 @@ const PaymentOptionsGrid = () => {
|
|
|
138
139
|
role="radiogroup"
|
|
139
140
|
aria-label={t('checkout.one_page.payment.title')}
|
|
140
141
|
onKeyDown={handleKeyDown}
|
|
142
|
+
{...partAttrs('payment-options')}
|
|
141
143
|
>
|
|
142
144
|
{visibleOptions.map((option, index) => {
|
|
143
145
|
const selected = preOrder?.payment_option?.pk === option.pk;
|
|
@@ -152,45 +154,55 @@ const PaymentOptionsGrid = () => {
|
|
|
152
154
|
className={clsx(
|
|
153
155
|
'group relative flex min-h-[5rem] items-center gap-3',
|
|
154
156
|
'rounded-xl border px-4 py-3 text-left',
|
|
157
|
+
// Typography and colour sit on the button — the element the
|
|
158
|
+
// payment-option part is stamped on — instead of on the label
|
|
159
|
+
// span. A part rule can only reach the label by inheritance, and
|
|
160
|
+
// a utility class declared on the span outranks an inherited
|
|
161
|
+
// value however !important the rule is, so every Text Color /
|
|
162
|
+
// Font Size / Font Weight control in the panel was a silent
|
|
163
|
+
// no-op while these lived one level down.
|
|
164
|
+
'text-sm font-medium leading-tight',
|
|
155
165
|
'transition-all duration-200 ease-out',
|
|
156
166
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-black-800 focus-visible:ring-offset-2',
|
|
157
167
|
selected
|
|
158
|
-
? 'border-black-800 bg-black-800/[0.03] shadow-[inset_0_0_0_1px_rgb(31,31,31)]'
|
|
159
|
-
: 'border-gray-200 bg-white hover:border-gray-400'
|
|
168
|
+
? 'border-black-800 bg-black-800/[0.03] text-black-800 shadow-[inset_0_0_0_1px_rgb(31,31,31)]'
|
|
169
|
+
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-400'
|
|
160
170
|
)}
|
|
161
171
|
data-testid={`one-page-payment-card-${option.pk}`}
|
|
162
172
|
data-payment-type={option.payment_type}
|
|
173
|
+
{...partAttrs('payment-option', { selected })}
|
|
163
174
|
>
|
|
175
|
+
{/*
|
|
176
|
+
`Icon` renders an icon FONT (<i class="pz-icon-…">), so the
|
|
177
|
+
`fill-*` utilities that used to sit here never coloured
|
|
178
|
+
anything — fill only applies to SVG. The glyph takes its colour
|
|
179
|
+
from `color`, which now comes from the button, so the icon
|
|
180
|
+
follows both the selected state and the panel's Text Color.
|
|
181
|
+
*/}
|
|
164
182
|
<Icon
|
|
165
183
|
name={getOptionIcon(option)}
|
|
166
184
|
size={20}
|
|
167
|
-
className=
|
|
168
|
-
'flex-shrink-0 transition-colors',
|
|
169
|
-
selected ? 'fill-black-800' : 'fill-gray-600'
|
|
170
|
-
)}
|
|
185
|
+
className="flex-shrink-0 transition-colors"
|
|
171
186
|
aria-hidden="true"
|
|
187
|
+
{...partAttrs('payment-option-icon')}
|
|
172
188
|
/>
|
|
189
|
+
<span className="flex-1">{option.name}</span>
|
|
190
|
+
{/*
|
|
191
|
+
A ring of `currentColor` with a transparent centre: the marker
|
|
192
|
+
follows whatever colour the composition sets on the button and
|
|
193
|
+
the middle shows the card's own background, so it stays legible
|
|
194
|
+
on any palette. The geometry is what the filled circle plus its
|
|
195
|
+
6px inner dot drew before.
|
|
196
|
+
*/}
|
|
173
197
|
<span
|
|
174
198
|
className={clsx(
|
|
175
|
-
'flex-
|
|
176
|
-
selected ? 'text-black-800' : 'text-gray-700'
|
|
177
|
-
)}
|
|
178
|
-
>
|
|
179
|
-
{option.name}
|
|
180
|
-
</span>
|
|
181
|
-
<span
|
|
182
|
-
className={clsx(
|
|
183
|
-
'flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full border-2 transition-all',
|
|
199
|
+
'flex h-4 w-4 flex-shrink-0 rounded-full transition-all',
|
|
184
200
|
selected
|
|
185
|
-
? 'border-
|
|
186
|
-
: 'border-gray-300
|
|
201
|
+
? 'border-[5px] border-current'
|
|
202
|
+
: 'border-2 border-gray-300 group-hover:border-gray-400'
|
|
187
203
|
)}
|
|
188
204
|
aria-hidden="true"
|
|
189
|
-
|
|
190
|
-
{selected && (
|
|
191
|
-
<span className="block h-1.5 w-1.5 rounded-full bg-white" />
|
|
192
|
-
)}
|
|
193
|
-
</span>
|
|
205
|
+
/>
|
|
194
206
|
</button>
|
|
195
207
|
);
|
|
196
208
|
})}
|
|
@@ -4,6 +4,7 @@ import { useAppSelector } from '@akinon/next/redux/hooks';
|
|
|
4
4
|
import { useSetAddressesMutation } from '@akinon/next/data/client/checkout';
|
|
5
5
|
import { useAddAddressMutation } from '@akinon/next/data/client/address';
|
|
6
6
|
import { Button, Checkbox, Icon, Modal, Radio } from '@theme/components';
|
|
7
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
7
8
|
import { AddressForm } from '@theme/views/account/address-form';
|
|
8
9
|
import { useLocalization } from '@akinon/next/hooks';
|
|
9
10
|
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
@@ -295,6 +296,7 @@ const AddressSection = ({
|
|
|
295
296
|
}
|
|
296
297
|
onClick={onComplete}
|
|
297
298
|
data-testid="one-page-address-continue"
|
|
299
|
+
{...partAttrs('continue-button')}
|
|
298
300
|
>
|
|
299
301
|
{t('checkout.one_page.continue_to_shipping')}
|
|
300
302
|
</Button>
|
|
@@ -2,6 +2,7 @@ import clsx from 'clsx';
|
|
|
2
2
|
import { useAppSelector } from '@akinon/next/redux/hooks';
|
|
3
3
|
import { useLocalization } from '@akinon/next/hooks';
|
|
4
4
|
import SelectedPaymentOptionView from '@akinon/next/components/selected-payment-option-view';
|
|
5
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
5
6
|
import type { RootState } from '@theme/redux/store';
|
|
6
7
|
import AccordionSection, { SectionStatus } from '../accordion-section';
|
|
7
8
|
import PaymentOptionsGrid from '../payment-options-grid';
|
|
@@ -51,6 +52,7 @@ const PaymentSection = ({ status, onEdit }: PaymentSectionProps) => {
|
|
|
51
52
|
<div
|
|
52
53
|
className="border-t border-gray-200"
|
|
53
54
|
data-testid="one-page-selected-payment"
|
|
55
|
+
{...partAttrs('payment-panel')}
|
|
54
56
|
>
|
|
55
57
|
<SelectedPaymentOptionView />
|
|
56
58
|
</div>
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { setSelectedShippingOptions } from '@akinon/next/redux/reducers/checkout';
|
|
10
10
|
import { useLocalization } from '@akinon/next/hooks';
|
|
11
11
|
import { Button, Price, Radio } from '@theme/components';
|
|
12
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
12
13
|
import type { RootState } from '@theme/redux/store';
|
|
13
14
|
import AccordionSection, { SectionStatus } from '../accordion-section';
|
|
14
15
|
import { ShippingOptionsSkeleton } from '../skeletons/shipping-skeleton';
|
|
@@ -284,6 +285,7 @@ const ShippingSection = ({
|
|
|
284
285
|
<Button
|
|
285
286
|
onClick={onComplete}
|
|
286
287
|
data-testid="one-page-shipping-continue"
|
|
288
|
+
{...partAttrs('continue-button')}
|
|
287
289
|
>
|
|
288
290
|
{t('checkout.one_page.continue_to_payment')}
|
|
289
291
|
</Button>
|