@akinon/next 1.124.0-rc.0 → 1.124.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,22 +1,14 @@
1
1
  # @akinon/next
2
2
 
3
- ## 1.124.0-rc.0
4
-
5
- ### Minor Changes
6
-
7
- - ZERO-4160: Enhance oauth-login middleware with improved request handling and logging
8
- - b55acb76: ZERO-2577: Fix pagination bug and update usePagination hook and ensure pagination controls rendering correctly
9
- - 760258c1: ZERO-4160: Enhance oauth-login middleware to handle fetch errors and improve response handling
10
- - 143be2b9: ZERO-3457: Crop styles are customizable and logic improved for rendering similar products modal
11
- - cd68a97a: ZERO-4126: Enhance error handling in appFetch and related data handlers to throw notFound on 404 and 422 errors
12
- - 9f8cd3bc: ZERO-3449: AI Search Active Filters & Crop Style changes have been implemented
13
- - bfafa3f4: ZERO-4160: Refactor oauth-login middleware to use fetchCommerce for API calls and improve cookie handling
14
- - d99a6a7d: ZERO-3457_1: Fixed the settings prop and made sure everything is customizable.
15
- - 591e345e: ZERO-3855: Enhance credit card payment handling in checkout middlewares
16
- - 4de5303c: ZERO-2504: add cookie filter to api client request
17
- - 95b139dc: ZERO-3795: Remove duplicate entry for SavedCard in PluginComponents map
18
- - 3909d322: Edit the duplicate Plugin.SimilarProducts in the plugin-module.
19
- - e18836b2: ZERO-4160: Restore scope in Sentry addon configuration in akinon.json
3
+ ## 1.124.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 929374c5: ZERO-3278: refactor variable names for better readability and consistency
8
+ - 71f8011d: ZERO-3271: remove code repetition in logger functions using closures
9
+ - 9be2c081: ZERO-3243: Improve basket update query handling with optimistic updates
10
+ - bd431e36: ZERO-3278: improve checkout validation error messages for better user guidance
11
+ - 54eac86b: ZERO-3271: add development logger system
20
12
 
21
13
  ## 1.123.0
22
14
 
@@ -1,6 +1,9 @@
1
1
  'use client';
2
2
 
3
+ import React from 'react';
3
4
  import { useMobileIframeHandler } from '../hooks';
5
+ import { LoggerPopup } from './logger-popup';
6
+ import { LoggerProvider } from '../hooks/use-logger-context';
4
7
  import * as Sentry from '@sentry/nextjs';
5
8
  import { initSentry } from '../sentry';
6
9
  import { useEffect } from 'react';
