@akinon/projectzero 2.0.0-beta.12 → 2.0.0-beta.14

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 (179) hide show
  1. package/CHANGELOG.md +102 -23
  2. package/app-template/.env.example +1 -0
  3. package/app-template/.github/instructions/account.instructions.md +749 -0
  4. package/app-template/.github/instructions/checkout.instructions.md +678 -0
  5. package/app-template/.github/instructions/default.instructions.md +279 -0
  6. package/app-template/.github/instructions/edge-cases.instructions.md +73 -0
  7. package/app-template/.github/instructions/routing.instructions.md +603 -0
  8. package/app-template/.github/instructions/settings.instructions.md +338 -0
  9. package/app-template/.gitignore +3 -0
  10. package/app-template/AGENTS.md +7 -0
  11. package/app-template/CHANGELOG.md +1387 -310
  12. package/app-template/Procfile +1 -1
  13. package/app-template/akinon.json +0 -3
  14. package/app-template/build.sh +10 -0
  15. package/app-template/docs/advanced-usage.md +101 -0
  16. package/app-template/docs/sentry-usage.md +35 -0
  17. package/app-template/next-env.d.ts +1 -0
  18. package/app-template/{next.config.ts → next.config.mjs} +6 -6
  19. package/app-template/package.json +58 -51
  20. package/app-template/postcss.config.mjs +1 -4
  21. package/app-template/public/locales/en/checkout.json +11 -0
  22. package/app-template/public/locales/en/common.json +50 -1
  23. package/app-template/public/locales/en/product.json +62 -1
  24. package/app-template/public/locales/tr/checkout.json +11 -0
  25. package/app-template/public/locales/tr/common.json +50 -1
  26. package/app-template/public/locales/tr/product.json +63 -0
  27. package/app-template/public/masterpass-javascript-sdk-web.min.js +1 -0
  28. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/[...prettyurl]/page.tsx +9 -9
  29. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/layout.tsx +2 -2
  30. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/orders/[id]/cancellation/page.tsx +6 -6
  31. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/orders/[id]/page.tsx +6 -6
  32. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/page.tsx +1 -1
  33. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/profile/page.tsx +2 -2
  34. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/address/stores/page.tsx +2 -2
  35. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/auth/page.tsx +1 -1
  36. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/basket/page.tsx +2 -2
  37. package/app-template/src/app/[pz]/category/[pk]/page.tsx +27 -0
  38. package/app-template/src/app/[pz]/flat-page/[pk]/page.tsx +23 -0
  39. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/forms/[pk]/generate/page.tsx +2 -3
  40. package/app-template/src/app/[pz]/group-product/[pk]/page.tsx +93 -0
  41. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/landing-page/[pk]/page.tsx +2 -4
  42. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/layout.tsx +3 -10
  43. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/list/page.tsx +2 -4
  44. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/not-found.tsx +5 -7
  45. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/orders/completed/[token]/page.tsx +6 -4
  46. package/app-template/src/app/[pz]/product/[pk]/page.tsx +102 -0
  47. package/app-template/src/app/[pz]/special-page/[pk]/page.tsx +35 -0
  48. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/users/email-set-primary/[[...id]]/page.tsx +3 -4
  49. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/users/registration/account-confirm-email/[[...id]]/page.tsx +3 -3
  50. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/users/reset/[[...id]]/page.tsx +6 -12
  51. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/xml-sitemap/[node]/route.ts +2 -2
  52. package/app-template/src/app/api/auth/[...nextauth]/route.ts +3 -0
  53. package/app-template/src/app/api/form/[...id]/route.ts +1 -7
  54. package/app-template/src/app/api/image-proxy/route.ts +1 -0
  55. package/app-template/src/app/api/product-categories/route.ts +1 -0
  56. package/app-template/src/app/api/similar-product-list/route.ts +1 -0
  57. package/app-template/src/app/api/similar-products/route.ts +1 -0
  58. package/app-template/src/app/api/virtual-try-on/limited-categories/route.ts +1 -0
  59. package/app-template/src/app/api/virtual-try-on/route.ts +1 -0
  60. package/app-template/src/assets/globals.scss +4 -133
  61. package/app-template/src/auth.ts +3 -0
  62. package/app-template/src/components/__tests__/badge.test.tsx +2 -2
  63. package/app-template/src/components/__tests__/link.test.tsx +2 -0
  64. package/app-template/src/components/accordion.tsx +23 -20
  65. package/app-template/src/components/button.tsx +1 -1
  66. package/app-template/src/components/carousel-core.tsx +4 -11
  67. package/app-template/src/components/checkbox.tsx +1 -1
  68. package/app-template/src/components/currency-select.tsx +1 -0
  69. package/app-template/src/components/file-input.tsx +27 -7
  70. package/app-template/src/components/generate-form-fields.tsx +49 -10
  71. package/app-template/src/components/input.tsx +11 -5
  72. package/app-template/src/components/modal.tsx +32 -16
  73. package/app-template/src/components/pagination.tsx +1 -0
  74. package/app-template/src/components/price.tsx +1 -1
  75. package/app-template/src/components/pwa-tags.tsx +1 -0
  76. package/app-template/src/components/select.tsx +39 -27
  77. package/app-template/src/components/shimmer.tsx +1 -1
  78. package/app-template/src/components/types/index.ts +25 -1
  79. package/app-template/src/hooks/use-fav-button.tsx +4 -8
  80. package/app-template/src/hooks/use-product-cart.ts +77 -0
  81. package/app-template/src/hooks/use-stock-alert.ts +74 -0
  82. package/app-template/src/plugins.js +12 -2
  83. package/app-template/src/redux/middlewares/category.ts +5 -4
  84. package/app-template/src/redux/store.ts +21 -1
  85. package/app-template/src/routes/index.ts +2 -1
  86. package/app-template/src/settings.js +3 -1
  87. package/app-template/src/types/index.ts +74 -3
  88. package/app-template/src/types/next-auth.d.ts +2 -2
  89. package/app-template/src/utils/variant-validation.ts +41 -0
  90. package/app-template/src/views/account/address-form.tsx +8 -4
  91. package/app-template/src/views/account/contact-form.tsx +2 -2
  92. package/app-template/src/views/account/content-header.tsx +4 -3
  93. package/app-template/src/views/account/faq/faq-tabs.tsx +8 -2
  94. package/app-template/src/views/account/order.tsx +1 -1
  95. package/app-template/src/views/account/orders/order-cancellation-item.tsx +1 -1
  96. package/app-template/src/views/anonymous-tracking/order-detail/index.tsx +1 -1
  97. package/app-template/src/views/basket/basket-item.tsx +6 -1
  98. package/app-template/src/views/basket/summary.tsx +16 -0
  99. package/app-template/src/views/breadcrumb.tsx +2 -2
  100. package/app-template/src/views/category/category-info.tsx +2 -1
  101. package/app-template/src/views/category/filters/index.tsx +1 -1
  102. package/app-template/src/views/checkout/auth.tsx +1 -1
  103. package/app-template/src/views/checkout/layout/header.tsx +1 -1
  104. package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +22 -6
  105. package/app-template/src/views/checkout/steps/payment/options/funds-transfer.tsx +25 -5
  106. package/app-template/src/views/checkout/steps/payment/options/loyalty.tsx +21 -2
  107. package/app-template/src/views/checkout/steps/payment/options/redirection.tsx +22 -4
  108. package/app-template/src/views/checkout/steps/payment/options/store-credit.tsx +121 -0
  109. package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +4 -4
  110. package/app-template/src/views/checkout/steps/shipping/address-box.tsx +3 -3
  111. package/app-template/src/views/checkout/steps/shipping/addresses.tsx +1 -1
  112. package/app-template/src/views/checkout/summary.tsx +12 -2
  113. package/app-template/src/views/find-in-store/index.tsx +2 -2
  114. package/app-template/src/views/header/action-menu.tsx +2 -6
  115. package/app-template/src/views/header/band.tsx +2 -2
  116. package/app-template/src/views/header/index.tsx +1 -1
  117. package/app-template/src/views/header/mini-basket.tsx +2 -2
  118. package/app-template/src/views/header/mobile-menu.tsx +6 -6
  119. package/app-template/src/views/header/navbar.tsx +1 -1
  120. package/app-template/src/views/header/pwa-back-button.tsx +1 -1
  121. package/app-template/src/views/header/search/index.tsx +13 -3
  122. package/app-template/src/views/header/search/results.tsx +1 -1
  123. package/app-template/src/views/header/user-menu.tsx +1 -3
  124. package/app-template/src/views/login/index.tsx +14 -13
  125. package/app-template/src/views/otp-login/index.tsx +11 -6
  126. package/app-template/src/views/product/layout.tsx +15 -1
  127. package/app-template/src/views/product/product-actions.tsx +165 -0
  128. package/app-template/src/views/product/product-info.tsx +69 -261
  129. package/app-template/src/views/product/product-share.tsx +56 -0
  130. package/app-template/src/views/product/product-variants.tsx +26 -0
  131. package/app-template/src/views/product/slider.tsx +22 -1
  132. package/app-template/src/views/product-pointer-banner-item.tsx +1 -1
  133. package/app-template/src/views/register/index.tsx +17 -21
  134. package/app-template/src/views/sales-contract-modal/index.tsx +17 -17
  135. package/app-template/src/widgets/footer-info.tsx +1 -1
  136. package/app-template/src/widgets/footer-menu.tsx +7 -3
  137. package/app-template/src/widgets/footer-subscription/index.tsx +1 -1
  138. package/app-template/src/widgets/home-stories-eng.tsx +43 -35
  139. package/app-template/tailwind.config.js +129 -1
  140. package/app-template/tsconfig.json +29 -11
  141. package/codemods/migrate-segments/index.js +591 -0
  142. package/commands/plugins.ts +62 -14
  143. package/dist/commands/plugins.js +62 -14
  144. package/package.json +1 -1
  145. package/app-template/src/app/[commerce]/[locale]/[currency]/category/[pk]/page.tsx +0 -22
  146. package/app-template/src/app/[commerce]/[locale]/[currency]/flat-page/[pk]/page.tsx +0 -20
  147. package/app-template/src/app/[commerce]/[locale]/[currency]/group-product/[pk]/page.tsx +0 -74
  148. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/page.tsx +0 -84
  149. package/app-template/src/app/[commerce]/[locale]/[currency]/special-page/[pk]/page.tsx +0 -27
  150. package/app-template/src/pages/api/auth/[...nextauth].ts +0 -3
  151. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/address/page.tsx +0 -0
  152. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/change-email/page.tsx +0 -0
  153. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/change-password/page.tsx +0 -0
  154. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/contact/page.tsx +0 -0
  155. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/coupons/page.tsx +0 -0
  156. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/email-verification/page.tsx +0 -0
  157. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/faq/page.tsx +0 -0
  158. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/favourite-products/page.tsx +0 -0
  159. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/my-quotations/page.tsx +0 -0
  160. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/orders/[id]/layout.tsx +0 -0
  161. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/orders/page.tsx +0 -0
  162. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/anonymous-tracking/page.tsx +0 -0
  163. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/auth/oauth-login/page.tsx +0 -0
  164. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/basket-b2b/page.tsx +0 -0
  165. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/category/[pk]/loading.tsx +0 -0
  166. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/client-root.tsx +0 -0
  167. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/contact-us/page.tsx +0 -0
  168. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/error.tsx +0 -0
  169. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/flat-page/[pk]/loading.tsx +0 -0
  170. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/group-product/[pk]/loading.tsx +0 -0
  171. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/landing-page/[pk]/loading.tsx +0 -0
  172. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/list/loading.tsx +0 -0
  173. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/orders/checkout/page.tsx +0 -0
  174. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/orders/completed/[token]/layout.tsx +0 -0
  175. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/page.tsx +0 -0
  176. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/special-page/[pk]/loading.tsx +0 -0
  177. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/template.tsx +0 -0
  178. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/users/password/reset/page.tsx +0 -0
  179. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/xml-sitemap/route.ts +0 -0
