@akinon/next 1.21.0 → 1.22.0-rc.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,21 @@
1
1
  # @akinon/next
2
2
 
3
+ ## 1.22.0-rc.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 32668ed: ZERO-2296: Implement Checkout Provider Middleware and Enhancements
8
+ - 0181251: ZERO-2440: move otp popup state to redux
9
+ - 07cc81a: Add infinite and more types to pagination
10
+ - 8d6caba: ZERO-2434: enhance error handling and logging in appFetch function
11
+ - 1b4b0fa: Redis connection change to connection pool
12
+ - b4452e9: ZERO-2463: Refactor Sentry initialization and add Sentry DSN option to settings
13
+ - b2da5e4: Revert ZERO-2435
14
+
15
+ ### Patch Changes
16
+
17
+ - da1e501: ZERO-2296: Fix ROUTES import
18
+
3
19
  ## 1.21.0
4
20
 
5
21
  ### Minor Changes
package/api/auth.ts CHANGED
@@ -220,6 +220,17 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
220
220
  pages: {
221
221
  signIn: ROUTES.AUTH,
222
222
  error: ROUTES.AUTH
223
+ },
224
+ cookies: {
225
+ sessionToken: {
226
+ name: `__Secure-next-auth.session-token`,
227
+ options: {
228
+ httpOnly: true,
229
+ sameSite: 'none',
230
+ path: '/',
231
+ secure: true
232
+ }
233
+ }
223
234
  }
224
235
  };
225
236
  };
@@ -1,39 +1,71 @@
1
- #!/usr/bin/env node
2
-
3
1
  const fs = require('fs');
4
2
  const path = require('path');
5
- const rootDir = path.resolve(process.cwd());
6
- const spawn = require('cross-spawn');
7
- const availablePlugins = require('../plugins');
3
+ const { execSync } = require('child_process');
4
+
5
+ function findBaseDir() {
6
+ const insideNodeModules = __dirname.includes('node_modules');
7
+ return insideNodeModules
8
+ ? process.cwd()
9
+ : path.resolve(__dirname, '../../../apps/projectzeronext');
10
+ }
8
11
 
9
- let plugins;
12
+ const BASE_DIR = findBaseDir();
13
+ const getFullPath = (relativePath) => path.join(BASE_DIR, relativePath);
14
+
15
+ const packageJsonPath = getFullPath('package.json');
16
+ let packageJson;
10
17
 
11
18
  try {
12
- plugins = require(path.resolve(rootDir, './src/plugins.js'));
19
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
13
20
  } catch (error) {
14
- console.error('No plugins.js file found, skipping plugin installation.');
15
- process.exit(0);
21
+ console.error('Error reading package.json:', error);
22
+ process.exit(1);
16
23
  }
17
24
 
18
- let installCmd = [];
25
+ const pluginsJsPath = getFullPath('src/plugins.js');
26
+ let installedPlugins;
27
+
28
+ try {
29
+ installedPlugins = require(pluginsJsPath);
30
+ } catch (error) {
31
+ console.error('Error loading installed plugins:', error);
32
+ process.exit(1);
33
+ }
19
34
 
20
- availablePlugins
21
- .filter((p) => plugins?.includes(p))
22
- .forEach((name) => {
23
- installCmd.push(`@akinon/${name}`);
24
- });
35
+ const protectedPackages = ['@akinon/next', 'next'];
25
36
 
26
- spawn.sync('yarn', ['cache clean']);
37
+ const pluginsToRemove = Object.keys(packageJson.dependencies || {}).filter(
38
+ (dep) =>
39
+ dep.startsWith('@akinon/') &&
40
+ !installedPlugins.includes(dep.replace('@akinon/', '')) &&
41
+ !protectedPackages.includes(dep)
42
+ );
27
43
 
