@akinon/next 2.0.0-beta.0 → 2.0.0-beta.10

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 (69) hide show
  1. package/.eslintrc.js +12 -0
  2. package/CHANGELOG.md +56 -0
  3. package/api/auth.ts +17 -0
  4. package/api/client.ts +3 -2
  5. package/bin/run-prebuild-tests.js +46 -0
  6. package/components/accordion.tsx +1 -1
  7. package/components/button.tsx +51 -36
  8. package/components/client-root.tsx +20 -0
  9. package/components/input.tsx +1 -1
  10. package/components/modal.tsx +1 -1
  11. package/components/pz-root.tsx +1 -1
  12. package/components/select.tsx +1 -1
  13. package/components/selected-payment-option-view.tsx +1 -1
  14. package/data/client/api.ts +2 -0
  15. package/data/client/basket.ts +27 -5
  16. package/data/client/checkout.ts +62 -7
  17. package/data/client/misc.ts +25 -1
  18. package/data/client/product.ts +19 -2
  19. package/data/client/user.ts +16 -8
  20. package/data/server/flatpage.ts +8 -4
  21. package/data/server/form.ts +12 -4
  22. package/data/server/landingpage.ts +8 -4
  23. package/data/server/menu.ts +7 -2
  24. package/data/server/product.ts +16 -5
  25. package/data/server/seo.ts +11 -4
  26. package/data/server/widget.ts +19 -4
  27. package/data/urls.ts +11 -3
  28. package/hocs/client/with-segment-defaults.tsx +1 -1
  29. package/hocs/server/with-segment-defaults.tsx +6 -3
  30. package/hooks/index.ts +1 -0
  31. package/hooks/use-router.ts +5 -2
  32. package/hooks/use-sentry-uncaught-errors.ts +24 -0
  33. package/instrumentation/index.ts +10 -0
  34. package/lib/cache-handler.mjs +2 -2
  35. package/lib/cache.ts +4 -4
  36. package/localization/index.ts +2 -1
  37. package/middlewares/default.ts +6 -15
  38. package/middlewares/locale.ts +31 -10
  39. package/middlewares/url-redirection.ts +16 -0
  40. package/package.json +8 -7
  41. package/plugins.js +2 -1
  42. package/redux/middlewares/checkout.ts +16 -144
  43. package/redux/middlewares/index.ts +4 -2
  44. package/redux/middlewares/pre-order/address.ts +50 -0
  45. package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +46 -0
  46. package/redux/middlewares/pre-order/data-source-shipping-option.ts +43 -0
  47. package/redux/middlewares/pre-order/delivery-option.ts +40 -0
  48. package/redux/middlewares/pre-order/index.ts +29 -0
  49. package/redux/middlewares/pre-order/installment-option.ts +42 -0
  50. package/redux/middlewares/pre-order/payment-option.ts +34 -0
  51. package/redux/middlewares/pre-order/pre-order-validation.ts +24 -0
  52. package/redux/middlewares/pre-order/redirection.ts +40 -0
  53. package/redux/middlewares/pre-order/set-pre-order.ts +22 -0
  54. package/redux/middlewares/pre-order/shipping-option.ts +44 -0
  55. package/redux/middlewares/pre-order/shipping-step.ts +38 -0
  56. package/redux/reducers/checkout.ts +8 -2
  57. package/redux/reducers/index.ts +5 -3
  58. package/redux/reducers/root.ts +7 -2
  59. package/sentry/index.ts +36 -17
  60. package/types/commerce/account.ts +5 -1
  61. package/types/commerce/checkout.ts +24 -0
  62. package/types/index.ts +8 -4
  63. package/utils/app-fetch.ts +2 -2
  64. package/utils/index.ts +11 -8
  65. package/utils/localization.ts +4 -0
  66. package/utils/override-middleware.ts +25 -0
  67. package/utils/redirect.ts +2 -2
  68. package/views/error-page.tsx +93 -0
  69. package/with-pz-config.js +4 -3
