@akinon/next 1.91.0-rc.2 → 1.91.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/api/auth.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { NextApiRequest, NextApiResponse } from 'next';
2
- import NextAuth, { Session } from 'next-auth';
2
+ import NextAuth, { Session, NextAuthOptions } from 'next-auth';
3
3
  import CredentialProvider from 'next-auth/providers/credentials';
4
4
  import { ROUTES } from 'routes';
5
5
  import { URLS, user } from '../data/urls';
@@ -7,6 +7,8 @@ import Settings from 'settings';
7
7
  import { urlLocaleMatcherRegex } from '../utils';
8
8
  import logger from '@akinon/next/utils/log';
9
9
  import { AuthError } from '../types';
10
+ import getRootHostname from '../utils/get-root-hostname';
11
+ import { LocaleUrlStrategy } from '../localization';
10
12
 
11
13
  async function getCurrentUser(sessionId: string, currency = '') {
12
14
  const headers = {
@@ -40,7 +42,15 @@ async function getCurrentUser(sessionId: string, currency = '') {
40
42
  };
41
43
  }
42
44
 
43
- const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
45
+ type CustomNextAuthOptions = (
46
+ req: NextApiRequest,
47
+ res: NextApiResponse
48
+ ) => Partial<NextAuthOptions>;
49
+
50
+ const defaultNextAuthOptions = (
51
+ req: NextApiRequest,
52
+ res: NextApiResponse
53
+ ): NextAuthOptions => {
44
54
  return {
45
55
  providers: [
46
56
  CredentialProvider({
@@ -150,7 +160,19 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
150
160
 
151
161
  if (sessionId) {
152
162
  const maxAge = 30 * 24 * 60 * 60; // 30 days in seconds
153
- const cookieOptions = `Path=/; HttpOnly; Secure; Max-Age=${maxAge}`;
163
+ const { localeUrlStrategy } = Settings.localization;
164
+
165
+ const fallbackHost =
166
+ req.headers['x-forwarded-host']?.toString() ||
167
+ req.headers.host?.toString();
168
+ const hostname =
169
+ process.env.NEXT_PUBLIC_URL || `https://${fallbackHost}`;
170
+ const rootHostname =
171
+ localeUrlStrategy === LocaleUrlStrategy.Subdomain
172
+ ? getRootHostname(hostname)
173
+ : null;
174
+ const domainOption = rootHostname ? `Domain=${rootHostname};` : '';
175
+ const cookieOptions = `Path=/; HttpOnly; Secure; Max-Age=${maxAge}; ${domainOption}`;
154
176
 
155
177
  res.setHeader('Set-Cookie', [
156
178
  `osessionid=${sessionId}; ${cookieOptions}`,
@@ -253,8 +275,36 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
253
275
  };
254
276
  };
255
277
 
256
- const Auth = (req, res) => {
257
- return NextAuth(req, res, nextAuthOptions(req, res));
278
+ const Auth = (
279
+ req: NextApiRequest,
280
+ res: NextApiResponse,
281
+ customOptions?: CustomNextAuthOptions
282
+ ) => {
283
+ const baseOptions = defaultNextAuthOptions(req, res);
284
+ const customOptionsResult = customOptions ? customOptions(req, res) : {};
285
+
286
+ const mergedOptions = {
287
+ ...baseOptions,
288
+ ...customOptionsResult,
289
+ providers: [
290
+ ...baseOptions.providers,
291
+ ...(customOptionsResult.providers || [])
292
+ ],
293
+ callbacks: {
294
+ ...baseOptions.callbacks,
295
+ ...customOptionsResult.callbacks
296
+ },
297
+ events: {
298
+ ...baseOptions.events,
299
+ ...customOptionsResult.events
300
+ },
301
+ pages: {
302
+ ...baseOptions.pages,
303
+ ...customOptionsResult.pages
304
+ }
305
+ };
306
+
307
+ return NextAuth(req, res, mergedOptions);
258
308
  };
259
309
 
260
310
  export default Auth;
package/api/client.ts CHANGED
@@ -5,6 +5,8 @@ import logger from '../utils/log';
5
5
  import formatCookieString from '../utils/format-cookie-string';
6
6
  import cookieParser from 'set-cookie-parser';
7
7
  import { cookies } from 'next/headers';
8
+ import getRootHostname from '../utils/get-root-hostname';
9
+ import { LocaleUrlStrategy } from '../localization';
8
10
 
9
11
  interface RouteParams {
10
12
  params: {
@@ -190,8 +192,23 @@ async function proxyRequest(...args) {
190
192
  const responseHeaders: any = {};
191
193
 
192
194
  if (filteredCookies.length > 0) {
195
+ const { localeUrlStrategy } = settings.localization;
196
+
197
+ const fallbackHost =
198
+ req.headers.get('x-forwarded-host') || req.headers.get('host');
199
+ const hostname = process.env.NEXT_PUBLIC_URL || `https://${fallbackHost}`;
200
+ const rootHostname =
201
+ localeUrlStrategy === LocaleUrlStrategy.Subdomain
202
+ ? getRootHostname(hostname)
203
+ : null;
204
+
193
205
  responseHeaders['set-cookie'] = filteredCookies
194
- .map(formatCookieString)
206
+ .map((cookie) => {
207
+ if (!cookie.domain && rootHostname) {
208
+ cookie.domain = rootHostname;
209
+ }
210
+ return formatCookieString(cookie);
211
+ })
195
212
  .join(', ');
196
213
  }
197
214
 
@@ -1,8 +1,6 @@
1
1
  import clsx from 'clsx';
2
2
  import { forwardRef, FocusEvent, useState, Ref } from 'react';
3
3
  import { Controller } from 'react-hook-form';
4
-
5
- // @ts-ignore
6
4
  import { PatternFormat, PatternFormatProps } from 'react-number-format';
7
5
  import { InputProps } from '../types';
8
6
  import { twMerge } from 'tailwind-merge';
@@ -10,9 +10,7 @@ type LinkProps = Omit<
10
10
  React.AnchorHTMLAttributes<HTMLAnchorElement>,
11
11
  keyof NextLinkProps
12
12
  > &
13
- NextLinkProps & {
14
- href: string;
15
- };
13
+ NextLinkProps;
16
14
 
17
15
  export const Link = ({ children, href, ...rest }: LinkProps) => {
18
16
  const { locale, defaultLocaleValue, localeUrlStrategy } = useLocalization();
@@ -28,21 +26,19 @@ export const Link = ({ children, href, ...rest }: LinkProps) => {
28
26
  return href;
29
27
  }
30
28
 
31
- if (typeof href === 'string' && !href.startsWith('http')) {
32
- const pathnameWithoutLocale = href.replace(urlLocaleMatcherRegex, '');
33
- const hrefWithLocale = `/${locale}${pathnameWithoutLocale}`;
34
-
35
- if (localeUrlStrategy === LocaleUrlStrategy.ShowAllLocales) {
36
- return hrefWithLocale;
37
- } else if (
38
- localeUrlStrategy === LocaleUrlStrategy.HideDefaultLocale &&
39
- locale !== defaultLocaleValue
40
- ) {
41
- return hrefWithLocale;
42
- }
29
+ const pathnameWithoutLocale = href.replace(urlLocaleMatcherRegex, '');
30
+ const hrefWithLocale = `/${locale}${pathnameWithoutLocale}`;
31
+
32
+ if (localeUrlStrategy === LocaleUrlStrategy.ShowAllLocales) {
33
+ return hrefWithLocale;
34
+ } else if (
35
+ localeUrlStrategy === LocaleUrlStrategy.HideDefaultLocale &&
36
+ locale !== defaultLocaleValue
37
+ ) {
38
+ return hrefWithLocale;
43
39
  }
44
40
 
45
- return href;
41
+ return href || '#';
46
42
  }, [href, defaultLocaleValue, locale, localeUrlStrategy]);
47
43
 
48
44
  return (
@@ -72,13 +72,10 @@ const addRootLayoutProps = async (componentProps: RootLayoutProps) => {
72
72
  const checkRedisVariables = () => {
73
73
  const requiredVariableValues = [
74
74
  process.env.CACHE_HOST,
75
- process.env.CACHE_PORT
75
+ process.env.CACHE_PORT,
76
+ process.env.CACHE_SECRET
76
77
  ];
77
78
 
78
- if (!settings.usePrettyUrlRoute) {
79
- requiredVariableValues.push(process.env.CACHE_SECRET);
80
- }
81
-
82
79
  if (
83
80
  !requiredVariableValues.every((v) => v) &&
84
81
  process.env.NODE_ENV === 'production'
@@ -4,19 +4,17 @@ import { Resource } from '@opentelemetry/resources';
4
4
  import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
5
5
  import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node';
6
6
 
7
- if (process.env.NODE_ENV === 'development') {
8
- const sdk = new NodeSDK({
9
- resource: new Resource({
10
- [SemanticResourceAttributes.SERVICE_NAME]: 'pz-next-app'
11
- }),
12
- spanProcessor: new SimpleSpanProcessor(
13
- new OTLPTraceExporter({
14
- url: `${
15
- process.env.PZ_DASHBOARD_URL ?? 'http://localhost:3005'
16
- }/api/traces`
17
- })
18
- )
19
- });
7
+ const sdk = new NodeSDK({
8
+ resource: new Resource({
9
+ [SemanticResourceAttributes.SERVICE_NAME]: 'pz-next-app'
10
+ }),
11
+ spanProcessor: new SimpleSpanProcessor(
12
+ new OTLPTraceExporter({
13
+ url: `${
14
+ process.env.PZ_DASHBOARD_URL ?? 'http://localhost:3005'
15
+ }/api/traces`
16
+ })
17
+ )
18
+ });
20
19
 
21
- sdk.start();
22
- }
20
+ sdk.start();
package/lib/cache.ts CHANGED
@@ -31,8 +31,6 @@ export const CacheKey = {
31
31
  `category_${pk}_${encodeURIComponent(
32
32
  JSON.stringify(searchParams)
33
33
  )}${hashCacheKey(headers)}`,
34
- Basket: (namespace?: string) => `basket${namespace ? `_${namespace}` : ''}`,
35
- AllBaskets: () => 'all_baskets',
36
34
  CategorySlug: (slug: string) => `category_${slug}`,
37
35
  SpecialPage: (
38
36
  pk: number,
@@ -145,8 +145,7 @@ const withCompleteGpay =
145
145
  logger.info('Redirecting to order success page', {
146
146
  middleware: 'complete-gpay',
147
147
  redirectUrlWithLocale,
148
- ip,
149
- setCookie: request.headers.get('set-cookie')
148
+ ip
150
149
  });
151
150
 
152
151
  // Using POST method while redirecting causes an error,
@@ -145,8 +145,7 @@ const withCompleteMasterpass =
145
145
  logger.info('Redirecting to order success page', {
146
146
  middleware: 'complete-masterpass',
147
147
  redirectUrlWithLocale,
148
- ip,
149
- setCookie: request.headers.get('set-cookie')
148
+ ip
150
149
  });
151
150
 
152
151
  // Using POST method while redirecting causes an error,
@@ -19,6 +19,8 @@ import withLocale from './locale';
19
19
  import logger from '../utils/log';
20
20
  import { user } from '../data/urls';
21
21
  import { getUrlPathWithLocale } from '../utils/localization';
22
+ import getRootHostname from '../utils/get-root-hostname';
23
+ import { LocaleUrlStrategy } from '../localization';
22
24
 
23
25
  const withPzDefault =
24
26
  (middleware: NextMiddleware) =>
@@ -296,7 +298,7 @@ const withPzDefault =
296
298
  !req.middlewareParams.found &&
297
299
  Settings.customNotFoundEnabled
298
300
  ) {
299
- let pathname = url.pathname
301
+ const pathname = url.pathname
300
302
  .replace(/\/+$/, '')
301
303
  .split('/');
302
304
  url.pathname = url.pathname.replace(
@@ -341,6 +343,21 @@ const withPzDefault =
341
343
  middlewareResult = NextResponse.rewrite(url);
342
344
  }
343
345
 
346
+ const { localeUrlStrategy } =
347
+ Settings.localization;
348
+
349
+ const fallbackHost =
350
+ req.headers.get('x-forwarded-host') ||
351
+ req.headers.get('host');
352
+ const hostname =
353
+ process.env.NEXT_PUBLIC_URL ||
354
+ `https://${fallbackHost}`;
355
+ const rootHostname =
356
+ localeUrlStrategy ===
357
+ LocaleUrlStrategy.Subdomain
358
+ ? getRootHostname(hostname)
359
+ : null;
360
+
344
361
  if (
345
362
  !url.pathname.startsWith(`/${currency}/orders`)
346
363
  ) {
@@ -350,6 +367,7 @@ const withPzDefault =
350
367
  ? locale
351
368
  : defaultLocaleValue,
352
369
  {
370
+ domain: rootHostname,
353
371
  sameSite: 'none',
354
372
  secure: true,
355
373
  expires: new Date(
@@ -358,10 +376,12 @@ const withPzDefault =
358
376
  }
359
377
  );
360
378
  }
379
+
361
380
  middlewareResult.cookies.set(
362
381
  'pz-currency',
363
382
  currency,
364
383
  {
384
+ domain: rootHostname,
365
385
  sameSite: 'none',
366
386
  secure: true,
367
387
  expires: new Date(
@@ -410,7 +430,10 @@ const withPzDefault =
410
430
  ).json();
411
431
  middlewareResult.cookies.set(
412
432
  'csrftoken',
413
- csrf_token
433
+ csrf_token,
434
+ {
435
+ domain: rootHostname
436
+ }
414
437
  );
415
438
  }
416
439
  } catch (error) {
@@ -13,7 +13,8 @@ const getMatchedLocale = (pathname: string, req: PzNextRequest) => {
13
13
  const { localeUrlStrategy, defaultLocaleValue } = settings.localization;
14
14
 
15
15
  if (localeUrlStrategy === LocaleUrlStrategy.Subdomain) {
16
- const host = req.headers.get('x-forwarded-host');
16
+ const host =
17
+ req.headers.get('x-forwarded-host') || req.headers.get('host') || '';
17
18
 
18
19
  if (host) {
19
20
  const subDomain = host.split('.')[0] || '';
@@ -44,7 +45,9 @@ const withLocale =
44
45
  try {
45
46
  const url = req.nextUrl.clone();
46
47
  const matchedLocale = getMatchedLocale(url.pathname, req);
47
- let { localeUrlStrategy, defaultLocaleValue, redirectToDefaultLocale } =
48
+ let { localeUrlStrategy } = settings.localization;
49
+
50
+ const { defaultLocaleValue, redirectToDefaultLocale } =
48
51
  settings.localization;
49
52
 
50
53
  localeUrlStrategy =
@@ -146,8 +146,7 @@ const withRedirectionPayment =
146
146
  logger.info('Redirecting to order success page', {
147
147
  middleware: 'redirection-payment',
148
148
  redirectUrlWithLocale,
149
- ip,
150
- setCookie: request.headers.get('set-cookie')
149
+ ip
151
150
  });
152
151
 
153
152
  // Using POST method while redirecting causes an error,
@@ -145,8 +145,7 @@ const withSavedCardRedirection =
145
145
  logger.info('Redirecting to order success page', {
146
146
  middleware: 'saved-card-redirection',
147
147
  redirectUrlWithLocale,
148
- ip,
149
- setCookie: request.headers.get('set-cookie')
148
+ ip
150
149
  });
151
150
 
152
151
  // Using POST method while redirecting causes an error,
@@ -145,8 +145,7 @@ const withThreeDRedirection =
145
145
  logger.info('Redirecting to order success page', {
146
146
  middleware: 'three-d-redirection',
147
147
  redirectUrlWithLocale,
148
- ip,
149
- setCookie: request.headers.get('set-cookie')
148
+ ip
150
149
  });
151
150
 
152
151
  // Using POST method while redirecting causes an error,
@@ -4,7 +4,6 @@ import { PzNextRequest } from '.';
4
4
  import logger from '../utils/log';
5
5
  import { urlLocaleMatcherRegex } from '../utils';
6
6
  import { getUrlPathWithLocale } from '../utils/localization';
7
- import { shouldIgnoreRedirect } from '../utils/redirect-ignore';
8
7
  import { ROUTES } from 'routes';
9
8
 
10
9
  // This middleware is used to handle url redirections set in Omnitron
@@ -61,13 +60,20 @@ const withUrlRedirection =
61
60
 
62
61
  const setCookies = request.headers.getSetCookie();
63
62
 
64
- if (
65
- shouldIgnoreRedirect(
66
- url.pathname,
67
- req.middlewareParams.rewrites.locale
68
- )
69
- ) {
70
- return middleware(req, event);
63
+ if (settings.commerceRedirectionIgnoreList) {
64
+ const shouldIgnoreRedirect =
65
+ settings.commerceRedirectionIgnoreList.some((ignorePath) =>
66
+ redirectUrl.pathname.startsWith(
67
+ getUrlPathWithLocale(
68
+ ignorePath,
69
+ req.middlewareParams.rewrites.locale
70
+ )
71
+ )
72
+ );
73
+
74
+ if (shouldIgnoreRedirect) {
75
+ return middleware(req, event);
76
+ }
71
77
  }
72
78
 
73
79
  const response = NextResponse.redirect(redirectUrl.toString(), {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@akinon/next",
3
3
  "description": "Core package for Project Zero Next",
4
- "version": "1.91.0-rc.2",
4
+ "version": "1.91.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -17,13 +17,13 @@
17
17
  "test": "jest"
18
18
  },
19
19
  "dependencies": {
20
+ "@neshca/cache-handler": "1.5.1",
20
21
  "@opentelemetry/exporter-trace-otlp-http": "0.46.0",
21
22
  "@opentelemetry/resources": "1.19.0",
22
23
  "@opentelemetry/sdk-node": "0.46.0",
23
24
  "@opentelemetry/sdk-trace-node": "1.19.0",
24
25
  "@opentelemetry/semantic-conventions": "1.19.0",
25
26
  "@reduxjs/toolkit": "1.9.7",
26
- "@neshca/cache-handler": "1.9.0",
27
27
  "@sentry/nextjs": "9.5.0",
28
28
  "cross-spawn": "7.0.3",
29
29
  "generic-pool": "3.9.0",
@@ -34,7 +34,11 @@
34
34
  "set-cookie-parser": "2.6.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@akinon/eslint-plugin-projectzero": "1.91.0-rc.2",
37
+ "@akinon/eslint-plugin-projectzero": "1.91.0",
38
+ "@babel/core": "7.26.10",
39
+ "@babel/preset-env": "7.26.9",
40
+ "@babel/preset-typescript": "7.27.0",
41
+ "@types/jest": "29.5.14",
38
42
  "@types/react-redux": "7.1.30",
39
43
  "@types/set-cookie-parser": "2.4.7",
40
44
  "@typescript-eslint/eslint-plugin": "6.7.4",
@@ -20,8 +20,7 @@ import {
20
20
  setShippingOptions,
21
21
  setHepsipayAvailability,
22
22
  setWalletPaymentData,
23
- setPayOnDeliveryOtpModalActive,
24
- setUnavailablePaymentOptions
23
+ setPayOnDeliveryOtpModalActive
25
24
  } from '../../redux/reducers/checkout';
26
25
  import { RootState, TypedDispatch } from 'redux/store';
27
26
  import { checkoutApi } from '../../data/client/checkout';
@@ -51,11 +50,7 @@ export const errorMiddleware: Middleware = ({ dispatch }: MiddlewareParams) => {
51
50
  const result: CheckoutResult = next(action);
52
51
  const errors = result?.payload?.errors;
53
52
 
54
- if (
55
- !!errors &&
56
- ((typeof errors === 'object' && Object.keys(errors).length > 0) ||
57
- (Array.isArray(errors) && errors.length > 0))
58
- ) {
53
+ if (errors) {
59
54
  dispatch(setErrors(errors));
60
55
  }
61
56
 
@@ -181,14 +176,6 @@ export const contextListMiddleware: Middleware = ({
181
176
  dispatch(setPaymentOptions(context.page_context.payment_options));
182
177
  }
183
178
 
184
- if (context.page_context.unavailable_options) {
185
- dispatch(
186
- setUnavailablePaymentOptions(
187
- context.page_context.unavailable_options
188
- )
189
- );
190
- }
191
-
192
179
  if (context.page_context.credit_payment_options) {
193
180
  dispatch(
194
181
  setCreditPaymentOptions(context.page_context.credit_payment_options)
@@ -40,7 +40,6 @@ export interface CheckoutState {
40
40
  shippingOptions: ShippingOption[];
41
41
  dataSourceShippingOptions: DataSource[];
42
42
  paymentOptions: PaymentOption[];
43
- unavailablePaymentOptions: PaymentOption[];
44
43
  creditPaymentOptions: CheckoutCreditPaymentOption[];
45
44
  selectedCreditPaymentPk: number;
46
45
  paymentChoices: PaymentChoice[];
@@ -95,7 +94,6 @@ const initialState: CheckoutState = {
95
94
  shippingOptions: [],
96
95
  dataSourceShippingOptions: [],
97
96
  paymentOptions: [],
98
- unavailablePaymentOptions: [],
99
97
  creditPaymentOptions: [],
100
98
  selectedCreditPaymentPk: null,
101
99
  paymentChoices: [],
@@ -159,9 +157,6 @@ const checkoutSlice = createSlice({
159
157
  setPaymentOptions(state, { payload }) {
160
158
  state.paymentOptions = payload;
161
159
  },
162
- setUnavailablePaymentOptions(state, { payload }) {
163
- state.unavailablePaymentOptions = payload;
164
- },
165
160
  setPaymentChoices(state, { payload }) {
166
161
  state.paymentChoices = payload;
167
162
  },
@@ -223,10 +218,9 @@ export const {
223
218
  setShippingOptions,
224
219
  setDataSourceShippingOptions,
225
220
  setPaymentOptions,
226
- setUnavailablePaymentOptions,
227
- setPaymentChoices,
228
221
  setCreditPaymentOptions,
229
222
  setSelectedCreditPaymentPk,
223
+ setPaymentChoices,
230
224
  setCardType,
231
225
  setInstallmentOptions,
232
226
  setBankAccounts,
@@ -0,0 +1,16 @@
1
+ const defaultPlugins = require('../plugins');
2
+
3
+ const getAkinonNextContent = (plugins = defaultPlugins) => {
4
+ return [
5
+ `./node_modules/@akinon/next/components/**/*.{js,ts,jsx,tsx}`,
6
+ `../../node_modules/@akinon/next/components/**/*.{js,ts,jsx,tsx}`,
7
+ ...plugins
8
+ .map((plugin) => [
9
+ `./node_modules/@akinon/${plugin}/**/*.{js,ts,jsx,tsx}`,
10
+ `../../node_modules/@akinon/${plugin}/**/*.{js,ts,jsx,tsx}`
11
+ ])
12
+ .flat()
13
+ ];
14
+ };
15
+
16
+ module.exports = getAkinonNextContent;
@@ -114,7 +114,6 @@ export interface Order {
114
114
  pk: number;
115
115
  name: string;
116
116
  slug: string;
117
- logo: string;
118
117
  [key: string]: any;
119
118
  };
120
119
  }
@@ -43,12 +43,12 @@ const appFetch = async <T>({
43
43
  const requestURL = `${decodeURIComponent(commerceUrl)}${url}`;
44
44
 
45
45
  init.headers = {
46
- cookie: nextCookies.toString(),
47
46
  ...(init.headers ?? {}),
48
47
  ...(ServerVariables.globalHeaders ?? {}),
49
48
  'Accept-Language': currentLocale.apiValue,
50
49
  'x-currency': currency,
51
- 'x-forwarded-for': ip
50
+ 'x-forwarded-for': ip,
51
+ cookie: nextCookies.toString()
52
52
  };
53
53
 
54
54
  init.next = {
@@ -0,0 +1,28 @@
1
+ import Settings from 'settings';
2
+
3
+ export default function getRootHostname(
4
+ url: string | undefined
5
+ ): string | null {
6
+ if (!url) return null;
7
+
8
+ try {
9
+ const urlObj = new URL(url);
10
+ const hostname = urlObj.hostname;
11
+ const parts = hostname.split('.');
12
+
13
+ const firstPart = parts[0];
14
+
15
+ const isLocale = Settings.localization.locales.some(
16
+ (locale) => locale.value === firstPart
17
+ );
18
+
19
+ if (isLocale) {
20
+ const hostnameAfterLocale = parts.slice(1).join('.');
21
+ return `.${hostnameAfterLocale}`;
22
+ }
23
+
24
+ return `.${hostname}`;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }