@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/app-template/.env.example +1 -0
  3. package/app-template/AGENTS.md +8 -0
  4. package/app-template/CHANGELOG.md +77 -0
  5. package/app-template/README.md +25 -1
  6. package/app-template/next.config.mjs +7 -1
  7. package/app-template/package.json +41 -41
  8. package/app-template/public/locales/en/checkout.json +50 -0
  9. package/app-template/public/locales/tr/checkout.json +50 -0
  10. package/app-template/src/app/[pz]/basket/page.tsx +33 -6
  11. package/app-template/src/app/[pz]/orders/checkout/page.tsx +57 -92
  12. package/app-template/src/app/api/theme-settings/route.ts +21 -2
  13. package/app-template/src/app/global-error.tsx +22 -0
  14. package/app-template/src/data/server/theme.ts +6 -25
  15. package/app-template/src/hooks/index.ts +2 -0
  16. package/app-template/src/plugins.js +1 -0
  17. package/app-template/src/proxy.ts +2 -1
  18. package/app-template/src/views/checkout/variants/basket-and-checkout/basket-section.tsx +149 -0
  19. package/app-template/src/views/checkout/variants/basket-and-checkout/index.tsx +132 -0
  20. package/app-template/src/views/checkout/variants/multi-step/index.tsx +73 -0
  21. package/app-template/src/views/checkout/variants/one-page/accordion-section.tsx +140 -0
  22. package/app-template/src/views/checkout/variants/one-page/hooks/use-checkout-funnel-gtm.ts +60 -0
  23. package/app-template/src/views/checkout/variants/one-page/hooks/use-section-state.ts +99 -0
  24. package/app-template/src/views/checkout/variants/one-page/index.tsx +125 -0
  25. package/app-template/src/views/checkout/variants/one-page/mobile-summary-sheet.tsx +109 -0
  26. package/app-template/src/views/checkout/variants/one-page/one-page-summary.tsx +237 -0
  27. package/app-template/src/views/checkout/variants/one-page/payment-options-grid.tsx +201 -0
  28. package/app-template/src/views/checkout/variants/one-page/sections/address-section.tsx +318 -0
  29. package/app-template/src/views/checkout/variants/one-page/sections/contact-section.tsx +62 -0
  30. package/app-template/src/views/checkout/variants/one-page/sections/payment-section.tsx +71 -0
  31. package/app-template/src/views/checkout/variants/one-page/sections/shipping-section.tsx +298 -0
  32. package/app-template/src/views/checkout/variants/one-page/skeletons/address-skeleton.tsx +25 -0
  33. package/app-template/src/views/checkout/variants/one-page/skeletons/checkout-skeleton.tsx +27 -0
  34. package/app-template/src/views/checkout/variants/one-page/skeletons/index.ts +6 -0
  35. package/app-template/src/views/checkout/variants/one-page/skeletons/payment-skeleton.tsx +23 -0
  36. package/app-template/src/views/checkout/variants/one-page/skeletons/section-skeleton.tsx +47 -0
  37. package/app-template/src/views/checkout/variants/one-page/skeletons/shipping-skeleton.tsx +20 -0
  38. package/app-template/src/views/checkout/variants/one-page/skeletons/summary-skeleton.tsx +38 -0
  39. package/app-template/src/views/checkout/variants/one-page/trust-badges.tsx +30 -0
  40. package/app-template/src/views/checkout/variants/registry.ts +32 -0
  41. package/app-template/src/views/checkout/variants/types.ts +30 -0
  42. package/app-template/src/views/checkout/variants/use-resolved-config.ts +89 -0
  43. package/app-template/src/views/header/search/index.tsx +13 -5
  44. package/app-template/src/views/product/slider.tsx +85 -38
  45. package/commands/plugins.ts +4 -0
  46. package/dist/commands/plugins.js +4 -0
  47. package/package.json +1 -1
@@ -1,66 +1,53 @@
1
1
  'use client';
2
2
 
3
- import { CheckoutStepList, Summary } from '@theme/views/checkout';
4
- import PaymentStep from '@theme/views/checkout/steps/payment';
5
- import ShippingStep from '@theme/views/checkout/steps/shipping';
6
- import { useEffect, useMemo, useRef } from 'react';
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
- resetCheckoutState,
10
- setCurrentStep
11
- } from '@akinon/next/redux/reducers/checkout';
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 dynamic from 'next/dynamic';
20
- import PluginModule, { Component } from '@akinon/next/components/plugin-module';
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 { steps, preOrder } = useAppSelector(
25
- (state: RootState) => state.checkout
26
- );
24
+ const router = useRouter();
25
+ const dispatch = useAppDispatch();
27
26
 
28
- const { data: indexData, isLoading: isResetStateLoading } = useResetCheckoutStateQuery(null);
27
+ const { data: indexData, isLoading: isResetStateLoading } =
28
+ useResetCheckoutStateQuery(null);
29
29
 
30
30
  const {
31
31
  data: checkoutData,
32
- isFetching,
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
- const initialStepChanged = useRef<boolean>(false);
41
- const router = useRouter();
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
- useEffect(() => {
53
- if (steps.shipping.completed && !initialStepChanged.current) {
54
- dispatch(setCurrentStep(CheckoutStep.Payment));
55
- initialStepChanged.current = true;
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
- useEffect(() => {
72
- if (isSuccess) {
73
- const products = checkoutData?.pre_order?.basket?.basketitem_set.map(
74
- (basketItem) => ({
75
- ...basketItem.product
76
- })
77
- );
78
- if (steps.current === 'shipping') {
79
- pushAddShippingInfo(products);
80
- }
81
- if (steps.current === 'payment') {
82
- pushAddPaymentInfo(products, String(preOrder?.payment_option?.name));
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 (isResetStateLoading || isFetching || isError) {
82
+ if (isError) {
101
83
  return (
102
84
  <div className="flex flex-col items-center justify-center h-80">
103
- {isResetStateLoading || isFetching ? (
104
- <LoaderSpinner />
105
- ) : (
106
- <>
107
- <div>{t('checkout.error.title')}</div>
108
- <div className="mt-5">
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
- return (
120
- <PluginModule component={Component.MasterpassProvider}>
121
- <PluginModule component={Component.MasterpassDeleteConfirmationModal} />
122
- <PluginModule component={Component.MasterpassOtpModal} />
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
- </PluginModule>
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
- return NextResponse.json({}, { status: 500 });
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
  /**
@@ -1 +1,3 @@
1
+ export * from './use-contract';
2
+ export * from './use-fav-button';
1
3
  export * from './use-add-product-to-basket';
@@ -19,6 +19,7 @@ module.exports = [
19
19
  'pz-hepsipay',
20
20
  'pz-flow-payment',
21
21
  'pz-virtual-try-on',
22
+ 'pz-similar-products',
22
23
  'pz-cybersource-uc',
23
24
  'pz-hepsipay',
24
25
  'pz-masterpass-rest',
@@ -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;