@akinon/projectzero 1.126.20-v1-rc.17 → 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.
Files changed (80) hide show
  1. package/CHANGELOG.md +1 -5
  2. package/app-template/.env.example +6 -0
  3. package/app-template/CHANGELOG.md +39 -35
  4. package/app-template/package.json +29 -28
  5. package/app-template/public/locales/en/checkout.json +50 -0
  6. package/app-template/public/locales/tr/checkout.json +50 -0
  7. package/app-template/src/app/[pz]/account/(dashboard)/layout.tsx +20 -0
  8. package/app-template/src/app/[pz]/account/account-page-slot.tsx +35 -0
  9. package/app-template/src/app/[pz]/account/address/layout.tsx +18 -0
  10. package/app-template/src/app/[pz]/account/change-email/layout.tsx +18 -0
  11. package/app-template/src/app/[pz]/account/change-password/layout.tsx +18 -0
  12. package/app-template/src/app/[pz]/account/contact/layout.tsx +18 -0
  13. package/app-template/src/app/[pz]/account/coupons/layout.tsx +18 -0
  14. package/app-template/src/app/[pz]/account/faq/layout.tsx +14 -0
  15. package/app-template/src/app/[pz]/account/favourite-products/layout.tsx +21 -0
  16. package/app-template/src/app/[pz]/account/layout.tsx +36 -3
  17. package/app-template/src/app/[pz]/account/orders/(list)/layout.tsx +20 -0
  18. package/app-template/src/app/[pz]/account/orders/[id]/(detail)/layout.tsx +40 -0
  19. package/app-template/src/app/[pz]/account/profile/layout.tsx +18 -0
  20. package/app-template/src/app/[pz]/auth/(login)/layout.tsx +37 -0
  21. package/app-template/src/app/[pz]/basket/layout.tsx +25 -0
  22. package/app-template/src/app/[pz]/client-root.tsx +2 -0
  23. package/app-template/src/app/[pz]/configure-page-context.ts +17 -0
  24. package/app-template/src/app/[pz]/layout.tsx +22 -3
  25. package/app-template/src/app/[pz]/orders/checkout/layout.tsx +47 -0
  26. package/app-template/src/app/[pz]/orders/checkout/page.tsx +75 -97
  27. package/app-template/src/app/[pz]/orders/completed/[token]/layout.tsx +24 -2
  28. package/app-template/src/app/[pz]/users/password/reset/layout.tsx +21 -0
  29. package/app-template/src/app/[pz]/users/reset/[[...id]]/layout.tsx +30 -0
  30. package/app-template/src/components/checkbox.tsx +22 -3
  31. package/app-template/src/components/route-handler.tsx +48 -0
  32. package/app-template/src/routes/index.ts +6 -6
  33. package/app-template/src/views/checkout/__tests__/part-style-reachability.test.ts +119 -0
  34. package/app-template/src/views/checkout/step-button.tsx +13 -2
  35. package/app-template/src/views/checkout/step-list.tsx +5 -1
  36. package/app-template/src/views/checkout/steps/payment/agreements.tsx +9 -1
  37. package/app-template/src/views/checkout/steps/payment/index.tsx +10 -2
  38. package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +2 -0
  39. package/app-template/src/views/checkout/steps/payment/options/funds-transfer.tsx +2 -0
  40. package/app-template/src/views/checkout/steps/payment/payment-header.tsx +5 -1
  41. package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +16 -4
  42. package/app-template/src/views/checkout/steps/shipping/addresses.tsx +2 -1
  43. package/app-template/src/views/checkout/steps/shipping/index.tsx +2 -0
  44. package/app-template/src/views/checkout/steps/shipping/shipping-options.tsx +2 -0
  45. package/app-template/src/views/checkout/summary.tsx +56 -18
  46. package/app-template/src/views/checkout/variants/__tests__/registry.test.tsx +82 -0
  47. package/app-template/src/views/checkout/variants/__tests__/use-resolved-config.test.tsx +208 -0
  48. package/app-template/src/views/checkout/variants/basket-and-checkout/basket-section.tsx +150 -0
  49. package/app-template/src/views/checkout/variants/basket-and-checkout/index.tsx +135 -0
  50. package/app-template/src/views/checkout/variants/multi-step/index.tsx +78 -0
  51. package/app-template/src/views/checkout/variants/one-page/accordion-section.tsx +165 -0
  52. package/app-template/src/views/checkout/variants/one-page/hooks/use-checkout-funnel-gtm.ts +60 -0
  53. package/app-template/src/views/checkout/variants/one-page/hooks/use-section-state.ts +103 -0
  54. package/app-template/src/views/checkout/variants/one-page/index.tsx +125 -0
  55. package/app-template/src/views/checkout/variants/one-page/mobile-summary-sheet.tsx +109 -0
  56. package/app-template/src/views/checkout/variants/one-page/one-page-summary.tsx +301 -0
  57. package/app-template/src/views/checkout/variants/one-page/payment-options-grid.tsx +220 -0
  58. package/app-template/src/views/checkout/variants/one-page/sections/address-section.tsx +320 -0
  59. package/app-template/src/views/checkout/variants/one-page/sections/contact-section.tsx +62 -0
  60. package/app-template/src/views/checkout/variants/one-page/sections/payment-section.tsx +73 -0
  61. package/app-template/src/views/checkout/variants/one-page/sections/shipping-section.tsx +328 -0
  62. package/app-template/src/views/checkout/variants/one-page/skeletons/address-skeleton.tsx +25 -0
  63. package/app-template/src/views/checkout/variants/one-page/skeletons/checkout-skeleton.tsx +27 -0
  64. package/app-template/src/views/checkout/variants/one-page/skeletons/index.ts +6 -0
  65. package/app-template/src/views/checkout/variants/one-page/skeletons/payment-skeleton.tsx +23 -0
  66. package/app-template/src/views/checkout/variants/one-page/skeletons/section-skeleton.tsx +47 -0
  67. package/app-template/src/views/checkout/variants/one-page/skeletons/shipping-skeleton.tsx +20 -0
  68. package/app-template/src/views/checkout/variants/one-page/skeletons/summary-skeleton.tsx +38 -0
  69. package/app-template/src/views/checkout/variants/one-page/trust-badges.tsx +35 -0
  70. package/app-template/src/views/checkout/variants/registry.ts +43 -0
  71. package/app-template/src/views/checkout/variants/types.ts +30 -0
  72. package/app-template/src/views/checkout/variants/use-resolved-config.ts +168 -0
  73. package/app-template/tailwind.config.js +3 -1
  74. package/dist/commands/plugins.js +23 -2
  75. package/package.json +1 -1
  76. package/app-template/src/app/[pz]/account/orders/[id]/layout.tsx +0 -5
  77. /package/app-template/src/app/[pz]/account/{page.tsx → (dashboard)/page.tsx} +0 -0
  78. /package/app-template/src/app/[pz]/account/orders/{page.tsx → (list)/page.tsx} +0 -0
  79. /package/app-template/src/app/[pz]/account/orders/[id]/{page.tsx → (detail)/page.tsx} +0 -0
  80. /package/app-template/src/app/[pz]/auth/{page.tsx → (login)/page.tsx} +0 -0
@@ -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
+ });
@@ -0,0 +1,150 @@
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 =
19
+ (settings as { plugins?: { multiBasket?: boolean } })?.plugins
20
+ ?.multiBasket ?? false;
21
+
22
+ const totalQuantity = basket?.total_quantity ?? 0;
23
+ const totalAmount = basket?.total_amount ?? '0';
24
+ const initialSnapshotRef = useRef<{
25
+ qty: number;
26
+ amount: string;
27
+ } | null>(null);
28
+
29
+ useEffect(() => {
30
+ if (!isSuccess) return;
31
+ if (initialSnapshotRef.current === null) {
32
+ initialSnapshotRef.current = { qty: totalQuantity, amount: totalAmount };
33
+ return;
34
+ }
35
+ const prev = initialSnapshotRef.current;
36
+ if (prev.qty !== totalQuantity || prev.amount !== totalAmount) {
37
+ initialSnapshotRef.current = { qty: totalQuantity, amount: totalAmount };
38
+ dispatch(checkoutApi.util.invalidateTags(['Checkout']));
39
+ }
40
+ }, [isSuccess, totalQuantity, totalAmount, dispatch]);
41
+
42
+ if (isLoading) {
43
+ return (
44
+ <section
45
+ 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]"
46
+ data-testid="basket-section-loading"
47
+ >
48
+ <div className="flex justify-center">
49
+ <LoaderSpinner />
50
+ </div>
51
+ </section>
52
+ );
53
+ }
54
+
55
+ if (
56
+ isSuccess &&
57
+ (!basket?.basketitem_set || basket.basketitem_set.length === 0)
58
+ ) {
59
+ return (
60
+ <section
61
+ className="mb-5 rounded-2xl border-2 border-dashed border-gray-200 bg-white px-6 py-12 text-center"
62
+ data-testid="basket-section-empty"
63
+ >
64
+ <div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-gray-100">
65
+ <Icon
66
+ name="basket"
67
+ size={24}
68
+ className="fill-gray-500"
69
+ aria-hidden="true"
70
+ />
71
+ </div>
72
+ <h2 className="mb-2 text-lg font-semibold text-black-800">
73
+ {t('basket.empty.title')}
74
+ </h2>
75
+ <p className="mb-5 text-sm text-gray-600">
76
+ {t('basket.empty.content_first')}
77
+ </p>
78
+ <Link href={ROUTES.HOME} passHref>
79
+ <Button
80
+ appearance="filled"
81
+ className="px-6"
82
+ data-testid="basket-section-empty-cta"
83
+ >
84
+ {t('basket.empty.button')}
85
+ </Button>
86
+ </Link>
87
+ </section>
88
+ );
89
+ }
90
+
91
+ if (!basket) return null;
92
+
93
+ const itemCount = basket.basketitem_set.length;
94
+
95
+ return (
96
+ <section
97
+ 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]"
98
+ data-testid="basket-section"
99
+ >
100
+ <header className="flex items-center justify-between gap-3 px-6 py-5 sm:px-7">
101
+ <div className="flex items-baseline gap-3">
102
+ <h2 className="text-base font-semibold leading-tight text-black-800 sm:text-lg">
103
+ {t('checkout.one_page.basket.title')}
104
+ </h2>
105
+ <span className="text-[11px] font-medium uppercase tracking-wider text-gray-500">
106
+ {itemCount} {t('checkout.summary.items')}
107
+ </span>
108
+ </div>
109
+ <button
110
+ type="button"
111
+ onClick={() => setCollapsed((prev) => !prev)}
112
+ aria-expanded={!collapsed}
113
+ aria-controls="basket-section-content"
114
+ 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"
115
+ data-testid="basket-section-toggle"
116
+ >
117
+ {collapsed
118
+ ? t('checkout.one_page.basket.show')
119
+ : t('checkout.one_page.basket.hide')}
120
+ </button>
121
+ </header>
122
+
123
+ {!collapsed && (
124
+ <div
125
+ id="basket-section-content"
126
+ className="max-h-[28rem] overflow-y-auto border-t border-gray-100 px-6 sm:px-7"
127
+ >
128
+ <ul>
129
+ {multiBasket ? (
130
+ <PluginModule
131
+ component={Component.MultiBasket}
132
+ props={{ BasketItem }}
133
+ />
134
+ ) : (
135
+ basket.basketitem_set.map((basketItem, index) => (
136
+ <BasketItem
137
+ basketItem={basketItem}
138
+ namespace={basket.namespace}
139
+ key={`${basketItem.id}-${index}`}
140
+ />
141
+ ))
142
+ )}
143
+ </ul>
144
+ </div>
145
+ )}
146
+ </section>
147
+ );
148
+ };
149
+
150
+ export default BasketSection;
@@ -0,0 +1,135 @@
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 { CheckoutPiece } from '@akinon/pz-theme/src/chrome/checkout-piece';
7
+ import ContactSection from '../one-page/sections/contact-section';
8
+ import AddressSection from '../one-page/sections/address-section';
9
+ import ShippingSection from '../one-page/sections/shipping-section';
10
+ import PaymentSection from '../one-page/sections/payment-section';
11
+ import OnePageSummary from '../one-page/one-page-summary';
12
+ import MobileSummarySheet from '../one-page/mobile-summary-sheet';
13
+ import TrustBadges from '../one-page/trust-badges';
14
+ import {
15
+ useSectionState,
16
+ type SectionId
17
+ } from '../one-page/hooks/use-section-state';
18
+ import { useCheckoutFunnelGtm } from '../one-page/hooks/use-checkout-funnel-gtm';
19
+ import BasketSection from './basket-section';
20
+ import type { CheckoutVariantProps } from '../types';
21
+
22
+ const BasketAndCheckout = ({ options }: CheckoutVariantProps) => {
23
+ const autoAdvance = options?.autoAdvance ?? true;
24
+ const stickyMobileCta = options?.stickyMobileCta ?? true;
25
+ const enableEditMode = options?.enableEditMode ?? true;
26
+ const showOrderReview = options?.showOrderReview ?? true;
27
+ const showTrustBadges = options?.showTrustBadges ?? true;
28
+ const gtmEnabled = options?.gtmEnabled ?? true;
29
+ const compactMode = options?.compactMode ?? false;
30
+
31
+ const { sections, activeSection, openSection } = useSectionState({
32
+ autoAdvance
33
+ });
34
+ useCheckoutFunnelGtm(activeSection, { enabled: gtmEnabled });
35
+
36
+ const sectionStatus = useCallback(
37
+ (id: SectionId) =>
38
+ sections.find((section) => section.id === id)?.status ?? 'locked',
39
+ [sections]
40
+ );
41
+
42
+ const goTo = useCallback(
43
+ (id: SectionId) => () => openSection(id),
44
+ [openSection]
45
+ );
46
+
47
+ return (
48
+ <PluginModule component={Component.MasterpassProvider}>
49
+ <PluginModule component={Component.MasterpassDeleteConfirmationModal} />
50
+ <PluginModule component={Component.MasterpassOtpModal} />
51
+ <PluginModule component={Component.MasterpassLinkModal} />
52
+
53
+ <div
54
+ className={clsx(
55
+ 'min-h-screen bg-[#fafafa]',
56
+ compactMode ? 'pb-12 md:pb-8' : 'pb-20 md:pb-16'
57
+ )}
58
+ data-variant="basket-and-checkout"
59
+ data-compact={compactMode || undefined}
60
+ >
61
+ <div
62
+ className={clsx(
63
+ 'container w-full px-4 md:px-6 lg:px-8',
64
+ compactMode ? 'pt-4' : 'pt-6 md:pt-10'
65
+ )}
66
+ >
67
+ <div
68
+ className={clsx(
69
+ 'flex flex-col lg:flex-row lg:items-start',
70
+ compactMode ? 'gap-4 lg:gap-6' : 'gap-6 lg:gap-8'
71
+ )}
72
+ >
73
+ <div
74
+ className="w-full lg:w-2/3"
75
+ data-testid="basket-and-checkout-sections"
76
+ >
77
+ <BasketSection />
78
+
79
+ <ContactSection
80
+ status={sectionStatus('contact')}
81
+ onEdit={goTo('contact')}
82
+ />
83
+ <AddressSection
84
+ status={sectionStatus('address')}
85
+ onEdit={goTo('address')}
86
+ onComplete={goTo('shipping')}
87
+ editable={enableEditMode}
88
+ />
89
+ <ShippingSection
90
+ status={sectionStatus('shipping')}
91
+ onEdit={goTo('shipping')}
92
+ onComplete={goTo('payment')}
93
+ editable={enableEditMode}
94
+ />
95
+ <PaymentSection
96
+ status={sectionStatus('payment')}
97
+ onEdit={goTo('payment')}
98
+ />
99
+
100
+ {showTrustBadges && <TrustBadges />}
101
+ </div>
102
+
103
+ <aside
104
+ className="hidden w-full lg:block lg:w-1/3"
105
+ data-testid="basket-and-checkout-summary"
106
+ >
107
+ <div className="sticky top-6">
108
+ <CheckoutPiece piece="summary">
109
+ <OnePageSummary
110
+ onEditSection={openSection}
111
+ showReview={showOrderReview}
112
+ />
113
+ </CheckoutPiece>
114
+ </div>
115
+ </aside>
116
+ </div>
117
+ </div>
118
+
119
+ {stickyMobileCta && (
120
+ <div
121
+ className="fixed inset-x-0 bottom-0 z-30 lg:hidden"
122
+ data-testid="basket-and-checkout-mobile-cta"
123
+ >
124
+ <MobileSummarySheet
125
+ onEditSection={openSection}
126
+ showReview={showOrderReview}
127
+ />
128
+ </div>
129
+ )}
130
+ </div>
131
+ </PluginModule>
132
+ );
133
+ };
134
+
135
+ export default BasketAndCheckout;
@@ -0,0 +1,78 @@
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 { CheckoutPiece } from '@akinon/pz-theme/src/chrome/checkout-piece';
10
+ import { CheckoutStepList } from '@theme/views/checkout/step-list';
11
+ import { Summary } from '@theme/views/checkout/summary';
12
+ import ShippingStep from '@theme/views/checkout/steps/shipping';
13
+ import PaymentStep from '@theme/views/checkout/steps/payment';
14
+ import { pushAddPaymentInfo, pushAddShippingInfo } from '@theme/utils/gtm';
15
+ import type { CheckoutVariantProps } from '../types';
16
+
17
+ const MultiStepCheckout = (_props: CheckoutVariantProps) => {
18
+ const { steps, preOrder } = useAppSelector(
19
+ (state: RootState) => state.checkout
20
+ );
21
+ const dispatch = useAppDispatch();
22
+ const initialStepChanged = useRef<boolean>(false);
23
+
24
+ useEffect(() => {
25
+ if (steps.shipping.completed && !initialStepChanged.current) {
26
+ dispatch(setCurrentStep(CheckoutStep.Payment));
27
+ initialStepChanged.current = true;
28
+ }
29
+ }, [steps.shipping.completed]); // eslint-disable-line react-hooks/exhaustive-deps
30
+
31
+ useEffect(() => {
32
+ if (preOrder && !preOrder.shipping_option) {
33
+ initialStepChanged.current = true;
34
+ }
35
+ }, [preOrder]);
36
+
37
+ useEffect(() => {
38
+ if (!preOrder?.basket?.basketitem_set) return;
39
+ const products = preOrder.basket.basketitem_set.map((item) => ({
40
+ ...item.product
41
+ }));
42
+ if (steps.current === CheckoutStep.Shipping) {
43
+ pushAddShippingInfo(products);
44
+ }
45
+ if (steps.current === CheckoutStep.Payment) {
46
+ pushAddPaymentInfo(products, String(preOrder?.payment_option?.name));
47
+ }
48
+ }, [steps.current, preOrder?.payment_option?.name]); // eslint-disable-line react-hooks/exhaustive-deps
49
+
50
+ return (
51
+ <PluginModule component={Component.MasterpassProvider}>
52
+ <PluginModule component={Component.MasterpassDeleteConfirmationModal} />
53
+ <PluginModule component={Component.MasterpassOtpModal} />
54
+ <PluginModule component={Component.MasterpassLinkModal} />
55
+
56
+ <div className="container flex flex-col flex-wrap w-full px-4 md:px-0">
57
+ <CheckoutPiece piece="steps">
58
+ <CheckoutStepList />
59
+ </CheckoutPiece>
60
+
61
+ <div className="w-full flex flex-wrap">
62
+ <div className="w-full h-fit-content lg:w-2/3">
63
+ {steps.current === CheckoutStep.Shipping && <ShippingStep />}
64
+ {steps.current === CheckoutStep.Payment && <PaymentStep />}
65
+ </div>
66
+
67
+ <div className="w-full h-fit-content mt-6 lg:w-1/3 lg:pl-8 lg:mt-0">
68
+ <CheckoutPiece piece="summary">
69
+ <Summary />
70
+ </CheckoutPiece>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </PluginModule>
75
+ );
76
+ };
77
+
78
+ export default MultiStepCheckout;