@@ -0,0 +1,38 @@
1
+ import { Middleware } from '@reduxjs/toolkit';
2
+ import { CheckoutResult, MiddlewareParams } from '../../../types';
3
+ import { setShippingStepCompleted } from '../../reducers/checkout';
4
+
5
+ export const shippingStepMiddleware: Middleware = ({
6
+ getState,
7
+ dispatch
8
+ }: MiddlewareParams) => {
9
+ return (next) => (action) => {
10
+ const result: CheckoutResult = next(action);
11
+ const preOrder = result?.payload?.pre_order;
12
+
13
+ if (!preOrder) {
14
+ return result;
15
+ }
16
+
17
+ const { addressList: addresses } = getState().checkout;
18
+
19
+ if (preOrder) {
20
+ const stepCompleted = [
21
+ preOrder.delivery_option?.delivery_option_type === 'retail_store'
22
+ ? true
23
+ : preOrder.shipping_address?.pk,
24
+ preOrder.billing_address?.pk,
25
+ preOrder.shipping_option?.pk,
26
+ addresses.length > 0
27
+ ].every(Boolean);
28
+
29
+ dispatch(setShippingStepCompleted(stepCompleted));
30
+ }
31
+
32
+ return result;
33
+ };
34
+ };
35
+
36
+ Object.defineProperty(shippingStepMiddleware, 'name', {
37
+ value: 'shippingStepMiddleware'
38
+ });
@@ -70,6 +70,7 @@ export interface CheckoutState {
70
70
  };
71
71
  supportedMethods: string;
72
72
  };
73
+ payOnDeliveryOtpModalActive: boolean;
73
74
  }
74
75
 
75
76
  const initialState: CheckoutState = {
@@ -103,7 +104,8 @@ const initialState: CheckoutState = {
103
104
  retailStores: [],
104
105
  attributeBasedShippingOptions: [],
105
106
  selectedShippingOptions: {},
106
- hepsipayAvailability: false
107
+ hepsipayAvailability: false,
108
+ payOnDeliveryOtpModalActive: false
107
109
  };
108
110
 
109
111
  const checkoutSlice = createSlice({
@@ -193,6 +195,9 @@ const checkoutSlice = createSlice({
193
195
  },
194
196
  setWalletPaymentData(state, { payload }) {
195
197
  state.walletPaymentData = payload;
198
+ },
199
+ setPayOnDeliveryOtpModalActive(state, { payload }) {
200
+ state.payOnDeliveryOtpModalActive = payload;
196
201
  }
197
202
  }
198
203
  });
@@ -225,7 +230,8 @@ export const {
225
230
  setAttributeBasedShippingOptions,
226
231
  setSelectedShippingOptions,
227
232
  setHepsipayAvailability,
228
- setWalletPaymentData
233
+ setWalletPaymentData,
234
+ setPayOnDeliveryOtpModalActive
229
235
  } = checkoutSlice.actions;
230
236
 
231
237
  export default checkoutSlice.reducer;
@@ -9,15 +9,17 @@ import { masterpassReducer } from '@akinon/pz-masterpass';
9
9
  import { otpReducer } from '@akinon/pz-otp';
10
10
  import { savedCardReducer } from '@akinon/pz-saved-card';
11
11
 
12
+ const fallbackReducer = (state = {}) => state;
13
+
12
14
  const reducers = {
13
15
  [api.reducerPath]: api.reducer,
14
16
  root: rootReducer,
15
17
  checkout: checkoutReducer,
16
18
  config: configReducer,
17
19
  header: headerReducer,
18
- masterpass: masterpassReducer,
19
- otp: otpReducer,
20
- savedCard: savedCardReducer
20
+ masterpass: masterpassReducer || fallbackReducer,
21
+ otp: otpReducer || fallbackReducer,
22
+ savedCard: savedCardReducer || fallbackReducer
21
23
  };
22
24
 
23
25
  export default reducers;
@@ -14,7 +14,8 @@ const initialState = {
14
14
  open: false,
15
15
  title: null,
16
16
  content: null
17
- }
17
+ },
18
+ userPhoneNumber: null
18
19
  };
19
20
 