@@ -12,7 +15,9 @@ export default function ClientRoot({
12
15
  children: React.ReactNode;
13
16
  sessionId?: string;
14
17
  }) {
15
- const { preventPageRender } = useMobileIframeHandler({ sessionId });
18
+ const { preventPageRender } = useMobileIframeHandler({
19
+ sessionId: sessionId || ''
20
+ });
16
21
 
17
22
  const initializeSentry = async () => {
18
23
  const response = await fetch('/api/sentry', { next: { revalidate: 0 } });
@@ -35,5 +40,10 @@ export default function ClientRoot({
35
40
  return null;
36
41
  }
37
42
 
38
- return <>{children}</>;
43
+ return (
44
+ <LoggerProvider>
45
+ {children}
46
+ <LoggerPopup />
47
+ </LoggerProvider>
48
+ );
39
49
  }
@@ -21,3 +21,4 @@ export * from './link';
21
21
  export * from './pagination';
22
22
  export * from './live-commerce';
23
23
  export * from './file-input';
24
+ export * from './logger-popup';
@@ -0,0 +1,213 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useCallback, memo, useMemo } from 'react';
4
+ import { LogEntry } from '../hooks/use-logger';
5
+ import { useLoggerContext } from '../hooks/use-logger-context';
6
+
7
+ const LoggerAnimations = ({ color = '#dc2626' }: { color?: string }) => (
8
+ <style jsx global>{`
9
+ @keyframes pulse {
10
+ 0% {
11
+ transform: scale(0.95);
12
+ box-shadow: 0 0 0 0 ${color}80;
13
+ }
14
+
15
+ 70% {
16
+ transform: scale(1.05);
17
+ box-shadow: 0 0 0 10px ${color}00;
18
+ }
19
+
20
+ 100% {
21
+ transform: scale(0.95);
22
+ box-shadow: 0 0 0 0 ${color}00;
23
+ }
24
+ }
25
+ `}</style>
26
+ );
27
+
28
+ const LogLevelColors = {
29
+ warn: '#ff9800', // orange
30
+ error: '#f44336' // red
31
+ };
32
+
33
+ const LoggerTrigger = memo(
34
+ ({
35
+ onClick,
36
+ logCount = 0,
37
+ currentColor
38
+ }: {
39
+ onClick: () => void;
40
+ logCount?: number;
41
+ currentColor: string;
42
+ }) => {
43
+ return (
44
+ <button
45
+ onClick={onClick}
46
+ className="fixed bottom-4 right-4 w-14 h-14 border border-white rounded-full flex items-center justify-center shadow-lg z-[9999] hover:opacity-90 transition-colors"
47
+ aria-label="Open Logger"
48
+ style={{
49
+ backgroundColor: currentColor,
50
+ ...(logCount > 0 && {
51
+ animation: 'pulse 2s infinite'
52
+ })
53
+ }}
54
+ >
55
+ <svg
56
+ xmlns="http://www.w3.org/2000/svg"
57
+ fill="none"
58
+ viewBox="0 0 24 24"
59
+ strokeWidth="1.5"
60
+ stroke="currentColor"
61
+ className="text-white size-6"
62
+ >
63
+ <path
64
+ strokeLinecap="round"
65
+ strokeLinejoin="round"
66
+ d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
67
+ />
68
+ </svg>
69
+
70
+ {logCount > 0 && (
71
+ <span
72
+ className="absolute w-5 h-5 -bottom-[5px] p-1 -left-[5px] border-2 border-white rounded-full flex items-center justify-center text-[8px] text-white font-bold"
73
+ style={{
74
+ backgroundColor: currentColor
75
+ }}
76
+ >
77
+ {logCount > 99 ? '99+' : logCount}
78
+ </span>
79
+ )}
80
+ </button>
81
+ );
82
+ }
83
+ );
84
+
85
+ LoggerTrigger.displayName = 'LoggerTrigger';
86
+
87
+ const LogItem = memo(({ log }: { log: LogEntry }) => {
88
+ const [expanded, setExpanded] = useState(false);
89
+ const hasPayload = log.payload && Object.keys(log.payload).length > 0;
90
+
91
+ const toggleExpanded = useCallback(() => {
92
+ setExpanded((prev) => !prev);
93
+ }, []);
94
+
95
+ return (
96
+ <div className="border-b border-gray-200 py-2">
97
+ <div className="relative">
98
+ <div
99
+ className="absolute top-0 left-0 w-3 h-3 rounded-full mt-1.5 mr-2 flex-shrink-0"
100
+ style={{
101
+ backgroundColor: LogLevelColors[log.level],
102
+ boxShadow: `0 0 5px ${LogLevelColors[log.level]}`
103
+ }}
104
+ />
105
+ <div className="ml-6">
106
+ <div className="flex justify-between">
107
+ <span
108
+ className="font-medium capitalize"
109
+ style={{
110
+ color: LogLevelColors[log.level]
111
+ }}
112
+ >
113
+ {log.level}
114
+ </span>
115
+ <span className="text-xs text-gray-500">
116
+ {log.timestamp.toLocaleTimeString()}
117
+ </span>
118
+ </div>
119
+ <p className="text-sm">{log.message}</p>
120
+ {hasPayload && (
121
+ <button
122
+ onClick={toggleExpanded}
123
+ className="text-xs text-blue-500 mt-1 hover:text-blue-700 transition-colors"
124
+ >
125
+ {expanded ? 'Hide Details' : 'Show Details'}
126
+ </button>
127
+ )}
128
+ {expanded && hasPayload && (
129
+ <pre className="text-xs bg-gray-100 p-2 mt-1 rounded overflow-auto max-h-96 max-w-full">
130
+ {JSON.stringify(log.payload, null, 2)}
131
+ </pre>
132
+ )}
133
+ </div>
134
+ </div>
135
+ </div>
136
+ );
137
+ });
138
+
139
+ LogItem.displayName = 'LogItem';
140
+
141
+ export const LoggerPopup = () => {
142
+ const {
143
+ logs,
144
+ isVisible,
145
+ toggleVisibility,
146
+ clearLogs,
147
+ isDevelopment,
148
+ hasError,
149
+ hasWarning
150
+ } = useLoggerContext();
151
+
152
+ const currentColor = useMemo(() => {
153
+ if (logs.length === 0) return '#b5afaf';
154
+ if (hasError) return '#dc2626';
155
+ if (hasWarning) return '#ff9800';
156
+
157
+ return '#b5afaf';
158
+ }, [logs.length, hasError, hasWarning]);
159
+
160
+ if (!isDevelopment) {
161
+ return null;
162
+ }
163
+
164
+ if (!isVisible) {
165
+ return (
166
+ <>
167
+ <LoggerAnimations color={currentColor} />
168
+ <LoggerTrigger
169
+ onClick={toggleVisibility}
170
+ logCount={logs.length}
171
+ currentColor={currentColor}
172
+ />
173
+ </>
174
+ );
175
+ }
176
+
177
+ return (
178
+ <>
179
+ <LoggerAnimations color={currentColor} />
180
+ <LoggerTrigger
181
+ onClick={toggleVisibility}
182
+ logCount={logs.length}
183
+ currentColor={currentColor}
184
+ />
185
+ <div className="fixed bottom-20 right-4 w-96 max-w-[calc(100vw-2rem)] bg-white rounded-lg shadow-xl z-50 border-2 border-gray-200 max-h-[70vh] flex flex-col">
186
+ <div className="flex items-center justify-between p-3 border-b border-gray-200">
187
+ <h3 className="font-bold flex items-center">Development Logger</h3>
188
+ <div className="flex space-x-2">
189
+ <button
190
+ onClick={clearLogs}
191
+ className="text-xs bg-gray-200 hover:bg-gray-300 px-2 py-1 rounded transition-colors"
192
+ >
193
+ Clear
194
+ </button>
195
+ <button
196
+ onClick={toggleVisibility}
197
+ className="text-xs bg-gray-200 hover:bg-gray-300 px-2 py-1 rounded transition-colors"
198
+ >
199
+ Close
200
+ </button>
201
+ </div>
202
+ </div>
203
+ <div className="overflow-y-auto flex-grow p-3">
204
+ {logs.length === 0 ? (
205
+ <p className="text-gray-500 text-center py-4">No logs yet</p>
206
+ ) : (
207
+ logs.map((log) => <LogItem key={log.id} log={log} />)
208
+ )}
209
+ </div>
210
+ </div>
211
+ </>
212
+ );
213
+ };
@@ -114,6 +114,7 @@ const PluginComponents = new Map([
114
114
  ]
115
115
  ],
116
116
  [Plugin.SavedCard, [Component.SavedCard, Component.IyzicoSavedCard]],
117
+ [Plugin.SavedCard, [Component.SavedCard]],
117
118
  [Plugin.FlowPayment, [Component.FlowPayment]],
118
119
  [
119
120
  Plugin.VirtualTryOn,
@@ -104,6 +104,45 @@ export const basketApi = api.injectEndpoints({
104
104
  method: 'PUT',
105
105
  body
106
106
  }),
107
+ async onQueryStarted(_, { dispatch, queryFulfilled }) {
108
+ try {
109
+ const { data } = await queryFulfilled;
110
+
111
+ dispatch(
112
+ basketApi.util.updateQueryData(
113
+ 'getBasket',
114
+ undefined,
115
+ () => data.basket
116
+ )
117
+ );
118
+
119
+ if (data.basket.namespace) {
120
+ dispatch(
121
+ basketApi.util.updateQueryData(
122
+ 'getBasketDetail',
123
+ { namespace: data.basket.namespace },
124
+ () => data.basket
125
+ )
126
+ );
127
+ }
128
+
129
+ dispatch(
130
+ basketApi.util.updateQueryData(
131
+ 'getAllBaskets',
132
+ undefined,
133
+ (baskets) => {
134
+ if (!baskets) return baskets;
135
+
136
+ return baskets.map((basket) =>
137
+ basket.pk === data.basket.pk ? data.basket : basket
138
+ );
139
+ }
140
+ )
141
+ );
142
+ } catch (error) {
143
+ console.error('Error updating quantity:', error);
144
+ }
145
+ },
107
146
  invalidatesTags: ['MultiBasket', 'Basket', 'MiniBasket']
108
147
  }),
109
148
  clearBasket: build.mutation<Basket, void>({
@@ -33,6 +33,16 @@ import {
33
33
  buildDirectPurchaseForm,
34
34
  buildPurchaseForm
35
35
  } from '@akinon/pz-masterpass/src/utils';
36
+ import { devLogger } from '@akinon/next/hooks/use-logger-context';
37
+ import { LogLevel } from '@akinon/next/hooks/use-logger';
38
+
39
+ const getLatestState = async (getState: () => any): Promise<any> => {
40
+ await new Promise((resolve) => setTimeout(resolve, 250));
41
+
42
+ return getState();
43
+ };
44
+
45
+ const recentLogMessages = new Map<string, number>();
36
46
 
37
47
  const getStore = async (): Promise<AppStore> => {
38
48
  const { store } = await import('redux/store');
@@ -49,6 +59,53 @@ interface CheckoutResponse {
49
59
  redirect_url?: string;
50
60
  }
51
61
 
62
+ const validateCheckoutState = (
63
+ state: any,
64
+ validations: Array<{
65
+ condition: (state: any) => boolean;
66
+ errorMessage: string;
67
+ severity?: LogLevel;
68
+ data?: any;
69
+ action?: () => void;
70
+ }>
71
+ ) => {
72
+ validations.forEach(
73
+ ({
74
+ condition,
75
+ errorMessage,
76
+ severity = 'error',
77
+ data: logData,
78
+ action
79
+ }) => {
80
+ if (condition(state)) {
81
+ const now = Date.now();
82
+ const lastLogged = recentLogMessages.get(errorMessage) || 0;
83
+
84
+ if (now - lastLogged > 2000) {
85
+ recentLogMessages.set(errorMessage, now);
86
+
87
+ switch (severity) {
88
+ case 'error':
89
+ devLogger.error(
90
+ errorMessage,
91
+ logData || state.checkout?.preOrder
92
+ );
93
+ action?.();
94
+ break;
95
+ case 'warn':
96
+ devLogger.warn(errorMessage, logData || state.checkout?.preOrder);
97
+ action?.();
98
+ break;
99
+ default:
100
+ devLogger.info(errorMessage, logData || state.checkout?.preOrder);
101
+ action?.();
102
+ }
103
+ }
104
+ }
105
+ }
106
+ );
107
+ };
108
+
52
109
  interface SetAddressesParams {
53
110
  shippingAddressPk: number;
54
111
  billingAddressPk: number;
@@ -195,6 +252,20 @@ const completeMasterpassPayment = async (
195
252
  });
196
253
  };
197
254
 
255
+ let checkoutAbortController = new AbortController();
256
+
257
+ export const getCheckoutAbortSignal = () => {
258
+ if (checkoutAbortController.signal.aborted) {
259
+ checkoutAbortController = new AbortController();
260
+ }
261
+ return checkoutAbortController.signal;
262
+ };
263
+
264
+ const abortCheckout = () => {
265
+ checkoutAbortController.abort();
266
+ checkoutAbortController = new AbortController();
267
+ };
268
+
198
269
  export const checkoutApi = api.injectEndpoints({
199
270
  endpoints: (build) => ({
200
271
  fetchCheckout: build.query<CheckoutResponse, void>({
@@ -381,12 +452,33 @@ export const checkoutApi = api.injectEndpoints({
381
452
  method: 'POST',
382
453
  body: {
383
454
  delivery_option: String(pk)
384
- }
455
+ },
456
+ signal: getCheckoutAbortSignal()
385
457
  }),
386
- async onQueryStarted(arg, { dispatch, queryFulfilled }) {
458
+ async onQueryStarted(arg, { dispatch, queryFulfilled, getState }) {
387
459
  dispatch(setShippingStepBusy(true));
388
- await queryFulfilled;
389
- dispatch(setShippingStepBusy(false));
460
+
461
+ const state = await getLatestState(getState);
462
+
463
+ validateCheckoutState(state, [
464
+ {
465
+ condition: (state) => {
466
+ const preOrder = state.checkout?.preOrder;
467
+
468
+ return preOrder?.basket?.basketitem_set?.length === 0;
469
+ },
470
+ errorMessage:
471
+ 'Your shopping basket is empty. Please add items to your basket before selecting a delivery option.',
472
+ action: () => abortCheckout()
473
+ }
474
+ ]);
475
+
476
+ try {
477
+ await queryFulfilled;
478
+ dispatch(setShippingStepBusy(false));
479
+ } catch (error) {
480
+ dispatch(setShippingStepBusy(false));
481
+ }
390
482
  }
391
483
  }),
392
484
  setAddresses: build.mutation<CheckoutResponse, SetAddressesParams>({
@@ -398,12 +490,36 @@ export const checkoutApi = api.injectEndpoints({
398
490
  body: {
399
491
  shipping_address: String(shippingAddressPk),
400
492
  billing_address: String(billingAddressPk)
401
- }
493
+ },
494
+ signal: getCheckoutAbortSignal()
402
495
  }),
403
- async onQueryStarted(arg, { dispatch, queryFulfilled }) {
496
+ async onQueryStarted(arg, { dispatch, queryFulfilled, getState }) {
404
497
  dispatch(setShippingStepBusy(true));
405
- await queryFulfilled;
406
- dispatch(setShippingStepBusy(false));
498
+
499
+ const state = await getLatestState(getState);
500
+
501
+ validateCheckoutState(state, [
502
+ {
503
+ condition: (state) => {
504
+ const deliveryOptions = state.checkout?.deliveryOptions;
505
+ const preOrder = state.checkout?.preOrder;
506
+
507
+ return deliveryOptions?.length > 0
508
+ ? preOrder && !preOrder.delivery_option?.pk
509
+ : false;
510
+ },
511
+ errorMessage:
512
+ 'You need to select a delivery option before setting your addresses. Dispatch setAddresses action after delivery option selection.',
513
+ action: () => abortCheckout()
514
+ }
515
+ ]);
516
+
517
+ try {
518
+ await queryFulfilled;
519
+ dispatch(setShippingStepBusy(false));
520
+ } catch (error) {
521
+ dispatch(setShippingStepBusy(false));
522
+ }
407
523
  }
408
524
  }),
409
525
  setShippingOption: build.mutation<CheckoutResponse, number>({
@@ -414,12 +530,43 @@ export const checkoutApi = api.injectEndpoints({
414
530
  method: 'POST',
415
531
  body: {
416
532
  shipping_option: String(pk)
417
- }
533
+ },
534
+ signal: getCheckoutAbortSignal()
418
535
  }),
419
- async onQueryStarted(arg, { dispatch, queryFulfilled }) {
536
+ async onQueryStarted(arg, { dispatch, queryFulfilled, getState }) {
420
537
  dispatch(setShippingStepBusy(true));
421
- await queryFulfilled;
422
- dispatch(setShippingStepBusy(false));
538
+
539
+ const state = await getLatestState(getState);
540
+
541
+ validateCheckoutState(state, [
542
+ {
543
+ condition: (state) => {
544
+ const preOrder = state.checkout?.preOrder;
545
+
546
+ return !preOrder?.billing_address;
547
+ },
548
+ errorMessage:
549
+ 'You need to provide a billing address before selecting a shipping option. Dispatch setShippingOption action after billing address selection.',
550
+ action: () => abortCheckout()
551
+ },
552
+ {
553
+ condition: (state) => {
554
+ const preOrder = state.checkout?.preOrder;
555
+
556
+ return !preOrder?.shipping_address;
557
+ },
558
+ errorMessage:
559
+ 'You need to provide a shipping address before selecting a shipping option. Dispatch setShippingOption action after shipping address selection.',
560
+ action: () => abortCheckout()
561
+ }
562
+ ]);
563
+
564
+ try {
565
+ await queryFulfilled;
566
+ dispatch(setShippingStepBusy(false));
567
+ } catch (error) {
568
+ dispatch(setShippingStepBusy(false));
569
+ }
423
570
  }
424
571
  }),
425
572
  setDataSourceShippingOptions: build.mutation<CheckoutResponse, number[]>({
@@ -467,16 +614,37 @@ export const checkoutApi = api.injectEndpoints({
467
614
  method: 'POST',
468
615
  body: {
469
616
  payment_option: String(pk)
470
- }
617
+ },
618
+ signal: getCheckoutAbortSignal()
471
619
  }),
472
- async onQueryStarted(arg, { dispatch, queryFulfilled }) {
620
+ async onQueryStarted(arg, { dispatch, queryFulfilled, getState }) {
473
621
  dispatch(setPaymentStepBusy(true));
474
622
  dispatch(setInstallmentOptions([]));
475
623
  dispatch(setBankAccounts([]));
476
624
  dispatch(setSelectedBankAccountPk(null));
477
625
  dispatch(setCardType(null));
478
- await queryFulfilled;
479
- dispatch(setPaymentStepBusy(false));
626
+
627
+ const state = await getLatestState(getState);
628
+
629
+ validateCheckoutState(state, [
630
+ {
631
+ condition: (state) => {
632
+ const preOrder = state.checkout?.preOrder;
633
+
634
+ return !preOrder?.shipping_option?.pk;
635
+ },
636
+ errorMessage:
637
+ 'You need to select a shipping option before choosing a payment method. Dispatch setPaymentOption action after shipping option selection.',
638
+ action: () => abortCheckout()
639
+ }
640
+ ]);
641
+
642
+ try {
643
+ await queryFulfilled;
644
+ dispatch(setPaymentStepBusy(false));
645
+ } catch (error) {
646
+ dispatch(setPaymentStepBusy(false));
647
+ }
480
648
  }
481
649
  }),
482
650
  setWalletSelectionPage: build.mutation<
package/data/urls.ts CHANGED
@@ -183,11 +183,7 @@ export const product = {
183
183
  breadcrumbUrl: (menuitemmodel: string) =>
184
184
  `/menus/generate_breadcrumb/?item=${menuitemmodel}&generator_name=menu_item`,
185
185
  bundleProduct: (productPk: string, queryString: string) =>
186
- `/bundle-product/${productPk}/?${queryString}`,
187
- similarProducts: (params?: string) =>
188
- `/similar-products${params ? `?${params}` : ''}`,
189
- similarProductsList: (params?: string) =>
190
- `/similar-product-list${params ? `?${params}` : ''}`
186
+ `/bundle-product/${productPk}/?${queryString}`
191
187
  };
192
188
 
193
189
  export const wishlist = {
package/hooks/index.ts CHANGED
@@ -10,4 +10,6 @@ export * from './use-mobile-iframe-handler';
10
10
  export * from './use-payment-options';
11
11
  export * from './use-pagination';
12
12
  export * from './use-message-listener';
13
+ export * from './use-logger';
14
+ export * from './use-logger-context';
13
15
  export * from './use-sentry-uncaught-errors';
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ import React, {
4
+ createContext,
5
+ useContext,
6
+ ReactNode,
7
+ useMemo,
8
+ useRef,
9
+ useEffect
10
+ } from 'react';
11
+ import { useLogger, LogEntry, LogLevel } from './use-logger';
12
+
13
+ const LOG_LEVELS: LogLevel[] = ['info', 'warn', 'error'];
14
+
15
+ interface LoggerContextType {
16
+ logs: LogEntry[];
17
+ isVisible: boolean;
18
+ toggleVisibility: () => void;
19
+ clearLogs: () => void;
20
+ info: (message: string, payload?: any) => string;
21
+ warn: (message: string, payload?: any) => string;
22
+ error: (message: string, payload?: any) => string;
23
+ isDevelopment: boolean;
24
+ hasError: boolean;
25
+ hasWarning: boolean;
26
+ }
27
+
28
+ const LoggerContext = createContext<LoggerContextType | undefined>(undefined);
29
+
30
+ let globalAddLogFunction:
31
+ | ((level: string, message: string, payload?: any) => string)
32
+ | null = null;
33
+
34
+ // temporary queue for logs generated before the logger is initialized
35
+ const pendingLogs: Array<{ level: string; message: string; payload?: any }> =
36
+ [];
37
+
38
+ const createLogFunction =
39
+ (level: LogLevel) => (message: string, payload?: any) => {
40
+ if (
41
+ typeof window !== 'undefined' &&
42
+ process.env.NODE_ENV === 'development'
43
+ ) {
44
+ try {
45
+ if (globalAddLogFunction) {
46
+ globalAddLogFunction(level, message, payload);
47
+ } else {
48
+ pendingLogs.push({ level, message, payload });
49
+ }
50
+ } catch (err) {
51
+ // prevent errors
52
+ }
53
+ }
54
+
55
+ return '';
56
+ };
57
+
58
+ const stableLogger = LOG_LEVELS.reduce((logger, level) => {
59
+ logger[level] = createLogFunction(level);
60
+
61
+ return logger;
62
+ }, {} as Record<LogLevel, (message: string, payload?: any) => string>);
63
+
64
+ export const LoggerProvider = ({ children }: { children: ReactNode }) => {
65
+ const loggerHook = useLogger();
66
+
67
+ const addLogRef = useRef<
68
+ (level: string, message: string, payload?: any) => string
69
+ >((level, message, payload) => {
70
+ if (LOG_LEVELS.includes(level as LogLevel)) {
71
+ return loggerHook[level as LogLevel](message, payload);
72
+ }
73
+ return '';
74
+ });
75
+
76
+ useEffect(() => {
77
+ globalAddLogFunction = addLogRef.current;
78
+
79
+ if (pendingLogs.length > 0) {
80
+ pendingLogs.forEach((log) => {
81
+ if (globalAddLogFunction) {
82
+ globalAddLogFunction(log.level, log.message, log.payload);
83
+ }
84
+ });
85
+
86
+ pendingLogs.length = 0;
87
+ }
88
+
89
+ return () => {
90
+ globalAddLogFunction = null;
91
+ };
92
+ }, []);
93
+
94
+ const contextValue = useMemo(
95
+ () => loggerHook,
96
+ [loggerHook.logs, loggerHook.isVisible, loggerHook.isDevelopment] // eslint-disable-line react-hooks/exhaustive-deps
97
+ );
98
+
99
+ return (
100
+ <LoggerContext.Provider value={contextValue}>
101
+ {children}
102
+ </LoggerContext.Provider>
103
+ );
104
+ };
105
+
106
+ export const useLoggerContext = () => {
107
+ const context = useContext(LoggerContext);
108
+ if (context === undefined) {
109
+ throw new Error('useLoggerContext must be used within a LoggerProvider');
110
+ }
111
+ return context;
112
+ };
113
+
114
+ export const devLogger = stableLogger;
@@ -0,0 +1,92 @@
1
+ import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
2
+
3
+ export type LogLevel = 'info' | 'warn' | 'error';
4
+
5
+ export interface LogEntry {
6
+ id: string;
7
+ level: LogLevel;
8
+ message: string;
9
+ timestamp: Date;
10
+ payload?: any;
11
+ }
12
+
13
+ const LOG_LEVELS: LogEntry['level'][] = ['info', 'warn', 'error'];
14
+
15
+ export function useLogger() {
16
+ const [logs, setLogs] = useState<LogEntry[]>([]);
17
+ const [isVisible, setIsVisible] = useState(false);
18
+ const logsRef = useRef<LogEntry[]>([]);
19
+ const [isDevelopment, setIsDevelopment] = useState(false);
20
+
21
+ useEffect(() => {
22
+ setIsDevelopment(
23
+ process.env.NODE_ENV === 'development' ||
24
+ window.location.hostname === 'localhost' ||
25
+ window.location.hostname === '127.0.0.1'
26
+ );
27
+ }, []);
28
+
29
+ useEffect(() => {
30
+ logsRef.current = logs;
31
+ }, [logs]);
32
+
33
+ const addLog = useCallback(
34
+ (level: LogEntry['level'], message: string, payload?: any) => {
35
+ const newLog: LogEntry = {
36
+ id: Math.random().toString(36).substring(2, 9),
37
+ level,
38
+ message,
39
+ timestamp: new Date(),
40
+ payload
41
+ };
42
+
43
+ setLogs((prevLogs) => [newLog, ...prevLogs]);
44
+
45
+ return newLog.id;
46
+ },
47
+ []
48
+ );
49
+
50
+ const clearLogs = useCallback(() => {
51
+ setLogs([]);
52
+ }, []);
53
+
54
+ const toggleVisibility = useCallback(() => {
55
+ setIsVisible((prev) => !prev);
56
+ }, []);
57
+
58
+ const createLogMethod = useCallback(
59
+ (level: LogEntry['level']) => (message: string, payload?: any) =>
60
+ addLog(level, message, payload),
61
+ [addLog]
62
+ );
63
+
64
+ const logMethods = useMemo(
65
+ () =>
66
+ LOG_LEVELS.reduce((methods, level) => {
67
+ methods[level] = createLogMethod(level);
68
+ return methods;
69
+ }, {} as Record<LogEntry['level'], (message: string, payload?: any) => string>),
70
+ [createLogMethod]
71
+ );
72
+
73
+ const hasError = useMemo(
74
+ () => logs.some((log) => log.level === 'error'),
75
+ [logs]
76
+ );
77
+ const hasWarning = useMemo(
78
+ () => logs.some((log) => log.level === 'warn'),
79
+ [logs]
80
+ );
81
+
82
+ return {
83
+ logs,
84
+ isVisible,
85
+ toggleVisibility,
86
+ clearLogs,
87
+ ...logMethods,
88
+ isDevelopment,
89
+ hasError,
90
+ hasWarning
91
+ };
92
+ }
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.124.0-rc.0",
4
+ "version": "1.124.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -35,7 +35,7 @@
35
35
  "set-cookie-parser": "2.6.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@akinon/eslint-plugin-projectzero": "1.124.0-rc.0",
38
+ "@akinon/eslint-plugin-projectzero": "1.124.0",
39
39
  "@babel/core": "7.26.10",
40
40
  "@babel/preset-env": "7.26.9",
41
41
  "@babel/preset-typescript": "7.27.0",
package/plugins.d.ts CHANGED
@@ -37,16 +37,6 @@ declare module '@akinon/pz-cybersource-uc/src/redux/middleware' {
37
37
  export default middleware as any;
38
38
  }
39
39
 
40
- declare module '@akinon/pz-apple-pay' {}
41
-
42
- declare module '@akinon/pz-similar-products' {
43
- export const SimilarProductsModal: any;
44
- export const SimilarProductsFilterSidebar: any;
45
- export const SimilarProductsResultsGrid: any;
46
- export const SimilarProductsPlugin: any;
47
- export const SimilarProductsButtonPlugin: any;
48
- }
49
-
50
40
  declare module '@akinon/pz-cybersource-uc/src/redux/reducer' {
51
41
  export default reducer as any;
52
42
  }
package/plugins.js CHANGED
@@ -16,7 +16,6 @@ module.exports = [
16
16
  'pz-tabby-extension',
17
17
  'pz-apple-pay',
18
18
  'pz-tamara-extension',
19
- 'pz-similar-products',
20
19
  'pz-cybersource-uc',
21
20
  'pz-hepsipay',
22
21
  'pz-flow-payment',
@@ -209,25 +209,15 @@ export const contextListMiddleware: Middleware = ({
209
209
  (ctx) => ctx.page_name === 'DeliveryOptionSelectionPage'
210
210
  )
211
211
  ) {
212
- const isCreditCardPayment =
213
- preOrder?.payment_option?.payment_type === 'credit_card' ||
214
- preOrder?.payment_option?.payment_type === 'masterpass';
215
-
216
212
  if (context.page_context.card_type) {
217
213
  dispatch(setCardType(context.page_context.card_type));
218
- } else if (isCreditCardPayment) {
219
- dispatch(setCardType(null));
220
214
  }
221
215
 
222
216
  if (
223
217
  context.page_context.installments &&
224
218
  preOrder?.payment_option?.payment_type !== 'masterpass_rest'
225
219
  ) {
226
- if (!isCreditCardPayment || context.page_context.card_type) {
227
- dispatch(
228
- setInstallmentOptions(context.page_context.installments)
229
- );
230
- }
220
+ dispatch(setInstallmentOptions(context.page_context.installments));
231
221
  }
232
222
  }
233
223
 
@@ -14,17 +14,9 @@ export const installmentOptionMiddleware: Middleware = ({
14
14
  return result;
15
15
  }
16
16
 
17
- const { installmentOptions, cardType } = getState().checkout;
17
+ const { installmentOptions } = getState().checkout;
18
18
  const { endpoints: apiEndpoints } = checkoutApi;
19
19
 
20
- const isCreditCardPayment =
21
- preOrder?.payment_option?.payment_type === 'credit_card' ||
22
- preOrder?.payment_option?.payment_type === 'masterpass';
23
-
24
- if (isCreditCardPayment && !cardType) {
25
- return result;
26
- }
27
-
28
20
  if (
29
21
  !preOrder?.installment &&
30
22
  preOrder?.payment_option?.payment_type !== 'saved_card' &&
package/types/index.ts CHANGED
@@ -83,12 +83,6 @@ export interface Settings {
83
83
  };
84
84
  usePrettyUrlRoute?: boolean;
85
85
  commerceUrl: string;
86
- /**
87
- * This option allows you to track Sentry events on the client side, in addition to server and edge environments.
88
- *
89
- * It overrides process.env.NEXT_PUBLIC_SENTRY_DSN and process.env.SENTRY_DSN.
90
- */
91
- sentryDsn?: string;
92
86
  redis: {
93
87
  defaultExpirationTime: number;
94
88
  };
@@ -2,7 +2,6 @@ import Settings from 'settings';
2
2
  import logger from '../utils/log';
3
3
  import { headers, cookies } from 'next/headers';
4
4
  import { ServerVariables } from './server-variables';
5
- import { notFound } from 'next/navigation';
6
5
 
7
6
  export enum FetchResponseType {
8
7
  JSON = 'json',
@@ -61,15 +60,13 @@ const appFetch = async <T>({
61
60
  status = req.status;
62
61
  logger.debug(`FETCH END ${url}`, { status: req.status, ip });
63
62
 
64
- if (req.ok) {
65
- if (responseType === FetchResponseType.JSON) {
66
- response = (await req.json()) as T;
67
- } else {
68
- response = (await req.text()) as unknown as T;
69
- }
70
-
71
- logger.trace(`FETCH RESPONSE`, { url, response, ip });
63
+ if (responseType === FetchResponseType.JSON) {
64
+ response = (await req.json()) as T;
65
+ } else {
66
+ response = (await req.text()) as unknown as T;
72
67
  }
68
+
69
+ logger.trace(`FETCH RESPONSE`, { url, response, ip });
73
70
  } catch (error) {
74
71
  const logType = status === 500 ? 'fatal' : 'error';
75
72
 
@@ -78,10 +75,6 @@ const appFetch = async <T>({
78
75
  }
79
76
  }
80
77
 
81
- if (status === 422) {
82
- notFound();
83
- }
84
-
85
78
  return response;
86
79
  };
87
80