@akinon/next 1.93.0 → 1.95.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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @akinon/next
2
2
 
3
+ ## 1.95.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 35dfb8f: ZERO-3363: Refactor URL handling in checkout and redirection middlewares to use url.origin instead of process.env.NEXT_PUBLIC_URL
8
+ - 99b6e7b: ZERO-3421: Enhance Sentry error handling by adding network error detection logic and refining initialization options
9
+ - cbdb5c1: ZERO-3448: fix set cookie domain handling for subdomain locale strategy
10
+
11
+ ## 1.94.0
12
+
13
+ ### Minor Changes
14
+
15
+ - 6e6b0a9: ZERO-3422: Add pz-flow-payment package
16
+ - adf0eeb: ZERO-3597: Add copilot instruction files
17
+ - c806fad: ZERO-3422: Add Flow Payment plugin to the defined plugins list
18
+ - 0abde6b: ZERO-3422: Update fetch method to use dynamic request method in wallet complete redirection middleware
19
+ - 72ad7bb: ZERO-3422: Add Flow Payment to the list of available plugins
20
+ - e7cd3a5: ZERO-3435: Add Accept-Language to requestHeaders
21
+ - 17bfadc: ZERO-3275: Disable OpenTelemetry monitoring in production environment
22
+ - dfaceff: ZERO-3356: Add useLoyaltyAvailability hook and update checkout state management
23
+ - 86642cf: ZERO-3531: Add saveSampleProducts endpoint and update URLs in checkout
24
+ - 485e8ef: ZERO-3422: Refactor parameter handling in wallet complete redirection middleware to use forEach
25
+ - b434ac8: ZERO-3545: Update fetchCheckout API URL to include page parameter
26
+ - fee608d: ZERO-3422: Refactor body handling in wallet complete redirection middleware
27
+
3
28
  ## 1.93.0
4
29
 
5
30
  ### Minor Changes
@@ -20,7 +20,8 @@ enum Plugin {
20
20
  B2B = 'pz-b2b',
21
21
  Akifast = 'pz-akifast',
22
22
  MultiBasket = 'pz-multi-basket',
23
- SavedCard = 'pz-saved-card'
23
+ SavedCard = 'pz-saved-card',
24
+ FlowPayment = 'pz-flow-payment'
24
25
  }
25
26
 