28
- for (const plugin of availablePlugins) {
29
- spawn.sync('yarn', ['remove', `@akinon/${plugin}`]);
30
- }
44
+ pluginsToRemove.forEach((plugin) => {
45
+ try {
46
+ execSync(`yarn remove ${plugin}`, { stdio: 'inherit', cwd: BASE_DIR });
47
+ } catch (error) {
48
+ console.warn(
49
+ `Warning: Could not remove ${plugin}. It may not have been installed.`
50
+ );
51
+ }
52
+ });
31
53
 
32
- if (
33
- installCmd.length > 0 &&
34
- !fs.existsSync(path.resolve(rootDir, '../../turbo.json'))
35
- ) {
36
- spawn.sync('yarn', ['add', ...installCmd, '--ignore-scripts'], {
37
- stdio: 'inherit'
38
- });
39
- }
54
+ installedPlugins.forEach((plugin) => {
55
+ const packageName = `@akinon/${plugin}`;
56
+ if (
57
+ !Object.keys(packageJson.dependencies || {}).includes(packageName) &&
58
+ !protectedPackages.includes(packageName)
59
+ ) {
60
+ try {
61
+ const version = packageJson.dependencies['@akinon/next'].replace('^', '');
62
+ const command = `yarn add ${packageName}@${version} --exact --ignore-scripts`;
63
+ execSync(command, { stdio: 'inherit', cwd: BASE_DIR });
64
+ } catch (error) {
65
+ console.warn(
66
+ '\n\x1b[33m%s\x1b[0m',
67
+ `Error adding ${packageName}: ${error}`
68
+ );
69
+ }
70
+ }
71
+ });
@@ -20,6 +20,8 @@ interface GetStockParams {
20
20
  }
21
21
 
22
22
  interface GetFavoritesResponse {
23
+ next?: string | null;
24
+ previous?: string | null;
23
25
  count: number;
24
26
  results: FavoriteItem[];
25
27
  }
