@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useAppDispatch } from '@akinon/next/redux/hooks';
|
|
3
3
|
import { setCurrentStep } from '@akinon/next/redux/reducers/checkout';
|
|
4
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
4
5
|
import { Icon } from '@theme/components';
|
|
5
6
|
import clsx from 'clsx';
|
|
6
7
|
import { CheckoutStep } from '@akinon/next/types';
|
|
@@ -40,6 +41,7 @@ export const CheckoutStepButton = ({
|
|
|
40
41
|
{ 'mr-6': completed }
|
|
41
42
|
)}
|
|
42
43
|
onClick={handleClick}
|
|
44
|
+
{...partAttrs('step', { selected, completed, disabled })}
|
|
43
45
|
>
|
|
44
46
|
<span
|
|
45
47
|
className={clsx(
|
|
@@ -50,10 +52,18 @@ export const CheckoutStepButton = ({
|
|
|
50
52
|
{ 'text-black': selected }
|
|
51
53
|
)}
|
|
52
54
|
>
|
|
53
|
-
<span
|
|
55
|
+
<span
|
|
56
|
+
className="order-1 flex-1 md:mr-1 md:order-none md:flex-none"
|
|
57
|
+
{...partAttrs('step-number', { selected, completed })}
|
|
58
|
+
>
|
|
54
59
|
{number} {number && `.`}
|
|
55
60
|
</span>
|
|
56
|
-
<span
|
|
61
|
+
<span
|
|
62
|
+
className="order-3 uppercase md:order-none"
|
|
63
|
+
{...partAttrs('step-label', { selected, completed })}
|
|
64
|
+
>
|
|
65
|
+
{label}
|
|
66
|
+
</span>
|
|
57
67
|
<Icon
|
|
58
68
|
name="check"
|
|
59
69
|
size={12}
|
|
@@ -62,6 +72,7 @@ export const CheckoutStepButton = ({
|
|
|
62
72
|
{ 'opacity-0 scale-x-0': !completed },
|
|
63
73
|
{ 'opacity-100 scale-x-100': completed }
|
|
64
74
|
)}
|
|
75
|
+
{...partAttrs('step-icon')}
|
|
65
76
|
/>
|
|
66
77
|
</span>
|
|
67
78
|
</button>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useAppSelector } from '@akinon/next/redux/hooks';
|
|
2
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
2
3
|
import { RootState } from '@theme/redux/store';
|
|
3
4
|
import { Icon } from '@theme/components';
|
|
4
5
|
import { CheckoutStepButton } from './step-button';
|
|
@@ -19,7 +20,10 @@ export const CheckoutStepList = () => {
|
|
|
19
20
|
);
|
|
20
21
|
|
|
21
22
|
return (
|
|
22
|
-
<div
|
|
23
|
+
<div
|
|
24
|
+
className="w-full flex items-center justify-center py-5 md:pl-1 md:justify-start md:py-8 lg:py-11"
|
|
25
|
+
{...partAttrs('steps')}
|
|
26
|
+
>
|
|
23
27
|
<CheckoutStepButton
|
|
24
28
|
number="1"
|
|
25
29
|
label={t('checkout.step.shipping')}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import useContract from '../../../../hooks/use-contract';
|
|
2
2
|
import { Controller } from 'react-hook-form';
|
|
3
3
|
import { Checkbox } from '@theme/components';
|
|
4
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
4
5
|
import { useLocalization } from '@akinon/next/hooks';
|
|
5
6
|
import { Trans } from '@akinon/next/components/trans';
|
|
6
7
|
|
|
@@ -22,7 +23,14 @@ const CheckoutAgreements = ({ control, fieldId, error }) => {
|
|
|
22
23
|
control={control}
|
|
23
24
|
defaultValue={false}
|
|
24
25
|
render={({ field }) => (
|
|
25
|
-
|
|
26
|
+
// The stamp addresses the agreement ROW (Checkbox routes it to its
|
|
27
|
+
// label wrapper): LOOK only — the consent itself is untouchable.
|
|
28
|
+
<Checkbox
|
|
29
|
+
{...field}
|
|
30
|
+
error={error}
|
|
31
|
+
data-testid="checkout-agreement"
|
|
32
|
+
{...partAttrs('agreement')}
|
|
33
|
+
>
|
|
26
34
|
<Trans
|
|
27
35
|
i18nKey="checkout.agreement.content"
|
|
28
36
|
components={{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import SelectedPaymentOptionView from '@akinon/next/components/selected-payment-option-view';
|
|
2
2
|
import { useAppSelector } from '@akinon/next/redux/hooks';
|
|
3
3
|
import { CheckoutPaymentOption } from '@akinon/next/types';
|
|
4
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
4
5
|
import clsx from 'clsx';
|
|
5
6
|
import { RootState } from '@theme/redux/store';
|
|
6
7
|
import PaymentOptionButtons from './payment-option-buttons';
|
|
@@ -17,11 +18,18 @@ const PaymentStep = () => {
|
|
|
17
18
|
className={clsx('flex flex-wrap w-full', {
|
|
18
19
|
'pointer-events-none opacity-30': isPaymentStepBusy
|
|
19
20
|
})}
|
|
21
|
+
{...partAttrs('section')}
|
|
20
22
|
>
|
|
21
|
-
<div
|
|
23
|
+
<div
|
|
24
|
+
className="w-full mt-4 flex justify-start border border-gray-400 -mb-px z-10 md:mt-0 md:border-r-0 md:border-b-0 md:w-auto order-2 md:order-none overflow-x-auto"
|
|
25
|
+
{...partAttrs('payment-options')}
|
|
26
|
+
>
|
|
22
27
|
<PaymentOptionButtons />
|
|
23
28
|
</div>
|
|
24
|
-
<div
|
|
29
|
+
<div
|
|
30
|
+
className="w-full border border-solid border-gray-400 bg-white"
|
|
31
|
+
{...partAttrs('payment-panel')}
|
|
32
|
+
>
|
|
25
33
|
<SelectedPaymentOptionView />
|
|
26
34
|
</div>
|
|
27
35
|
</div>
|
|
@@ -18,6 +18,7 @@ import { useLocalization } from '@akinon/next/hooks';
|
|
|
18
18
|
import { Image } from '@akinon/next/components/image';
|
|
19
19
|
import { getPosError, checkPaymentWillRedirect } from '@akinon/next/utils';
|
|
20
20
|
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
21
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
21
22
|
import { PaymentOption } from '@akinon/next/types';
|
|
22
23
|
|
|
23
24
|
const creditCardFormSchema = (
|
|
@@ -409,6 +410,7 @@ const CheckoutCreditCard = () => {
|
|
|
409
410
|
type="submit"
|
|
410
411
|
disabled={isButtonDisabled}
|
|
411
412
|
data-testid="checkout-credit-card-place-order"
|
|
413
|
+
{...partAttrs('place-order-button')}
|
|
412
414
|
>
|
|
413
415
|
<span>{t('checkout.payment.credit_card.form.button')}</span>
|
|
414
416
|
<Icon
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useSetFundsTransferOptionMutation
|
|
9
9
|
} from '@akinon/next/data/client/checkout';
|
|
10
10
|
import { Button, Icon, Price, Radio } from '@theme/components';
|
|
11
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
11
12
|
import * as yup from 'yup';
|
|
12
13
|
import CheckoutAgreements from '../agreements';
|
|
13
14
|
import PaymentHeader from '../payment-header';
|
|
@@ -176,6 +177,7 @@ const CheckoutFundsTransfer = () => {
|
|
|
176
177
|
type="submit"
|
|
177
178
|
disabled={isButtonDisabled}
|
|
178
179
|
data-testid="checkout-bank-account-place-order"
|
|
180
|
+
{...partAttrs('place-order-button')}
|
|
179
181
|
>
|
|
180
182
|
<span> {t('checkout.payment.fund_transfer.button')}</span>
|
|
181
183
|
<Icon
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
2
3
|
|
|
3
4
|
export type PaymentHeaderProps = {
|
|
4
5
|
title: string;
|
|
@@ -7,7 +8,10 @@ export type PaymentHeaderProps = {
|
|
|
7
8
|
const PaymentHeader = ({ title }: PaymentHeaderProps) => {
|
|
8
9
|
return (
|
|
9
10
|
<div className="flex justify-start items-center border-solid border-gray-400 px-4 py-2 sm:px-6 sm:py-3 sm:min-h-15">
|
|
10
|
-
<span
|
|
11
|
+
<span
|
|
12
|
+
className="text-black-800 text-lg font-medium sm:text-2xl"
|
|
13
|
+
{...partAttrs('section-title')}
|
|
14
|
+
>
|
|
11
15
|
{title}
|
|
12
16
|
</span>
|
|
13
17
|
</div>
|
|
@@ -4,6 +4,7 @@ import { RootState } from '@theme/redux/store';
|
|
|
4
4
|
import { useSetPaymentOptionMutation } from '@akinon/next/data/client/checkout';
|
|
5
5
|
import { CheckoutPaymentOption } from '@akinon/next/types';
|
|
6
6
|
import { Radio } from '@theme/components';
|
|
7
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
7
8
|
import { usePaymentOptions } from '@akinon/next/hooks/use-payment-options';
|
|
8
9
|
import { useMemo } from 'react';
|
|
9
10
|
|
|
@@ -42,10 +43,20 @@ const PaymentOptionButtons = () => {
|
|
|
42
43
|
<>
|
|
43
44
|
<div className="w-full space-y-4 px-4 flex flex-col mb-8 md:hidden">
|
|
44
45
|
{displayedPaymentOptions.map((option) => (
|
|
46
|
+
// The mobile radio row's typography sits on the <label> — the
|
|
47
|
+
// element the payment-option part is stamped on — so an inherited
|
|
48
|
+
// part rule actually reaches the method name. Declared on the inner
|
|
49
|
+
// span (as it was) every typography control in the panel was dead
|
|
50
|
+
// here while working on the desktop tab below, which made a
|
|
51
|
+
// template's white-on-dark selected state readable on desktop and
|
|
52
|
+
// black-on-black on every phone.
|
|
45
53
|
<label
|
|
46
54
|
key={`payment-option-${option.pk}`}
|
|
47
|
-
className="border px-4 py-3 mt-3 flex h-12"
|
|
55
|
+
className="border px-4 py-3 mt-3 flex h-12 text-primary-800 font-light text-lg"
|
|
48
56
|
onClick={scrollToTop}
|
|
57
|
+
{...partAttrs('payment-option', {
|
|
58
|
+
selected: preOrder?.payment_option?.pk === option.pk
|
|
59
|
+
})}
|
|
49
60
|
>
|
|
50
61
|
<Radio
|
|
51
62
|
type="radio"
|
|
@@ -55,9 +66,7 @@ const PaymentOptionButtons = () => {
|
|
|
55
66
|
onChange={() => onClickHandler(option)}
|
|
56
67
|
className="mr-2 mt-1"
|
|
57
68
|
/>
|
|
58
|
-
<span className="flex
|
|
59
|
-
{option.name}
|
|
60
|
-
</span>
|
|
69
|
+
<span className="flex">{option.name}</span>
|
|
61
70
|
</label>
|
|
62
71
|
))}
|
|
63
72
|
</div>
|
|
@@ -77,6 +86,9 @@ const PaymentOptionButtons = () => {
|
|
|
77
86
|
}
|
|
78
87
|
)}
|
|
79
88
|
data-testid={`checkout-payment-tab-${option.pk}`}
|
|
89
|
+
{...partAttrs('payment-option', {
|
|
90
|
+
selected: preOrder?.payment_option?.pk === option.pk
|
|
91
|
+
})}
|
|
80
92
|
>
|
|
81
93
|
{option.name}
|
|
82
94
|
</button>
|
|
@@ -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 { AttributeBasedShippingOption, CheckoutStep } from '@akinon/next/types';
|
|
15
16
|
import { useLocalization } from '@akinon/next/hooks';
|
|
16
17
|
|
|
@@ -295,6 +296,7 @@ const ShippingOptions: React.FC = () => {
|
|
|
295
296
|
disabled={!steps.shipping.completed}
|
|
296
297
|
onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
|
|
297
298
|
data-testid="checkout-shipping-save"
|
|
299
|
+
{...partAttrs('continue-button')}
|
|
298
300
|
>
|
|
299
301
|
{t('checkout.address.shipping.button')}
|
|
300
302
|
</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
|
+
});
|