@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
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { useAppSelector } from '@akinon/next/redux/hooks';
|
|
2
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
3
|
+
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
4
|
+
import { Price, Link } from '@theme/components';
|
|
5
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
6
|
+
import { Image } from '@akinon/next/components/image';
|
|
7
|
+
import { StoreCredits } from '@theme/views/checkout/steps/payment/options/store-credit';
|
|
8
|
+
import type { RootState } from '@theme/redux/store';
|
|
9
|
+
import type { SectionId } from './hooks/use-section-state';
|
|
10
|
+
|
|
11
|
+
interface OnePageSummaryProps {
|
|
12
|
+
onEditSection: (section: SectionId) => void;
|
|
13
|
+
showReview?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const OnePageSummary = ({
|
|
17
|
+
onEditSection,
|
|
18
|
+
showReview = true
|
|
19
|
+
}: OnePageSummaryProps) => {
|
|
20
|
+
const { t } = useLocalization();
|
|
21
|
+
const preOrder = useAppSelector(
|
|
22
|
+
(state: RootState) => state.checkout.preOrder
|
|
23
|
+
);
|
|
24
|
+
const { isMobileApp } = useAppSelector((state: RootState) => state.root);
|
|
25
|
+
|
|
26
|
+
if (!preOrder?.basket) return null;
|
|
27
|
+
|
|
28
|
+
const itemCount = preOrder.basket.basketitem_set.length;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex flex-col gap-4">
|
|
32
|
+
<PluginModule
|
|
33
|
+
component={Component.CheckoutGiftPack}
|
|
34
|
+
props={{
|
|
35
|
+
className:
|
|
36
|
+
'flex flex-col w-full rounded-2xl bg-white shadow-[0_1px_3px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.04]'
|
|
37
|
+
}}
|
|
38
|
+
/>
|
|
39
|
+
<StoreCredits />
|
|
40
|
+
|
|
41
|
+
<section
|
|
42
|
+
className="overflow-hidden rounded-2xl bg-white shadow-[0_1px_3px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.04]"
|
|
43
|
+
{...partAttrs('summary')}
|
|
44
|
+
>
|
|
45
|
+
<header
|
|
46
|
+
className="flex items-baseline justify-between px-6 py-5"
|
|
47
|
+
{...partAttrs('summary-header')}
|
|
48
|
+
>
|
|
49
|
+
<span
|
|
50
|
+
className="text-lg font-semibold tracking-tight text-black-800"
|
|
51
|
+
{...partAttrs('summary-title')}
|
|
52
|
+
>
|
|
53
|
+
{t('checkout.summary.title')}
|
|
54
|
+
</span>
|
|
55
|
+
<span className="text-[11px] font-medium uppercase tracking-wider text-gray-500">
|
|
56
|
+
{itemCount} {t('checkout.summary.items')}
|
|
57
|
+
</span>
|
|
58
|
+
</header>
|
|
59
|
+
|
|
60
|
+
<ul className="max-h-72 divide-y divide-gray-100 overflow-y-auto border-t border-gray-100">
|
|
61
|
+
{preOrder.basket.basketitem_set.map((item, index) => (
|
|
62
|
+
<li
|
|
63
|
+
key={`one-page-summary-item-${index}`}
|
|
64
|
+
className="flex gap-3 px-6 py-4"
|
|
65
|
+
{...partAttrs('summary-item')}
|
|
66
|
+
>
|
|
67
|
+
<Link
|
|
68
|
+
href={item.product.absolute_url || '#'}
|
|
69
|
+
passHref
|
|
70
|
+
className={
|
|
71
|
+
isMobileApp
|
|
72
|
+
? 'pointer-events-none flex-shrink-0'
|
|
73
|
+
: 'flex-shrink-0 overflow-hidden rounded-md'
|
|
74
|
+
}
|
|
75
|
+
>
|
|
76
|
+
<Image
|
|
77
|
+
src={item.product.productimage_set[0]?.image}
|
|
78
|
+
alt={item.product.name}
|
|
79
|
+
width={56}
|
|
80
|
+
height={84}
|
|
81
|
+
/>
|
|
82
|
+
</Link>
|
|
83
|
+
<div className="flex flex-1 flex-col justify-between text-xs">
|
|
84
|
+
<Link
|
|
85
|
+
href={item.product.absolute_url || '#'}
|
|
86
|
+
className="line-clamp-2 text-sm font-medium text-black-800 transition-colors hover:text-secondary"
|
|
87
|
+
>
|
|
88
|
+
{item.product.name}
|
|
89
|
+
</Link>
|
|
90
|
+
<div className="flex items-end justify-between gap-2">
|
|
91
|
+
<span className="text-gray-500">
|
|
92
|
+
{t('checkout.summary.quantity')}:{' '}
|
|
93
|
+
<span className="font-medium tabular-nums text-black-800">
|
|
94
|
+
{item.quantity}
|
|
95
|
+
</span>
|
|
96
|
+
</span>
|
|
97
|
+
<div className="flex flex-col items-end gap-0.5">
|
|
98
|
+
{item.product.retail_price !== item.product.price && (
|
|
99
|
+
<span className="text-[10px] text-gray-400 line-through">
|
|
100
|
+
<Price value={item.product.retail_price} />
|
|
101
|
+
</span>
|
|
102
|
+
)}
|
|
103
|
+
<span className="text-sm font-semibold text-black-800 tabular-nums">
|
|
104
|
+
<Price value={item.product.price} />
|
|
105
|
+
</span>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</li>
|
|
110
|
+
))}
|
|
111
|
+
</ul>
|
|
112
|
+
|
|
113
|
+
<dl className="space-y-2.5 border-t border-gray-100 px-6 py-5 text-sm text-gray-600">
|
|
114
|
+
<div
|
|
115
|
+
className="flex items-center justify-between"
|
|
116
|
+
{...partAttrs('summary-row')}
|
|
117
|
+
>
|
|
118
|
+
<dt {...partAttrs('summary-row-label')}>
|
|
119
|
+
{t('checkout.summary.subtotal')}
|
|
120
|
+
</dt>
|
|
121
|
+
{/*
|
|
122
|
+
The amount declares its own weight and colour, so a rule on the
|
|
123
|
+
row can never reach it by inheritance — it gets its own part
|
|
124
|
+
instead of leaving the row's Text Color a dead control.
|
|
125
|
+
*/}
|
|
126
|
+
<dd
|
|
127
|
+
className="font-medium tabular-nums text-black-800"
|
|
128
|
+
{...partAttrs('summary-row-value')}
|
|
129
|
+
>
|
|
130
|
+
<Price value={preOrder.basket.total_amount} />
|
|
131
|
+
</dd>
|
|
132
|
+
</div>
|
|
133
|
+
<div
|
|
134
|
+
className="flex items-center justify-between"
|
|
135
|
+
{...partAttrs('summary-row')}
|
|
136
|
+
>
|
|
137
|
+
<dt {...partAttrs('summary-row-label')}>
|
|
138
|
+
{t('checkout.summary.shipping')}
|
|
139
|
+
</dt>
|
|
140
|
+
<dd
|
|
141
|
+
className="font-medium tabular-nums text-black-800"
|
|
142
|
+
{...partAttrs('summary-row-value')}
|
|
143
|
+
>
|
|
144
|
+
<Price value={preOrder.shipping_amount} />
|
|
145
|
+
</dd>
|
|
146
|
+
</div>
|
|
147
|
+
{parseFloat(preOrder.loyalty_money || '0') > 0 && (
|
|
148
|
+
<div
|
|
149
|
+
className="flex items-center justify-between"
|
|
150
|
+
{...partAttrs('summary-row')}
|
|
151
|
+
>
|
|
152
|
+
<dt {...partAttrs('summary-row-label')}>
|
|
153
|
+
{t('checkout.summary.loyalty_money_total')}
|
|
154
|
+
</dt>
|
|
155
|
+
<dd
|
|
156
|
+
className="font-medium tabular-nums text-black-800"
|
|
157
|
+
{...partAttrs('summary-row-value')}
|
|
158
|
+
>
|
|
159
|
+
<Price value={preOrder.loyalty_money} />
|
|
160
|
+
</dd>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
{parseFloat(preOrder.basket.total_discount_amount || '0') > 0 && (
|
|
164
|
+
<div
|
|
165
|
+
className="flex items-center justify-between"
|
|
166
|
+
{...partAttrs('summary-row')}
|
|
167
|
+
>
|
|
168
|
+
<dt {...partAttrs('summary-row-label')}>
|
|
169
|
+
{t('checkout.summary.discounts_total')}
|
|
170
|
+
</dt>
|
|
171
|
+
<dd
|
|
172
|
+
className="font-medium tabular-nums text-black-800"
|
|
173
|
+
{...partAttrs('summary-row-value')}
|
|
174
|
+
>
|
|
175
|
+
<Price
|
|
176
|
+
value={preOrder.basket.total_discount_amount}
|
|
177
|
+
useNegative
|
|
178
|
+
/>
|
|
179
|
+
</dd>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
</dl>
|
|
183
|
+
|
|
184
|
+
{/*
|
|
185
|
+
The row carries the baseline typography that used to live on the
|
|
186
|
+
label span, so the row's own controls have something to change; the
|
|
187
|
+
amount keeps its distinct treatment and answers to its own part.
|
|
188
|
+
*/}
|
|
189
|
+
<div
|
|
190
|
+
className="flex items-baseline justify-between border-t border-gray-100 px-6 py-5 text-sm font-medium uppercase tracking-wider text-gray-700"
|
|
191
|
+
{...partAttrs('summary-total')}
|
|
192
|
+
>
|
|
193
|
+
<span {...partAttrs('summary-total-label')}>
|
|
194
|
+
{t('checkout.summary.total')}
|
|
195
|
+
</span>
|
|
196
|
+
<span
|
|
197
|
+
className="text-2xl font-bold tracking-tight text-black-800 tabular-nums"
|
|
198
|
+
{...partAttrs('summary-total-value')}
|
|
199
|
+
>
|
|
200
|
+
<Price value={preOrder.unpaid_amount} />
|
|
201
|
+
</span>
|
|
202
|
+
</div>
|
|
203
|
+
</section>
|
|
204
|
+
|
|
205
|
+
{showReview &&
|
|
206
|
+
(preOrder.shipping_address ||
|
|
207
|
+
preOrder.shipping_option ||
|
|
208
|
+
preOrder.payment_option) && (
|
|
209
|
+
<section className="overflow-hidden rounded-2xl bg-white shadow-[0_1px_3px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.04]">
|
|
210
|
+
<header className="px-6 py-5">
|
|
211
|
+
<span className="text-base font-semibold tracking-tight text-black-800">
|
|
212
|
+
{t('checkout.one_page.summary.review_title')}
|
|
213
|
+
</span>
|
|
214
|
+
</header>
|
|
215
|
+
|
|
216
|
+
<ul className="divide-y divide-gray-100 border-t border-gray-100 text-sm text-black-800">
|
|
217
|
+
{preOrder.shipping_address && (
|
|
218
|
+
<li className="flex items-start justify-between gap-3 px-6 py-4">
|
|
219
|
+
<div className="flex-1">
|
|
220
|
+
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
|
|
221
|
+
{t('checkout.one_page.summary.deliver_to')}
|
|
222
|
+
</div>
|
|
223
|
+
<div className="font-medium">
|
|
224
|
+
{preOrder.shipping_address.title}
|
|
225
|
+
</div>
|
|
226
|
+
<div className="text-xs text-gray-600">
|
|
227
|
+
{preOrder.shipping_address.line},{' '}
|
|
228
|
+
{preOrder.shipping_address.township?.name},{' '}
|
|
229
|
+
{preOrder.shipping_address.city?.name}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
onClick={() => onEditSection('address')}
|
|
235
|
+
className="rounded-md px-2 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-black-800"
|
|
236
|
+
data-testid="one-page-summary-edit-address"
|
|
237
|
+
{...partAttrs('summary-edit')}
|
|
238
|
+
>
|
|
239
|
+
{t('checkout.one_page.edit')}
|
|
240
|
+
</button>
|
|
241
|
+
</li>
|
|
242
|
+
)}
|
|
243
|
+
|
|
244
|
+
{preOrder.shipping_option && (
|
|
245
|
+
<li className="flex items-start justify-between gap-3 px-6 py-4">
|
|
246
|
+
<div className="flex-1">
|
|
247
|
+
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
|
|
248
|
+
{t('checkout.one_page.summary.shipping_method')}
|
|
249
|
+
</div>
|
|
250
|
+
<div className="font-medium">
|
|
251
|
+
{preOrder.shipping_option.name}
|
|
252
|
+
</div>
|
|
253
|
+
{preOrder.shipping_option.shipping_amount && (
|
|
254
|
+
<div className="text-xs tabular-nums text-gray-600">
|
|
255
|
+
<Price
|
|
256
|
+
value={preOrder.shipping_option.shipping_amount}
|
|
257
|
+
/>
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
<button
|
|
262
|
+
type="button"
|
|
263
|
+
onClick={() => onEditSection('shipping')}
|
|
264
|
+
className="rounded-md px-2 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-black-800"
|
|
265
|
+
data-testid="one-page-summary-edit-shipping"
|
|
266
|
+
{...partAttrs('summary-edit')}
|
|
267
|
+
>
|
|
268
|
+
{t('checkout.one_page.edit')}
|
|
269
|
+
</button>
|
|
270
|
+
</li>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
{preOrder.payment_option && (
|
|
274
|
+
<li className="flex items-start justify-between gap-3 px-6 py-4">
|
|
275
|
+
<div className="flex-1">
|
|
276
|
+
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
|
|
277
|
+
{t('checkout.one_page.summary.payment_method')}
|
|
278
|
+
</div>
|
|
279
|
+
<div className="font-medium">
|
|
280
|
+
{preOrder.payment_option.name}
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
<button
|
|
284
|
+
type="button"
|
|
285
|
+
onClick={() => onEditSection('payment')}
|
|
286
|
+
className="rounded-md px-2 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-black-800"
|
|
287
|
+
data-testid="one-page-summary-edit-payment"
|
|
288
|
+
{...partAttrs('summary-edit')}
|
|
289
|
+
>
|
|
290
|
+
{t('checkout.one_page.edit')}
|
|
291
|
+
</button>
|
|
292
|
+
</li>
|
|
293
|
+
)}
|
|
294
|
+
</ul>
|
|
295
|
+
</section>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export default OnePageSummary;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { useAppSelector } from '@akinon/next/redux/hooks';
|
|
4
|
+
import { useSetPaymentOptionMutation } from '@akinon/next/data/client/checkout';
|
|
5
|
+
import { usePaymentOptions } from '@akinon/next/hooks/use-payment-options';
|
|
6
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
7
|
+
import { partAttrs } from '@akinon/pz-theme/src/utils/part-styles';
|
|
8
|
+
import { Icon } from '@theme/components';
|
|
9
|
+
import type { RootState } from '@theme/redux/store';
|
|
10
|
+
import type { CheckoutPaymentOption } from '@akinon/next/types';
|
|
11
|
+
|
|
12
|
+
const PAYMENT_TYPE_ICON: Record<string, string> = {
|
|
13
|
+
credit_card: 'cvc',
|
|
14
|
+
funds_transfer: 'file',
|
|
15
|
+
pay_on_delivery: 'address',
|
|
16
|
+
bkm_express: 'akinon',
|
|
17
|
+
masterpass: 'cvc',
|
|
18
|
+
masterpass_rest: 'cvc',
|
|
19
|
+
saved_card: 'cvc',
|
|
20
|
+
gpay: 'google',
|
|
21
|
+
google_pay: 'google',
|
|
22
|
+
apple_pay: 'apple',
|
|
23
|
+
wallet: 'akinon',
|
|
24
|
+
redirection: 'globe',
|
|
25
|
+
credit_payment: 'installment',
|
|
26
|
+
loyalty: 'heart-full',
|
|
27
|
+
hepsipay: 'akinon',
|
|
28
|
+
tabby: 'installment',
|
|
29
|
+
tamara: 'installment',
|
|
30
|
+
iyzico: 'cvc',
|
|
31
|
+
cybersource_uc: 'cvc',
|
|
32
|
+
flow_payment: 'installment'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const SLUG_KEYWORD_ICON: Array<[string, string]> = [
|
|
36
|
+
['apple', 'apple'],
|
|
37
|
+
['google', 'google'],
|
|
38
|
+
['gpay', 'google'],
|
|
39
|
+
['hepsi', 'akinon'],
|
|
40
|
+
['tabby', 'installment'],
|
|
41
|
+
['tamara', 'installment'],
|
|
42
|
+
['flow', 'installment'],
|
|
43
|
+
['saved', 'cvc'],
|
|
44
|
+
['credit', 'cvc'],
|
|
45
|
+
['transfer', 'file'],
|
|
46
|
+
['delivery', 'address'],
|
|
47
|
+
['wallet', 'akinon']
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const getOptionIcon = (option: CheckoutPaymentOption): string => {
|
|
51
|
+
if (PAYMENT_TYPE_ICON[option.payment_type]) {
|
|
52
|
+
return PAYMENT_TYPE_ICON[option.payment_type];
|
|
53
|
+
}
|
|
54
|
+
const slug = option.slug?.toLowerCase() || '';
|
|
55
|
+
for (const [keyword, icon] of SLUG_KEYWORD_ICON) {
|
|
56
|
+
if (slug.includes(keyword)) return icon;
|
|
57
|
+
}
|
|
58
|
+
return 'default';
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const PaymentOptionsGrid = () => {
|
|
62
|
+
const { t } = useLocalization();
|
|
63
|
+
const { preOrder, attributeBasedShippingOptions } = useAppSelector(
|
|
64
|
+
(state: RootState) => state.checkout
|
|
65
|
+
);
|
|
66
|
+
const [setPaymentOption, { isLoading }] = useSetPaymentOptionMutation();
|
|
67
|
+
const { filteredPaymentOptions } = usePaymentOptions();
|
|
68
|
+
|
|
69
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
70
|
+
|
|
71
|
+
const visibleOptions = useMemo(() => {
|
|
72
|
+
if (
|
|
73
|
+
attributeBasedShippingOptions &&
|
|
74
|
+
Object.keys(attributeBasedShippingOptions).length > 0
|
|
75
|
+
) {
|
|
76
|
+
return filteredPaymentOptions.filter(
|
|
77
|
+
(option) => option.slug?.toLowerCase() !== 'pay-on-delivery'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return filteredPaymentOptions;
|
|
81
|
+
}, [filteredPaymentOptions, attributeBasedShippingOptions]);
|
|
82
|
+
|
|
83
|
+
const handleKeyDown = useCallback(
|
|
84
|
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
85
|
+
if (
|
|
86
|
+
![
|
|
87
|
+
'ArrowRight',
|
|
88
|
+
'ArrowLeft',
|
|
89
|
+
'ArrowDown',
|
|
90
|
+
'ArrowUp',
|
|
91
|
+
'Home',
|
|
92
|
+
'End'
|
|
93
|
+
].includes(event.key)
|
|
94
|
+
) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
const buttons = Array.from(
|
|
99
|
+
containerRef.current?.querySelectorAll<HTMLButtonElement>(
|
|
100
|
+
'button[role="radio"]'
|
|
101
|
+
) ?? []
|
|
102
|
+
);
|
|
103
|
+
if (buttons.length === 0) return;
|
|
104
|
+
const currentIndex = buttons.findIndex(
|
|
105
|
+
(btn) => btn === document.activeElement
|
|
106
|
+
);
|
|
107
|
+
let nextIndex = currentIndex;
|
|
108
|
+
switch (event.key) {
|
|
109
|
+
case 'ArrowRight':
|
|
110
|
+
case 'ArrowDown':
|
|
111
|
+
nextIndex = (currentIndex + 1 + buttons.length) % buttons.length;
|
|
112
|
+
break;
|
|
113
|
+
case 'ArrowLeft':
|
|
114
|
+
case 'ArrowUp':
|
|
115
|
+
nextIndex = (currentIndex - 1 + buttons.length) % buttons.length;
|
|
116
|
+
break;
|
|
117
|
+
case 'Home':
|
|
118
|
+
nextIndex = 0;
|
|
119
|
+
break;
|
|
120
|
+
case 'End':
|
|
121
|
+
nextIndex = buttons.length - 1;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
buttons[nextIndex]?.focus();
|
|
125
|
+
},
|
|
126
|
+
[]
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (visibleOptions.length === 0) {
|
|
130
|
+
return (
|
|
131
|
+
<p className="rounded-xl border border-dashed border-gray-200 bg-gray-50 px-5 py-4 text-sm italic text-gray-600">
|
|
132
|
+
{t('checkout.one_page.payment.no_options')}
|
|
133
|
+
</p>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div
|
|
139
|
+
ref={containerRef}
|
|
140
|
+
className={clsx(
|
|
141
|
+
'grid grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-3',
|
|
142
|
+
{ 'pointer-events-none opacity-60': isLoading }
|
|
143
|
+
)}
|
|
144
|
+
role="radiogroup"
|
|
145
|
+
aria-label={t('checkout.one_page.payment.title')}
|
|
146
|
+
onKeyDown={handleKeyDown}
|
|
147
|
+
{...partAttrs('payment-options')}
|
|
148
|
+
>
|
|
149
|
+
{visibleOptions.map((option, index) => {
|
|
150
|
+
const selected = preOrder?.payment_option?.pk === option.pk;
|
|
151
|
+
return (
|
|
152
|
+
<button
|
|
153
|
+
key={option.pk}
|
|
154
|
+
type="button"
|
|
155
|
+
role="radio"
|
|
156
|
+
aria-checked={selected}
|
|
157
|
+
tabIndex={
|
|
158
|
+
selected || (!preOrder?.payment_option && index === 0) ? 0 : -1
|
|
159
|
+
}
|
|
160
|
+
onClick={() => setPaymentOption(option.pk)}
|
|
161
|
+
className={clsx(
|
|
162
|
+
'group relative flex min-h-[5rem] items-center gap-3',
|
|
163
|
+
'rounded-xl border px-4 py-3 text-left',
|
|
164
|
+
// Typography and colour sit on the button — the element the
|
|
165
|
+
// payment-option part is stamped on — instead of on the label
|
|
166
|
+
// span. A part rule can only reach the label by inheritance, and
|
|
167
|
+
// a utility class declared on the span outranks an inherited
|
|
168
|
+
// value however !important the rule is, so every Text Color /
|
|
169
|
+
// Font Size / Font Weight control in the panel was a silent
|
|
170
|
+
// no-op while these lived one level down.
|
|
171
|
+
'text-sm font-medium leading-tight',
|
|
172
|
+
'transition-all duration-200 ease-out',
|
|
173
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-black-800 focus-visible:ring-offset-2',
|
|
174
|
+
selected
|
|
175
|
+
? 'border-black-800 bg-black-800/[0.03] text-black-800 shadow-[inset_0_0_0_1px_rgb(31,31,31)]'
|
|
176
|
+
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-400'
|
|
177
|
+
)}
|
|
178
|
+
data-testid={`one-page-payment-card-${option.pk}`}
|
|
179
|
+
data-payment-type={option.payment_type}
|
|
180
|
+
{...partAttrs('payment-option', { selected })}
|
|
181
|
+
>
|
|
182
|
+
{/*
|
|
183
|
+
`Icon` renders an icon FONT (<i class="pz-icon-…">), so the
|
|
184
|
+
`fill-*` utilities that used to sit here never coloured
|
|
185
|
+
anything — fill only applies to SVG. The glyph takes its colour
|
|
186
|
+
from `color`, which now comes from the button, so the icon
|
|
187
|
+
follows both the selected state and the panel's Text Color.
|
|
188
|
+
*/}
|
|
189
|
+
<Icon
|
|
190
|
+
name={getOptionIcon(option)}
|
|
191
|
+
size={20}
|
|
192
|
+
className="flex-shrink-0 transition-colors"
|
|
193
|
+
aria-hidden="true"
|
|
194
|
+
{...partAttrs('payment-option-icon')}
|
|
195
|
+
/>
|
|
196
|
+
<span className="flex-1">{option.name}</span>
|
|
197
|
+
{/*
|
|
198
|
+
A ring of `currentColor` with a transparent centre: the marker
|
|
199
|
+
follows whatever colour the composition sets on the button and
|
|
200
|
+
the middle shows the card's own background, so it stays legible
|
|
201
|
+
on any palette. The geometry is what the filled circle plus its
|
|
202
|
+
6px inner dot drew before.
|
|
203
|
+
*/}
|
|
204
|
+
<span
|
|
205
|
+
className={clsx(
|
|
206
|
+
'flex h-4 w-4 flex-shrink-0 rounded-full transition-all',
|
|
207
|
+
selected
|
|
208
|
+
? 'border-[5px] border-current'
|
|
209
|
+
: 'border-2 border-gray-300 group-hover:border-gray-400'
|
|
210
|
+
)}
|
|
211
|
+
aria-hidden="true"
|
|
212
|
+
/>
|
|
213
|
+
</button>
|
|
214
|
+
);
|
|
215
|
+
})}
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export default PaymentOptionsGrid;
|