@@ -15,7 +15,7 @@ function getCategoryDataHandler(
15
15
  const params = generateCommerceSearchParams(searchParams);
16
16
 
17
17
  const rawData = await appFetch<string>(
18
- `${category.getCategoryByPk(pk)}${params ? params : ''}`,
18
+ `${category.getCategoryByPk(pk)}s${params ? params : ''}`,
19
19
  {
20
20
  headers: {
21
21
  Accept: 'application/json',
@@ -39,7 +39,7 @@ function getCategoryDataHandler(
39
39
  numberValueParser
40
40
  ) as GetCategoryResponse;
41
41
  } catch (error) {
42
- logger.error('Error while parsing category data', {
42
+ logger.fatal('Error while parsing category data', {
43
43
  handler: 'getCategoryDataHandler',
44
44
  error,
45
45
  rawData: rawData.startsWith('<!DOCTYPE html>')
@@ -108,7 +108,7 @@ function getCategoryBySlugDataHandler(slug: string) {
108
108
  numberValueParser
109
109
  ) as GetCategoryResponse;
110
110
  } catch (error) {
111
- logger.error('Error while parsing category data', {
111
+ logger.fatal('Error while parsing category data', {
112
112
  handler: 'getCategoryBySlugDataHandler',
113
113
  error,
114
114
  rawData: rawData.startsWith('<!DOCTYPE html>')
@@ -38,7 +38,7 @@ const getListDataHandler = (
38
38
  numberValueParser
39
39
  ) as GetCategoryResponse;
40
40
  } catch (error) {
41
- logger.error('Error while parsing list data', {
41
+ logger.fatal('Error while parsing list data', {
42
42
  error,
43
43
  rawData: rawData.startsWith('<!DOCTYPE html>')
44
44
  ? `${rawData.substring(0, 50)}...`
@@ -70,10 +70,6 @@ export default function usePagination(
70
70
  dispatch({ type: 'setLimit', payload: limit });
71
71
  }, [limit]);
72
72
 
73
- useEffect(() => {
74
- window.scrollTo(0, 0);
75
- }, [state.page, state.limit]);
76
-
77
73
  const setTotal = useCallback(
78
74
  (total: number) => {
79
75
  dispatch({ type: 'setTotal', payload: total });
package/lib/cache.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { createPool, Pool } from 'generic-pool';
1
2
  import { RedisClientType } from 'redis';
2
3
  import Settings from 'settings';
3
4
  import { CacheOptions } from '../types';
@@ -64,21 +65,38 @@ export class Cache {
64
65
  );
65
66
  }
66
67
 
67
- static async getClient() {
68
- const { createClient } = await import('redis');
69
- const redisUrl = `redis://${process.env.CACHE_HOST}:${
70
- process.env.CACHE_PORT
71
- }/${process.env.CACHE_BUCKET ?? '0'}`;
68
+ static clientPool: Pool<RedisClientType> = createPool(
69
+ {
70
+ create: async () => {
71
+ const { createClient } = await import('redis');
72
+ const redisUrl = `redis://${process.env.CACHE_HOST}:${
73
+ process.env.CACHE_PORT
74
+ }/${process.env.CACHE_BUCKET ?? '0'}`;
75
+
76
+ const client: RedisClientType = createClient({
77
+ url: redisUrl
78
+ });
72
79
 
73
- const client: RedisClientType = createClient({
74
- url: redisUrl
75
- });
80
+ client.on('error', (error) => {
81
+ logger.error('Redis client error', { redisUrl, error });
82
+ });
76
83
 
77
- client.on('error', (error) => {
78
- logger.error('Redis client error', { redisUrl, error });
79
- });
84
+ await client.connect();
85
+
86
+ return client;
87
+ },
88
+ destroy: async (client: RedisClientType) => {
89
+ await client.disconnect();
90
+ }
91
+ },
92
+ {
93
+ max: 500,
94
+ min: 2
95
+ }
96
+ );
80
97
 
81
- return client;
98
+ static async getClient() {
99
+ return await Cache.clientPool.acquire();
82
100
  }
83
101
 
84
102
  static async get(key: string) {
@@ -87,39 +105,49 @@ export class Cache {
87
105
 
88
106
  try {
89
107
  client = await Cache.getClient();
90
- await client.connect();
91
- value = JSON.parse(await client.get(key));
92
-
93
- logger.debug('Redis get success', { key });
94
- logger.trace('Redis get success', { key, value });
108
+ const response = await client.get(key);
109
+ if (response) {
110
+ value = JSON.parse(response);
111
+ } else {
112
+ value = null;
113
+ }
114
+ logger.debug('Redis get success', { key, value });
95
115
  } catch (error) {
96
116
  logger.error('Redis get error', { key, error });
117
+ value = null;
97
118
  } finally {
98
- await client?.disconnect();
119
+ if (client) {
120
+ await Cache.clientPool.release(client);
121
+ }
99
122
  }
100
123
 
101
124
  return value;
102
125
  }
103
126
 
104
- static async set(key: string, value: string, expire?: number) {
127
+ static async set(key: string, value: any, expire?: number) {
105
128
  let success = false;
106
129
  let client;
107
130
 
108
131
  try {
109
132
  client = await Cache.getClient();
110
- await client.connect();
111
- await client.set(key, value, {
112
- EX: expire
113
- });
133
+ const serializedValue =
134
+ typeof value === 'object' ? JSON.stringify(value) : value;
114
135
 
115
- success = true;
136
+ if (expire) {
137
+ await client.set(key, serializedValue, { EX: expire });
138
+ } else {
139
+ await client.set(key, serializedValue);
140
+ }
116
141
 
117
- logger.debug('Redis set success', { key });
118
- logger.trace('Redis set success', { key, value });
142
+ success = true;
143
+ logger.debug('Redis set success', { key, value });
119
144
  } catch (error) {
120
145
  logger.error('Redis set error', { key, error });
146
+ success = false;
121
147
  } finally {
122
- await client?.disconnect();
148
+ if (client) {
149
+ await Cache.clientPool.release(client);
150
+ }
123
151
  }
124
152
 
125
153
  return success;
@@ -0,0 +1,88 @@
1
+ import { NextFetchEvent, NextMiddleware, NextResponse } from 'next/server';
2
+ import settings from 'settings';
3
+ import { PzNextRequest } from '.';
4
+ import logger from '../utils/log';
5
+ import { urlLocaleMatcherRegex } from '../utils';
6
+ import { getUrlPathWithLocale } from '../utils/localization';
7
+
8
+ const withCheckoutProvider =
9
+ (middleware: NextMiddleware) =>
10
+ async (req: PzNextRequest, event: NextFetchEvent) => {
11
+ const url = req.nextUrl.clone();
12
+ const ip = req.headers.get('x-forwarded-for') ?? '';
13
+ const pathnameWithoutLocale = url.pathname.replace(
14
+ urlLocaleMatcherRegex,
15
+ ''
16
+ );
17
+ const searchParams = new URLSearchParams(url.search);
18
+
19
+ if (pathnameWithoutLocale.startsWith('/orders/checkout-provider/')) {
20
+ try {
21
+ const request = await fetch(
22
+ `${encodeURI(settings.commerceUrl)}${url.pathname.replace(
23
+ urlLocaleMatcherRegex,
24
+ ''
25
+ )}?${searchParams.toString()}`,
26
+ {
27
+ next: {
28
+ revalidate: 0
29
+ },
30
+ headers: {
31
+ Cookie: req.headers.get('cookie') || '',
32
+ Accept: 'application/json',
33
+ 'X-Requested-With': 'XMLHttpRequest'
34
+ }
35
+ }
36
+ );
37
+
38
+ return NextResponse.json(await request.json());
39
+ } catch (error) {
40
+ logger.error('withCheckoutProvider error', {
41
+ error,
42
+ ip
43
+ });
44
+ return NextResponse.next();
45
+ }
46
+ }
47
+
48
+ if (pathnameWithoutLocale.startsWith('/orders/checkout-provider-cancel')) {
49
+ try {
50
+ const request = await fetch(
51
+ `${settings.commerceUrl}${pathnameWithoutLocale}${url.search}`,
52
+ {
53
+ redirect: 'manual',
54
+ next: {
55
+ revalidate: 0
56
+ },
57
+ headers: {
58
+ Cookie: req.headers.get('cookie') || ''
59
+ }
60
+ }
61
+ );
62
+
63
+ if (request.headers.get('location')) {
64
+ const location = request.headers.get('location');
65
+ const redirectUrl = new URL(
66
+ request.headers.get('location'),
67
+ location.startsWith('http') ? '' : process.env.NEXT_PUBLIC_URL
68
+ );
69
+
70
+ redirectUrl.pathname = getUrlPathWithLocale(
71
+ redirectUrl.pathname,
72
+ req.middlewareParams.rewrites.locale
73
+ );
74
+
75
+ return Response.redirect(redirectUrl.toString(), request.status);
76
+ }
77
+ } catch (error) {
78
+ logger.error('withCheckoutProvider error', {
79
+ error,
80
+ ip
81
+ });
82
+ }
83
+ }
84
+
85
+ return middleware(req, event);
86
+ };
87
+
88
+ export default withCheckoutProvider;
@@ -1,7 +1,9 @@
1
1
  import { NextFetchEvent, NextMiddleware, NextResponse } from 'next/server';
2
2
  import Settings from 'settings';
3
+ import { ROUTES } from 'routes';
3
4
  import {
4
5
  PzNextRequest,
6
+ withCheckoutProvider,
5
7
  withCompleteGpay,
6
8
  withCompleteMasterpass,
7
9
  withOauthLogin,
@@ -112,6 +114,36 @@ const withPzDefault =
112
114
  return NextResponse.redirect(redirectUrlWithLocale, 303);
113
115
  }
114
116
 
117
+ if (req.nextUrl.pathname.startsWith('/orders/checkout-provider/')) {
118
+ try {
119
+ const data = await req.json();
120
+
121
+ const request = await fetch(
122
+ `${encodeURI(Settings.commerceUrl)}${url.pathname.replace(
123
+ urlLocaleMatcherRegex,
124
+ ''
125
+ )}?${searchParams.toString()}`,
126
+ {
127
+ method: 'POST',
128
+ body: JSON.stringify(data),
129
+ next: {
130
+ revalidate: 0
131
+ },
132
+ headers: {
133
+ Cookie: req.headers.get('cookie') || '',
134
+ Accept: 'application/json',
135
+ 'Content-Type': 'application/json',
136
+ 'X-Requested-With': 'XMLHttpRequest'
137
+ }
138
+ }
139
+ );
140
+
141
+ return NextResponse.json(await request.json());
142
+ } catch (error) {
143
+ return NextResponse.redirect(ROUTES.BASKET);
144
+ }
145
+ }
146
+
115
147
  req.middlewareParams = {
116
148
  commerceUrl,
117
149
  rewrites: {}
@@ -123,64 +155,82 @@ const withPzDefault =
123
155
  withPrettyUrl(
124
156
  withRedirectionPayment(
125
157
  withThreeDRedirection(
126
- withUrlRedirection(
127
- withCompleteGpay(
128
- withCompleteMasterpass(
129
- async (req: PzNextRequest, event: NextFetchEvent) => {
130
- let middlewareResult: NextResponse | void =
131
- NextResponse.next();
132
-
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
- ''
141
- );
142
-
143
- url.basePath = `/${commerceUrl}`;
144
- url.pathname = `/${
145
- locale.length ? `${locale}/` : ''
146
- }${currency}${prettyUrl ?? pathnameWithoutLocale}`;
147
-
148
- Settings.rewrites.forEach((rewrite) => {
149
- url.pathname = url.pathname.replace(
150
- rewrite.source,
151
- rewrite.destination
158
+ withCheckoutProvider(
159
+ withUrlRedirection(
160
+ withCompleteGpay(
161
+ withCompleteMasterpass(
162
+ async (req: PzNextRequest, event: NextFetchEvent) => {
163
+ let middlewareResult: NextResponse | void =
164
+ NextResponse.next();
165
+
166
+ try {
167
+ const { locale, prettyUrl, currency } =
168
+ req.middlewareParams.rewrites;
169
+ const { defaultLocaleValue } =
170
+ Settings.localization;
171
+ const url = req.nextUrl.clone();
172
+ const pathnameWithoutLocale = url.pathname.replace(
173
+ urlLocaleMatcherRegex,
174
+ ''
152
175
  );
153
- });
154
176
 
155
- middlewareResult = (await middleware(
156
- req,
157
- event
158
- )) as NextResponse | void;
177
+ url.basePath = `/${commerceUrl}`;
178
+ url.pathname = `/${
179
+ locale.length ? `${locale}/` : ''
180
+ }${currency}${prettyUrl ?? pathnameWithoutLocale}`;
181
+
182
+ Settings.rewrites.forEach((rewrite) => {
183
+ url.pathname = url.pathname.replace(
184
+ rewrite.source,
185
+ rewrite.destination
186
+ );
187
+ });
188
+
189
+ middlewareResult = (await middleware(
190
+ req,
191
+ event
192
+ )) as NextResponse | void;
193
+
194
+ // if middleware.ts has a return value for current url
195
+ if (middlewareResult instanceof NextResponse) {
196
+ // pz-override-response header is used to prevent 404 page for custom responses.
197
+ if (
198
+ middlewareResult.headers.get(
199
+ 'pz-override-response'
200
+ ) !== 'true'
201
+ ) {
202
+ middlewareResult.headers.set(
203
+ 'x-middleware-rewrite',
204
+ url.href
205
+ );
206
+ }
207
+ } else {
208
+ // if middleware.ts doesn't have a return value.
209
+ // e.g. NextResponse.next() doesn't exist in middleware.ts
210
+
211
+ middlewareResult = NextResponse.rewrite(url);
212
+ }
159
213
 
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
214
  if (
164
- middlewareResult.headers.get(
165
- 'pz-override-response'
166
- ) !== 'true'
215
+ !url.pathname.startsWith(`/${currency}/orders`)
167
216
  ) {
168
- middlewareResult.headers.set(
169
- 'x-middleware-rewrite',
170
- url.href
217
+ middlewareResult.cookies.set(
218
+ 'pz-locale',
219
+ locale?.length > 0
220
+ ? locale
221
+ : defaultLocaleValue,
222
+ {
223
+ sameSite: 'none',
224
+ secure: true,
225
+ expires: new Date(
226
+ Date.now() + 1000 * 60 * 60 * 24 * 7
227
+ ) // 7 days
228
+ }
171
229
  );
172
230
  }
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
231
  middlewareResult.cookies.set(
182
- 'pz-locale',
183
- locale?.length > 0 ? locale : defaultLocaleValue,
232
+ 'pz-currency',
233
+ currency,
184
234
  {
185
235
  sameSite: 'none',
186
236
  secure: true,
@@ -189,74 +239,65 @@ const withPzDefault =
189
239
  ) // 7 days
190
240
  }
191
241
  );
192
- }
193
- middlewareResult.cookies.set(
194
- 'pz-currency',
195
- currency,
196
- {
197
- sameSite: 'none',
198
- secure: true,
199
- expires: new Date(
200
- Date.now() + 1000 * 60 * 60 * 24 * 7
201
- ) // 7 days
202
- }
203
- );
204
-
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
- }
215
-
216
- middlewareResult.headers.set(
217
- 'pz-url',
218
- req.nextUrl.toString()
219
- );
220
242
 
221
- if (req.cookies.get('pz-set-currency')) {
222
- middlewareResult.cookies.delete('pz-set-currency');
223
- }
243
+ if (
244
+ req.cookies.get('pz-locale') &&
245
+ req.cookies.get('pz-locale').value !== locale
246
+ ) {
247
+ logger.debug('Locale changed', {
248
+ locale,
249
+ oldLocale: req.cookies.get('pz-locale')?.value,
250
+ ip
251
+ });
252
+ }
224
253
 
225
- if (process.env.ACC_APP_VERSION) {
226
254
  middlewareResult.headers.set(
227
- 'acc-app-version',
228
- process.env.ACC_APP_VERSION
255
+ 'pz-url',
256
+ req.nextUrl.toString()
229
257
  );
230
- }
231
258
 
232
- // Set CSRF token if not set
233
- try {
234
- const url = `${Settings.commerceUrl}${user.csrfToken}`;
259
+ if (req.cookies.get('pz-set-currency')) {
260
+ middlewareResult.cookies.delete(
261
+ 'pz-set-currency'
262
+ );
263
+ }
235
264
 
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
265
+ if (process.env.ACC_APP_VERSION) {
266
+ middlewareResult.headers.set(
267
+ 'acc-app-version',
268
+ process.env.ACC_APP_VERSION
243
269
  );
244
270
  }
271
+
272
+ // Set CSRF token if not set
273
+ try {
274
+ const url = `${Settings.commerceUrl}${user.csrfToken}`;
275
+
276
+ if (!req.cookies.get('csrftoken')) {
277
+ const { csrf_token } = await (
278
+ await fetch(url)
279
+ ).json();
280
+ middlewareResult.cookies.set(
281
+ 'csrftoken',
282
+ csrf_token
283
+ );
284
+ }
285
+ } catch (error) {
286
+ logger.error('CSRF Error', {
287
+ error,
288
+ ip
289
+ });
290
+ }
245
291
  } catch (error) {
246
- logger.error('CSRF Error', {
292
+ logger.error('withPzDefault Error', {
247
293
  error,
248
294
  ip
249
295
  });
250
296
  }
251
- } catch (error) {
252
- logger.error('withPzDefault Error', {
253
- error,
254
- ip
255
- });
256
- }
257
297
 
258
- return middlewareResult;
259
- }
298
+ return middlewareResult;
299
+ }
300
+ )
260
301
  )
261
302
  )
262
303
  )
@@ -7,6 +7,7 @@ import withOauthLogin from './oauth-login';
7
7
  import withUrlRedirection from './url-redirection';
8
8
  import withCompleteGpay from './complete-gpay';
9
9
  import withCompleteMasterpass from './complete-masterpass';
10
+ import withCheckoutProvider from './checkout-provider';
10
11
  import { NextRequest } from 'next/server';
11
12
 
12
13
  export {
@@ -18,7 +19,8 @@ export {
18
19
  withOauthLogin,
19
20
  withUrlRedirection,
20
21
  withCompleteGpay,
21
- withCompleteMasterpass
22
+ withCompleteMasterpass,
23
+ withCheckoutProvider
22
24
  };
23
25
 
24
26
  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.21.0",
4
+ "version": "1.22.0-rc.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -14,13 +14,14 @@
14
14
  "pz-postdev": "bin/pz-postdev.js"
15
15
  },
16
16
  "dependencies": {
17
- "@opentelemetry/sdk-node": "0.46.0",
18
17
  "@opentelemetry/exporter-trace-otlp-http": "0.46.0",
19
18
  "@opentelemetry/resources": "1.19.0",
20
- "@opentelemetry/semantic-conventions": "1.19.0",
19
+ "@opentelemetry/sdk-node": "0.46.0",
21
20
  "@opentelemetry/sdk-trace-node": "1.19.0",
21
+ "@opentelemetry/semantic-conventions": "1.19.0",
22
22
  "@reduxjs/toolkit": "1.9.7",
23
23
  "cross-spawn": "7.0.3",
24
+ "generic-pool": "3.9.0",
24
25
  "react-redux": "8.1.3",
25
26
  "react-string-replace": "1.1.1",
26
27
  "redis": "4.5.1",
@@ -31,7 +32,7 @@
31
32
  "@typescript-eslint/eslint-plugin": "6.7.4",
32
33
  "@typescript-eslint/parser": "6.7.4",
33
34
  "eslint": "^8.14.0",
34
- "@akinon/eslint-plugin-projectzero": "1.21.0",
35
+ "@akinon/eslint-plugin-projectzero": "1.22.0-rc.0",
35
36
  "eslint-config-prettier": "8.5.0"
36
37
  }
37
38
  }
package/plugins.d.ts CHANGED
@@ -1,3 +1,7 @@
1
1
  declare module '@akinon/pz-masterpass' {
2
2
  export const masterpassReducer: unknown;
3
3
  }
4
+
5
+ declare module '@akinon/pz-otp' {
6
+ export const otpReducer: unknown;
7
+ }
@@ -6,6 +6,7 @@ import { api } from '../../data/client/api';
6
6
 
7
7
  // Plugin reducers
8
8
  import { masterpassReducer } from '@akinon/pz-masterpass';
9
+ import { otpReducer } from '@akinon/pz-otp';
9
10
 
10
11
  const reducers = {
11
12
  [api.reducerPath]: api.reducer,
@@ -13,7 +14,8 @@ const reducers = {
13
14
  checkout: checkoutReducer,
14
15
  config: configReducer,
15
16
  header: headerReducer,
16
- masterpass: masterpassReducer
17
+ masterpass: masterpassReducer,
18
+ otp: otpReducer
17
19
  };
18
20
 
19
21
  export default reducers;
package/sentry/index.ts CHANGED
@@ -1,27 +1,33 @@
1
1
  import * as Sentry from '@sentry/nextjs';
2
+ import settings from 'settings';
2
3
 
3
4
  const SENTRY_DSN: string =
4
- process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
5
+ settings.sentryDsn ||
6
+ process.env.SENTRY_DSN ||
7
+ process.env.NEXT_PUBLIC_SENTRY_DSN;
5
8
 
6
9
  export const initSentry = (
7
10
  type: 'Server' | 'Client' | 'Edge',
8
- options: Sentry.BrowserOptions | Sentry.NodeOptions | Sentry.EdgeOptions = {}
11
+ options: Sentry.BrowserOptions | Sentry.NodeOptions | Sentry.EdgeOptions = {
12
+ dsn: SENTRY_DSN,
13
+ integrations: [],
14
+ tracesSampleRate: 1.0
15
+ }
9
16
  ) => {
10
- // TODO: Handle options with ESLint rules
11
-
12
- // TODO: Remove Zero Project DSN
13
-
14
- Sentry.init({
15
- dsn:
16
- SENTRY_DSN ||
17
- 'https://d8558ef8997543deacf376c7d8d7cf4b@o64293.ingest.sentry.io/4504338423742464',
17
+ const initOptions = {
18
+ ...options,
18
19
  initialScope: {
19
20
  tags: {
21
+ ...((
22
+ options.initialScope as {
23
+ tags?: Record<string, string>;
24
+ }
25
+ )?.tags ?? {}),
20
26
  APP_TYPE: 'ProjectZeroNext',
21
27
  TYPE: type
22
28
  }
23
- },
24
- tracesSampleRate: 1.0,
25
- integrations: []
26
- });
29
+ }
30
+ };
31
+
32
+ Sentry.init(initOptions);
27
33
  };
package/types/index.ts CHANGED
@@ -71,6 +71,12 @@ export interface Currency {
71
71
 
72
72
  export interface Settings {
73
73
  commerceUrl: string;
74
+ /**
75
+ * This option allows you to track Sentry events on the client side, in addition to server and edge environments.
76
+ *
77
+ * It overrides process.env.NEXT_PUBLIC_SENTRY_DSN and process.env.SENTRY_DSN.
78
+ */
79
+ sentryDsn?: string;
74
80
  redis: {
75
81
  defaultExpirationTime: number;
76
82
  };
@@ -12,7 +12,7 @@ const appFetch = async <T>(
12
12
  url: RequestInfo,
13
13
  init: RequestInit = {},
14
14
  responseType = FetchResponseType.JSON
15
- ) => {
15
+ ): Promise<T> => {
16
16
  let response: T;
17
17
  let status: number;
18
18
  let ip = '';
@@ -28,7 +28,7 @@ const appFetch = async <T>(
28
28
 
29
29
  if (commerceUrl === 'default') {
30
30
  logger.error('Commerce URL is not set. Current value is "default"');
31
- return undefined;
31
+ throw new Error('Commerce URL is not set');
32
32
  }
33
33
 
34
34
  const requestURL = `${decodeURIComponent(commerceUrl)}${url}`;
@@ -48,19 +48,30 @@ const appFetch = async <T>(
48
48
  status = req.status;
49
49
  logger.debug(`FETCH END ${url}`, { status: req.status, ip });
50
50
 
51
- const rawData = await req.text();
51
+ if (!req.ok) {
52
+ throw new Error(`Request failed with status ${status}`);
53
+ }
52
54
 
53
55
  if (responseType === FetchResponseType.JSON) {
54
- response = JSON.parse(rawData);
56
+ response = (await req.json()) as T;
55
57
  } else {
56
- response = rawData as unknown as T;
58
+ response = (await req.text()) as unknown as T;
57
59
  }
58
60
 
59
61
  logger.trace(`FETCH RESPONSE`, { url, response, ip });
60
62
  } catch (error) {
63
+ const logType = status === 500 ? 'fatal' : 'error';
64
+
61
65
  if (!url.toString().includes('/cms/seo/')) {
62
- logger.error(`FETCH FAILED`, { url, status, error, ip });
66
+ logger[logType](`FETCH FAILED`, { url, status, error, ip });
63
67
  }
68
+
69
+ // throw the error if it's fatal, so it can be caught and handled at higher levels
70
+ if (logType === 'fatal') {
71
+ throw error;
72
+ }
73
+
74
+ return Promise.reject(error);
64
75
  }
65
76
 
66
77
  return response;