20
21
  const rootSlice = createSlice({
@@ -45,6 +46,9 @@ const rootSlice = createSlice({
45
46
  state.rootModal.open = false;
46
47
  state.rootModal.title = null;
47
48
  state.rootModal.content = null;
49
+ },
50
+ setUserPhoneNumber(state, { payload }) {
51
+ state.userPhoneNumber = payload;
48
52
  }
49
53
  }
50
54
  });
@@ -55,7 +59,8 @@ export const {
55
59
  toggleMiniBasket,
56
60
  setHighlightedItem,
57
61
  openRootModal,
58
- closeRootModal
62
+ closeRootModal,
63
+ setUserPhoneNumber
59
64
  } = rootSlice.actions;
60
65
 
61
66
  export default rootSlice.reducer;
package/sentry/index.ts CHANGED
@@ -1,29 +1,48 @@
1
1
  import * as Sentry from '@sentry/nextjs';
2
2
 
3
- const SENTRY_DSN: string =
3
+ const SENTRY_DSN: string | undefined =
4
4
  process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
5
5
 
6
+ export enum ClientLogType {
7
+ UNCAUGHT_ERROR_PAGE = 'UNCAUGHT_ERROR_PAGE',
8
+ CHECKOUT = 'CHECKOUT'
9
+ }
10
+
11
+ const ALLOWED_CLIENT_LOG_TYPES: ClientLogType[] = [
12
+ ClientLogType.UNCAUGHT_ERROR_PAGE,
13
+ ClientLogType.CHECKOUT
14
+ ];
15
+
6
16
  export const initSentry = (
7
17
  type: 'Server' | 'Client' | 'Edge',
8
18
  options: Sentry.BrowserOptions | Sentry.NodeOptions | Sentry.EdgeOptions = {}
9
19
  ) => {
10
20
  // TODO: Handle options with ESLint rules
11
21
 
12
- // TODO: Remove Zero Project DSN
22
+ Sentry.init({
23
+ dsn:
24
+ options.dsn ||
25
+ SENTRY_DSN ||
26
+ 'https://d8558ef8997543deacf376c7d8d7cf4b@o64293.ingest.sentry.io/4504338423742464',
27
+ initialScope: {
28
+ tags: {
29
+ APP_TYPE: 'ProjectZeroNext',
30
+ TYPE: type
31
+ }
32
+ },
33
+ tracesSampleRate: 0,
34
+ integrations: [],
35
+ beforeSend: (event, hint) => {
36
+ if (
37
+ type === 'Client' &&
38
+ !ALLOWED_CLIENT_LOG_TYPES.includes(
39
+ event.tags?.LOG_TYPE as ClientLogType
40
+ )
41
+ ) {
42
+ return null;
43
+ }
13
44
 
14
- if (type === 'Server' || type === 'Edge') {
15
- Sentry.init({
16
- dsn:
17
- SENTRY_DSN ||
18
- 'https://d8558ef8997543deacf376c7d8d7cf4b@o64293.ingest.sentry.io/4504338423742464',
19
- initialScope: {
20
- tags: {
21
- APP_TYPE: 'ProjectZeroNext',
22
- TYPE: type
23
- }
24
- },
25
- tracesSampleRate: 1.0,
26
- integrations: []
27
- });
28
- }
45
+ return event;
46
+ }
47
+ });
29
48
  };
@@ -19,6 +19,10 @@ export type AccountOrderCancellation = {
19
19
  description: string;
20
20
  order_item: string;
21
21
  reason: string;
22
+ cancellation_request_image_set?: Array<{
23
+ image: string;
24
+ description: string;
25
+ }>;
22
26
  }>;
23
27
  };
24
28
 
@@ -61,5 +65,5 @@ export type ContactFormType = {
61
65
  order?: string;
62
66
  country_code?: string;
63
67
  order_needed?: boolean;
64
- file?: FileList
68
+ file?: FileList;
65
69
  };
@@ -3,6 +3,8 @@ import { Basket } from './basket';
3
3
  import { RetailStore } from './misc';
4
4
  import { PaymentOption } from './order';
