@akinon/next 2.0.0-beta.1 → 2.0.0-beta.11

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.
Files changed (74) hide show
  1. package/.eslintrc.js +12 -0
  2. package/CHANGELOG.md +56 -0
  3. package/__tests__/next-config.test.ts +83 -0
  4. package/__tests__/tsconfig.json +23 -0
  5. package/api/auth.ts +72 -5
  6. package/api/client.ts +18 -1
  7. package/babel.config.js +6 -0
  8. package/bin/pz-prebuild.js +1 -0
  9. package/bin/pz-run-tests.js +99 -0
  10. package/bin/run-prebuild-tests.js +46 -0
  11. package/components/accordion.tsx +1 -1
  12. package/components/button.tsx +51 -36
  13. package/components/client-root.tsx +20 -0
  14. package/components/input.tsx +1 -1
  15. package/components/modal.tsx +1 -1
  16. package/components/price.tsx +2 -2
  17. package/components/select.tsx +1 -1
  18. package/components/selected-payment-option-view.tsx +1 -1
  19. package/data/client/api.ts +2 -0
  20. package/data/client/basket.ts +27 -5
  21. package/data/client/checkout.ts +62 -7
  22. package/data/client/misc.ts +25 -1
  23. package/data/client/product.ts +19 -2
  24. package/data/client/user.ts +16 -8
  25. package/data/server/flatpage.ts +8 -4
  26. package/data/server/form.ts +12 -4
  27. package/data/server/landingpage.ts +8 -4
  28. package/data/server/menu.ts +7 -2
  29. package/data/server/product.ts +16 -5
  30. package/data/server/seo.ts +11 -4
  31. package/data/server/widget.ts +19 -4
  32. package/data/urls.ts +11 -3
  33. package/hooks/index.ts +1 -0
  34. package/hooks/use-localization.ts +24 -10
  35. package/hooks/use-router.ts +5 -2
  36. package/hooks/use-sentry-uncaught-errors.ts +24 -0
  37. package/instrumentation/index.ts +10 -0
  38. package/jest.config.js +19 -0
  39. package/lib/cache-handler.mjs +2 -2
  40. package/lib/cache.ts +4 -4
  41. package/localization/index.ts +2 -1
  42. package/middlewares/default.ts +31 -4
  43. package/middlewares/locale.ts +35 -11
  44. package/middlewares/url-redirection.ts +16 -0
  45. package/package.json +8 -4
  46. package/plugins.js +2 -1
  47. package/redux/middlewares/checkout.ts +30 -130
  48. package/redux/middlewares/index.ts +6 -2
  49. package/redux/middlewares/pre-order/address.ts +50 -0
  50. package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +46 -0
  51. package/redux/middlewares/pre-order/data-source-shipping-option.ts +43 -0
  52. package/redux/middlewares/pre-order/delivery-option.ts +40 -0
  53. package/redux/middlewares/pre-order/index.ts +29 -0
  54. package/redux/middlewares/pre-order/installment-option.ts +42 -0
  55. package/redux/middlewares/pre-order/payment-option.ts +34 -0
  56. package/redux/middlewares/pre-order/pre-order-validation.ts +24 -0
  57. package/redux/middlewares/pre-order/redirection.ts +40 -0
  58. package/redux/middlewares/pre-order/set-pre-order.ts +22 -0
  59. package/redux/middlewares/pre-order/shipping-option.ts +44 -0
  60. package/redux/middlewares/pre-order/shipping-step.ts +38 -0
  61. package/redux/reducers/checkout.ts +8 -2
  62. package/redux/reducers/index.ts +5 -3
  63. package/redux/reducers/root.ts +7 -2
  64. package/sentry/index.ts +36 -17
  65. package/tailwind/content.js +16 -0
  66. package/types/commerce/account.ts +5 -1
  67. package/types/commerce/checkout.ts +23 -0
  68. package/types/index.ts +7 -2
  69. package/utils/get-root-hostname.ts +28 -0
  70. package/utils/index.ts +6 -2
  71. package/utils/localization.ts +4 -0
  72. package/utils/override-middleware.ts +25 -0
  73. package/views/error-page.tsx +93 -0
  74. package/with-pz-config.js +4 -3