@@ -16,7 +16,7 @@ import PaymentHeader from '../../payment-header';
16
16
  import CreditCardInstallments from './installments';
17
17
  import { useLocalization } from '@akinon/next/hooks';
18
18
  import { Image } from '@akinon/next/components/image';
19
- import { getPosError } from '@akinon/next/utils';
19
+ import { getPosError, checkPaymentWillRedirect } from '@akinon/next/utils';
20
20
  import PluginModule, { Component } from '@akinon/next/components/plugin-module';
21
21
  import { PaymentOption } from '@akinon/next/types';
22
22
 
@@ -101,7 +101,7 @@ const CheckoutCreditCard = () => {
101
101
  setValue: setFormValue,
102
102
  trigger: validateInput,
103
103
  control,
104
- formState: { errors },
104
+ formState: { errors, isSubmitting },
105
105
  setError,
106
106
  getValues,
107
107
  clearErrors
@@ -119,8 +119,12 @@ const CheckoutCreditCard = () => {
119
119
  const [years, setYears] = useState([]);
120
120
  const [formError, setFormError] = useState(null);
121
121
  const [cardBinNumber, setCardBinNumber] = useState(null);
122
+ const [isProcessing, setIsProcessing] = useState(false);
122
123
  const [setBinNumber] = useSetBinNumberMutation();
123
- const [completeCreditCardPayment] = useCompleteCreditCardPaymentMutation();
124
+ const [completeCreditCardPayment, { isLoading: isCompletingPayment }] =
125
+ useCompleteCreditCardPaymentMutation();
126
+
127
+ const isButtonDisabled = isSubmitting || isCompletingPayment || isProcessing;
124
128
 
125
129
  const handleCardNumberChange = (value) => {
126
130
  const binNumber = value.replace(/\D/g, '').substring(0, 6);
@@ -136,9 +140,20 @@ const CheckoutCreditCard = () => {
136
140
  };
137
141
 
138
142
  const onSubmit: SubmitHandler<CreditCardForm> = async (data) => {
139
- const response = await completeCreditCardPayment(data).unwrap();
143
+ if (isButtonDisabled) return;
144
+
145
+ setIsProcessing(true);
146
+
147
+ try {
148
+ const response = await completeCreditCardPayment(data).unwrap();
149
+ setFormError(response?.errors);
140
150
 
141
- setFormError(response?.errors);
151
+ if (response?.errors || !checkPaymentWillRedirect(response)) {
152
+ setIsProcessing(false);
153
+ }
154
+ } catch (error) {
155
+ setIsProcessing(false);
156
+ }
142
157
  };
143
158
 
144
159
  useEffect(() => {
@@ -319,7 +334,7 @@ const CheckoutCreditCard = () => {
319
334
  size={16}
320
335
  className="leading-none ml-2"
321
336
  />
322
- <div className="hidden group-hover:block absolute right-0 bottom-5 w-[11rem] lg:w-[21rem] lg:left-auto lg:right-auto border-2 border-gray-200">
337
+ <div className="hidden group-hover:block absolute right-0 bottom-5 w-[11rem] lg:w-[21rem] lg:left-auto lg:right-auto border-2">
323
338
  {/* TODO: Fix this */}
324
339
  <Image
325
340
  src="/cvv.jpg"
@@ -392,6 +407,7 @@ const CheckoutCreditCard = () => {
392
407
  <Button
393
408
  className="group uppercase mt-4 inline-flex items-center justify-center"
394
409
  type="submit"
410
+ disabled={isButtonDisabled}
395
411
  data-testid="checkout-credit-card-place-order"
396
412
  >
397
413
  <span>{t('checkout.payment.credit_card.form.button')}</span>
@@ -12,6 +12,7 @@ import * as yup from 'yup';
12
12
  import CheckoutAgreements from '../agreements';
13
13
  import PaymentHeader from '../payment-header';
14
14
  import { useLocalization } from '@akinon/next/hooks';
15
+ import { checkPaymentWillRedirect } from '@akinon/next/utils';
15
16
  import { Image } from '@akinon/next/components/image';
16
17
  import { Trans } from '@akinon/next/components/trans';
17
18
 
@@ -25,21 +26,39 @@ const CheckoutFundsTransfer = () => {
25
26
  const {
26
27
  handleSubmit,
27
28
  control,
28
- formState: { errors }
29
+ formState: { errors, isSubmitting }
29
30
  } = useForm({
30
31
  resolver: yupResolver(fundsTransferFormSchema())
31
32
  });
32
33
  const [formError, setFormError] = useState(null);
34
+ const [isProcessing, setIsProcessing] = useState(false);
33
35
  const { bankAccounts, selectedBankAccountPk, preOrder } = useAppSelector(
34
36
  (state: RootState) => state.checkout
35
37
  );
36
38
  const [setFundsTransferOption] = useSetFundsTransferOptionMutation();
37
- const [completeFundsTransfer] = useCompleteFundsTransferMutation();
39
+ const [completeFundsTransfer, { isLoading: isCompletingPayment }] =
40
+ useCompleteFundsTransferMutation();
38
41
 
39
- const onSubmit: SubmitHandler<null> = async () => {
40
- const response = await completeFundsTransfer().unwrap();
42
+ const isButtonDisabled = isSubmitting || isCompletingPayment || isProcessing;
41
43
 
42
- setFormError(response?.errors?.non_field_errors);
44
+ const onSubmit: SubmitHandler<null> = async () => {
45
+ if (isButtonDisabled) return;
46
+
47
+ setIsProcessing(true);
48
+
49
+ try {
50
+ const response = await completeFundsTransfer().unwrap();
51
+ setFormError(response?.errors?.non_field_errors);
52
+
53
+ if (
54
+ response?.errors?.non_field_errors ||
55
+ !checkPaymentWillRedirect(response)
56
+ ) {
57
+ setIsProcessing(false);
58
+ }
59
+ } catch (error) {
60
+ setIsProcessing(false);
61
+ }
43
62
  };
44
63
 
45
64
  useEffect(() => {
@@ -155,6 +174,7 @@ const CheckoutFundsTransfer = () => {
155
174
  <Button
156
175
  className="group uppercase mt-4 inline-flex items-center justify-center w-full"
157
176
  type="submit"
177
+ disabled={isButtonDisabled}
158
178
  data-testid="checkout-bank-account-place-order"
159
179
  >
160
180
  <span> {t('checkout.payment.fund_transfer.button')}</span>
@@ -1,16 +1,35 @@
1
1
  'use client';
2
2
 
3
+ import { useState } from 'react';
3
4
  import { useCompleteLoyaltyPaymentMutation } from '@akinon/next/data/client/checkout';
4
5
  import { useAppSelector } from '@akinon/next/redux/hooks';
5
6
  import { useForm } from 'react-hook-form';
7
+ import { checkPaymentWillRedirect } from '@akinon/next/utils';
6
8
 
7
9
  export default function LoyaltyPayment() {
8
10
  const { payment_option } = useAppSelector((state) => state.checkout.preOrder);
9
11
  const { handleSubmit } = useForm();
10
- const [completeLoyaltyPayment] = useCompleteLoyaltyPaymentMutation();
12
+ const [completeLoyaltyPayment, { isLoading: isCompletingPayment }] =
13
+ useCompleteLoyaltyPaymentMutation();
14
+ const [isProcessing, setIsProcessing] = useState(false);
15
+
16
+ const isButtonDisabled = isCompletingPayment || isProcessing;
11
17
 
12
18
  const onSubmit = async () => {
13
- completeLoyaltyPayment();
19
+ if (isButtonDisabled) return;
20
+
21
+ setIsProcessing(true);
22
+
23
+ try {
24
+ const response = await completeLoyaltyPayment().unwrap();
25
+
26
+ if (response?.errors || !checkPaymentWillRedirect(response)) {
27
+ setIsProcessing(false);
28
+ }
29
+ } catch (error) {
30
+ console.error('Error completing loyalty payment:', error);
31
+ setIsProcessing(false);
32
+ }
14
33
  };
15
34
 
16
35
  return (
@@ -8,7 +8,7 @@ import { useForm } from 'react-hook-form';
8
8
  import { twMerge } from 'tailwind-merge';
9
9
  import * as yup from 'yup';
10
10
  import { useEffect, useState } from 'react';
11
- import { getPosError } from '@akinon/next/utils';
11
+ import { getPosError, checkPaymentWillRedirect } from '@akinon/next/utils';
12
12
  import { useMessageListener } from '@akinon/next/hooks';
13
13
 
14
14
  interface FormValues {
@@ -26,16 +26,33 @@ const formSchema = () =>
26
26
  export default function RedirectionPayment() {
27
27
  const { payment_option } = useAppSelector((state) => state.checkout.preOrder);
28
28
  const [formError, setFormError] = useState(null);
29
+ const [isProcessing, setIsProcessing] = useState(false);
29
30
  const {
30
31
  register,
31
32
  handleSubmit,
32
- formState: { errors }
33
+ formState: { errors, isSubmitting }
33
34
  } = useForm<FormValues>({
34
35
  resolver: yupResolver(formSchema())
35
36
  });
36
- const [completeRedirectionPayment] = useCompleteRedirectionPaymentMutation();
37
+ const [completeRedirectionPayment, { isLoading: isCompletingPayment }] =
38
+ useCompleteRedirectionPaymentMutation();
39
+
40
+ const isButtonDisabled = isSubmitting || isCompletingPayment || isProcessing;
41
+
37
42
  const onSubmit = async () => {
38
- completeRedirectionPayment();
43
+ if (isButtonDisabled) return;
44
+
45
+ setIsProcessing(true);
46
+
47
+ try {
48
+ const response = await completeRedirectionPayment().unwrap();
49
+
50
+ if (response?.errors || !checkPaymentWillRedirect(response)) {
51
+ setIsProcessing(false);
52
+ }
53
+ } catch (error) {
54
+ setIsProcessing(false);
55
+ }
39
56
  };
40
57
 
41
58
  useMessageListener();
@@ -91,6 +108,7 @@ export default function RedirectionPayment() {
91
108
 
92
109
  <Button
93
110
  data-testid="checkout-submit-button"
111
+ disabled={isButtonDisabled}
94
112
  className={twMerge('w-full md:w-36 px-4 md:px-0')}
95
113
  >
96
114
  {payment_option.name}
@@ -0,0 +1,121 @@
1
+ import clsx from 'clsx';
2
+ import {
3
+ useGetCheckoutLoyaltyBalanceQuery,
4
+ usePayWithLoyaltyBalanceMutation
5
+ } from '@akinon/next/data/client/checkout';
6
+ import { useAppSelector } from '@akinon/next/redux/hooks';
7
+ import { useMemo, useState } from 'react';
8
+ import { useLocalization } from '@akinon/next/hooks';
9
+ import { twMerge } from 'tailwind-merge';
10
+ import { Icon, Price } from '@theme/components';
11
+ import { Trans } from '@akinon/next/components';
12
+ import { LoaderSpinner } from '@theme/components';
13
+
14
+ export const StoreCredits = () => {
15
+ const { t } = useLocalization();
16
+
17
+ const [payWithLoyaltyBalance, { isLoading: isPayWithLoyaltyBalanceLoading }] =
18
+ usePayWithLoyaltyBalanceMutation();
19
+
20
+ const { loyaltyBalance, preOrder } = useAppSelector(
21
+ (state) => state.checkout
22
+ );
23
+
24
+ const { isLoading: isLoyaltyBalanceLoading } =
25
+ useGetCheckoutLoyaltyBalanceQuery(undefined, {
26
+ refetchOnMountOrArgChange: true,
27
+ skip: !preOrder?.payment_option
28
+ });
29
+
30
+ const isLoyaltyBalanceUsed = useMemo(() => {
31
+ return parseFloat(preOrder?.loyalty_money ?? '0') > 0;
32
+ }, [preOrder?.loyalty_money]);
33
+
34
+ const [isLoading, setIsLoading] = useState(false);
35
+
36
+ const handleClick = async () => {
37
+ if (isLoading) return;
38
+ setIsLoading(true);
39
+
40
+ try {
41
+ await payWithLoyaltyBalance(isLoyaltyBalanceUsed ? '0' : loyaltyBalance);
42
+ } finally {
43
+ setIsLoading(false);
44
+ }
45
+ };
46
+
47
+ if (preOrder?.is_guest) {
48
+ return null;
49
+ }
50
+
51
+ if (isLoyaltyBalanceLoading) {
52
+ return (
53
+ <div className="mb-3 px-4 py-3 xs:px-0">
54
+ <LoaderSpinner />
55
+ </div>
56
+ );
57
+ }
58
+
59
+ if (parseFloat(loyaltyBalance) <= 0) {
60
+ return null;
61
+ }
62
+ return (
63
+ <div
64
+ className={twMerge(
65
+ 'hidden flex-col w-full mb-4 border border-solid border-gray-400 px-0 md:p-4',
66
+ isPayWithLoyaltyBalanceLoading && 'pointer-events-none opacity-30',
67
+ parseFloat(loyaltyBalance) > 0 && 'block'
68
+ )}
69
+ >
70
+ <div className="flex w-full items-center">
71
+ <button onClick={handleClick}>
72
+ <span
73
+ className={clsx(
74
+ 'flex h-5 w-5 items-center justify-center rounded border border-solid border-primary',
75
+ isLoyaltyBalanceUsed ? 'bg-primary' : 'bg-white'
76
+ )}
77
+ >
78
+ <Icon
79
+ name={isLoyaltyBalanceUsed ? 'check' : ''}
80
+ size={10}
81
+ className={clsx({ 'text-white': isLoyaltyBalanceUsed })}
82
+ />
83
+ </span>
84
+ </button>
85
+
86
+ <div className="w-full pl-4">
87
+ <p className="cursor-pointer text-sm" onClick={handleClick}>
88
+ {t('checkout.payment.store_credit.use_my_store_credits')}
89
+ </p>
90
+ <p className="flex text-sm text-[#606060]">
91
+ {t('checkout.payment.store_credit.available_balance')}:
92
+ <Price
93
+ value={loyaltyBalance}
94
+ currencyCode={preOrder?.currency_type_label}
95
+ className="pe-1 font-bold"
96
+ />
97
+ </p>
98
+ </div>
99
+ </div>
100
+
101
+ {isLoyaltyBalanceUsed && parseFloat(preOrder?.unpaid_amount) > 0 && (
102
+ <p className="my-4 text-[15px] font-light italic text-[#707070] max-xs:text-xs">
103
+ <Trans
104
+ i18nKey="checkout.payment.store_credit.insufficient_balance"
105
+ components={{
106
+ Amount: (
107
+ <div className="inline-flex">
108
+ <Price
109
+ value={preOrder?.unpaid_amount}
110
+ currencyCode={preOrder?.currency_type_label}
111
+ className="text-primary"
112
+ />
113
+ </div>
114
+ )
115
+ }}
116
+ />
117
+ </p>
118
+ )}
119
+ </div>
120
+ );
121
+ };
@@ -44,7 +44,7 @@ const PaymentOptionButtons = () => {
44
44
  {displayedPaymentOptions.map((option) => (
45
45
  <label
46
46
  key={`payment-option-${option.pk}`}
47
- className="border border-gray-200 px-4 py-3 mt-3 flex h-12"
47
+ className="border px-4 py-3 mt-3 flex h-12"
48
48
  onClick={scrollToTop}
49
49
  >
50
50
  <Radio
@@ -69,10 +69,10 @@ const PaymentOptionButtons = () => {
69
69
  onClick={() => onClickHandler(option)}
70
70
  className={clsx(
71
71
  'flex items-center justify-center border-r border-b border-solid',
72
- 'border-gray-400 text-xs uppercase text-black-800/60 font-medium',
73
- 'bg-white h-11 px-5 transition-colors sm:h-15 sm:px-8 sm:py-8 hover:text-secondary',
72
+ 'border-gray-400 text-xs uppercase text-black-800 font-medium',
73
+ 'text-opacity-60 bg-white h-11 px-5 transition-colors sm:h-15 sm:px-8 sm:py-8 hover:text-secondary',
74
74
  {
75
- 'text-black-800/100 border-b-transparent':
75
+ 'text-opacity-100 border-b-transparent':
76
76
  preOrder?.payment_option?.pk === option.pk
77
77
  }
78
78
  )}
@@ -130,7 +130,7 @@ const AddressBox = ({
130
130
  onClick={() => handleAddressClick(addressType, address)}
131
131
  key={address.pk}
132
132
  className={clsx(
133
- 'cursor-pointer relative w-full border border-gray-200 shadow-sm p-4 min-h-[8rem]',
133
+ 'cursor-pointer relative w-full border shadow p-4 min-h-[8rem]',
134
134
  "hover:after:content-[''] hover:after:border-4 hover:after:opacity-30 hover:after:transition-opacity",
135
135
  'after:border-secondary-400 after:absolute after:inset-0 after:duration-150 after:-z-10',
136
136
  {
@@ -167,7 +167,7 @@ const AddressBox = ({
167
167
  <div className="text-xs flex justify-between">
168
168
  <Button
169
169
  appearance="ghost"
170
- className="italic underline hover:text-secondary-500 hover:bg-white! hover:border-white! p-0 h-auto"
170
+ className="italic underline hover:text-secondary-500 hover:!bg-white hover:!border-white p-0 h-auto"
171
171
  onClick={() => setIsEditAddressModalOpen(true)}
172
172
  data-testid="checkout-address-edit"
173
173
  >
@@ -193,7 +193,7 @@ const AddressBox = ({
193
193
  </Modal>
194
194
  <Button
195
195
  appearance="ghost"
196
- className="italic underline hover:text-secondary-500 hover:bg-white! hover:border-white! p-0 h-auto"
196
+ className="italic underline hover:text-secondary-500 hover:!bg-white hover:!border-white p-0 h-auto"
197
197
  onClick={() => setRemoveAddressModalOpen(true)}
198
198
  data-testid="checkout-address-remove"
199
199
  >
@@ -153,7 +153,7 @@ const Addresses = () => {
153
153
  role="button"
154
154
  onClick={() => setIsModalOpen(true)}
155
155
  className={clsx(
156
- 'relative cursor-pointer w-full min-h-[8rem] border border-gray-200 shadow-sm p-4',
156
+ 'relative cursor-pointer w-full min-h-[8rem] border shadow p-4',
157
157
  "hover:after:content-[''] hover:after:border-4 hover:after:opacity-30 hover:after:transition-opacity",
158
158
  'after:border-secondary-400 after:absolute after:inset-0 after:opacity-0 after:duration-150 after:-z-10',
159
159
  {
@@ -8,6 +8,7 @@ import PluginModule, { Component } from '@akinon/next/components/plugin-module';
8
8
  import { twMerge } from 'tailwind-merge';
9
9
  import { Image } from '@akinon/next/components/image';
10
10
  import { Trans } from '@akinon/next/components/trans';
11
+ import { StoreCredits } from './steps/payment/options/store-credit';
11
12
 
12
13
  export const Summary = () => {
13
14
  const { t } = useLocalization();
@@ -38,6 +39,7 @@ export const Summary = () => {
38
39
  'flex flex-col w-full mb-4 border border-solid border-gray-400'
39
40
  }}
40
41
  />
42
+ <StoreCredits />
41
43
  <div className="flex flex-col w-full border border-solid border-gray-400">
42
44
  <div className="flex justify-between items-center flex-row border-b border-solid border-gray-400 px-4 py-2 sm:px-5 sm:py-4 sm:min-h-15">
43
45
  <span className="text-black-800 text-xl font-light sm:text-2xl">
@@ -118,6 +120,14 @@ export const Summary = () => {
118
120
  <Price value={preOrder?.shipping_amount} />
119
121
  </span>
120
122
  </div>
123
+ {parseFloat(preOrder?.loyalty_money) > 0 && (
124
+ <div className="flex items-center justify-between w-full text-xs text-black-800 py-1 px-4 sm:px-5">
125
+ <span>{t('checkout.summary.loyalty_money_total')}</span>
126
+ <span>
127
+ <Price value={preOrder?.loyalty_money} />
128
+ </span>
129
+ </div>
130
+ )}
121
131
  <div className="flex items-center justify-between w-full text-xs text-black-800 py-1 px-4 sm:px-5">
122
132
  <span>{t('checkout.summary.discounts_total')}</span>
123
133
  <span>
@@ -149,7 +159,7 @@ export const Summary = () => {
149
159
  </div>
150
160
  </div>
151
161
  <div className="flex flex-col py-4 px-4 text-black-800 text-xs sm:px-5">
152
- <div className="w-full overflow-hidden text-ellipsis mb-1 last:mb-0">
162
+ <div className="w-full overflow-hidden overflow-ellipsis mb-1 last:mb-0">
153
163
  <Trans
154
164
  i18nKey="checkout.summary.info"
155
165
  components={{
@@ -162,7 +172,7 @@ export const Summary = () => {
162
172
  }}
163
173
  />
164
174
  </div>
165
- <div className="w-full overflow-hidden text-ellipsis mb-1 last:mb-0">
175
+ <div className="w-full overflow-hidden overflow-ellipsis mb-1 last:mb-0">
166
176
  {preOrder.shipping_address?.line}{' '}
167
177
  {preOrder.shipping_address?.postcode}{' '}
168
178
  {preOrder.shipping_address?.district && (
@@ -123,14 +123,14 @@ export const FindInStore = ({ productPk, productName, variants }) => {
123
123
  className="w-full"
124
124
  options={retailStoreOptions}
125
125
  {...register('city_id')}
126
- error={errors.city_id}
126
+ error={errors.city_id as any}
127
127
  />
128
128
  {sizeOptions.length > 1 && (
129
129
  <Select
130
130
  className="w-full"
131
131
  options={sizeOptions}
132
132
  {...register('size')}
133
- error={errors.size}
133
+ error={errors.size as any}
134
134
  />
135
135
  )}
136
136
  </div>
@@ -76,7 +76,7 @@ export default function ActionMenu() {
76
76
  : 'bg-secondary-500 text-white'
77
77
  )}
78
78
  >
79
- {totalQuantity}
79
+ <span data-testid="header-basket-count">{totalQuantity}</span>
80
80
  </Badge>
81
81
  ),
82
82
  miniBasket: <MiniBasket />
@@ -92,11 +92,7 @@ export default function ActionMenu() {
92
92
  ref={menu.miniBasket ? miniBasketRef : null}
93
93
  >
94
94
  {menu.action ? (
95
- <button
96
- onClick={menu.action}
97
- data-testid={menu.dataTestId}
98
- className="cursor-pointer"
99
- >
95
+ <button onClick={menu.action} data-testid={menu.dataTestId}>
100
96
  <Icon name={menu.icon} size={24} />
101
97
  {menu.badge}
102
98
  </button>
@@ -16,13 +16,13 @@ export default function HeaderBand() {
16
16
  </div>
17
17
 
18
18
  <div className="header-grid-area-nav bg-gray-100 h-auto p-3 sm:header-grid-area-band-m sm:h-9 sm:py-0">
19
- <div className="text-center text-ellipsis line-clamp-2 h-full flex items-center justify-center">
19
+ <div className="text-center overflow-ellipsis line-clamp-2 h-full flex items-center justify-center">
20
20
  <HeaderBandText />
21
21
  </div>
22
22
  </div>
23
23
 
24
24
  <div className="header-grid-area-main-r h-full pr-4 sm:header-grid-area-band-r sm:pr-0">
25
- <div className="flex items-center justify-end h-full gap-10">
25
+ <div className="flex items-center justify-end h-full">
26
26
  <UserMenu isMobile={false} />
27
27
  <ActionMenu />
28
28
  </div>
@@ -13,7 +13,7 @@ import { getMenu } from '@akinon/next/data/server';
13
13
  import { Image } from '@akinon/next/components/image';
14
14
 
15
15
  export default async function Header() {
16
- const response = await getMenu();
16
+ const response = await getMenu({ depth: 3 });
17
17
  const menu = menuGenerator(response);
18
18
 
19
19
  return (
@@ -203,7 +203,7 @@ export default function MiniBasket() {
203
203
  miniBasketOpen
204
204
  ? 'opacity-100 visible lg:invisible'
205
205
  : 'opacity-0 invisible',
206
- 'fixed top-0 left-0 z-50 w-screen h-screen bg-black/80 transition-all duration-300'
206
+ 'fixed top-0 left-0 z-50 w-screen h-screen bg-black bg-opacity-80 transition-all duration-300'
207
207
  )}
208
208
  onClick={() => {
209
209
  dispatch(closeMiniBasket());
@@ -217,7 +217,7 @@ export default function MiniBasket() {
217
217
  'fixed lg:absolute bottom-0 lg:-bottom-1 right-0 w-80 h-screen lg:h-auto bg-white lg:border-l lg:border-t lg:border-r-2 lg:border-b-2 lg:border-gray-500 p-5 z-50 transition-all duration-300'
218
218
  )}
219
219
  >
220
- <header className="flex items-center gap-2 pb-3 border-b border-gray-200">
220
+ <header className="flex items-center gap-2 pb-3 border-b">
221
221
  <h3 className="text-xl font-bold">
222
222
  {t('basket.mini_basket.my_bag')}
223
223
  </h3>
@@ -58,9 +58,9 @@ export default function MobileMenu(props: MobileMenuProps) {
58
58
  <>
59
59
  <div
60
60
  className={clsx(
61
- 'fixed top-0 left-0 z-30 w-screen h-screen invisible opacity-0 bg-black/80 transition duration-500',
61
+ 'fixed top-0 left-0 z-30 w-screen h-screen invisible opacity-0 bg-black bg-opacity-80 transition duration-500',
62
62
  {
63
- 'visible! opacity-100! scroll-lock': isMobileMenuOpen
63
+ '!visible !opacity-100 scroll-lock': isMobileMenuOpen
64
64
  }
65
65
  )}
66
66
  />
@@ -70,7 +70,7 @@ export default function MobileMenu(props: MobileMenuProps) {
70
70
  className={clsx(
71
71
  'fixed top-0 left-0 z-50 flex flex-col bg-white w-72 pt-4 h-screen invisible opacity-0 transition duration-500 transform -translate-x-72',
72
72
  {
73
- 'visible! opacity-100! translate-x-0': isMobileMenuOpen
73
+ '!visible !opacity-100 translate-x-0': isMobileMenuOpen
74
74
  }
75
75
  )}
76
76
  >
@@ -106,15 +106,15 @@ export default function MobileMenu(props: MobileMenuProps) {
106
106
  className={clsx(
107
107
  'absolute top-0 left-0 right-0 px-8 bg-white invisible opacity-0 transition duration-500 transform translate-x-full',
108
108
  {
109
- 'visible! opacity-100! translate-x-0!': selectedSubMenu
109
+ '!visible !opacity-100 !translate-x-0': selectedSubMenu
110
110
  }
111
111
  )}
112
112
  >
113
- <header className="flex items-center justify-between border-b border-gray-200 h-[61px] py-4 mb-4">
113
+ <header className="flex items-center justify-between border-b h-[61px] py-4 mb-4">
114
114
  <Button
115
115
  appearance="ghost"
116
116
  onClick={() => setSelectedSubMenu(null)}
117
- className="underline text-xs font-semibold flex items-center gap-2 p-0!"
117
+ className="underline text-xs font-semibold flex items-center gap-2 !p-0"
118
118
  >
119
119
  <Icon name="chevron-start" size={12} className="shrink-0" />
120
120
  {t('common.mobile_menu.back')}
@@ -102,7 +102,7 @@ export default function Navbar(props: NavbarProps) {
102
102
  'after:bg-gray'
103
103
  ],
104
104
  {
105
- 'visible! opacity-100! delay-500':
105
+ '!visible !opacity-100 delay-500':
106
106
  openedMenu === item.pk
107
107
  }
108
108
  )}
@@ -34,7 +34,7 @@ export const PwaBackButton = () => {
34
34
  return (
35
35
  <div className="relative z-10 md:top-0 md:left-0 md:fixed">
36
36
  <button
37
- className="bg-secondary-600 text-white flex items-center justify-center shrink-0 w-12 h-12 md:w-10 md:h-9 active:bg-secondary-700"
37
+ className="bg-secondary-600 text-white flex items-center justify-center flex-shrink-0 w-12 h-12 md:w-10 md:h-9 active:bg-secondary-700"
38
38
  onClick={() => router.back()}
39
39
  >
40
40
  <svg
@@ -9,6 +9,7 @@ import { Icon } from '@theme/components';
9
9
  import Results from './results';
10
10
  import { ROUTES } from '@theme/routes';
11
11
  import { useLocalization, useRouter } from '@akinon/next/hooks';
12
+ import PluginModule, { Component } from '@akinon/next/components/plugin-module';
12
13
 
13
14
  export default function Search() {
14
15
  const { t } = useLocalization();
@@ -46,7 +47,7 @@ export default function Search() {
46
47
  <div
47
48
  className={clsx(
48
49
  // 177px is the height of the header
49
- 'absolute bg-black/75 w-screen h-screen transition duration-500 left-0 bottom-0 translate-y-full z-30',
50
+ 'absolute bg-black opacity-75 w-screen h-screen transition duration-500 left-0 bottom-0 translate-y-full z-30',
50
51
  isSearchOpen && searchText
51
52
  ? 'visible opacity-100'
52
53
  : 'invisible opacity-0'
@@ -60,7 +61,7 @@ export default function Search() {
60
61
  isSearchOpen ? 'visible opacity-100' : 'invisible opacity-0'
61
62
  )}
62
63
  >
63
- <div className="max-w-(--breakpoint-2xl) mx-auto flex flex-col gap-12">
64
+ <div className="max-w-screen-2xl mx-auto flex flex-col gap-12">
64
65
  <div className="border-b border-gray-400 flex flex-col py-1.5 gap-2 self-center items-center md:flex-row">
65
66
  <span className="text-xl lg:text-2xl">
66
67
  {t('common.search.results_for')}
@@ -74,10 +75,19 @@ export default function Search() {
74
75
  router.push(`${ROUTES.LIST}/?search_text=${searchText}`);
75
76
  }
76
77
  }}
77
- className="border-0 text-2xl outline-hidden text-secondary placeholder:text-xl lg:placeholder:text-2xl"
78
+ className="border-0 text-2xl outline-none text-secondary placeholder:text-xl placeholder:lg:text-2xl"
78
79
  placeholder={t('common.search.placeholder')}
79
80
  ref={inputRef}
80
81
  />
82
+
83
+ <PluginModule
84
+ component={Component.HeaderImageSearchFeature}
85
+ props={{
86
+ enableTextSearch: true,
87
+ isEnabled: true
88
+ }}
89
+ />
90
+
81
91
  <Icon
82
92
  name="close"
83
93
  size={14}
@@ -80,7 +80,7 @@ export default function Results(props: ResultsProps) {
80
80
  <div className="grid grid-cols-2 sm:grid-cols-4 gap-8">
81
81
  {products.map((product, index) => (
82
82
  <Link href={product?.url} key={index} className="flex flex-col">
83
- <div className="relative aspect-315/448">
83
+ <div className="relative aspect-[315/448]">
84
84
  {product.extra.image ? (
85
85
  <Image
86
86
  src={product.extra.image}