@akinon/next 1.92.0-rc.9 → 1.92.0-snapshot-ZERO-3449-20250618101111
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 +33 -1180
- package/api/similar-product-list.ts +63 -0
- package/api/similar-products.ts +109 -0
- package/components/accordion.tsx +5 -20
- package/components/file-input.tsx +3 -65
- package/components/input.tsx +0 -2
- package/components/link.tsx +12 -16
- package/components/modal.tsx +16 -32
- package/components/plugin-module.tsx +3 -13
- package/components/selected-payment-option-view.tsx +0 -11
- package/data/client/similar-products.ts +122 -0
- package/data/urls.ts +5 -1
- package/hocs/server/with-segment-defaults.tsx +2 -5
- package/hooks/index.ts +2 -0
- package/hooks/use-image-cropper.ts +160 -0
- package/hooks/use-similar-products.ts +720 -0
- package/instrumentation/node.ts +13 -15
- package/lib/cache.ts +0 -2
- package/middlewares/complete-gpay.ts +1 -2
- package/middlewares/complete-masterpass.ts +1 -2
- package/middlewares/default.ts +184 -196
- package/middlewares/index.ts +1 -3
- package/middlewares/redirection-payment.ts +1 -2
- package/middlewares/saved-card-redirection.ts +1 -2
- package/middlewares/three-d-redirection.ts +1 -2
- package/middlewares/url-redirection.ts +14 -8
- package/package.json +3 -3
- package/plugins.d.ts +0 -2
- package/plugins.js +1 -3
- package/redux/middlewares/checkout.ts +2 -15
- package/redux/reducers/checkout.ts +1 -9
- package/sentry/index.ts +17 -54
- package/types/commerce/order.ts +0 -1
- package/types/index.ts +73 -26
- package/utils/app-fetch.ts +2 -2
- package/utils/image-validation.ts +303 -0
- package/utils/redirect.ts +3 -5
- package/with-pz-config.js +5 -1
- package/data/server/basket.ts +0 -72
- package/hooks/use-loyalty-availability.ts +0 -21
- package/middlewares/wallet-complete-redirection.ts +0 -179
- package/utils/redirect-ignore.ts +0 -35
|
@@ -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[];
|
|
@@ -61,8 +60,6 @@ export interface CheckoutState {
|
|
|
61
60
|
countryCode: string;
|
|
62
61
|
currencyCode: string;
|
|
63
62
|
version: string;
|
|
64
|
-
public_key: string;
|
|
65
|
-
[key: string]: any;
|
|
66
63
|
};
|
|
67
64
|
detail: {
|
|
68
65
|
label: string;
|
|
@@ -97,7 +94,6 @@ const initialState: CheckoutState = {
|
|
|
97
94
|
shippingOptions: [],
|
|
98
95
|
dataSourceShippingOptions: [],
|
|
99
96
|
paymentOptions: [],
|
|
100
|
-
unavailablePaymentOptions: [],
|
|
101
97
|
creditPaymentOptions: [],
|
|
102
98
|
selectedCreditPaymentPk: null,
|
|
103
99
|
paymentChoices: [],
|
|
@@ -161,9 +157,6 @@ const checkoutSlice = createSlice({
|
|
|
161
157
|
setPaymentOptions(state, { payload }) {
|
|
162
158
|
state.paymentOptions = payload;
|
|
163
159
|
},
|
|
164
|
-
setUnavailablePaymentOptions(state, { payload }) {
|
|
165
|
-
state.unavailablePaymentOptions = payload;
|
|
166
|
-
},
|
|
167
160
|
setPaymentChoices(state, { payload }) {
|
|
168
161
|
state.paymentChoices = payload;
|
|
169
162
|
},
|
|
@@ -225,10 +218,9 @@ export const {
|
|
|
225
218
|
setShippingOptions,
|
|
226
219
|
setDataSourceShippingOptions,
|
|
227
220
|
setPaymentOptions,
|
|
228
|
-
setUnavailablePaymentOptions,
|
|
229
|
-
setPaymentChoices,
|
|
230
221
|
setCreditPaymentOptions,
|
|
231
222
|
setSelectedCreditPaymentPk,
|
|
223
|
+
setPaymentChoices,
|
|
232
224
|
setCardType,
|
|
233
225
|
setInstallmentOptions,
|
|
234
226
|
setBankAccounts,
|
package/sentry/index.ts
CHANGED
|
@@ -13,73 +13,36 @@ 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
|
-
|
|
41
16
|
export const initSentry = (
|
|
42
17
|
type: 'Server' | 'Client' | 'Edge',
|
|
43
18
|
options: Sentry.BrowserOptions | Sentry.NodeOptions | Sentry.EdgeOptions = {}
|
|
44
19
|
) => {
|
|
45
|
-
// TODO:
|
|
20
|
+
// TODO: Handle options with ESLint rules
|
|
46
21
|
|
|
47
|
-
|
|
22
|
+
Sentry.init({
|
|
48
23
|
dsn:
|
|
49
|
-
SENTRY_DSN ||
|
|
50
24
|
options.dsn ||
|
|
25
|
+
SENTRY_DSN ||
|
|
51
26
|
'https://d8558ef8997543deacf376c7d8d7cf4b@o64293.ingest.sentry.io/4504338423742464',
|
|
52
27
|
initialScope: {
|
|
53
28
|
tags: {
|
|
54
29
|
APP_TYPE: 'ProjectZeroNext',
|
|
55
|
-
TYPE: type
|
|
56
|
-
...((options.initialScope as any)?.tags || {})
|
|
30
|
+
TYPE: type
|
|
57
31
|
}
|
|
58
32
|
},
|
|
59
33
|
tracesSampleRate: 0,
|
|
60
|
-
integrations: []
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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;
|
|
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;
|
|
82
43
|
}
|
|
83
|
-
|
|
84
|
-
|
|
44
|
+
|
|
45
|
+
return event;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
85
48
|
};
|
package/types/commerce/order.ts
CHANGED
package/types/index.ts
CHANGED
|
@@ -83,6 +83,12 @@ 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;
|
|
86
92
|
redis: {
|
|
87
93
|
defaultExpirationTime: number;
|
|
88
94
|
};
|
|
@@ -283,13 +289,7 @@ export interface ButtonProps
|
|
|
283
289
|
target?: '_blank' | '_self' | '_parent' | '_top';
|
|
284
290
|
}
|
|
285
291
|
|
|
286
|
-
export
|
|
287
|
-
fileClassName?: string;
|
|
288
|
-
fileNameWrapperClassName?: string;
|
|
289
|
-
fileInputClassName?: string;
|
|
290
|
-
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
291
|
-
buttonClassName?: string;
|
|
292
|
-
}
|
|
292
|
+
export type FileInputProps = React.HTMLProps<HTMLInputElement>;
|
|
293
293
|
|
|
294
294
|
export interface PriceProps {
|
|
295
295
|
currencyCode?: string;
|
|
@@ -310,19 +310,15 @@ export interface InputProps extends React.HTMLProps<HTMLInputElement> {
|
|
|
310
310
|
|
|
311
311
|
export interface AccordionProps {
|
|
312
312
|
isCollapse?: boolean;
|
|
313
|
-
collapseClassName?: string;
|
|
314
313
|
title?: string;
|
|
315
314
|
subTitle?: string;
|
|
316
315
|
icons?: string[];
|
|
317
316
|
iconSize?: number;
|
|
318
317
|
iconColor?: string;
|
|
319
318
|
children?: ReactNode;
|
|
320
|
-
headerClassName?: string;
|
|
321
319
|
className?: string;
|
|
322
320
|
titleClassName?: string;
|
|
323
|
-
subTitleClassName?: string;
|
|
324
321
|
dataTestId?: string;
|
|
325
|
-
contentClassName?: string;
|
|
326
322
|
}
|
|
327
323
|
|
|
328
324
|
export interface PluginModuleComponentProps {
|
|
@@ -348,19 +344,70 @@ export interface PaginationProps {
|
|
|
348
344
|
isLoading?: boolean;
|
|
349
345
|
}
|
|
350
346
|
|
|
351
|
-
export interface
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
347
|
+
export interface FilterSidebarProps {
|
|
348
|
+
isFilterMenuOpen: boolean;
|
|
349
|
+
setIsFilterMenuOpen: (open: boolean) => void;
|
|
350
|
+
searchResults: any;
|
|
351
|
+
isLoading: boolean;
|
|
352
|
+
handleFacetChange: (facetKey: string, choiceValue: string | number) => void;
|
|
353
|
+
removeFacetFilter: (facetKey: string, choiceValue: string | number) => void;
|
|
354
|
+
currentImageUrl: string;
|
|
355
|
+
isCropping: boolean;
|
|
356
|
+
imageRef: React.RefObject<HTMLImageElement>;
|
|
357
|
+
fileInputRef: React.RefObject<HTMLInputElement>;
|
|
358
|
+
crop: any;
|
|
359
|
+
setCrop: (crop: any) => void;
|
|
360
|
+
completedCrop: any;
|
|
361
|
+
setCompletedCrop: (crop: any) => void;
|
|
362
|
+
handleCropComplete: any;
|
|
363
|
+
toggleCropMode: () => void;
|
|
364
|
+
processCompletedCrop: (crop: any) => void;
|
|
365
|
+
resetCrop: () => void;
|
|
366
|
+
product: any;
|
|
367
|
+
activeIndex: number;
|
|
368
|
+
hasUploadedImage: boolean;
|
|
369
|
+
handleFileUpload: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
370
|
+
handleResetToOriginal: () => void;
|
|
371
|
+
fileError: string;
|
|
372
|
+
showResetButton?: boolean;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export interface SimilarProductsModalProps {
|
|
376
|
+
isOpen: boolean;
|
|
377
|
+
onClose: () => void;
|
|
378
|
+
searchResults: any;
|
|
379
|
+
resultsKey: number;
|
|
380
|
+
isLoading: boolean;
|
|
381
|
+
isFilterMenuOpen: boolean;
|
|
382
|
+
setIsFilterMenuOpen: (open: boolean) => void;
|
|
383
|
+
handleSortChange: (value: string) => void;
|
|
384
|
+
handlePageChange: (page: number, sortValue?: string) => void;
|
|
385
|
+
handleFacetChange: (facetKey: string, choiceValue: string | number) => void;
|
|
386
|
+
removeFacetFilter: (facetKey: string, choiceValue: string | number) => void;
|
|
387
|
+
currentImageUrl: string;
|
|
388
|
+
isCropping: boolean;
|
|
389
|
+
imageRef: React.RefObject<HTMLImageElement>;
|
|
390
|
+
fileInputRef: React.RefObject<HTMLInputElement>;
|
|
391
|
+
crop: any;
|
|
392
|
+
setCrop: (crop: any) => void;
|
|
393
|
+
completedCrop: any;
|
|
394
|
+
setCompletedCrop: (crop: any) => void;
|
|
395
|
+
handleCropComplete: any;
|
|
396
|
+
toggleCropMode: () => void;
|
|
397
|
+
processCompletedCrop: (crop: any) => void;
|
|
398
|
+
resetCrop: () => void;
|
|
399
|
+
product: any;
|
|
400
|
+
activeIndex: number;
|
|
401
|
+
hasUploadedImage: boolean;
|
|
402
|
+
handleFileUpload: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
403
|
+
handleResetToOriginal: () => void;
|
|
404
|
+
fileError: string;
|
|
405
|
+
showResetButton?: boolean;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export interface ResultsGridProps {
|
|
409
|
+
searchResults: any;
|
|
410
|
+
resultsKey: number;
|
|
411
|
+
isLoading: boolean;
|
|
412
|
+
handlePageChange: (page: number, sortValue?: string) => void;
|
|
366
413
|
}
|
package/utils/app-fetch.ts
CHANGED
|
@@ -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,303 @@
|
|
|
1
|
+
export interface ImageValidationResult {
|
|
2
|
+
isValid: boolean;
|
|
3
|
+
error?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const IMAGE_SIGNATURES = {
|
|
7
|
+
jpeg: [
|
|
8
|
+
{ bytes: 'ffd8', minLength: 2, description: 'JPEG (Generic)' },
|
|
9
|
+
{ bytes: 'ffd8ffe0', minLength: 4, description: 'JPEG/JFIF' },
|
|
10
|
+
{ bytes: 'ffd8ffe1', minLength: 4, description: 'JPEG/EXIF' },
|
|
11
|
+
{ bytes: 'ffd8ffe2', minLength: 4, description: 'JPEG/Canon' },
|
|
12
|
+
{ bytes: 'ffd8ffe3', minLength: 4, description: 'JPEG/Samsung' },
|
|
13
|
+
{ bytes: 'ffd8ffe8', minLength: 4, description: 'JPEG/SPIFF' },
|
|
14
|
+
{ bytes: 'ffd8ffed', minLength: 4, description: 'JPEG/Photoshop' }
|
|
15
|
+
],
|
|
16
|
+
|
|
17
|
+
png: [{ bytes: '89504e470d0a1a0a', minLength: 8, description: 'PNG' }],
|
|
18
|
+
|
|
19
|
+
gif: [
|
|
20
|
+
{ bytes: '474946383761', minLength: 6, description: 'GIF87a' },
|
|
21
|
+
{ bytes: '474946383961', minLength: 6, description: 'GIF89a' }
|
|
22
|
+
],
|
|
23
|
+
|
|
24
|
+
webp: {
|
|
25
|
+
riff: '52494646',
|
|
26
|
+
webp: '57454250',
|
|
27
|
+
minLength: 12,
|
|
28
|
+
description: 'WebP'
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
avif: {
|
|
32
|
+
ftyp: '66747970',
|
|
33
|
+
avif: '61766966',
|
|
34
|
+
minLength: 12,
|
|
35
|
+
description: 'AVIF'
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
heic: [
|
|
39
|
+
{
|
|
40
|
+
ftyp: '66747970',
|
|
41
|
+
brand: '68656963',
|
|
42
|
+
minLength: 12,
|
|
43
|
+
description: 'HEIC'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
ftyp: '66747970',
|
|
47
|
+
brand: '6d696631',
|
|
48
|
+
minLength: 12,
|
|
49
|
+
description: 'HEIF'
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
|
|
53
|
+
bmp: [{ bytes: '424d', minLength: 2, description: 'BMP' }],
|
|
54
|
+
|
|
55
|
+
tiff: [
|
|
56
|
+
{ bytes: '49492a00', minLength: 4, description: 'TIFF (Little Endian)' },
|
|
57
|
+
{ bytes: '4d4d002a', minLength: 4, description: 'TIFF (Big Endian)' }
|
|
58
|
+
]
|
|
59
|
+
} as const;
|
|
60
|
+
|
|
61
|
+
const SUPPORTED_MIME_TYPES = {
|
|
62
|
+
'image/jpeg': 'jpeg',
|
|
63
|
+
'image/jpg': 'jpeg',
|
|
64
|
+
'image/png': 'png',
|
|
65
|
+
'image/gif': 'gif',
|
|
66
|
+
'image/webp': 'webp',
|
|
67
|
+
'image/avif': 'avif',
|
|
68
|
+
'image/heic': 'heic',
|
|
69
|
+
'image/heif': 'heic',
|
|
70
|
+
'image/bmp': 'bmp',
|
|
71
|
+
'image/tiff': 'tiff',
|
|
72
|
+
'image/tif': 'tiff'
|
|
73
|
+
} as const;
|
|
74
|
+
|
|
75
|
+
const FORMAT_NAMES = {
|
|
76
|
+
jpeg: 'JPEG',
|
|
77
|
+
png: 'PNG',
|
|
78
|
+
gif: 'GIF',
|
|
79
|
+
webp: 'WebP',
|
|
80
|
+
avif: 'AVIF',
|
|
81
|
+
heic: 'HEIC/HEIF',
|
|
82
|
+
bmp: 'BMP',
|
|
83
|
+
tiff: 'TIFF'
|
|
84
|
+
} as const;
|
|
85
|
+
|
|
86
|
+
export const isValidImage = (file: File): Promise<boolean> => {
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
if (!(file.type in SUPPORTED_MIME_TYPES)) {
|
|
89
|
+
resolve(false);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const img = new Image();
|
|
94
|
+
img.onload = () => resolve(img.width > 0 && img.height > 0);
|
|
95
|
+
img.onerror = () => resolve(false);
|
|
96
|
+
img.src = URL.createObjectURL(file);
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const checkImageSignature = (file: File): Promise<boolean> => {
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
const formatKey =
|
|
103
|
+
SUPPORTED_MIME_TYPES[file.type as keyof typeof SUPPORTED_MIME_TYPES];
|
|
104
|
+
if (!formatKey) {
|
|
105
|
+
resolve(false);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const reader = new FileReader();
|
|
110
|
+
reader.onloadend = (e) => {
|
|
111
|
+
const result = e.target?.result as ArrayBuffer;
|
|
112
|
+
const arr = new Uint8Array(result);
|
|
113
|
+
const header = Array.from(arr)
|
|
114
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
115
|
+
.join('');
|
|
116
|
+
|
|
117
|
+
let isValid = false;
|
|
118
|
+
|
|
119
|
+
switch (formatKey) {
|
|
120
|
+
case 'jpeg':
|
|
121
|
+
case 'png':
|
|
122
|
+
case 'gif':
|
|
123
|
+
case 'bmp':
|
|
124
|
+
case 'tiff':
|
|
125
|
+
const signatures = IMAGE_SIGNATURES[formatKey];
|
|
126
|
+
isValid = signatures.some((sig) => header.startsWith(sig.bytes));
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case 'webp':
|
|
130
|
+
if (header.length >= 24) {
|
|
131
|
+
const hasRiff = header.startsWith(IMAGE_SIGNATURES.webp.riff);
|
|
132
|
+
const hasWebp = header.substr(16, 8) === IMAGE_SIGNATURES.webp.webp;
|
|
133
|
+
isValid = hasRiff && hasWebp;
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'avif':
|
|
138
|
+
if (header.length >= 24) {
|
|
139
|
+
const hasFtyp = header.substr(8, 8) === IMAGE_SIGNATURES.avif.ftyp;
|
|
140
|
+
const hasAvif = header.indexOf(IMAGE_SIGNATURES.avif.avif) !== -1;
|
|
141
|
+
isValid = hasFtyp && hasAvif;
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case 'heic':
|
|
146
|
+
if (header.length >= 24) {
|
|
147
|
+
const hasFtyp =
|
|
148
|
+
header.substr(8, 8) === IMAGE_SIGNATURES.heic[0].ftyp;
|
|
149
|
+
const hasHeic = IMAGE_SIGNATURES.heic.some(
|
|
150
|
+
(variant) => header.indexOf(variant.brand) !== -1
|
|
151
|
+
);
|
|
152
|
+
isValid = hasFtyp && hasHeic;
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
resolve(isValid);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
reader.onerror = () => resolve(false);
|
|
161
|
+
|
|
162
|
+
const bytesToRead = ['webp', 'avif', 'heic'].includes(formatKey) ? 24 : 8;
|
|
163
|
+
reader.readAsArrayBuffer(file.slice(0, bytesToRead));
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const validateImageMetadata = (dataUrl: string): Promise<boolean> => {
|
|
168
|
+
return new Promise((resolve) => {
|
|
169
|
+
const img = new Image();
|
|
170
|
+
img.onload = () => {
|
|
171
|
+
if (img.width > 10000 || img.height > 10000) {
|
|
172
|
+
resolve(false);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const ratio = img.width / img.height;
|
|
177
|
+
if (ratio > 5 || ratio < 0.2) {
|
|
178
|
+
resolve(false);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
resolve(true);
|
|
183
|
+
};
|
|
184
|
+
img.onerror = () => resolve(false);
|
|
185
|
+
img.src = dataUrl;
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const validateImageFile = async (
|
|
190
|
+
file: File,
|
|
191
|
+
maxSizeInMB: number = 5
|
|
192
|
+
): Promise<ImageValidationResult> => {
|
|
193
|
+
const supportedFormats = Object.values(FORMAT_NAMES);
|
|
194
|
+
const supportedTypes = Object.keys(SUPPORTED_MIME_TYPES);
|
|
195
|
+
|
|
196
|
+
if (file.size > maxSizeInMB * 1024 * 1024) {
|
|
197
|
+
return {
|
|
198
|
+
isValid: false,
|
|
199
|
+
error: `File size should be less than ${maxSizeInMB}MB. Supported formats: ${supportedFormats.join(
|
|
200
|
+
', '
|
|
201
|
+
)}`
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!supportedTypes.includes(file.type)) {
|
|
206
|
+
return {
|
|
207
|
+
isValid: false,
|
|
208
|
+
error: `Unsupported file type. Please use: ${supportedFormats.join(', ')}`
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const isImage = await isValidImage(file);
|
|
213
|
+
if (!isImage) {
|
|
214
|
+
return {
|
|
215
|
+
isValid: false,
|
|
216
|
+
error: `The selected file is not a valid image. Supported formats: ${supportedFormats.join(
|
|
217
|
+
', '
|
|
218
|
+
)}`
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const validSignature = await checkImageSignature(file);
|
|
223
|
+
if (!validSignature) {
|
|
224
|
+
return {
|
|
225
|
+
isValid: false,
|
|
226
|
+
error: `Invalid image format detected. Supported formats: ${supportedFormats.join(
|
|
227
|
+
', '
|
|
228
|
+
)}`
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { isValid: true };
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export const validateImageFromDataUrl = async (
|
|
236
|
+
dataUrl: string
|
|
237
|
+
): Promise<ImageValidationResult> => {
|
|
238
|
+
const validMetadata = await validateImageMetadata(dataUrl);
|
|
239
|
+
if (!validMetadata) {
|
|
240
|
+
return {
|
|
241
|
+
isValid: false,
|
|
242
|
+
error: 'Invalid image properties detected (dimensions or aspect ratio)'
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { isValid: true };
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
export const debugImageSignature = async (
|
|
250
|
+
file: File
|
|
251
|
+
): Promise<{
|
|
252
|
+
mimeType: string;
|
|
253
|
+
format: string;
|
|
254
|
+
expectedSignatures: string[];
|
|
255
|
+
actualHeader: string;
|
|
256
|
+
isValid: boolean;
|
|
257
|
+
}> => {
|
|
258
|
+
const formatKey =
|
|
259
|
+
SUPPORTED_MIME_TYPES[file.type as keyof typeof SUPPORTED_MIME_TYPES];
|
|
260
|
+
|
|
261
|
+
return new Promise((resolve) => {
|
|
262
|
+
const reader = new FileReader();
|
|
263
|
+
reader.onloadend = (e) => {
|
|
264
|
+
const result = e.target?.result as ArrayBuffer;
|
|
265
|
+
const arr = new Uint8Array(result);
|
|
266
|
+
const header = Array.from(arr.slice(0, 24))
|
|
267
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
268
|
+
.join('');
|
|
269
|
+
|
|
270
|
+
let expectedSignatures: string[] = [];
|
|
271
|
+
|
|
272
|
+
if (formatKey && formatKey in IMAGE_SIGNATURES) {
|
|
273
|
+
const sigs = IMAGE_SIGNATURES[formatKey];
|
|
274
|
+
if (Array.isArray(sigs)) {
|
|
275
|
+
expectedSignatures = sigs.map((s) => s.bytes);
|
|
276
|
+
} else if ('riff' in sigs) {
|
|
277
|
+
expectedSignatures = [sigs.riff + 'XXXXXXXX' + sigs.webp];
|
|
278
|
+
} else if ('ftyp' in sigs) {
|
|
279
|
+
expectedSignatures = [sigs.ftyp];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
resolve({
|
|
284
|
+
mimeType: file.type,
|
|
285
|
+
format: formatKey || 'unknown',
|
|
286
|
+
expectedSignatures,
|
|
287
|
+
actualHeader: header,
|
|
288
|
+
isValid: formatKey ? true : false
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
reader.onerror = () =>
|
|
293
|
+
resolve({
|
|
294
|
+
mimeType: file.type,
|
|
295
|
+
format: 'error',
|
|
296
|
+
expectedSignatures: [],
|
|
297
|
+
actualHeader: '',
|
|
298
|
+
isValid: false
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
reader.readAsArrayBuffer(file.slice(0, 24));
|
|
302
|
+
});
|
|
303
|
+
};
|
package/utils/redirect.ts
CHANGED
|
@@ -3,23 +3,21 @@ import Settings from 'settings';
|
|
|
3
3
|
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
|
-
import { urlLocaleMatcherRegex } from '@akinon/next/utils';
|
|
7
6
|
|
|
8
7
|
export const redirect = (path: string, type?: RedirectType) => {
|
|
9
8
|
const nextHeaders = headers();
|
|
10
9
|
const pageUrl = new URL(
|
|
11
|
-
nextHeaders.get('pz-url') ?? process.env.NEXT_PUBLIC_URL
|
|
10
|
+
nextHeaders.get('pz-url') ?? process.env.NEXT_PUBLIC_URL
|
|
12
11
|
);
|
|
13
12
|
|
|
14
13
|
const currentLocale = Settings.localization.locales.find(
|
|
15
14
|
(locale) => locale.value === ServerVariables.locale
|
|
16
15
|
);
|
|
17
16
|
|
|
18
|
-
const callbackUrl = pageUrl.pathname
|
|
19
|
-
|
|
17
|
+
const callbackUrl = pageUrl.pathname;
|
|
20
18
|
const redirectUrlWithLocale = getUrlPathWithLocale(
|
|
21
19
|
path,
|
|
22
|
-
currentLocale
|
|
20
|
+
currentLocale.localePath ?? currentLocale.value
|
|
23
21
|
);
|
|
24
22
|
|
|
25
23
|
const redirectUrl = `${redirectUrlWithLocale}?callbackUrl=${callbackUrl}`;
|
package/with-pz-config.js
CHANGED