@akinon/next 1.17.1 → 1.19.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.
@@ -0,0 +1,136 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useMemo, useReducer } from 'react';
4
+ import { usePathname, useSearchParams } from 'next/navigation';
5
+
6
+ export type UsePaginationType = ReturnType<typeof usePagination>;
7
+
8
+ type InitialState = {
9
+ page: number;
10
+ last: number;
11
+ limit: number;
12
+ total: number;
13
+ };
14
+
15
+ type ACTIONTYPE =
16
+ | { type: 'setPage'; payload: number }
17
+ | { type: 'setTotal'; payload: number }
18
+ | { type: 'setLimit'; payload: number };
19
+
20
+ function reducer(state: InitialState, action: ACTIONTYPE) {
21
+ switch (action.type) {
22
+ case 'setTotal':
23
+ return {
24
+ ...state,
25
+ total: action.payload,
26
+ last: Math.ceil(action.payload / state.limit)
27
+ };
28
+ case 'setPage':
29
+ return { ...state, page: action.payload };
30
+ case 'setLimit':
31
+ return { ...state, limit: action.payload };
32
+ default:
33
+ throw new Error();
34
+ }
35
+ }
36
+
37
+ export default function usePagination(
38
+ _total = 0,
39
+ _limit = 12,
40
+ _page: number | undefined,
41
+ _last: number | undefined
42
+ ) {
43
+ const pathname = usePathname();
44
+ const searchParams = useSearchParams();
45
+ const urlSearchParams = useMemo(
46
+ () => new URLSearchParams(searchParams.toString()),
47
+ [searchParams]
48
+ );
49
+ const { page, limit } = useMemo(
50
+ () => ({
51
+ page: _page || Number(searchParams.get('page')) || 1,
52
+ limit: _limit || Number(searchParams.get('limit'))
53
+ }),
54
+ [searchParams, _page, _limit]
55
+ );
56
+
57
+ const initialState: InitialState = {
58
+ page,
59
+ limit,
60
+ last: _last || Math.ceil(_total / limit) || 1,
61
+ total: _total
62
+ };
63
+ const [state, dispatch] = useReducer(reducer, initialState);
64
+
65
+ useEffect(() => {
66
+ dispatch({ type: 'setPage', payload: page });
67
+ }, [page]);
68
+
69
+ useEffect(() => {
70
+ dispatch({ type: 'setLimit', payload: limit });
71
+ }, [limit]);
72
+
73
+ useEffect(() => {
74
+ window.scrollTo(0, 0);
75
+ }, [state.page, state.limit]);
76
+
77
+ const setTotal = useCallback(
78
+ (total: number) => {
79
+ dispatch({ type: 'setTotal', payload: total });
80
+ },
81
+ [dispatch]
82
+ );
83
+
84
+ const setPage = useCallback(
85
+ (page: number) => {
86
+ if (page > 0 && page <= state.total) {
87
+ dispatch({ type: 'setPage', payload: page });
88
+ }
89
+ },
90
+ [dispatch, state.total]
91
+ );
92
+
93
+ const setLimit = useCallback(
94
+ (limit: number) => {
95
+ dispatch({ type: 'setLimit', payload: limit });
96
+ },
97
+ [dispatch]
98
+ );
99
+
100
+ const pageList = useMemo(() => {
101
+ return Array.from({ length: state.last }, (_, i) => {
102
+ urlSearchParams.set('page', (i + 1).toString());
103
+
104
+ return {
105
+ page: i + 1,
106
+ url: `${pathname}?${urlSearchParams.toString()}`
107
+ };
108
+ });
109
+ }, [state.last, pathname, urlSearchParams]);
110
+
111
+ const prev = useMemo(() => {
112
+ if (state.page > 1) {
113
+ urlSearchParams.set('page', (Number(state.page) - 1).toString());
114
+ return `${pathname}?${urlSearchParams.toString()}`;
115
+ }
116
+ return null;
117
+ }, [state.page, pathname, urlSearchParams]);
118
+
119
+ const next = useMemo(() => {
120
+ if (Number(state.page) < Number(state.last)) {
121
+ urlSearchParams.set('page', (Number(state.page) + 1).toString());
122
+ return `${pathname}?${urlSearchParams.toString()}`;
123
+ }
124
+ return null;
125
+ }, [state.page, state.last, pathname, urlSearchParams]);
126
+
127
+ return {
128
+ ...state,
129
+ setTotal,
130
+ setPage,
131
+ setLimit,
132
+ pageList,
133
+ prev,
134
+ next
135
+ };
136
+ }
@@ -18,6 +18,7 @@ export const usePaymentOptions = () => {
18
18
  pay_on_delivery: 'pz-pay-on-delivery',
19
19
  bkm_express: 'pz-bkm',
20
20
  credit_payment: 'pz-credit-payment',
21
+ masterpass: 'pz-masterpass'
21
22
  };