26
27
  export enum Component {
@@ -45,7 +46,8 @@ export enum Component {
45
46
  AkifastQuickLoginButton = 'QuickLoginButton',
46
47
  AkifastCheckoutButton = 'CheckoutButton',
47
48
  MultiBasket = 'MultiBasket',
48
- SavedCard = 'SavedCardOption'
49
+ SavedCard = 'SavedCardOption',
50
+ FlowPayment = 'FlowPayment'
49
51
  }
50
52
 
51
53
  const PluginComponents = new Map([
@@ -78,7 +80,8 @@ const PluginComponents = new Map([
78
80
  [Component.AkifastQuickLoginButton, Component.AkifastCheckoutButton]
79
81
  ],
80
82
  [Plugin.MultiBasket, [Component.MultiBasket]],
81
- [Plugin.SavedCard, [Component.SavedCard]]
83
+ [Plugin.SavedCard, [Component.SavedCard]],
84
+ [Plugin.FlowPayment, [Component.FlowPayment]]
82
85
  ]);
83
86
 
84
87
  const getPlugin = (component: Component) => {
@@ -143,6 +146,8 @@ export default function PluginModule({
143
146
  promise = import(`${'@akinon/pz-multi-basket'}`);
144
147
  } else if (plugin === Plugin.SavedCard) {
145
148
  promise = import(`${'@akinon/pz-saved-card'}`);
149
+ } else if (plugin === Plugin.FlowPayment) {
150
+ promise = import(`${'@akinon/pz-flow-payment'}`);
146
151
  }
147
152
  } catch (error) {
148
153
  logger.error(error);
@@ -62,6 +62,17 @@ export default function SelectedPaymentOptionView({
62
62
  : fallbackView;
63
63
  }
64
64
 
65
+ if (
66
+ payment_option?.payment_type === 'wallet' &&
67
+ wallet_method === 'checkout_flow'
68
+ ) {
69
+ const mod = await import('@akinon/pz-flow-payment');
70
+
71
+ return typeof mod?.default === 'function'
72
+ ? mod.default
73
+ : fallbackView;
74
+ }
75
+
65
76
  const view = paymentTypeToView[payment_option?.payment_type] || null;
66
77
 
67
78
  if (view) {
@@ -181,7 +181,10 @@ export const checkoutApi = api.injectEndpoints({
181
181
  endpoints: (build) => ({
182
182
  fetchCheckout: build.query<CheckoutResponse, void>({
183
183
  query: () => ({
184
- url: buildClientRequestUrl(checkout.fetchCheckout, {})
184
+ url: buildClientRequestUrl(
185
+ `${checkout.fetchCheckout}?page=IndexPage`,
186
+ {}
187
+ )
185
188
  }),
186
189
  providesTags: ['Checkout']
187
190
  }),
@@ -867,6 +870,22 @@ export const checkoutApi = api.injectEndpoints({
867
870
  method: 'POST',
868
871
  body
869
872
  })
873
+ }),
874
+ saveSampleProducts: build.mutation<CheckoutResponse, number[] | undefined>({
875
+ query: (products = []) => {
876
+ const formData = new FormData();
877
+
878
+ products.forEach((product) => {
879
+ formData.append('sample_products', String(product));
880
+ });
881
+
882
+ return {
883
+ url: buildClientRequestUrl(checkout.saveSampleProducts),
884
+ method: 'POST',
885
+ body: formData
886
+ };
887
+ },
888
+ invalidatesTags: ['Checkout']
870
889
  })
871
890
  }),
872
891
  overrideExisting: false
@@ -914,5 +933,6 @@ export const {
914
933
  useSetWalletCompletePageMutation,
915
934
  useSendSmsMutation,
916
935
  useVerifySmsMutation,
917
- useResetCheckoutStateQuery
936
+ useResetCheckoutStateQuery,
937
+ useSaveSampleProductsMutation
918
938
  } = checkoutApi;
package/data/urls.ts CHANGED
@@ -142,7 +142,8 @@ export const checkout = {
142
142
  setOrderSelectionPage: '/orders/checkout/?page=OrderSelectionPage',
143
143
  loyaltyCardPage: '/orders/checkout/?page=LoyaltyCardPage',
144
144
  sendSmsPage: '/orders/checkout/?page=SendSmsPage',
145
- verifySmsPage: '/orders/checkout/?page=VerifySmsPage'
145
+ verifySmsPage: '/orders/checkout/?page=VerifySmsPage',
146
+ saveSampleProducts: '/orders/checkout/?page=SampleProductPage'
146
147
  };
147
148
 
148
149
  export const flatpage = {
@@ -0,0 +1,21 @@
1
+ import { useAppSelector } from '../redux/hooks';
2
+
3
+ export const useLoyaltyAvailability = () => {
4
+ const { paymentOptions, unavailablePaymentOptions } = useAppSelector(
5
+ (state) => state.checkout
6
+ );
7
+
8
+ const hasLoyaltyInAvailable = paymentOptions.some(
9
+ (option) =>
10
+ option.payment_type === 'loyalty_money' ||
11
+ option.payment_type === 'loyalty'
12
+ );
13
+
14
+ const hasLoyaltyInUnavailable = unavailablePaymentOptions.some(
15
+ (option) =>
16
+ option.payment_type === 'loyalty_money' ||
17
+ option.payment_type === 'loyalty'
18
+ );
19
+
20
+ return hasLoyaltyInAvailable || hasLoyaltyInUnavailable;
21
+ };
@@ -4,17 +4,19 @@ 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
- 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
- });
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
+ });
19
20
 
20
- sdk.start();
21
+ sdk.start();
22
+ }
@@ -64,7 +64,7 @@ const withCheckoutProvider =
64
64
  const location = request.headers.get('location');
65
65
  const redirectUrl = new URL(
66
66
  request.headers.get('location'),
67
- location.startsWith('http') ? '' : process.env.NEXT_PUBLIC_URL
67
+ location.startsWith('http') ? '' : url.origin
68
68
  );
69
69
 
70
70
  redirectUrl.pathname = getUrlPathWithLocale(
@@ -34,6 +34,7 @@ const withCompleteGpay =
34
34
  const url = req.nextUrl.clone();
35
35
  const ip = req.headers.get('x-forwarded-for') ?? '';
36
36
  const sessionId = req.cookies.get('osessionid');
37
+ const currentLocale = req.middlewareParams?.rewrites?.locale;
37
38
 
38
39
  if (url.search.indexOf('GPayCompletePage') === -1) {
39
40
  return middleware(req, event);
@@ -45,7 +46,9 @@ const withCompleteGpay =
45
46
  'Content-Type': 'application/x-www-form-urlencoded',
46
47
  Cookie: req.headers.get('cookie') ?? '',
47
48
  'x-currency': req.cookies.get('pz-currency')?.value ?? '',
48
- 'x-forwarded-for': ip
49
+ 'x-forwarded-for': ip,
50
+ 'Accept-Language':
51
+ currentLocale ?? req.cookies.get('pz-locale')?.value ?? ''
49
52
  };
50
53
 
51
54
  try {
@@ -4,6 +4,7 @@ import { Buffer } from 'buffer';
4
4
  import logger from '../utils/log';
5
5
  import { getUrlPathWithLocale } from '../utils/localization';
6
6
  import { PzNextRequest } from '.';
7
+ import { ServerVariables } from '../utils/server-variables';
7
8
 
8
9
  const streamToString = async (stream: ReadableStream<Uint8Array> | null) => {
9
10
  if (stream) {
@@ -34,6 +35,7 @@ const withCompleteMasterpass =
34
35
  const url = req.nextUrl.clone();
35
36
  const ip = req.headers.get('x-forwarded-for') ?? '';
36
37
  const sessionId = req.cookies.get('osessionid');
38
+ const currentLocale = req.middlewareParams?.rewrites?.locale;
37
39
 
38
40
  if (url.search.indexOf('MasterpassCompletePage') === -1) {
39
41
  return middleware(req, event);
@@ -45,7 +47,9 @@ const withCompleteMasterpass =
45
47
  'Content-Type': 'application/x-www-form-urlencoded',
46
48
  Cookie: req.headers.get('cookie') ?? '',
47
49
  'x-currency': req.cookies.get('pz-currency')?.value ?? '',
48
- 'x-forwarded-for': ip
50
+ 'x-forwarded-for': ip,
51
+ 'Accept-Language':
52
+ currentLocale ?? req.cookies.get('pz-locale')?.value ?? ''
49
53
  };
50
54
 
51
55
  try {
@@ -11,7 +11,8 @@ import {
11
11
  withRedirectionPayment,
12
12
  withSavedCardRedirection,
13
13
  withThreeDRedirection,
14
- withUrlRedirection
14
+ withUrlRedirection,
15
+ withWalletCompleteRedirection
15
16
  } from '.';
16
17
  import { urlLocaleMatcherRegex } from '../utils';
17
18
  import withCurrency from './currency';
@@ -228,144 +229,168 @@ const withPzDefault =
228
229
  withCompleteGpay(
229
230
  withCompleteMasterpass(
230
231
  withSavedCardRedirection(
231
- async (req: PzNextRequest, event: NextFetchEvent) => {
232
- let middlewareResult: NextResponse | void =
233
- NextResponse.next();
234
-
235
- try {
236
- const { locale, prettyUrl, currency } =
237
- req.middlewareParams.rewrites;
238
- const { defaultLocaleValue } =
239
- Settings.localization;
240
- const url = req.nextUrl.clone();
241
- const pathnameWithoutLocale =
242
- url.pathname.replace(urlLocaleMatcherRegex, '');
243
-
244
- middlewareResult = (await middleware(
245
- req,
246
- event
247
- )) as NextResponse | void;
248
-
249
- let customRewriteUrlDiff = '';
250
-
251
- if (
252
- middlewareResult instanceof NextResponse &&
253
- middlewareResult.headers.get(
254
- 'pz-override-response'
255
- ) &&
256
- middlewareResult.headers.get(
257
- 'x-middleware-rewrite'
258
- )
259
- ) {
260
- const rewriteUrl = new URL(
261
- middlewareResult.headers.get(
262
- 'x-middleware-rewrite'
263
- )
264
- );
265
- const originalUrl = new URL(req.url);
266
- customRewriteUrlDiff =
267
- rewriteUrl.pathname.replace(
268
- originalUrl.pathname,
232
+ withWalletCompleteRedirection(
233
+ async (
234
+ req: PzNextRequest,
235
+ event: NextFetchEvent
236
+ ) => {
237
+ let middlewareResult: NextResponse | void =
238
+ NextResponse.next();
239
+
240
+ try {
241
+ const { locale, prettyUrl, currency } =
242
+ req.middlewareParams.rewrites;
243
+ const { defaultLocaleValue } =
244
+ Settings.localization;
245
+ const url = req.nextUrl.clone();
246
+ const pathnameWithoutLocale =
247
+ url.pathname.replace(
248
+ urlLocaleMatcherRegex,
269
249
  ''
270
250
  );
271
- }
272
251
 
273
- url.basePath = `/${commerceUrl}`;
274
- url.pathname = `/${
275
- locale.length ? `${locale}/` : ''
276
- }${currency}/${customRewriteUrlDiff}${
277
- prettyUrl ?? pathnameWithoutLocale
278
- }`.replace(/\/+/g, '/');
279
-
280
- if (
281
- Settings.usePrettyUrlRoute &&
282
- url.searchParams.toString().length > 0 &&
283
- !Object.entries(ROUTES).find(([, value]) =>
284
- new RegExp(`^${value}/?$`).test(
285
- pathnameWithoutLocale
286
- )
287
- )
288
- ) {
289
- url.pathname =
290
- url.pathname +
291
- (/\/$/.test(url.pathname) ? '' : '/') +
292
- `searchparams|${encodeURIComponent(
293
- url.searchParams.toString()
294
- )}`;
295
- }
252
+ middlewareResult = (await middleware(
253
+ req,
254
+ event
255
+ )) as NextResponse | void;
296
256
 
297
- if (
298
- !req.middlewareParams.found &&
299
- Settings.customNotFoundEnabled
300
- ) {
301
- const pathname = url.pathname
302
- .replace(/\/+$/, '')
303
- .split('/');
304
- url.pathname = url.pathname.replace(
305
- pathname.pop(),
306
- 'pz-not-found'
307
- );
308
- }
309
-
310
- Settings.rewrites.forEach((rewrite) => {
311
- url.pathname = url.pathname.replace(
312
- rewrite.source,
313
- rewrite.destination
314
- );
315
- });
257
+ let customRewriteUrlDiff = '';
316
258
 
317
- // if middleware.ts has a return value for current url
318
- if (middlewareResult instanceof NextResponse) {
319
- // pz-override-response header is used to prevent 404 page for custom responses.
320
259
  if (
260
+ middlewareResult instanceof NextResponse &&
321
261
  middlewareResult.headers.get(
322
262
  'pz-override-response'
323
- ) !== 'true'
324
- ) {
325
- middlewareResult.headers.set(
326
- 'x-middleware-rewrite',
327
- url.href
328
- );
329
- } else if (
330
- middlewareResult.headers.get(
331
- 'x-middleware-rewrite'
332
263
  ) &&
333
264
  middlewareResult.headers.get(
334
- 'pz-override-response'
335
- ) === 'true'
265
+ 'x-middleware-rewrite'
266
+ )
267
+ ) {
268
+ const rewriteUrl = new URL(
269
+ middlewareResult.headers.get(
270
+ 'x-middleware-rewrite'
271
+ )
272
+ );
273
+ const originalUrl = new URL(req.url);
274
+ customRewriteUrlDiff =
275
+ rewriteUrl.pathname.replace(
276
+ originalUrl.pathname,
277
+ ''
278
+ );
279
+ }
280
+
281
+ url.basePath = `/${commerceUrl}`;
282
+ url.pathname = `/${
283
+ locale.length ? `${locale}/` : ''
284
+ }${currency}/${customRewriteUrlDiff}${
285
+ prettyUrl ?? pathnameWithoutLocale
286
+ }`.replace(/\/+/g, '/');
287
+
288
+ if (
289
+ Settings.usePrettyUrlRoute &&
290
+ url.searchParams.toString().length > 0 &&
291
+ !Object.entries(ROUTES).find(([, value]) =>
292
+ new RegExp(`^${value}/?$`).test(
293
+ pathnameWithoutLocale
294
+ )
295
+ )
336
296
  ) {
297
+ url.pathname =
298
+ url.pathname +
299
+ (/\/$/.test(url.pathname) ? '' : '/') +
300
+ `searchparams|${encodeURIComponent(
301
+ url.searchParams.toString()
302
+ )}`;
303
+ }
304
+
305
+ if (
306
+ !req.middlewareParams.found &&
307
+ Settings.customNotFoundEnabled
308
+ ) {
309
+ const pathname = url.pathname
310
+ .replace(/\/+$/, '')
311
+ .split('/');
312
+ url.pathname = url.pathname.replace(
313
+ pathname.pop(),
314
+ 'pz-not-found'
315
+ );
316
+ }
317
+
318
+ Settings.rewrites.forEach((rewrite) => {
319
+ url.pathname = url.pathname.replace(
320
+ rewrite.source,
321
+ rewrite.destination
322
+ );
323
+ });
324
+
325
+ // if middleware.ts has a return value for current url
326
+ if (middlewareResult instanceof NextResponse) {
327
+ // pz-override-response header is used to prevent 404 page for custom responses.
328
+ if (
329
+ middlewareResult.headers.get(
330
+ 'pz-override-response'
331
+ ) !== 'true'
332
+ ) {
333
+ middlewareResult.headers.set(
334
+ 'x-middleware-rewrite',
335
+ url.href
336
+ );
337
+ } else if (
338
+ middlewareResult.headers.get(
339
+ 'x-middleware-rewrite'
340
+ ) &&
341
+ middlewareResult.headers.get(
342
+ 'pz-override-response'
343
+ ) === 'true'
344
+ ) {
345
+ middlewareResult =
346
+ NextResponse.rewrite(url);
347
+ }
348
+ } else {
349
+ // if middleware.ts doesn't have a return value.
350
+ // e.g. NextResponse.next() doesn't exist in middleware.ts
351
+
337
352
  middlewareResult = NextResponse.rewrite(url);
338
353
  }
339
- } else {
340
- // if middleware.ts doesn't have a return value.
341
- // e.g. NextResponse.next() doesn't exist in middleware.ts
342
354
 
343
- middlewareResult = NextResponse.rewrite(url);
344
- }
355
+ const { localeUrlStrategy } =
356
+ Settings.localization;
357
+
358
+ const fallbackHost =
359
+ req.headers.get('x-forwarded-host') ||
360
+ req.headers.get('host');
361
+ const hostname =
362
+ process.env.NEXT_PUBLIC_URL ||
363
+ `https://${fallbackHost}`;
364
+ const rootHostname =
365
+ localeUrlStrategy ===
366
+ LocaleUrlStrategy.Subdomain
367
+ ? getRootHostname(hostname)
368
+ : null;
369
+
370
+ if (
371
+ !url.pathname.startsWith(
372
+ `/${currency}/orders`
373
+ )
374
+ ) {
375
+ middlewareResult.cookies.set(
376
+ 'pz-locale',
377
+ locale?.length > 0
378
+ ? locale
379
+ : defaultLocaleValue,
380
+ {
381
+ domain: rootHostname,
382
+ sameSite: 'none',
383
+ secure: true,
384
+ expires: new Date(
385
+ Date.now() + 1000 * 60 * 60 * 24 * 7
386
+ ) // 7 days
387
+ }
388
+ );
389
+ }
345
390
 
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
-
361
- if (
362
- !url.pathname.startsWith(`/${currency}/orders`)
363
- ) {
364
391
  middlewareResult.cookies.set(
365
- 'pz-locale',
366
- locale?.length > 0
367
- ? locale
368
- : defaultLocaleValue,
392
+ 'pz-currency',
393
+ currency,
369
394
  {
370
395
  domain: rootHostname,
371
396
  sameSite: 'none',
@@ -375,82 +400,69 @@ const withPzDefault =
375
400
  ) // 7 days
376
401
  }
377
402
  );
378
- }
379
403
 
380
- middlewareResult.cookies.set(
381
- 'pz-currency',
382
- currency,
383
- {
384
- domain: rootHostname,
385
- sameSite: 'none',
386
- secure: true,
387
- expires: new Date(
388
- Date.now() + 1000 * 60 * 60 * 24 * 7
389
- ) // 7 days
404
+ if (
405
+ req.cookies.get('pz-locale') &&
406
+ req.cookies.get('pz-locale').value !== locale
407
+ ) {
408
+ logger.debug('Locale changed', {
409
+ locale,
410
+ oldLocale:
411
+ req.cookies.get('pz-locale')?.value,
412
+ ip
413
+ });
390
414
  }
391
- );
392
-
393
- if (
394
- req.cookies.get('pz-locale') &&
395
- req.cookies.get('pz-locale').value !== locale
396
- ) {
397
- logger.debug('Locale changed', {
398
- locale,
399
- oldLocale:
400
- req.cookies.get('pz-locale')?.value,
401
- ip
402
- });
403
- }
404
-
405
- middlewareResult.headers.set(
406
- 'pz-url',
407
- req.nextUrl.toString()
408
- );
409
-
410
- if (req.cookies.get('pz-set-currency')) {
411
- middlewareResult.cookies.delete(
412
- 'pz-set-currency'
413
- );
414
- }
415
415
 
416
- if (process.env.ACC_APP_VERSION) {
417
416
  middlewareResult.headers.set(
418
- 'acc-app-version',
419
- process.env.ACC_APP_VERSION
417
+ 'pz-url',
418
+ req.nextUrl.toString()
420
419
  );
421
- }
422
420
 
423
- // Set CSRF token if not set
424
- try {
425
- const url = `${Settings.commerceUrl}${user.csrfToken}`;
421
+ if (req.cookies.get('pz-set-currency')) {
422
+ middlewareResult.cookies.delete(
423
+ 'pz-set-currency'
424
+ );
425
+ }
426
426
 
427
- if (!req.cookies.get('csrftoken')) {
428
- const { csrf_token } = await (
429
- await fetch(url)
430
- ).json();
431
- middlewareResult.cookies.set(
432
- 'csrftoken',
433
- csrf_token,
434
- {
435
- domain: rootHostname
436
- }
427
+ if (process.env.ACC_APP_VERSION) {
428
+ middlewareResult.headers.set(
429
+ 'acc-app-version',
430
+ process.env.ACC_APP_VERSION
437
431
  );
438
432
  }
433
+
434
+ // Set CSRF token if not set
435
+ try {
436
+ const url = `${Settings.commerceUrl}${user.csrfToken}`;
437
+
438
+ if (!req.cookies.get('csrftoken')) {
439
+ const { csrf_token } = await (
440
+ await fetch(url)
441
+ ).json();
442
+ middlewareResult.cookies.set(
443
+ 'csrftoken',
444
+ csrf_token,
445
+ {
446
+ domain: rootHostname
447
+ }
448
+ );
449
+ }
450
+ } catch (error) {
451
+ logger.error('CSRF Error', {
452
+ error,
453
+ ip
454
+ });
455
+ }
439
456
  } catch (error) {
440
- logger.error('CSRF Error', {
457
+ logger.error('withPzDefault Error', {
441
458
  error,
442
459
  ip
443
460
  });
444
461
  }
445
- } catch (error) {
446
- logger.error('withPzDefault Error', {
447
- error,
448
- ip
449
- });
450
- }
451
462
 
452
- return middlewareResult;
453
- }
463
+ return middlewareResult;
464
+ }
465
+ )
454
466
  )
455
467
  )
456
468
  )
@@ -9,6 +9,7 @@ import withCompleteGpay from './complete-gpay';
9
9
  import withCompleteMasterpass from './complete-masterpass';
10
10
  import withCheckoutProvider from './checkout-provider';
11
11
  import withSavedCardRedirection from './saved-card-redirection';
12
+ import withWalletCompleteRedirection from './wallet-complete-redirection';
12
13
  import { NextRequest } from 'next/server';
13
14
 
14
15
  export {
@@ -22,7 +23,8 @@ export {
22
23
  withCompleteGpay,
23
24
  withCompleteMasterpass,
24
25
  withCheckoutProvider,
25
- withSavedCardRedirection
26
+ withSavedCardRedirection,
27
+ withWalletCompleteRedirection
26
28
  };
27
29
 
28
30
  export interface PzNextRequest extends NextRequest {
@@ -35,6 +35,7 @@ const withRedirectionPayment =
35
35
  const searchParams = new URLSearchParams(url.search);
36
36
  const ip = req.headers.get('x-forwarded-for') ?? '';
37
37
  const sessionId = req.cookies.get('osessionid');
38
+ const currentLocale = req.middlewareParams?.rewrites?.locale;
38
39
 
39
40
  if (searchParams.get('page') !== 'RedirectionPageCompletePage') {
40
41
  return middleware(req, event);
@@ -46,7 +47,9 @@ const withRedirectionPayment =
46
47
  'Content-Type': 'application/x-www-form-urlencoded',
47
48
  Cookie: req.headers.get('cookie') ?? '',
48
49
  'x-currency': req.cookies.get('pz-currency')?.value ?? '',
49
- 'x-forwarded-for': ip
50
+ 'x-forwarded-for': ip,
51
+ 'Accept-Language':
52
+ currentLocale ?? req.cookies.get('pz-locale')?.value ?? ''
50
53
  };
51
54
 
52
55
  try {
@@ -4,6 +4,7 @@ import { Buffer } from 'buffer';
4
4
  import logger from '../utils/log';
5
5
  import { getUrlPathWithLocale } from '../utils/localization';
6
6
  import { PzNextRequest } from '.';
7
+ import { ServerVariables } from '../utils/server-variables';
7
8
 
8
9
  const streamToString = async (stream: ReadableStream<Uint8Array> | null) => {
9
10
  if (stream) {
@@ -34,6 +35,7 @@ const withSavedCardRedirection =
34
35
  const url = req.nextUrl.clone();
35
36
  const ip = req.headers.get('x-forwarded-for') ?? '';
36
37
  const sessionId = req.cookies.get('osessionid');
38
+ const currentLocale = req.middlewareParams?.rewrites?.locale;
37
39
 
38
40
  if (url.search.indexOf('SavedCardThreeDSecurePage') === -1) {
39
41
  return middleware(req, event);
@@ -45,7 +47,9 @@ const withSavedCardRedirection =
45
47
  'Content-Type': 'application/x-www-form-urlencoded',
46
48
  Cookie: req.headers.get('cookie') ?? '',
47
49
  'x-currency': req.cookies.get('pz-currency')?.value ?? '',
48
- 'x-forwarded-for': ip
50
+ 'x-forwarded-for': ip,
51
+ 'Accept-Language':
52
+ currentLocale ?? req.cookies.get('pz-locale')?.value ?? ''
49
53
  };
50
54
 
51
55
  try {
@@ -4,6 +4,7 @@ import { Buffer } from 'buffer';
4
4
  import logger from '../utils/log';
5
5
  import { getUrlPathWithLocale } from '../utils/localization';
6
6
  import { PzNextRequest } from '.';
7
+ import { ServerVariables } from '../utils/server-variables';
7
8
 
8
9
  const streamToString = async (stream: ReadableStream<Uint8Array> | null) => {
9
10
  if (stream) {
@@ -34,6 +35,7 @@ const withThreeDRedirection =
34
35
  const url = req.nextUrl.clone();
35
36
  const ip = req.headers.get('x-forwarded-for') ?? '';
36
37
  const sessionId = req.cookies.get('osessionid');
38
+ const currentLocale = req.middlewareParams?.rewrites?.locale;
37
39
 
38
40
  if (url.search.indexOf('CreditCardThreeDSecurePage') === -1) {
39
41
  return middleware(req, event);
@@ -45,7 +47,9 @@ const withThreeDRedirection =
45
47
  'Content-Type': 'application/x-www-form-urlencoded',
46
48
  Cookie: req.headers.get('cookie') ?? '',
47
49
  'x-currency': req.cookies.get('pz-currency')?.value ?? '',
48
- 'x-forwarded-for': ip
50
+ 'x-forwarded-for': ip,
51
+ 'Accept-Language':
52
+ currentLocale ?? req.cookies.get('pz-locale')?.value ?? ''
49
53
  };
50
54
 
51
55
  try {
@@ -50,7 +50,7 @@ const withUrlRedirection =
50
50
  const location = request.headers.get('location');
51
51
  const redirectUrl = new URL(
52
52
  request.headers.get('location'),
53
- location.startsWith('http') ? '' : process.env.NEXT_PUBLIC_URL
53
+ location.startsWith('http') ? '' : url.origin
54
54
  );
55
55
 
56
56
  redirectUrl.pathname = getUrlPathWithLocale(
@@ -0,0 +1,203 @@
1
+ import { NextFetchEvent, NextMiddleware, NextResponse } from 'next/server';
2
+ import Settings from 'settings';
3
+ import { Buffer } from 'buffer';
4
+ import logger from '../utils/log';
5
+ import { getUrlPathWithLocale } from '../utils/localization';
6
+ import { PzNextRequest } from '.';
7
+
8
+ const streamToString = async (stream: ReadableStream<Uint8Array> | null) => {
9
+ if (stream) {
10
+ const chunks = [];
11
+ let result = '';
12
+
13
+ try {
14
+ for await (const chunk of stream as any) {
15
+ chunks.push(Buffer.from(chunk));
16
+ }
17
+
18
+ result = Buffer.concat(chunks).toString('utf-8');
19
+ } catch (error) {
20
+ logger.error('Error while reading body stream', {
21
+ middleware: 'wallet-complete-redirection',
22
+ error
23
+ });
24
+ }
25
+
26
+ return result;
27
+ }
28
+ return null;
29
+ };
30
+
31
+ const withWalletCompleteRedirection =
32
+ (middleware: NextMiddleware) =>
33
+ async (req: PzNextRequest, event: NextFetchEvent) => {
34
+ const url = req.nextUrl.clone();
35
+ const ip = req.headers.get('x-forwarded-for') ?? '';
36
+ const sessionId = req.cookies.get('osessionid');
37
+
38
+ if (url.search.indexOf('WalletCompletePage') === -1) {
39
+ return middleware(req, event);
40
+ }
41
+
42
+ const requestUrl = `${Settings.commerceUrl}/orders/checkout/${url.search}`;
43
+ const requestHeaders = {
44
+ 'X-Requested-With': 'XMLHttpRequest',
45
+ 'Content-Type': 'application/x-www-form-urlencoded',
46
+ Cookie: req.headers.get('cookie') ?? '',
47
+ 'x-currency': req.cookies.get('pz-currency')?.value ?? '',
48
+ 'x-forwarded-for': ip
49
+ };
50
+
51
+ try {
52
+ const existingBody = await streamToString(req.body);
53
+ const queryParams = new URLSearchParams(url.search);
54
+ const bodyParams = new URLSearchParams();
55
+
56
+ if (existingBody) {
57
+ try {
58
+ const existingParams = new URLSearchParams(existingBody);
59
+
60
+ existingParams.forEach((value, key) => {
61
+ bodyParams.append(key, value);
62
+ });
63
+ } catch {
64
+ logger.error('Error parsing existing body as URL-encoded data', {
65
+ middleware: 'wallet-complete-redirection',
66
+ existingBody,
67
+ ip
68
+ });
69
+ }
70
+ }
71
+
72
+ queryParams.forEach((value, key) => {
73
+ bodyParams.append(key, value);
74
+ });
75
+
76
+ const body = bodyParams.toString();
77
+
78
+ if (!sessionId) {
79
+ logger.warn(
80
+ 'Make sure that the SESSION_COOKIE_SAMESITE environment variable is set to None in Commerce.',
81
+ {
82
+ middleware: 'wallet-complete-redirection',
83
+ ip
84
+ }
85
+ );
86
+
87
+ return NextResponse.redirect(
88
+ `${url.origin}${getUrlPathWithLocale(
89
+ '/orders/checkout/',
90
+ req.cookies.get('pz-locale')?.value
91
+ )}`,
92
+ 303
93
+ );
94
+ }
95
+
96
+ const request = await fetch(requestUrl, {
97
+ method: 'POST',
98
+ headers: requestHeaders,
99
+ body
100
+ });
101
+
102
+ logger.info('Complete wallet payment request', {
103
+ requestUrl,
104
+ status: request.status,
105
+ requestHeaders,
106
+ ip
107
+ });
108
+
109
+ const response = await request.json();
110
+
111
+ const { context_list: contextList, errors } = response;
112
+ const redirectionContext = contextList?.find(
113
+ (context) => context.page_context?.redirect_url
114
+ );
115
+ const redirectUrl = redirectionContext?.page_context?.redirect_url;
116
+
117
+ if (errors && Object.keys(errors).length) {
118
+ logger.error('Error while completing wallet payment', {
119
+ middleware: 'wallet-complete-redirection',
120
+ errors,
121
+ requestHeaders,
122
+ ip
123
+ });
124
+
125
+ return NextResponse.redirect(
126
+ `${url.origin}${getUrlPathWithLocale(
127
+ '/orders/checkout/',
128
+ req.cookies.get('pz-locale')?.value
129
+ )}`,
130
+ {
131
+ status: 303,
132
+ headers: {
133
+ 'Set-Cookie': `pz-pos-error=${JSON.stringify(errors)}; path=/;`
134
+ }
135
+ }
136
+ );
137
+ }
138
+
139
+ logger.info('Order success page context list', {
140
+ middleware: 'wallet-complete-redirection',
141
+ contextList,
142
+ ip
143
+ });
144
+
145
+ if (!redirectUrl) {
146
+ logger.warn(
147
+ 'No redirection url for order success page found in page_context. Redirecting to checkout page.',
148
+ {
149
+ middleware: 'wallet-complete-redirection',
150
+ requestHeaders,
151
+ response: JSON.stringify(response),
152
+ ip
153
+ }
154
+ );
155
+
156
+ const redirectUrlWithLocale = `${url.origin}${getUrlPathWithLocale(
157
+ '/orders/checkout/',
158
+ req.cookies.get('pz-locale')?.value
159
+ )}`;
160
+
161
+ return NextResponse.redirect(redirectUrlWithLocale, 303);
162
+ }
163
+
164
+ const redirectUrlWithLocale = `${url.origin}${getUrlPathWithLocale(
165
+ redirectUrl,
166
+ req.cookies.get('pz-locale')?.value
167
+ )}`;
168
+
169
+ logger.info('Redirecting to order success page', {
170
+ middleware: 'wallet-complete-redirection',
171
+ redirectUrlWithLocale,
172
+ ip
173
+ });
174
+
175
+ // Using POST method while redirecting causes an error,
176
+ // So we use 303 status code to change the method to GET
177
+ const nextResponse = NextResponse.redirect(redirectUrlWithLocale, 303);
178
+
179
+ nextResponse.headers.set(
180
+ 'Set-Cookie',
181
+ request.headers.get('set-cookie') ?? ''
182
+ );
183
+
184
+ return nextResponse;
185
+ } catch (error) {
186
+ logger.error('Error while completing wallet payment', {
187
+ middleware: 'wallet-complete-redirection',
188
+ error,
189
+ requestHeaders,
190
+ ip
191
+ });
192
+
193
+ return NextResponse.redirect(
194
+ `${url.origin}${getUrlPathWithLocale(
195
+ '/orders/checkout/',
196
+ req.cookies.get('pz-locale')?.value
197
+ )}`,
198
+ 303
199
+ );
200
+ }
201
+ };
202
+
203
+ export default withWalletCompleteRedirection;
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.93.0",
4
+ "version": "1.95.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -34,7 +34,7 @@
34
34
  "set-cookie-parser": "2.6.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@akinon/eslint-plugin-projectzero": "1.93.0",
37
+ "@akinon/eslint-plugin-projectzero": "1.95.0",
38
38
  "@babel/core": "7.26.10",
39
39
  "@babel/preset-env": "7.26.9",
40
40
  "@babel/preset-typescript": "7.27.0",
package/plugins.d.ts CHANGED
@@ -36,3 +36,5 @@ declare module '@akinon/pz-iyzico-saved-card' {
36
36
  }
37
37
 
38
38
  declare module '@akinon/pz-apple-pay' {}
39
+
40
+ declare module '@akinon/pz-flow-payment' {}
package/plugins.js CHANGED
@@ -15,5 +15,6 @@ module.exports = [
15
15
  'pz-saved-card',
16
16
  'pz-tabby-extension',
17
17
  'pz-apple-pay',
18
- 'pz-tamara-extension'
18
+ 'pz-tamara-extension',
19
+ 'pz-flow-payment'
19
20
  ];
@@ -20,7 +20,8 @@ import {
20
20
  setShippingOptions,
21
21
  setHepsipayAvailability,
22
22
  setWalletPaymentData,
23
- setPayOnDeliveryOtpModalActive
23
+ setPayOnDeliveryOtpModalActive,
24
+ setUnavailablePaymentOptions
24
25
  } from '../../redux/reducers/checkout';
25
26
  import { RootState, TypedDispatch } from 'redux/store';
26
27
  import { checkoutApi } from '../../data/client/checkout';
@@ -176,6 +177,14 @@ export const contextListMiddleware: Middleware = ({
176
177
  dispatch(setPaymentOptions(context.page_context.payment_options));
177
178
  }
178
179
 
180
+ if (context.page_context.unavailable_options) {
181
+ dispatch(
182
+ setUnavailablePaymentOptions(
183
+ context.page_context.unavailable_options
184
+ )
185
+ );
186
+ }
187
+
179
188
  if (context.page_context.credit_payment_options) {
180
189
  dispatch(
181
190
  setCreditPaymentOptions(context.page_context.credit_payment_options)
@@ -40,6 +40,7 @@ export interface CheckoutState {
40
40
  shippingOptions: ShippingOption[];
41
41
  dataSourceShippingOptions: DataSource[];
42
42
  paymentOptions: PaymentOption[];
43
+ unavailablePaymentOptions: PaymentOption[];
43
44
  creditPaymentOptions: CheckoutCreditPaymentOption[];
44
45
  selectedCreditPaymentPk: number;
45
46
  paymentChoices: PaymentChoice[];
@@ -60,6 +61,8 @@ export interface CheckoutState {
60
61
  countryCode: string;
61
62
  currencyCode: string;
62
63
  version: string;
64
+ public_key: string;
65
+ [key: string]: any;
63
66
  };
64
67
  detail: {
65
68
  label: string;
@@ -94,6 +97,7 @@ const initialState: CheckoutState = {
94
97
  shippingOptions: [],
95
98
  dataSourceShippingOptions: [],
96
99
  paymentOptions: [],
100
+ unavailablePaymentOptions: [],
97
101
  creditPaymentOptions: [],
98
102
  selectedCreditPaymentPk: null,
99
103
  paymentChoices: [],
@@ -157,6 +161,9 @@ const checkoutSlice = createSlice({
157
161
  setPaymentOptions(state, { payload }) {
158
162
  state.paymentOptions = payload;
159
163
  },
164
+ setUnavailablePaymentOptions(state, { payload }) {
165
+ state.unavailablePaymentOptions = payload;
166
+ },
160
167
  setPaymentChoices(state, { payload }) {
161
168
  state.paymentChoices = payload;
162
169
  },
@@ -218,9 +225,10 @@ export const {
218
225
  setShippingOptions,
219
226
  setDataSourceShippingOptions,
220
227
  setPaymentOptions,
228
+ setUnavailablePaymentOptions,
229
+ setPaymentChoices,
221
230
  setCreditPaymentOptions,
222
231
  setSelectedCreditPaymentPk,
223
- setPaymentChoices,
224
232
  setCardType,
225
233
  setInstallmentOptions,
226
234
  setBankAccounts,
package/sentry/index.ts CHANGED
@@ -13,36 +13,73 @@ const ALLOWED_CLIENT_LOG_TYPES: ClientLogType[] = [
13
13
  ClientLogType.CHECKOUT
14
14
  ];
15
15
 
16
+ const isNetworkError = (exception: unknown): boolean => {
17
+ if (!(exception instanceof Error)) return false;
18
+
19
+ const networkErrorPatterns = [
20
+ 'networkerror',
21
+ 'failed to fetch',
22
+ 'network request failed',
23
+ 'network error',
24
+ 'loading chunk',
25
+ 'chunk load failed'
26
+ ];
27
+
28
+ if (exception.name === 'NetworkError') return true;
29
+
30
+ if (exception.name === 'TypeError') {
31
+ return networkErrorPatterns.some((pattern) =>
32
+ exception.message.toLowerCase().includes(pattern)
33
+ );
34
+ }
35
+
36
+ return networkErrorPatterns.some((pattern) =>
37
+ exception.message.toLowerCase().includes(pattern)
38
+ );
39
+ };
40
+
16
41
  export const initSentry = (
17
42
  type: 'Server' | 'Client' | 'Edge',
18
43
  options: Sentry.BrowserOptions | Sentry.NodeOptions | Sentry.EdgeOptions = {}
19
44
  ) => {
20
- // TODO: Handle options with ESLint rules
45
+ // TODO: Remove Zero Project DSN
21
46
 
22
- Sentry.init({
47
+ const baseConfig = {
23
48
  dsn:
24
- options.dsn ||
25
49
  SENTRY_DSN ||
50
+ options.dsn ||
26
51
  'https://d8558ef8997543deacf376c7d8d7cf4b@o64293.ingest.sentry.io/4504338423742464',
27
52
  initialScope: {
28
53
  tags: {
29
54
  APP_TYPE: 'ProjectZeroNext',
30
- TYPE: type
55
+ TYPE: type,
56
+ ...((options.initialScope as any)?.tags || {})
31
57
  }
32
58
  },
33
59
  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
- }
60
+ integrations: []
61
+ };
44
62
 
45
- return event;
46
- }
47
- });
63
+ if (type === 'Server' || type === 'Edge') {
64
+ Sentry.init(baseConfig);
65
+ } else if (type === 'Client') {
66
+ Sentry.init({
67
+ ...baseConfig,
68
+ beforeSend: (event, hint) => {
69
+ if (
70
+ !ALLOWED_CLIENT_LOG_TYPES.includes(
71
+ event.tags?.LOG_TYPE as ClientLogType
72
+ )
73
+ ) {
74
+ return null;
75
+ }
76
+
77
+ if (isNetworkError(hint?.originalException)) {
78
+ return null;
79
+ }
80
+
81
+ return event;
82
+ }
83
+ });
84
+ }
48
85
  };
package/types/index.ts CHANGED
@@ -218,6 +218,14 @@ export interface CacheOptions {
218
218
  useProxy?: boolean;
219
219
  }
220
220
 
221
+ export interface SetCookieOptions {
222
+ expires?: number; // days
223
+ path?: string;
224
+ domain?: string;
225
+ secure?: boolean;
226
+ sameSite?: 'strict' | 'lax' | 'none';
227
+ }
228
+
221
229
  export interface ClientRequestOptions {
222
230
  useTrailingSlash?: boolean;
223
231
  useFormData?: boolean;
package/utils/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import settings from 'settings';
2
2
  import { LocaleUrlStrategy } from '../localization';
3
- import { CDNOptions, ClientRequestOptions } from '../types';
3
+ import { CDNOptions, ClientRequestOptions, SetCookieOptions } from '../types';
4
+ import getRootHostname from './get-root-hostname';
4
5
 
5
6
  export * from './get-currency';
6
7
  export * from './menu-generator';
@@ -20,14 +21,40 @@ export function getCookie(name: string) {
20
21
  }
21
22
  }
22
23
 
23
- export function setCookie(name: string, val: string) {
24
- const date = new Date();
25
- const value = val;
24
+ export function setCookie(
25
+ name: string,
26
+ value: string,
27
+ options: SetCookieOptions = {}
28
+ ) {
29
+ const cookieParts = [`${name}=${value}`];
30
+
31
+ if (options.expires) {
32
+ const date = new Date();
33
+ date.setTime(date.getTime() + options.expires * 24 * 60 * 60 * 1000);
34
+ cookieParts.push(`expires=${date.toUTCString()}`);
35
+ }
26
36
 
27
- date.setTime(date.getTime() + 7 * 24 * 60 * 60 * 1000);
37
+ cookieParts.push(`path=${options.path ?? '/'}`);
28
38
 
29
- document.cookie =
30
- name + '=' + value + '; expires=' + date.toUTCString() + '; path=/';
39
+ if (options.secure) {
40
+ cookieParts.push('secure');
41
+ }
42
+
43
+ if (options.sameSite) {
44
+ cookieParts.push(`sameSite=${options.sameSite}`);
45
+ }
46
+
47
+ const domain =
48
+ options.domain ??
49
+ (settings.localization.localeUrlStrategy === LocaleUrlStrategy.Subdomain
50
+ ? getRootHostname(document.location.href)
51
+ : null);
52
+
53
+ if (domain) {
54
+ cookieParts.push(`domain=${domain}`);
55
+ }
56
+
57
+ document.cookie = cookieParts.join('; ');
31
58
  }
32
59
 
33
60
  export function removeCookie(name: string) {
@@ -152,9 +179,6 @@ export function buildCDNUrl(url: string, config?: CDNOptions) {
152
179
  return `${rootWithoutOptions}${options}${fileExtension}`;
153
180
  }
154
181
 
155
- const { locales, localeUrlStrategy, defaultLocaleValue } =
156
- settings.localization;
157
-
158
182
  export const urlLocaleMatcherRegex = new RegExp(
159
183
  `^/(${settings.localization.locales
160
184
  .filter((l) =>