5
5
  import { Product } from './product';
6
+ import { JSX } from 'react';
7
+ import { RootState, TypedDispatch } from 'redux/store';
6
8
 
7
9
  export enum CheckoutStep {
8
10
  Shipping = 'shipping',
@@ -106,6 +108,7 @@ export interface PreOrder {
106
108
  context_extras?: ExtraField;
107
109
  token?: string;
108
110
  agreement_confirmed?: boolean;
111
+ phone_number?: string;
109
112
  }
110
113
 
111
114
  export type ExtraField = Record<string, any>;
@@ -185,3 +188,24 @@ export interface AttributeBasedShippingOption {
185
188
  attribute_key: string;
186
189
  [key: string]: any;
187
190
  }
191
+
192
+ export interface CheckoutResult {
193
+ payload: {
194
+ errors?: Record<string, string[]>;
195
+ pre_order?: PreOrder;
196
+ context_list?: CheckoutContext[];
197
+ };
198
+ }
199
+
200
+ export interface MiddlewareParams {
201
+ getState: () => RootState;
202
+ dispatch: TypedDispatch;
203
+ }
204
+
205
+ export type SendSmsType = {
206
+ phone_number: string;
207
+ };
208
+
209
+ export type VerifySmsType = {
210
+ verify_code: string;
211
+ };
package/types/index.ts CHANGED
@@ -199,15 +199,16 @@ export interface Settings {
199
199
  extraPaymentTypes?: string[];
200
200
  masterpassJsUrl?: string;
201
201
  };
202
- customNotFoundEnabled: boolean;
203
202
  useOptimizedTranslations?: boolean;
204
203
  plugins?: Record<string, Record<string, any>>;
205
204
  includedProxyHeaders?: string[];
205
+ commerceRedirectionIgnoreList?: string[];
206
206
  /**
207
207
  * By default, the currency will be reset when the currency is changed.
208
208
  * If you want to keep the basket when the currency is changed, you can set this option to `false`.
209
209
  */
210
210
  resetBasketOnCurrencyChange?: boolean;
211
+ frontendIds?: Record<string, number>;
211
212
  }
212
213
 
213
214
  export interface CacheOptions {
@@ -239,8 +240,8 @@ export type ImageOptions = CDNOptions & {
239
240
  export type Translations = { [key: string]: object };
240
241
 
241
242
  export interface PageProps<T = any> {
242
- params: T & { locale: string; currency: string };
243
- searchParams: URLSearchParams;
243
+ params: Promise<T & { locale: string; currency: string }>;
244
+ searchParams: Promise<URLSearchParams>;
244
245
  }
245
246
 
246
247
  export interface LayoutProps<T = any> extends PageProps<T> {
@@ -275,7 +276,10 @@ export interface IconProps extends React.ComponentPropsWithRef<'i'> {
275
276
 
276
277
  export interface ButtonProps
277
278
  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
278
- appearance?: 'filled' | 'outlined' | 'ghost';
279
+ appearance?: 'filled' | 'outlined' | 'ghost' | 'link' | string;
280
+ size?: 'sm' | 'md' | 'lg' | 'xl';
281
+ href?: string;
282
+ target?: '_blank' | '_self' | '_parent' | '_top';
279
283
  }
280
284
 
281
285
  export type FileInputProps = React.HTMLProps<HTMLInputElement>;
@@ -26,8 +26,8 @@ const appFetch = async <T>({
26
26
  let ip = '';
27
27
 
28
28
  try {
29
- const nextHeaders = headers();
30
- const nextCookies = cookies();
29
+ const nextHeaders = await headers();
30
+ const nextCookies = await cookies();
31
31
  ip = nextHeaders.get('x-forwarded-for') ?? '';
32
32
 
33
33
  const commerceUrl = Settings.commerceUrl;
package/utils/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import settings from 'settings';
2
2
  import { LocaleUrlStrategy } from '../localization';
3
- import { CDNOptions, ClientRequestOptions } from '../types';
3
+ import { CDNOptions, ClientRequestOptions, Locale } from '../types';
4
4
 
5
5
  export * from './get-currency';
6
6
  export * from './menu-generator';
@@ -152,14 +152,17 @@ export function buildCDNUrl(url: string, config?: CDNOptions) {
152
152
  return `${rootWithoutOptions}${options}${fileExtension}`;
153
153
  }
154
154
 
155
+ const { locales, localeUrlStrategy, defaultLocaleValue } =
156
+ settings.localization;
157
+
158
+ const isLocaleExcluded = (locale: Locale) =>
159
+ ![LocaleUrlStrategy.ShowAllLocales, LocaleUrlStrategy.Subdomain].includes(
160
+ localeUrlStrategy
161
+ ) && locale.value !== defaultLocaleValue;
162
+
155
163
  export const urlLocaleMatcherRegex = new RegExp(
156
- `^/(${settings.localization.locales
157
- .filter((l) =>
158
- settings.localization.localeUrlStrategy !==
159
- LocaleUrlStrategy.ShowAllLocales
160
- ? l.value !== settings.localization.defaultLocaleValue
161
- : l
162
- )
164
+ `^/(${locales
165
+ .filter((l) => !isLocaleExcluded(l))
163
166
  .map((l) => l.value)
164
167
  .join('|')})(?=/|$)`
165
168
  );
@@ -11,6 +11,10 @@ export const getUrlPathWithLocale = (
11
11
  currentLocale = defaultLocaleValue;
12
12
  }
13
13
 
14
+ if (localeUrlStrategy === LocaleUrlStrategy.Subdomain) {
15
+ return pathname;
16
+ }
17
+
14
18
  if (localeUrlStrategy === LocaleUrlStrategy.HideAllLocales) {
15
19
  return pathname;
16
20
  }
@@ -0,0 +1,25 @@
1
+ import { Middleware } from '@reduxjs/toolkit';
2
+
3
+ export enum MiddlewareNames {
4
+ PreOrderValidationMiddleware = 'preOrderValidationMiddleware',
5
+ RedirectionMiddleware = 'redirectionMiddleware',
6
+ SetPreOrderMiddleware = 'setPreOrderMiddleware',
7
+ DeliveryOptionMiddleware = 'deliveryOptionMiddleware',
8
+ SetAddressMiddleware = 'setAddressMiddleware',
9
+ ShippingOptionMiddleware = 'shippingOptionMiddleware',
10
+ DataSourceShippingOptionMiddleware = 'dataSourceShippingOptionMiddleware',
11
+ AttributeBasedShippingOptionMiddleware = 'attributeBasedShippingOptionMiddleware',
12
+ PaymentOptionMiddleware = 'paymentOptionMiddleware',
13
+ InstallmentOptionMiddleware = 'installmentOptionMiddleware',
14
+ ShippingStepMiddleware = 'shippingStepMiddleware'
15
+ }
16
+
17
+ export const overrideMiddleware = (
18
+ originalMiddlewares: Middleware[],
19
+ overrides: Record<string, Middleware>
20
+ ): Middleware[] => {
21
+ return originalMiddlewares.map((middleware) => {
22
+ const middlewareKey = middleware.name || middleware.toString();
23
+ return overrides[middlewareKey] || middleware;
24
+ });
25
+ };
package/utils/redirect.ts CHANGED
@@ -4,8 +4,8 @@ import { headers } from 'next/headers';
4
4
  import { ServerVariables } from '@akinon/next/utils/server-variables';
5
5
  import { getUrlPathWithLocale } from '@akinon/next/utils/localization';
6
6
 
7
- export const redirect = (path: string, type?: RedirectType) => {
8
- const nextHeaders = headers();
7
+ export const redirect = async (path: string, type?: RedirectType) => {
8
+ const nextHeaders = await headers();
9
9
  const pageUrl = new URL(
10
10
  nextHeaders.get('pz-url') ?? process.env.NEXT_PUBLIC_URL
11
11
  );
@@ -0,0 +1,93 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useLocalization } from '../hooks';
3
+ import { Button, Link } from '../components';
4
+ import { ROUTES } from 'routes';
5
+
6
+ export default function PzErrorPage({
7
+ error,
8
+ reset
9
+ }: {
10
+ error: Error & { digest?: string; isServerError?: boolean };
11
+ reset: () => void;
12
+ }) {
13
+ const [isServerError, setIsServerError] = useState(false);
14
+
15
+ useEffect(() => {
16
+ if ('isServerError' in error) {
17
+ setIsServerError(true);
18
+ return;
19
+ }
20
+
21
+ setIsServerError(!!error.digest);
22
+ }, [error]);
23
+
24
+ return isServerError ? (
25
+ <ServerErrorUI />
26
+ ) : (
27
+ <ClientErrorUI error={error} reset={reset} />
28
+ );
29
+ }
30
+
31
+ function ClientErrorUI({
32
+ error,
33
+ reset
34
+ }: {
35
+ error: Error & { digest?: string };
36
+ reset: () => void;
37
+ }) {
38
+ const { t } = useLocalization();
39
+
40
+ const errorMessage = error?.message || 'Unknown error';
41
+
42
+ return (
43
+ <section className="text-center px-6 py-6 my-14 md:px-0 md:m-14">
44
+ <div className="text-4xl font-bold md:text-6xl text-red-500">500</div>
45
+ <h1 className="text-lg md:text-xl mt-4">
46
+ {t('common.client_error.title')}
47
+ </h1>
48
+ <p className="text-lg md:text-xl mt-2">
49
+ {t('common.client_error.description')}
50
+ </p>
51
+
52
+ <div className="mt-4 mx-auto max-w-lg">
53
+ <p className="text-xs text-gray-600 font-mono bg-gray-100 p-3 rounded overflow-auto text-left">
54
+ <span className="font-semibold">Error:</span> {errorMessage}
55
+ </p>
56
+ </div>
57
+
58
+ <div className="mt-6 flex flex-col gap-4 items-center justify-center">
59
+ <Link href={ROUTES.HOME} className="text-lg underline">
60
+ {t('common.client_error.link_text')}
61
+ </Link>
62
+ <Button onClick={reset} className="text-lg">
63
+ {t('common.try_again')}
64
+ </Button>
65
+ </div>
66
+ </section>
67
+ );
68
+ }
69
+
70
+ function ServerErrorUI() {
71
+ const { t } = useLocalization();
72
+
73
+ const reloadPage = () => {
74
+ window.location.reload();
75
+ };
76
+
77
+ return (
78
+ <section className="text-center px-6 my-14 md:px-0 md:m-14">
79
+ <div className="text-7xl font-bold md:text-8xl">500</div>
80
+ <h1 className="text-lg md:text-xl"> {t('common.page_500.title')} </h1>
81
+ <p className="text-lg md:text-xl"> {t('common.page_500.description')} </p>
82
+
83
+ <div className="mt-6 flex flex-col gap-4 items-center justify-center">
84
+ <Link href={ROUTES.HOME} className="text-lg underline">
85
+ {t('common.page_500.link_text')}
86
+ </Link>
87
+ <Button onClick={reloadPage} className="text-lg">
88
+ {t('common.try_again')}
89
+ </Button>
90
+ </div>
91
+ </section>
92
+ );
93
+ }
package/with-pz-config.js CHANGED
@@ -44,8 +44,9 @@ const defaultConfig = {
44
44
  value: 'max-age=63072000; includeSubDomains; preload'
45
45
  },
46
46
  {
47
- key: 'X-Frame-Options',
48
- value: 'SAMEORIGIN'
47
+ key: 'Content-Security-Policy',
48
+ value:
49
+ "frame-ancestors 'self' https://*.akifast.com akifast.com https://*.akinoncloud.com akinoncloud.com"
49
50
  }
50
51
  ]
51
52
  }
@@ -64,7 +65,7 @@ const defaultConfig = {
64
65
  },
65
66
  sentry: {
66
67
  hideSourceMaps: true
67
- }
68
+ } // TODO: This section will be reviewed again in the Sentry 8 update.
68
69
  };
69
70
 
70
71
  const withPzConfig = (