22
23
 
23
24
  const isInitialTypeIncluded = (type: string) => initialTypes.has(type);
@@ -0,0 +1,159 @@
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: 'complete-masterpass',
22
+ error
23
+ });
24
+ }
25
+
26
+ return result;
27
+ }
28
+ return null;
29
+ };
30
+
31
+ const withCompleteMasterpass =
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
+
37
+ if (url.search.indexOf('MasterpassCompletePage') === -1) {
38
+ return middleware(req, event);
39
+ }
40
+
41
+ const requestUrl = `${Settings.commerceUrl}/orders/checkout/${url.search}`;
42
+ const requestHeaders = {
43
+ 'X-Requested-With': 'XMLHttpRequest',
44
+ 'Content-Type': 'application/x-www-form-urlencoded',
45
+ Cookie: `osessionid=${req.cookies.get('osessionid')?.value ?? ''}`,
46
+ 'x-currency': req.cookies.get('pz-currency')?.value ?? ''
47
+ };
48
+
49
+ try {
50
+ const body = await streamToString(req.body);
51
+
52
+ const request = await fetch(requestUrl, {
53
+ method: 'POST',
54
+ headers: requestHeaders,
55
+ body
56
+ });
57
+
58
+ logger.info('Complete Masterpass payment request', {
59
+ requestUrl,
60
+ status: request.status,
61
+ requestHeaders,
62
+ ip
63
+ });
64
+
65
+ const response = await request.json();
66
+
67
+ const { context_list: contextList, errors } = response;
68
+ const redirectionContext = contextList?.find(
69
+ (context) => context.page_context?.redirect_url
70
+ );
71
+ const redirectUrl = redirectionContext?.page_context?.redirect_url;
72
+
73
+ if (errors && Object.keys(errors).length) {
74
+ logger.error('Error while completing Masterpass payment', {
75
+ middleware: 'complete-masterpass',
76
+ errors,
77
+ requestHeaders,
78
+ ip
79
+ });
80
+
81
+ return NextResponse.redirect(
82
+ `${url.origin}${getUrlPathWithLocale(
83
+ '/orders/checkout/',
84
+ req.cookies.get('pz-locale')?.value
85
+ )}`,
86
+ {
87
+ status: 303,
88
+ headers: {
89
+ 'Set-Cookie': `pz-pos-error=${JSON.stringify(errors)}; path=/;`
90
+ }
91
+ }
92
+ );
93
+ }
94
+
95
+ logger.info('Order success page context list', {
96
+ middleware: 'complete-masterpass',
97
+ contextList,
98
+ ip
99
+ });
100
+
101
+ if (!redirectUrl) {
102
+ logger.warn(
103
+ 'No redirection url for order success page found in page_context. Redirecting to checkout page.',
104
+ {
105
+ middleware: 'complete-masterpass',
106
+ requestHeaders,
107
+ response: JSON.stringify(response),
108
+ ip
109
+ }
110
+ );
111
+
112
+ const redirectUrlWithLocale = `${url.origin}${getUrlPathWithLocale(
113
+ '/orders/checkout/',
114
+ req.cookies.get('pz-locale')?.value
115
+ )}`;
116
+
117
+ return NextResponse.redirect(redirectUrlWithLocale, 303);
118
+ }
119
+
120
+ const redirectUrlWithLocale = `${url.origin}${getUrlPathWithLocale(
121
+ redirectUrl,
122
+ req.cookies.get('pz-locale')?.value
123
+ )}`;
124
+
125
+ logger.info('Redirecting to order success page', {
126
+ middleware: 'complete-masterpass',
127
+ redirectUrlWithLocale,
128
+ ip
129
+ });
130
+
131
+ // Using POST method while redirecting causes an error,
132
+ // So we use 303 status code to change the method to GET
133
+ const nextResponse = NextResponse.redirect(redirectUrlWithLocale, 303);
134
+
135
+ nextResponse.headers.set(
136
+ 'Set-Cookie',
137
+ request.headers.get('set-cookie') ?? ''
138
+ );
139
+
140
+ return nextResponse;
141
+ } catch (error) {
142
+ logger.error('Error while completing Masterpass payment', {
143
+ middleware: 'complete-masterpass',
144
+ error,
145
+ requestHeaders,
146
+ ip
147
+ });
148
+
149
+ return NextResponse.redirect(
150
+ `${url.origin}${getUrlPathWithLocale(
151
+ '/orders/checkout/',
152
+ req.cookies.get('pz-locale')?.value
153
+ )}`,
154
+ 303
155
+ );
156
+ }
157
+ };
158
+
159
+ export default withCompleteMasterpass;
@@ -3,6 +3,7 @@ import Settings from 'settings';
3
3
  import {
4
4
  PzNextRequest,
5
5
  withCompleteGpay,
6
+ withCompleteMasterpass,
6
7
  withOauthLogin,
7
8
  withPrettyUrl,
8
9
  withRedirectionPayment,
@@ -124,61 +125,74 @@ const withPzDefault =
124
125
  withThreeDRedirection(
125
126
  withUrlRedirection(
126
127
  withCompleteGpay(
127
- async (req: PzNextRequest, event: NextFetchEvent) => {
128
- let middlewareResult: NextResponse | void =
129
- NextResponse.next();
128
+ withCompleteMasterpass(
129
+ async (req: PzNextRequest, event: NextFetchEvent) => {
130
+ let middlewareResult: NextResponse | void =
131
+ NextResponse.next();
130
132
 
131
- try {
132
- const { locale, prettyUrl, currency } =
133
- req.middlewareParams.rewrites;
134
- const { defaultLocaleValue } = Settings.localization;
135
- const url = req.nextUrl.clone();
136
- const pathnameWithoutLocale = url.pathname.replace(
137
- urlLocaleMatcherRegex,
138
- ''
139
- );
140
-
141
- url.basePath = `/${commerceUrl}`;
142
- url.pathname = `/${
143
- locale.length ? `${locale}/` : ''
144
- }${currency}${prettyUrl ?? pathnameWithoutLocale}`;
145
-
146
- Settings.rewrites.forEach((rewrite) => {
147
- url.pathname = url.pathname.replace(
148
- rewrite.source,
149
- rewrite.destination
133
+ try {
134
+ const { locale, prettyUrl, currency } =
135
+ req.middlewareParams.rewrites;
136
+ const { defaultLocaleValue } = Settings.localization;
137
+ const url = req.nextUrl.clone();
138
+ const pathnameWithoutLocale = url.pathname.replace(
139
+ urlLocaleMatcherRegex,
140
+ ''
150
141
  );
151
- });
152
142
 
153
- middlewareResult = (await middleware(
154
- req,
155
- event
156
- )) as NextResponse | void;
143
+ url.basePath = `/${commerceUrl}`;
144
+ url.pathname = `/${
145
+ locale.length ? `${locale}/` : ''
146
+ }${currency}${prettyUrl ?? pathnameWithoutLocale}`;
157
147
 
158
- // if middleware.ts has a return value for current url
159
- if (middlewareResult instanceof NextResponse) {
160
- // pz-override-response header is used to prevent 404 page for custom responses.
161
- if (
162
- middlewareResult.headers.get(
163
- 'pz-override-response'
164
- ) !== 'true'
165
- ) {
166
- middlewareResult.headers.set(
167
- 'x-middleware-rewrite',
168
- url.href
148
+ Settings.rewrites.forEach((rewrite) => {
149
+ url.pathname = url.pathname.replace(
150
+ rewrite.source,
151
+ rewrite.destination
169
152
  );
170
- }
171
- } else {
172
- // if middleware.ts doesn't have a return value.
173
- // e.g. NextResponse.next() doesn't exist in middleware.ts
153
+ });
174
154
 
175
- middlewareResult = NextResponse.rewrite(url);
176
- }
155
+ middlewareResult = (await middleware(
156
+ req,
157
+ event
158
+ )) as NextResponse | void;
177
159
 
178
- if (!url.pathname.startsWith(`/${currency}/orders`)) {
160
+ // if middleware.ts has a return value for current url
161
+ if (middlewareResult instanceof NextResponse) {
162
+ // pz-override-response header is used to prevent 404 page for custom responses.
163
+ if (
164
+ middlewareResult.headers.get(
165
+ 'pz-override-response'
166
+ ) !== 'true'
167
+ ) {
168
+ middlewareResult.headers.set(
169
+ 'x-middleware-rewrite',
170
+ url.href
171
+ );
172
+ }
173
+ } else {
174
+ // if middleware.ts doesn't have a return value.
175
+ // e.g. NextResponse.next() doesn't exist in middleware.ts
176
+
177
+ middlewareResult = NextResponse.rewrite(url);
178
+ }
179
+
180
+ if (!url.pathname.startsWith(`/${currency}/orders`)) {
181
+ middlewareResult.cookies.set(
182
+ 'pz-locale',
183
+ locale?.length > 0 ? locale : defaultLocaleValue,
184
+ {
185
+ sameSite: 'none',
186
+ secure: true,
187
+ expires: new Date(
188
+ Date.now() + 1000 * 60 * 60 * 24 * 7
189
+ ) // 7 days
190
+ }
191
+ );
192
+ }
179
193
  middlewareResult.cookies.set(
180
- 'pz-locale',
181
- locale?.length > 0 ? locale : defaultLocaleValue,
194
+ 'pz-currency',
195
+ currency,
182
196
  {
183
197
  sameSite: 'none',
184
198
  secure: true,
@@ -187,65 +201,63 @@ const withPzDefault =
187
201
  ) // 7 days
188
202
  }
189
203
  );
190
- }
191
- middlewareResult.cookies.set('pz-currency', currency, {
192
- sameSite: 'none',
193
- secure: true,
194
- expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7) // 7 days
195
- });
196
-
197
- if (
198
- req.cookies.get('pz-locale') &&
199
- req.cookies.get('pz-locale').value !== locale
200
- ) {
201
- logger.debug('Locale changed', {
202
- locale,
203
- oldLocale: req.cookies.get('pz-locale')?.value,
204
- ip
205
- });
206
- }
207
-
208
- middlewareResult.headers.set(
209
- 'pz-url',
210
- req.nextUrl.toString()
211
- );
212
204
 
213
- if (req.cookies.get('pz-set-currency')) {
214
- middlewareResult.cookies.delete('pz-set-currency');
215
- }
205
+ if (
206
+ req.cookies.get('pz-locale') &&
207
+ req.cookies.get('pz-locale').value !== locale
208
+ ) {
209
+ logger.debug('Locale changed', {
210
+ locale,
211
+ oldLocale: req.cookies.get('pz-locale')?.value,
212
+ ip
213
+ });
214
+ }
216
215
 
217
- if (process.env.ACC_APP_VERSION) {
218
216
  middlewareResult.headers.set(
219
- 'acc-app-version',
220
- process.env.ACC_APP_VERSION
217
+ 'pz-url',
218
+ req.nextUrl.toString()
221
219
  );
222
- }
223
220
 
224
- // Set CSRF token if not set
225
- try {
226
- const url = `${Settings.commerceUrl}${user.csrfToken}`;
221
+ if (req.cookies.get('pz-set-currency')) {
222
+ middlewareResult.cookies.delete('pz-set-currency');
223
+ }
227
224
 
228
- if (!req.cookies.get('csrftoken')) {
229
- const { csrf_token } = await (
230
- await fetch(url)
231
- ).json();
232
- middlewareResult.cookies.set('csrftoken', csrf_token);
225
+ if (process.env.ACC_APP_VERSION) {
226
+ middlewareResult.headers.set(
227
+ 'acc-app-version',
228
+ process.env.ACC_APP_VERSION
229
+ );
230
+ }
231
+
232
+ // Set CSRF token if not set
233
+ try {
234
+ const url = `${Settings.commerceUrl}${user.csrfToken}`;
235
+
236
+ if (!req.cookies.get('csrftoken')) {
237
+ const { csrf_token } = await (
238
+ await fetch(url)
239
+ ).json();
240
+ middlewareResult.cookies.set(
241
+ 'csrftoken',
242
+ csrf_token
243
+ );
244
+ }
245
+ } catch (error) {
246
+ logger.error('CSRF Error', {
247
+ error,
248
+ ip
249
+ });
233
250
  }
234
251
  } catch (error) {
235
- logger.error('CSRF Error', {
252
+ logger.error('withPzDefault Error', {
236
253
  error,
237
254
  ip
238
255
  });
239
256
  }
240
- } catch (error) {
241
- logger.error('withPzDefault Error', {
242
- error,
243
- ip
244
- });
245
- }
246
257
 
247
- return middlewareResult;
248
- }
258
+ return middlewareResult;
259
+ }
260
+ )
249
261
  )
250
262
  )
251
263
  )
@@ -6,6 +6,7 @@ import withLocale from './locale';
6
6
  import withOauthLogin from './oauth-login';
7
7
  import withUrlRedirection from './url-redirection';
8
8
  import withCompleteGpay from './complete-gpay';
9
+ import withCompleteMasterpass from './complete-masterpass';
9
10
  import { NextRequest } from 'next/server';
10
11
 
11
12
  export {
@@ -16,7 +17,8 @@ export {
16
17
  withLocale,
17
18
  withOauthLogin,
18
19
  withUrlRedirection,
19
- withCompleteGpay
20
+ withCompleteGpay,
21
+ withCompleteMasterpass
20
22
  };
21
23
 
22
24
  export interface PzNextRequest extends NextRequest {
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.17.1",
4
+ "version": "1.19.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -31,7 +31,7 @@
31
31
  "@typescript-eslint/eslint-plugin": "6.7.4",
32
32
  "@typescript-eslint/parser": "6.7.4",
33
33
  "eslint": "^8.14.0",
34
- "@akinon/eslint-plugin-projectzero": "1.17.1",
34
+ "@akinon/eslint-plugin-projectzero": "1.19.0",
35
35
  "eslint-config-prettier": "8.5.0"
36
36
  }
37
37
  }
package/plugins.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ declare module '@akinon/pz-masterpass' {
2
+ export const masterpassReducer: unknown;
3
+ }
package/plugins.js CHANGED
@@ -8,4 +8,6 @@ module.exports = [
8
8
  'pz-otp',
9
9
  'pz-bkm',
10
10
  'pz-credit-payment',
11
+ 'pz-masterpass',
12
+ 'pz-b2b'
11
13
  ];
@@ -4,12 +4,16 @@ import configReducer from './config';
4
4
  import headerReducer from './header';
5
5
  import { api } from '../../data/client/api';
6
6
 
7
+ // Plugin reducers
8
+ import { masterpassReducer } from '@akinon/pz-masterpass';
9
+
7
10
  const reducers = {
8
11
  [api.reducerPath]: api.reducer,
9
12
  root: rootReducer,
10
13
  checkout: checkoutReducer,
11
14
  config: configReducer,
12
- header: headerReducer
15
+ header: headerReducer,
16
+ masterpass: masterpassReducer
13
17
  };
14
18
 
15
- export default reducers;
19
+ export default reducers;
@@ -117,6 +117,7 @@ export interface CheckoutContext {
117
117
  redirect_url?: string;
118
118
  context_data?: any;
119
119
  balance?: string;
120
+ [key: string]: any;
120
121
  };
121
122
  }
122
123
 
@@ -10,14 +10,14 @@ export interface Product {
10
10
  };
11
11
  attributes_kwargs: any;
12
12
  base_code: string;
13
- basket_offers: {
13
+ basket_offers: Array<{
14
14
  kwargs: { show_benefit_products: boolean };
15
15
  label: string;
16
16
  listing_kwargs: {
17
17
  [key: string]: any;
18
18
  };
19
19
  pk: number;
20
- };
20
+ }>;
21
21
  currency_type: string;
22
22
  data_source: null;
23
23
  extra_attributes: {
@@ -0,0 +1,9 @@
1
+ import settings from 'settings';
2
+
3
+ export const getCurrencyLabel = (currencyCode: string) => {
4
+ const currencyLabel = settings.localization.currencies.find(
5
+ (item) => item.code === currencyCode
6
+ )?.label;
7
+
8
+ return currencyLabel ?? currencyCode;
9
+ };
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import getSymbolFromCurrency from 'currency-symbol-map';
4
+ import { getCurrencyLabel } from './get-currency-label';
4
5
 
5
6
  type GetCurrencyType = {
6
7
  currencyCode: string;
@@ -17,10 +18,11 @@ export const getCurrency = (args: GetCurrencyType) => {
17
18
  useCurrencySpace = true
18
19
  } = args;
19
20
 
21
+ const currencyLabel = getCurrencyLabel(currencyCode);
20
22
  const currencySpace = useCurrencySpace ? ' ' : '';
21
23
  const currency = useCurrencySymbol
22
- ? `${getSymbolFromCurrency(currencyCode)}`
23
- : currencyCode;
24
+ ? `${getSymbolFromCurrency(currencyLabel)}`
25
+ : currencyLabel;
24
26
  const currencyAfterPrice = useCurrencyAfterPrice
25
27
  ? `${currencySpace}${currency}`
26
28
  : `${currency}${currencySpace}`;
package/utils/index.ts CHANGED
@@ -5,6 +5,7 @@ import { CDNOptions, ClientRequestOptions } from '../types';
5
5
  export * from './get-currency';
6
6
  export * from './menu-generator';
7
7
  export * from './generate-commerce-search-params';
8
+ export * from './get-currency-label';
8
9
 
9
10
  export function getCookie(name: string) {
10
11
  if (typeof document === 'undefined') {