package/.eslintrc.js CHANGED
@@ -24,6 +24,18 @@ module.exports = {
24
24
  parserOptions: {
25
25
  sourceType: 'script'
26
26
  }
27
+ },
28
+ {
29
+ env: {
30
+ node: true
31
+ },
32
+ files: ['redux/middlewares/pre-order/index.ts'],
33
+ rules: {
34
+ '@akinon/projectzero/check-pre-order-middleware-order': 'error'
35
+ },
36
+ parserOptions: {
37
+ sourceType: 'script'
38
+ }
27
39
  }
28
40
  ],
29
41
  parser: '@typescript-eslint/parser',
package/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # @akinon/next
2
2
 
3
+ ## 2.0.0-beta.11
4
+
5
+ ### Minor Changes
6
+
7
+ - ac783d6: ZERO-3482: Update tailwindcss to version 4.1.11 and enhance button cursor styles
8
+
9
+ ## 2.0.0-beta.10
10
+
11
+ ### Minor Changes
12
+
13
+ - 2806320: ZERO-3390: Update version tailwindcss, autoprefixer, tailwind-merge, postcss
14
+
15
+ ## 2.0.0-beta.9
16
+
17
+ ### Minor Changes
18
+
19
+ - 0fe7711: ZERO-3387: Upgrade nextjs, eslint-config-next
20
+
21
+ ## 2.0.0-beta.8
22
+
23
+ ### Minor Changes
24
+
25
+ - 071d0f5: ZERO-3352: Resolve Single item size exceeds maxSize error and upgrade dependencies
26
+
27
+ ## 2.0.0-beta.7
28
+
29
+ ## 2.0.0-beta.6
30
+
31
+ ### Minor Changes
32
+
33
+ - 8f05f9b: ZERO-3250: Beta branch synchronized with Main branch
34
+
35
+ ## 2.0.0-beta.5
36
+
37
+ ### Minor Changes
38
+
39
+ - e791eab: ZERO-3133: Add fallbackReducer for handling missing plugin reducers
40
+
41
+ ## 2.0.0-beta.4
42
+
43
+ ## 2.0.0-beta.3
44
+
45
+ ### Minor Changes
46
+
47
+ - 5536b80: ZERO-3104: Add optional headers parameter
48
+
49
+ ## 2.0.0-beta.2
50
+
51
+ ### Minor Changes
52
+
53
+ - a006015: ZERO-3116: Add not-found page and update default middleware.
54
+ - 999168d: ZERO-3104: Remove local cache handler from CacheHandler initialization
55
+ - 1eeb3d8: ZERO-3116: Add not found page
56
+ - 86a5a62: ZERO-3104: Add optional headers parameter to data fetching functions
57
+ - dd69cc6: ZERO-3079: Modularize pre-order middleware
58
+
3
59
  ## 2.0.0-beta.1
4
60
 
5
61
  ### Minor Changes
@@ -0,0 +1,83 @@
1
+ import { resolve } from 'path';
2
+ import type { NextConfig } from 'next';
3
+
4
+ function findBaseDir() {
5
+ const insideNodeModules = __dirname.includes('node_modules');
6
+
7
+ if (insideNodeModules) {
8
+ return resolve(__dirname, '../../../../');
9
+ } else {
10
+ return resolve(__dirname, '../../../apps/projectzeronext');
11
+ }
12
+ }
13
+
14
+ const baseDir = findBaseDir();
15
+
16
+ jest.mock('next-pwa', () => {
17
+ return () => (config: NextConfig) => config;
18
+ });
19
+
20
+ jest.mock('@sentry/nextjs', () => ({
21
+ withSentryConfig: (config: NextConfig) => config
22
+ }));
23
+
24
+ jest.mock('../with-pz-config.js', () => {
25
+ return (config: NextConfig) => {
26
+ const originalHeaders = config.headers;
27
+
28
+ config.headers = async () => {
29
+ const originalHeadersResult = (await originalHeaders?.()) ?? [];
30
+
31
+ return [
32
+ {
33
+ source: '/(.*)',
34
+ headers: [
35
+ {
36
+ key: 'Content-Security-Policy',
37
+ value: 'https://*.akifast.com akifast.com'
38
+ }
39
+ ]
40
+ },
41
+ ...originalHeadersResult
42
+ ];
43
+ };
44
+ return config;
45
+ };
46
+ });
47
+
48
+ interface Header {
49
+ key: string;
50
+ value: string;
51
+ }
52
+
53
+ interface HeaderGroup {
54
+ source: string;
55
+ headers: Header[];
56
+ }
57
+
58
+ const nextConfigPath = resolve(baseDir, 'next.config.mjs');
59
+ let nextConfig: any;
60
+
61
+ beforeAll(async () => {
62
+ nextConfig = await import(nextConfigPath);
63
+ });
64
+
65
+ describe('Next.js Configuration', () => {
66
+ it('should contain Content-Security-Policy header with akifast domain values', async () => {
67
+ const headers = nextConfig.default.headers;
68
+ expect(headers).toBeDefined();
69
+
70
+ const headersResult = await headers();
71
+
72
+ const cspHeaders = headersResult
73
+ .flatMap((headerGroup: HeaderGroup) => headerGroup.headers)
74
+ .filter((header: Header) => header.key === 'Content-Security-Policy');
75
+
76
+ expect(cspHeaders.length).toBeGreaterThan(0);
77
+
78
+ const lastCspHeader = cspHeaders[cspHeaders.length - 1];
79
+
80
+ expect(lastCspHeader.value).toContain('akifast.com');
81
+ expect(lastCspHeader.value).toContain('https://*.akifast.com');
82
+ });
83
+ });
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "display": "Default",
4
+ "compilerOptions": {
5
+ "composite": false,
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "esModuleInterop": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "inlineSources": false,
11
+ "isolatedModules": true,
12
+ "moduleResolution": "node",
13
+ "noUnusedLocals": false,
14
+ "noUnusedParameters": false,
15
+ "preserveWatchOutput": true,
16
+ "skipLibCheck": true,
17
+ "strict": true,
18
+ "jsx": "react-jsx",
19
+ "allowJs": true
20
+ },
21
+ "exclude": ["node_modules"],
22
+ "include": [".", "../."]
23
+ }
package/api/auth.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { NextApiRequest, NextApiResponse } from 'next';
2
- import NextAuth, { Session } from 'next-auth';
2
+ import NextAuth, { Session, NextAuthOptions } from 'next-auth';
3
3
  import CredentialProvider from 'next-auth/providers/credentials';
4
4
  import { ROUTES } from 'routes';
5
5
  import { URLS, user } from '../data/urls';
@@ -7,6 +7,8 @@ import Settings from 'settings';
7
7
  import { urlLocaleMatcherRegex } from '../utils';
8
8
  import logger from '@akinon/next/utils/log';
9
9
  import { AuthError } from '../types';
10
+ import getRootHostname from '../utils/get-root-hostname';
11
+ import { LocaleUrlStrategy } from '../localization';
10
12
 
11
13
  async function getCurrentUser(sessionId: string, currency = '') {
12
14
  const headers = {
@@ -40,7 +42,15 @@ async function getCurrentUser(sessionId: string, currency = '') {
40
42
  };
41
43
  }
42
44
 
43
- const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
45
+ type CustomNextAuthOptions = (
46
+ req: NextApiRequest,
47
+ res: NextApiResponse
48
+ ) => Partial<NextAuthOptions>;
49
+
50
+ const defaultNextAuthOptions = (
51
+ req: NextApiRequest,
52
+ res: NextApiResponse
53
+ ): NextAuthOptions => {
44
54
  return {
45
55
  providers: [
46
56
  CredentialProvider({
@@ -92,11 +102,28 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
92
102
  req.headers['x-app-device']?.toString() ?? ''
93
103
  );
94
104
 
105
+ headers.set('x-frontend-id', req.cookies['pz-frontend-id'] || '');
106
+
95
107
  logger.debug('Trying to login/register', {
96
108
  formType: credentials.formType,
97
109
  userIp
98
110
  });
99
111
 
112
+ const checkCurrentUser = await getCurrentUser(
113
+ req.cookies['osessionid'] ?? '',
114
+ req.cookies['pz-currency'] ?? ''
115
+ );
116
+
117
+ if (checkCurrentUser?.pk) {
118
+ const sessionCookie = headers
119
+ .get('cookie')
120
+ ?.match(/osessionid=\w+/)?.[0]
121
+ .replace(/osessionid=/, '');
122
+ if (sessionCookie) {
123
+ headers.set('cookie', sessionCookie);
124
+ }
125
+ }
126
+
100
127
  const apiRequest = await fetch(
101
128
  `${Settings.commerceUrl}${user[credentials.formType]}`,
102
129
  {
@@ -133,7 +160,19 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
133
160
 
134
161
  if (sessionId) {
135
162
  const maxAge = 30 * 24 * 60 * 60; // 30 days in seconds
136
- const cookieOptions = `Path=/; HttpOnly; Secure; Max-Age=${maxAge}`;
163
+ const { localeUrlStrategy } = Settings.localization;
164
+
165
+ const fallbackHost =
166
+ req.headers['x-forwarded-host']?.toString() ||
167
+ req.headers.host?.toString();
168
+ const hostname =
169
+ process.env.NEXT_PUBLIC_URL || `https://${fallbackHost}`;
170
+ const rootHostname =
171
+ localeUrlStrategy === LocaleUrlStrategy.Subdomain
172
+ ? getRootHostname(hostname)
173
+ : null;
174
+ const domainOption = rootHostname ? `Domain=${rootHostname};` : '';
175
+ const cookieOptions = `Path=/; HttpOnly; Secure; Max-Age=${maxAge}; ${domainOption}`;
137
176
 
138
177
  res.setHeader('Set-Cookie', [
139
178
  `osessionid=${sessionId}; ${cookieOptions}`,
@@ -236,8 +275,36 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
236
275
  };
237
276
  };
238
277
 
239
- const Auth = (req, res) => {
240
- return NextAuth(req, res, nextAuthOptions(req, res));
278
+ const Auth = (
279
+ req: NextApiRequest,
280
+ res: NextApiResponse,
281
+ customOptions?: CustomNextAuthOptions
282
+ ) => {
283
+ const baseOptions = defaultNextAuthOptions(req, res);
284
+ const customOptionsResult = customOptions ? customOptions(req, res) : {};
285
+
286
+ const mergedOptions = {
287
+ ...baseOptions,
288
+ ...customOptionsResult,
289
+ providers: [
290
+ ...baseOptions.providers,
291
+ ...(customOptionsResult.providers || [])
292
+ ],
293
+ callbacks: {
294
+ ...baseOptions.callbacks,
295
+ ...customOptionsResult.callbacks
296
+ },
297
+ events: {
298
+ ...baseOptions.events,
299
+ ...customOptionsResult.events
300
+ },
301
+ pages: {
302
+ ...baseOptions.pages,
303
+ ...customOptionsResult.pages
304
+ }
305
+ };
306
+
307
+ return NextAuth(req, res, mergedOptions);
241
308
  };
242
309
 
243
310
  export default Auth;
package/api/client.ts CHANGED
@@ -5,6 +5,8 @@ import logger from '../utils/log';
5
5
  import formatCookieString from '../utils/format-cookie-string';
6
6
  import cookieParser from 'set-cookie-parser';
7
7
  import { cookies } from 'next/headers';
8
+ import getRootHostname from '../utils/get-root-hostname';
9
+ import { LocaleUrlStrategy } from '../localization';
8
10
 
9
11
  interface RouteParams {
10
12
  params: {
@@ -191,8 +193,23 @@ async function proxyRequest(...args) {
191
193
  const responseHeaders: any = {};
192
194
 
193
195
  if (filteredCookies.length > 0) {
196
+ const { localeUrlStrategy } = settings.localization;
197
+
198
+ const fallbackHost =
199
+ req.headers.get('x-forwarded-host') || req.headers.get('host');
200
+ const hostname = process.env.NEXT_PUBLIC_URL || `https://${fallbackHost}`;
201
+ const rootHostname =
202
+ localeUrlStrategy === LocaleUrlStrategy.Subdomain
203
+ ? getRootHostname(hostname)
204
+ : null;
205
+
194
206
  responseHeaders['set-cookie'] = filteredCookies
195
- .map(formatCookieString)
207
+ .map((cookie) => {
208
+ if (!cookie.domain && rootHostname) {
209
+ cookie.domain = rootHostname;
210
+ }
211
+ return formatCookieString(cookie);
212
+ })
196
213
  .join(', ');
197
214
  }
198
215
 
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ presets: [
3
+ ['@babel/preset-env', { targets: { node: 'current' } }],
4
+ '@babel/preset-typescript'
5
+ ]
6
+ };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const runScript = require('./run-script');
4
4
 
5
+ runScript('pz-run-tests.js');
5
6
  runScript('pz-install-theme.js');
6
7
  runScript('pz-pre-check-dist.js');
7
8
  runScript('pz-generate-translations.js');
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const glob = require('glob');
7
+ const findBaseDir = require('../utils/find-base-dir');
8
+ const checkMonorepo = require('../utils/check-monorepo');
9
+
10
+ const IS_MONOREPO = checkMonorepo() !== null;
11
+ const BASE_DIR = findBaseDir();
12
+ const PLUGINS = require(path.join(BASE_DIR, 'src', 'plugins.js'));
13
+
14
+ function findPluginTestFiles(akinonNextPackagePath) {
15
+ const pluginsRootPath = path.join(
16
+ akinonNextPackagePath,
17
+ '..',
18
+ '..',
19
+ IS_MONOREPO ? 'packages' : '@akinon'
20
+ );
21
+
22
+ if (!fs.existsSync(pluginsRootPath)) {
23
+ console.log('Plugins directory not found:', pluginsRootPath);
24
+ return [];
25
+ }
26
+
27
+ return PLUGINS.reduce((testFiles, pluginName) => {
28
+ const pluginDirectoryPath = path.join(pluginsRootPath, pluginName);
29
+ if (fs.existsSync(pluginDirectoryPath)) {
30
+ const pluginTestFiles = glob.sync('**/*.test.ts', {
31
+ cwd: pluginDirectoryPath,
32
+ absolute: true
33
+ });
34
+
35
+ return testFiles.concat(pluginTestFiles);
36
+ } else {
37
+ console.log(`Plugin directory not found: ${pluginName}`);
38
+ }
39
+ return testFiles;
40
+ }, []);
41
+ }
42
+
43
+ function isJestInstalled() {
44
+ try {
45
+ const jestExecutablePath = path.join(
46
+ BASE_DIR,
47
+ 'node_modules',
48
+ '.bin',
49
+ 'jest'
50
+ );
51
+ return fs.existsSync(jestExecutablePath);
52
+ } catch (error) {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ function getAkinonNextPackagePath() {
58
+ if (!IS_MONOREPO) {
59
+ return path.resolve(__dirname, '..');
60
+ }
61
+
62
+ return path.resolve(__dirname, '../../../packages/akinon-next');
63
+ }
64
+
65
+ if (!isJestInstalled()) {
66
+ console.error('\x1b[31mError: Jest is not installed in the project!\x1b[0m');
67
+ console.error(
68
+ 'Please install all dependencies by running one of the following commands:'
69
+ );
70
+ console.error(' npm install');
71
+ console.error(' yarn install');
72
+ process.exit(1);
73
+ }
74
+
75
+ const jestExecutablePath = path.join(BASE_DIR, 'node_modules', '.bin', 'jest');
76
+ const akinonNextPackagePath = getAkinonNextPackagePath();
77
+
78
+ const testFiles = [
79
+ ...glob.sync('__tests__/**/*.test.ts', {
80
+ cwd: akinonNextPackagePath,
81
+ absolute: true
82
+ }),
83
+ ...findPluginTestFiles(akinonNextPackagePath)
84
+ ];
85
+
86
+ console.log(`Found ${testFiles.length} test files to run`);
87
+
88
+ const jestArguments = [
89
+ ...testFiles,
90
+ `--config ${path.join(akinonNextPackagePath, 'jest.config.js')}`,
91
+ '--runTestsByPath',
92
+ '--passWithNoTests'
93
+ ];
94
+
95
+ spawn(jestExecutablePath, jestArguments, {
96
+ cwd: akinonNextPackagePath,
97
+ stdio: 'inherit',
98
+ shell: true
99
+ });
@@ -0,0 +1,46 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+
4
+ function runPrebuildTests() {
5
+ const workspaceRoot = process.cwd()
6
+ const configPath = path.join(workspaceRoot, 'config/prebuild-tests.json')
7
+
8
+ try {
9
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
10
+
11
+ if (config.tests && Array.isArray(config.tests)) {
12
+ for (const testScript of config.tests) {
13
+ console.log(`🧪 Running test script: ${testScript}`)
14
+ const result = require('child_process').spawnSync('yarn', [testScript], {
15
+ stdio: 'inherit',
16
+ shell: true
17
+ })
18
+
19
+ if (result.status !== 0) {
20
+ console.error(`❌ Test script '${testScript}' failed`)
21
+ process.exit(1)
22
+ }
23
+ }
24
+ console.log('✅ All prebuild tests passed successfully!')
25
+ }
26
+ } catch (error) {
27
+ if (error.code === 'ENOENT') {
28
+ console.log('🧪 Running default test: test:middleware')
29
+ const result = require('child_process').spawnSync('yarn', ['test:middleware'], {
30
+ stdio: 'inherit',
31
+ shell: true
32
+ })
33
+
34
+ if (result.status !== 0) {
35
+ console.error('❌ Middleware test failed')
36
+ process.exit(1)
37
+ }
38
+ console.log('✅ Default test passed successfully!')
39
+ } else {
40
+ console.error('❌ Error reading test configuration:', error)
41
+ process.exit(1)
42
+ }
43
+ }
44
+ }
45
+
46
+ module.exports = runPrebuildTests
@@ -22,7 +22,7 @@ export const Accordion = ({
22
22
  return (
23
23
  <div
24
24
  className={twMerge(
25
- 'flex flex-col justify-center border-b pb-4 mb-4 last:border-none',
25
+ 'flex flex-col justify-center border-b border-gray-200 pb-4 mb-4 last:border-none',
26
26
  className
27
27
  )}
28
28
  >
@@ -1,46 +1,61 @@
1
1
  'use client';
2
2
 
3
- import { ButtonProps } from '../types/index';
3
+ import { ButtonProps } from '../types';
4
4
  import clsx from 'clsx';
5
5
  import { twMerge } from 'tailwind-merge';
6
+ import { Link } from './link';
6
7
 
7
8
  export const Button = (props: ButtonProps) => {
8
- return (
9
- <button
10
- {...props}
11
- className={twMerge(
12
- clsx(
13
- [
14
- 'px-4',
15
- 'h-10',
16
- 'text-xs',
17
- 'bg-primary',
18
- 'text-primary-foreground',
19
- 'border',
20
- 'border-primary',
21
- 'transition-all',
22
- 'hover:bg-white',
23
- 'hover:border-primary',
24
- 'hover:text-primary'
25
- ],
26
- props.appearance === 'outlined' && [
27
- 'bg-transparent ',
28
- 'text-primary ',
29
- 'hover:bg-primary ',
30
- 'hover:text-primary-foreground'
31
- ],
32
- props.appearance === 'ghost' && [
33
- 'bg-transparent',
34
- 'border-transparent',
35
- 'text-primary',
36
- 'hover:bg-primary',
37
- 'hover:text-primary-foreground'
38
- ]
39
- ),
40
- props.className
41
- )}
9
+ const {
10
+ appearance = 'filled',
11
+ size = 'md',
12
+ href,
13
+ target,
14
+ children,
15
+ className,
16
+ ...rest
17
+ } = props;
18
+
19
+ const variants = {
20
+ filled:
21
+ 'bg-primary text-primary-foreground border border-primary hover:bg-white hover:border-primary hover:text-primary',
22
+ outlined:
23
+ 'bg-transparent text-primary hover:bg-primary hover:text-primary-foreground',
24
+ ghost:
25
+ 'bg-transparent border-transparent text-primary hover:bg-primary hover:text-primary-foreground',
26
+ link: 'px-0 h-auto underline underline-offset-2'
27
+ };
28
+
29
+ const sizes = {
30
+ sm: 'h-8',
31
+ md: 'h-10',
32
+ lg: 'h-12',
33
+ xl: 'h-14'
34
+ };
35
+
36
+ const buttonClasses = twMerge(
37
+ clsx(
38
+ 'px-4 text-xs transition-all duration-200 cursor-pointer',
39
+ 'inline-flex gap-2 justify-center items-center',
40
+ variants[appearance],
41
+ sizes[size],
42
+ className
43
+ ),
44
+ className
45
+ );
46
+
47
+ return props.href ? (
48
+ <Link
49
+ prefetch={false}
50
+ target={target}
51
+ href={href}
52
+ className={buttonClasses}
42
53
  >
43
- {props.children}
54
+ {children}
55
+ </Link>
56
+ ) : (
57
+ <button {...rest} className={buttonClasses}>
58
+ {children}
44
59
  </button>
45
60
  );
46
61
  };
@@ -1,6 +1,9 @@
1
1
  'use client';
2
2
 
3
3
  import { useMobileIframeHandler } from '../hooks';
4
+ import * as Sentry from '@sentry/nextjs';
5
+ import { initSentry } from '../sentry';
6
+ import { useEffect } from 'react';
4
7
 
5
8
  export default function ClientRoot({
6
9
  children,
@@ -11,6 +14,23 @@ export default function ClientRoot({
11
14
  }) {
12
15
  const { preventPageRender } = useMobileIframeHandler({ sessionId });
13
16
 
17
+ const initializeSentry = async () => {
18
+ const response = await fetch('/api/sentry', { next: { revalidate: 0 } });
19
+ const data = await response.json();
20
+
21
+ const options = {
22
+ dsn: data.dsn
23
+ };
24
+
25
+ initSentry('Client', options);
26
+ };
27
+
28
+ useEffect(() => {
29
+ if (!Sentry.isInitialized()) {
30
+ initializeSentry();
31
+ }
32
+ }, []);
33
+
14
34
  if (preventPageRender) {
15
35
  return null;
16
36
  }
@@ -40,7 +40,7 @@ export const Input = forwardRef<
40
40
  const inputClass = twMerge(
41
41
  clsx(
42
42
  'text-xs border px-2.5 h-10 placeholder:text-gray-600 peer',
43
- 'focus-visible:outline-none', // disable outline on focus
43
+ 'focus-visible:outline-hidden', // disable outline on focus
44
44
  error
45
45
  ? 'border-error focus:border-error'
46
46
  : 'border-gray-500 hover:border-black focus:border-black'
@@ -38,7 +38,7 @@ export const Modal = (props: ModalProps) => {
38
38
 
39
39
  return (
40
40
  <ReactPortal wrapperId={portalId}>
41
- <div className="fixed top-0 left-0 w-screen h-screen bg-primary bg-opacity-60 z-50" />
41
+ <div className="fixed top-0 left-0 w-screen h-screen bg-primary/60 z-50" />
42
42
  <section
43
43
  className={twMerge(
44
44
  'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-50 bg-white',
@@ -55,8 +55,8 @@ export const Price = (props: NumericFormatProps & PriceProps) => {
55
55
  : formattedValue;
56
56
 
57
57
  const currentCurrencyDecimalScale = Settings.localization.currencies.find(
58
- (currency) => currency.code === currencyCode_
59
- ).decimalScale;
58
+ (currency) => currency?.code === currencyCode_
59
+ )?.decimalScale;
60
60
 
61
61
  return (
62
62
  <NumericFormat