@akinon/projectzero 2.0.32 → 2.0.33-rc.0
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 -0
- package/app-template/.env.example +1 -0
- package/app-template/AGENTS.md +8 -0
- package/app-template/CHANGELOG.md +77 -0
- package/app-template/README.md +25 -1
- package/app-template/next.config.mjs +7 -1
- package/app-template/package.json +41 -41
- 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]/basket/page.tsx +33 -6
- package/app-template/src/app/[pz]/orders/checkout/page.tsx +57 -92
- package/app-template/src/app/api/theme-settings/route.ts +21 -2
- package/app-template/src/app/global-error.tsx +22 -0
- package/app-template/src/data/server/theme.ts +6 -25
- package/app-template/src/hooks/index.ts +2 -0
- package/app-template/src/plugins.js +1 -0
- package/app-template/src/proxy.ts +2 -1
- package/app-template/src/views/checkout/variants/basket-and-checkout/basket-section.tsx +149 -0
- package/app-template/src/views/checkout/variants/basket-and-checkout/index.tsx +132 -0
- package/app-template/src/views/checkout/variants/multi-step/index.tsx +73 -0
- package/app-template/src/views/checkout/variants/one-page/accordion-section.tsx +140 -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 +99 -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 +237 -0
- package/app-template/src/views/checkout/variants/one-page/payment-options-grid.tsx +201 -0
- package/app-template/src/views/checkout/variants/one-page/sections/address-section.tsx +318 -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 +71 -0
- package/app-template/src/views/checkout/variants/one-page/sections/shipping-section.tsx +298 -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 +30 -0
- package/app-template/src/views/checkout/variants/registry.ts +32 -0
- package/app-template/src/views/checkout/variants/types.ts +30 -0
- package/app-template/src/views/checkout/variants/use-resolved-config.ts +89 -0
- package/app-template/src/views/header/search/index.tsx +13 -5
- package/app-template/src/views/product/slider.tsx +85 -38
- package/commands/plugins.ts +4 -0
- package/dist/commands/plugins.js +4 -0
- package/package.json +1 -1
|
@@ -1,66 +1,53 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
3
|
+
import { useEffect, useMemo } from 'react';
|
|
4
|
+
import dynamic from 'next/dynamic';
|
|
5
|
+
import { useAppDispatch } from '@akinon/next/redux/hooks';
|
|
6
|
+
import { resetCheckoutState } from '@akinon/next/redux/reducers/checkout';
|
|
8
7
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from '@akinon/next/
|
|
12
|
-
import { RootState } from '@theme/redux/store';
|
|
13
|
-
import { ROUTES } from '@theme/routes';
|
|
14
|
-
import { useFetchCheckoutQuery, useResetCheckoutStateQuery } from '@akinon/next/data/client/checkout';
|
|
15
|
-
import { Button, LoaderSpinner } from '@theme/components';
|
|
16
|
-
import { pushAddPaymentInfo, pushAddShippingInfo } from '@theme/utils/gtm';
|
|
17
|
-
import { CheckoutStep } from '@akinon/next/types';
|
|
8
|
+
useFetchCheckoutQuery,
|
|
9
|
+
useResetCheckoutStateQuery
|
|
10
|
+
} from '@akinon/next/data/client/checkout';
|
|
18
11
|
import { useRouter, useLocalization } from '@akinon/next/hooks';
|
|
19
|
-
import
|
|
20
|
-
import
|
|
12
|
+
import { Button, LoaderSpinner } from '@theme/components';
|
|
13
|
+
import { ROUTES } from '@theme/routes';
|
|
14
|
+
import settings from '@theme/settings';
|
|
15
|
+
import {
|
|
16
|
+
getCheckoutVariant,
|
|
17
|
+
UNIFIED_VARIANTS
|
|
18
|
+
} from '@theme/views/checkout/variants/registry';
|
|
19
|
+
import { CheckoutOnePageSkeleton } from '@theme/views/checkout/variants/one-page/skeletons';
|
|
20
|
+
import { useResolvedCheckoutConfig } from '@theme/views/checkout/variants/use-resolved-config';
|
|
21
21
|
|
|
22
22
|
const Checkout = () => {
|
|
23
23
|
const { t } = useLocalization();
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
);
|
|
24
|
+
const router = useRouter();
|
|
25
|
+
const dispatch = useAppDispatch();
|
|
27
26
|
|
|
28
|
-
const { data: indexData, isLoading: isResetStateLoading } =
|
|
27
|
+
const { data: indexData, isLoading: isResetStateLoading } =
|
|
28
|
+
useResetCheckoutStateQuery(null);
|
|
29
29
|
|
|
30
30
|
const {
|
|
31
31
|
data: checkoutData,
|
|
32
|
-
|
|
32
|
+
isLoading: isCheckoutLoading,
|
|
33
33
|
isError,
|
|
34
|
-
isSuccess,
|
|
35
34
|
refetch: refetchCheckout
|
|
36
35
|
} = useFetchCheckoutQuery(null, {
|
|
37
36
|
refetchOnMountOrArgChange: true,
|
|
38
37
|
skip: isResetStateLoading || !indexData
|
|
39
38
|
});
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const dispatch = useAppDispatch();
|
|
39
|
+
|
|
40
|
+
const { variantId, options: variantOptions } = useResolvedCheckoutConfig();
|
|
43
41
|
|
|
44
42
|
const CheckoutAuth = useMemo(
|
|
45
|
-
() =>
|
|
46
|
-
dynamic(() => {
|
|
47
|
-
return import('@theme/views/checkout/auth');
|
|
48
|
-
}),
|
|
43
|
+
() => dynamic(() => import('@theme/views/checkout/auth')),
|
|
49
44
|
[]
|
|
50
45
|
);
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
}, [steps.shipping.completed]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
58
|
-
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
if (preOrder && !preOrder.shipping_option) {
|
|
61
|
-
initialStepChanged.current = true;
|
|
62
|
-
}
|
|
63
|
-
}, [preOrder]);
|
|
47
|
+
const CheckoutVariant = useMemo(
|
|
48
|
+
() => getCheckoutVariant(variantId),
|
|
49
|
+
[variantId]
|
|
50
|
+
);
|
|
64
51
|
|
|
65
52
|
useEffect(() => {
|
|
66
53
|
return () => {
|
|
@@ -68,23 +55,18 @@ const Checkout = () => {
|
|
|
68
55
|
};
|
|
69
56
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
70
57
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}, [isSuccess, steps, checkoutData, preOrder?.payment_option?.name]);
|
|
86
|
-
|
|
87
|
-
if (checkoutData?.redirect_url?.includes('basket')) {
|
|
58
|
+
// In unified mode the basket lives inside the checkout page and the basket
|
|
59
|
+
// page auto-redirects here while it has items — following the backend's
|
|
60
|
+
// basket redirect would bounce between the two pages forever. Check the
|
|
61
|
+
// settings.js baseline as well, since the basket page redirects based on it
|
|
62
|
+
// even when the async theme-settings override resolves to another variant.
|
|
63
|
+
const baselineVariant = (settings as { checkout?: { variant?: string } })
|
|
64
|
+
?.checkout?.variant;
|
|
65
|
+
const isUnifiedCheckout =
|
|
66
|
+
UNIFIED_VARIANTS.has(variantId) ||
|
|
67
|
+
UNIFIED_VARIANTS.has(baselineVariant ?? '');
|
|
68
|
+
|
|
69
|
+
if (!isUnifiedCheckout && checkoutData?.redirect_url?.includes('basket')) {
|
|
88
70
|
router.push(ROUTES.BASKET);
|
|
89
71
|
return null;
|
|
90
72
|
}
|
|
@@ -97,47 +79,30 @@ const Checkout = () => {
|
|
|
97
79
|
);
|
|
98
80
|
}
|
|
99
81
|
|
|
100
|
-
if (
|
|
82
|
+
if (isError) {
|
|
101
83
|
return (
|
|
102
84
|
<div className="flex flex-col items-center justify-center h-80">
|
|
103
|
-
{
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<Button onClick={refetchCheckout}>
|
|
110
|
-
{t('checkout.error.button')}
|
|
111
|
-
</Button>
|
|
112
|
-
</div>
|
|
113
|
-
</>
|
|
114
|
-
)}
|
|
85
|
+
<div>{t('checkout.error.title')}</div>
|
|
86
|
+
<div className="mt-5">
|
|
87
|
+
<Button onClick={refetchCheckout}>
|
|
88
|
+
{t('checkout.error.button')}
|
|
89
|
+
</Button>
|
|
90
|
+
</div>
|
|
115
91
|
</div>
|
|
116
92
|
);
|
|
117
93
|
}
|
|
118
94
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
<PluginModule component={Component.MasterpassLinkModal} />
|
|
124
|
-
|
|
125
|
-
<div className="container flex flex-col flex-wrap w-full px-4 md:px-0">
|
|
126
|
-
<CheckoutStepList />
|
|
127
|
-
|
|
128
|
-
<div className="w-full flex flex-wrap">
|
|
129
|
-
<div className="w-full h-fit-content lg:w-2/3">
|
|
130
|
-
{steps.current === CheckoutStep.Shipping && <ShippingStep />}
|
|
131
|
-
{steps.current === CheckoutStep.Payment && <PaymentStep />}
|
|
132
|
-
</div>
|
|
133
|
-
|
|
134
|
-
<div className="w-full h-fit-content mt-6 lg:w-1/3 lg:pl-8 lg:mt-0">
|
|
135
|
-
<Summary />
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
95
|
+
if (isResetStateLoading || isCheckoutLoading) {
|
|
96
|
+
return variantId === 'multi-step' ? (
|
|
97
|
+
<div className="flex flex-col items-center justify-center h-80">
|
|
98
|
+
<LoaderSpinner />
|
|
138
99
|
</div>
|
|
139
|
-
|
|
140
|
-
|
|
100
|
+
) : (
|
|
101
|
+
<CheckoutOnePageSkeleton />
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return <CheckoutVariant options={variantOptions} />;
|
|
141
106
|
};
|
|
142
107
|
|
|
143
108
|
export default Checkout;
|
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
import { getThemeSettings } from '@theme/data/server/theme';
|
|
3
3
|
|
|
4
|
+
export const dynamic = 'force-dynamic';
|
|
5
|
+
export const revalidate = 0;
|
|
6
|
+
|
|
4
7
|
export async function GET() {
|
|
5
8
|
try {
|
|
6
9
|
const settings = await getThemeSettings();
|
|
7
|
-
return NextResponse.json(settings
|
|
10
|
+
return NextResponse.json(settings, {
|
|
11
|
+
headers: {
|
|
12
|
+
'Cache-Control': 'no-store, must-revalidate',
|
|
13
|
+
'Pragma': 'no-cache'
|
|
14
|
+
}
|
|
15
|
+
});
|
|
8
16
|
} catch (error) {
|
|
9
17
|
console.error('Error in theme-settings API:', error);
|
|
10
|
-
|
|
18
|
+
// Return empty object with 200 so checkout falls back to settings.js silently
|
|
19
|
+
// without surfacing as a 5xx in monitoring tools.
|
|
20
|
+
return NextResponse.json(
|
|
21
|
+
{},
|
|
22
|
+
{
|
|
23
|
+
status: 200,
|
|
24
|
+
headers: {
|
|
25
|
+
'Cache-Control': 'no-store, must-revalidate',
|
|
26
|
+
'Pragma': 'no-cache'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
);
|
|
11
30
|
}
|
|
12
31
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useSentryUncaughtErrors } from '@akinon/next/hooks';
|
|
4
|
+
|
|
5
|
+
export default function GlobalError({
|
|
6
|
+
error,
|
|
7
|
+
reset
|
|
8
|
+
}: {
|
|
9
|
+
error: Error & { digest?: string };
|
|
10
|
+
reset: () => void;
|
|
11
|
+
}) {
|
|
12
|
+
useSentryUncaughtErrors(error);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<html>
|
|
16
|
+
<body>
|
|
17
|
+
<h2>Something went wrong!</h2>
|
|
18
|
+
<button onClick={() => reset()}>Try again</button>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -2,6 +2,11 @@ import 'server-only';
|
|
|
2
2
|
|
|
3
3
|
import { getWidgetData } from '@akinon/next/data/server';
|
|
4
4
|
|
|
5
|
+
export interface ThemeCheckoutSettings {
|
|
6
|
+
variant?: string;
|
|
7
|
+
options?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
interface ThemeSettings {
|
|
6
11
|
logo?: string;
|
|
7
12
|
favicon?: string;
|
|
@@ -16,31 +21,7 @@ interface ThemeSettings {
|
|
|
16
21
|
mainMenuSticky?: boolean;
|
|
17
22
|
breadcrumbSeparator?: string;
|
|
18
23
|
socialNetworks?: Record<string, { url?: string; icon?: string }>;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
interface ThemeConfigWidget {
|
|
22
|
-
attributes?: {
|
|
23
|
-
theme_name?: {
|
|
24
|
-
value?: string;
|
|
25
|
-
kwargs?: Record<string, unknown>;
|
|
26
|
-
};
|
|
27
|
-
theme_slug?: {
|
|
28
|
-
value?: string;
|
|
29
|
-
kwargs?: Record<string, unknown>;
|
|
30
|
-
};
|
|
31
|
-
theme_settings?: {
|
|
32
|
-
value?: string;
|
|
33
|
-
kwargs?: Record<string, unknown>;
|
|
34
|
-
};
|
|
35
|
-
theme_breakpoints?: {
|
|
36
|
-
value?: string;
|
|
37
|
-
kwargs?: Record<string, unknown>;
|
|
38
|
-
};
|
|
39
|
-
theme_dataSources?: {
|
|
40
|
-
value?: string;
|
|
41
|
-
kwargs?: Record<string, unknown>;
|
|
42
|
-
};
|
|
43
|
-
};
|
|
24
|
+
checkout?: ThemeCheckoutSettings;
|
|
44
25
|
}
|
|
45
26
|
|
|
46
27
|
/**
|
|
@@ -14,7 +14,8 @@ export const config = {
|
|
|
14
14
|
'/((?!api|_next|[\\w-\\/*]+\\.\\w+).*)',
|
|
15
15
|
'/(.*sitemap\\.xml)',
|
|
16
16
|
'/(.+\\.)(html|htm|aspx|asp|php)',
|
|
17
|
-
'/(.*orders\\/checkout-with-token.*)'
|
|
17
|
+
'/(.*orders\\/checkout-with-token.*)',
|
|
18
|
+
'/(.*orders\\/post-checkout.*)'
|
|
18
19
|
]
|
|
19
20
|
};
|
|
20
21
|
|
|
@@ -0,0 +1,149 @@
|
|
|
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 = (settings as { plugins?: { multiBasket?: boolean } })
|
|
19
|
+
?.plugins?.multiBasket ?? false;
|
|
20
|
+
|
|
21
|
+
const totalQuantity = basket?.total_quantity ?? 0;
|
|
22
|
+
const totalAmount = basket?.total_amount ?? '0';
|
|
23
|
+
const initialSnapshotRef = useRef<{
|
|
24
|
+
qty: number;
|
|
25
|
+
amount: string;
|
|
26
|
+
} | null>(null);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!isSuccess) return;
|
|
30
|
+
if (initialSnapshotRef.current === null) {
|
|
31
|
+
initialSnapshotRef.current = { qty: totalQuantity, amount: totalAmount };
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const prev = initialSnapshotRef.current;
|
|
35
|
+
if (prev.qty !== totalQuantity || prev.amount !== totalAmount) {
|
|
36
|
+
initialSnapshotRef.current = { qty: totalQuantity, amount: totalAmount };
|
|
37
|
+
dispatch(checkoutApi.util.invalidateTags(['Checkout']));
|
|
38
|
+
}
|
|
39
|
+
}, [isSuccess, totalQuantity, totalAmount, dispatch]);
|
|
40
|
+
|
|
41
|
+
if (isLoading) {
|
|
42
|
+
return (
|
|
43
|
+
<section
|
|
44
|
+
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]"
|
|
45
|
+
data-testid="basket-section-loading"
|
|
46
|
+
>
|
|
47
|
+
<div className="flex justify-center">
|
|
48
|
+
<LoaderSpinner />
|
|
49
|
+
</div>
|
|
50
|
+
</section>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
isSuccess &&
|
|
56
|
+
(!basket?.basketitem_set || basket.basketitem_set.length === 0)
|
|
57
|
+
) {
|
|
58
|
+
return (
|
|
59
|
+
<section
|
|
60
|
+
className="mb-5 rounded-2xl border-2 border-dashed border-gray-200 bg-white px-6 py-12 text-center"
|
|
61
|
+
data-testid="basket-section-empty"
|
|
62
|
+
>
|
|
63
|
+
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-gray-100">
|
|
64
|
+
<Icon
|
|
65
|
+
name="basket"
|
|
66
|
+
size={24}
|
|
67
|
+
className="fill-gray-500"
|
|
68
|
+
aria-hidden="true"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
<h2 className="mb-2 text-lg font-semibold text-black-800">
|
|
72
|
+
{t('basket.empty.title')}
|
|
73
|
+
</h2>
|
|
74
|
+
<p className="mb-5 text-sm text-gray-600">
|
|
75
|
+
{t('basket.empty.content_first')}
|
|
76
|
+
</p>
|
|
77
|
+
<Link href={ROUTES.HOME} passHref>
|
|
78
|
+
<Button
|
|
79
|
+
appearance="filled"
|
|
80
|
+
className="px-6"
|
|
81
|
+
data-testid="basket-section-empty-cta"
|
|
82
|
+
>
|
|
83
|
+
{t('basket.empty.button')}
|
|
84
|
+
</Button>
|
|
85
|
+
</Link>
|
|
86
|
+
</section>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!basket) return null;
|
|
91
|
+
|
|
92
|
+
const itemCount = basket.basketitem_set.length;
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<section
|
|
96
|
+
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]"
|
|
97
|
+
data-testid="basket-section"
|
|
98
|
+
>
|
|
99
|
+
<header className="flex items-center justify-between gap-3 px-6 py-5 sm:px-7">
|
|
100
|
+
<div className="flex items-baseline gap-3">
|
|
101
|
+
<h2 className="text-base font-semibold leading-tight text-black-800 sm:text-lg">
|
|
102
|
+
{t('checkout.one_page.basket.title')}
|
|
103
|
+
</h2>
|
|
104
|
+
<span className="text-[11px] font-medium uppercase tracking-wider text-gray-500">
|
|
105
|
+
{itemCount} {t('checkout.summary.items')}
|
|
106
|
+
</span>
|
|
107
|
+
</div>
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
onClick={() => setCollapsed((prev) => !prev)}
|
|
111
|
+
aria-expanded={!collapsed}
|
|
112
|
+
aria-controls="basket-section-content"
|
|
113
|
+
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"
|
|
114
|
+
data-testid="basket-section-toggle"
|
|
115
|
+
>
|
|
116
|
+
{collapsed
|
|
117
|
+
? t('checkout.one_page.basket.show')
|
|
118
|
+
: t('checkout.one_page.basket.hide')}
|
|
119
|
+
</button>
|
|
120
|
+
</header>
|
|
121
|
+
|
|
122
|
+
{!collapsed && (
|
|
123
|
+
<div
|
|
124
|
+
id="basket-section-content"
|
|
125
|
+
className="max-h-[28rem] overflow-y-auto border-t border-gray-100 px-6 sm:px-7"
|
|
126
|
+
>
|
|
127
|
+
<ul>
|
|
128
|
+
{multiBasket ? (
|
|
129
|
+
<PluginModule
|
|
130
|
+
component={Component.MultiBasket}
|
|
131
|
+
props={{ BasketItem }}
|
|
132
|
+
/>
|
|
133
|
+
) : (
|
|
134
|
+
basket.basketitem_set.map((basketItem, index) => (
|
|
135
|
+
<BasketItem
|
|
136
|
+
basketItem={basketItem}
|
|
137
|
+
namespace={basket.namespace}
|
|
138
|
+
key={`${basketItem.id}-${index}`}
|
|
139
|
+
/>
|
|
140
|
+
))
|
|
141
|
+
)}
|
|
142
|
+
</ul>
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
</section>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export default BasketSection;
|
|
@@ -0,0 +1,132 @@
|
|
|
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 ContactSection from '../one-page/sections/contact-section';
|
|
7
|
+
import AddressSection from '../one-page/sections/address-section';
|
|
8
|
+
import ShippingSection from '../one-page/sections/shipping-section';
|
|
9
|
+
import PaymentSection from '../one-page/sections/payment-section';
|
|
10
|
+
import OnePageSummary from '../one-page/one-page-summary';
|
|
11
|
+
import MobileSummarySheet from '../one-page/mobile-summary-sheet';
|
|
12
|
+
import TrustBadges from '../one-page/trust-badges';
|
|
13
|
+
import {
|
|
14
|
+
useSectionState,
|
|
15
|
+
type SectionId
|
|
16
|
+
} from '../one-page/hooks/use-section-state';
|
|
17
|
+
import { useCheckoutFunnelGtm } from '../one-page/hooks/use-checkout-funnel-gtm';
|
|
18
|
+
import BasketSection from './basket-section';
|
|
19
|
+
import type { CheckoutVariantProps } from '../types';
|
|
20
|
+
|
|
21
|
+
const BasketAndCheckout = ({ options }: CheckoutVariantProps) => {
|
|
22
|
+
const autoAdvance = options?.autoAdvance ?? true;
|
|
23
|
+
const stickyMobileCta = options?.stickyMobileCta ?? true;
|
|
24
|
+
const enableEditMode = options?.enableEditMode ?? true;
|
|
25
|
+
const showOrderReview = options?.showOrderReview ?? true;
|
|
26
|
+
const showTrustBadges = options?.showTrustBadges ?? true;
|
|
27
|
+
const gtmEnabled = options?.gtmEnabled ?? true;
|
|
28
|
+
const compactMode = options?.compactMode ?? false;
|
|
29
|
+
|
|
30
|
+
const { sections, activeSection, openSection } = useSectionState({
|
|
31
|
+
autoAdvance
|
|
32
|
+
});
|
|
33
|
+
useCheckoutFunnelGtm(activeSection, { enabled: gtmEnabled });
|
|
34
|
+
|
|
35
|
+
const sectionStatus = useCallback(
|
|
36
|
+
(id: SectionId) =>
|
|
37
|
+
sections.find((section) => section.id === id)?.status ?? 'locked',
|
|
38
|
+
[sections]
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const goTo = useCallback(
|
|
42
|
+
(id: SectionId) => () => openSection(id),
|
|
43
|
+
[openSection]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<PluginModule component={Component.MasterpassProvider}>
|
|
48
|
+
<PluginModule component={Component.MasterpassDeleteConfirmationModal} />
|
|
49
|
+
<PluginModule component={Component.MasterpassOtpModal} />
|
|
50
|
+
<PluginModule component={Component.MasterpassLinkModal} />
|
|
51
|
+
|
|
52
|
+
<div
|
|
53
|
+
className={clsx(
|
|
54
|
+
'min-h-screen bg-[#fafafa]',
|
|
55
|
+
compactMode ? 'pb-12 md:pb-8' : 'pb-20 md:pb-16'
|
|
56
|
+
)}
|
|
57
|
+
data-variant="basket-and-checkout"
|
|
58
|
+
data-compact={compactMode || undefined}
|
|
59
|
+
>
|
|
60
|
+
<div
|
|
61
|
+
className={clsx(
|
|
62
|
+
'container w-full px-4 md:px-6 lg:px-8',
|
|
63
|
+
compactMode ? 'pt-4' : 'pt-6 md:pt-10'
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
<div
|
|
67
|
+
className={clsx(
|
|
68
|
+
'flex flex-col lg:flex-row lg:items-start',
|
|
69
|
+
compactMode ? 'gap-4 lg:gap-6' : 'gap-6 lg:gap-8'
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
<div
|
|
73
|
+
className="w-full lg:w-2/3"
|
|
74
|
+
data-testid="basket-and-checkout-sections"
|
|
75
|
+
>
|
|
76
|
+
<BasketSection />
|
|
77
|
+
|
|
78
|
+
<ContactSection
|
|
79
|
+
status={sectionStatus('contact')}
|
|
80
|
+
onEdit={goTo('contact')}
|
|
81
|
+
/>
|
|
82
|
+
<AddressSection
|
|
83
|
+
status={sectionStatus('address')}
|
|
84
|
+
onEdit={goTo('address')}
|
|
85
|
+
onComplete={goTo('shipping')}
|
|
86
|
+
editable={enableEditMode}
|
|
87
|
+
/>
|
|
88
|
+
<ShippingSection
|
|
89
|
+
status={sectionStatus('shipping')}
|
|
90
|
+
onEdit={goTo('shipping')}
|
|
91
|
+
onComplete={goTo('payment')}
|
|
92
|
+
editable={enableEditMode}
|
|
93
|
+
/>
|
|
94
|
+
<PaymentSection
|
|
95
|
+
status={sectionStatus('payment')}
|
|
96
|
+
onEdit={goTo('payment')}
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
{showTrustBadges && <TrustBadges />}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<aside
|
|
103
|
+
className="hidden w-full lg:block lg:w-1/3"
|
|
104
|
+
data-testid="basket-and-checkout-summary"
|
|
105
|
+
>
|
|
106
|
+
<div className="sticky top-6">
|
|
107
|
+
<OnePageSummary
|
|
108
|
+
onEditSection={openSection}
|
|
109
|
+
showReview={showOrderReview}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
</aside>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{stickyMobileCta && (
|
|
117
|
+
<div
|
|
118
|
+
className="fixed inset-x-0 bottom-0 z-30 lg:hidden"
|
|
119
|
+
data-testid="basket-and-checkout-mobile-cta"
|
|
120
|
+
>
|
|
121
|
+
<MobileSummarySheet
|
|
122
|
+
onEditSection={openSection}
|
|
123
|
+
showReview={showOrderReview}
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
</PluginModule>
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export default BasketAndCheckout;
|
|
@@ -0,0 +1,73 @@
|
|
|
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 { CheckoutStepList } from '@theme/views/checkout/step-list';
|
|
10
|
+
import { Summary } from '@theme/views/checkout/summary';
|
|
11
|
+
import ShippingStep from '@theme/views/checkout/steps/shipping';
|
|
12
|
+
import PaymentStep from '@theme/views/checkout/steps/payment';
|
|
13
|
+
import { pushAddPaymentInfo, pushAddShippingInfo } from '@theme/utils/gtm';
|
|
14
|
+
import type { CheckoutVariantProps } from '../types';
|
|
15
|
+
|
|
16
|
+
const MultiStepCheckout = (_props: CheckoutVariantProps) => {
|
|
17
|
+
const { steps, preOrder } = useAppSelector(
|
|
18
|
+
(state: RootState) => state.checkout
|
|
19
|
+
);
|
|
20
|
+
const dispatch = useAppDispatch();
|
|
21
|
+
const initialStepChanged = useRef<boolean>(false);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (steps.shipping.completed && !initialStepChanged.current) {
|
|
25
|
+
dispatch(setCurrentStep(CheckoutStep.Payment));
|
|
26
|
+
initialStepChanged.current = true;
|
|
27
|
+
}
|
|
28
|
+
}, [steps.shipping.completed]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (preOrder && !preOrder.shipping_option) {
|
|
32
|
+
initialStepChanged.current = true;
|
|
33
|
+
}
|
|
34
|
+
}, [preOrder]);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!preOrder?.basket?.basketitem_set) return;
|
|
38
|
+
const products = preOrder.basket.basketitem_set.map((item) => ({
|
|
39
|
+
...item.product
|
|
40
|
+
}));
|
|
41
|
+
if (steps.current === CheckoutStep.Shipping) {
|
|
42
|
+
pushAddShippingInfo(products);
|
|
43
|
+
}
|
|
44
|
+
if (steps.current === CheckoutStep.Payment) {
|
|
45
|
+
pushAddPaymentInfo(products, String(preOrder?.payment_option?.name));
|
|
46
|
+
}
|
|
47
|
+
}, [steps.current, preOrder?.payment_option?.name]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<PluginModule component={Component.MasterpassProvider}>
|
|
51
|
+
<PluginModule component={Component.MasterpassDeleteConfirmationModal} />
|
|
52
|
+
<PluginModule component={Component.MasterpassOtpModal} />
|
|
53
|
+
<PluginModule component={Component.MasterpassLinkModal} />
|
|
54
|
+
|
|
55
|
+
<div className="container flex flex-col flex-wrap w-full px-4 md:px-0">
|
|
56
|
+
<CheckoutStepList />
|
|
57
|
+
|
|
58
|
+
<div className="w-full flex flex-wrap">
|
|
59
|
+
<div className="w-full h-fit-content lg:w-2/3">
|
|
60
|
+
{steps.current === CheckoutStep.Shipping && <ShippingStep />}
|
|
61
|
+
{steps.current === CheckoutStep.Payment && <PaymentStep />}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div className="w-full h-fit-content mt-6 lg:w-1/3 lg:pl-8 lg:mt-0">
|
|
65
|
+
<Summary />
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</PluginModule>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default MultiStepCheckout;
|