@evenicanpm/storefront-core 2.3.1 → 2.4.1

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 (90) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +42 -9
  3. package/src/api-manager/datasources/d365/d365-address.datasource.ts +148 -61
  4. package/src/api-manager/datasources/d365/d365-cart.datasource.ts +23 -1
  5. package/src/api-manager/datasources/d365/d365-order.datasource.ts +6 -1
  6. package/src/api-manager/datasources/d365/d365-user.datasource.ts +109 -37
  7. package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +44 -10
  8. package/src/api-manager/index.ts +2 -1
  9. package/src/api-manager/lib/get-graphql-client.ts +35 -7
  10. package/src/api-manager/schemas/product.schema.ts +1 -1
  11. package/src/api-manager/services/create-query.ts +36 -3
  12. package/src/api-manager/services/get-query-client.ts +10 -0
  13. package/src/auth/better-auth.ts +282 -15
  14. package/src/cms/blocks/block-manager.tsx +1 -1
  15. package/src/cms/blocks/components/product-section-fullwidth/index.tsx +3 -1
  16. package/src/cms/endpoints.ts +7 -2
  17. package/src/components/BazaarMenu.tsx +1 -1
  18. package/src/components/Typography.tsx +4 -2
  19. package/src/components/carousel-cards/carousel-card-1/CarouselCard1.stories.tsx +1 -1
  20. package/src/components/categories/category-list/category-list.tsx +9 -5
  21. package/src/components/categories/category-menu.tsx +1 -1
  22. package/src/components/countries-input.tsx +4 -0
  23. package/src/components/header/__tests__/user.test.tsx +5 -107
  24. package/src/components/header/components/user.tsx +72 -45
  25. package/src/components/header/sticky-header.tsx +0 -1
  26. package/src/components/navbar/mobile-menu/mobile-menu.test.tsx +1 -1
  27. package/src/components/navbar/utils/transform-nav.test.ts +1 -1
  28. package/src/components/navbar/utils/transform-nav.ts +1 -1
  29. package/src/components/product-cards/product-card/product-card.tsx +5 -2
  30. package/src/components/product-cards/product-card/styles/index.ts +1 -5
  31. package/src/components/products-view/compound/products-grid-view.tsx +5 -1
  32. package/src/components/products-view/compound/products-list-view.tsx +5 -1
  33. package/src/global.d.ts +3 -0
  34. package/src/hooks/use-nextauth-session.ts +0 -33
  35. package/src/lib/auth-client.ts +14 -0
  36. package/src/lib/auth.ts +4 -0
  37. package/src/lib/entra-native-auth.ts +138 -0
  38. package/src/lib/native-session.ts +434 -0
  39. package/src/pages/account/addresses/address-form.tsx +20 -2
  40. package/src/pages/account/profile/__tests__/profile-form.test.tsx +173 -0
  41. package/src/pages/account/profile/profile-form.tsx +285 -0
  42. package/src/pages/account/profile/profile-validation.ts +52 -0
  43. package/src/pages/account/wishlist/wishlist-item.tsx +1 -2
  44. package/src/pages/auth/auth-activate-page.tsx +509 -0
  45. package/src/pages/auth/auth-layout.tsx +73 -0
  46. package/src/pages/auth/auth-login-page.tsx +241 -0
  47. package/src/pages/auth/auth-reset-password-page.tsx +487 -0
  48. package/src/pages/auth/compound/auth-form.tsx +182 -0
  49. package/src/pages/auth/compound/auth-pages.tsx +21 -0
  50. package/src/pages/auth/lib/friendly-error.ts +70 -0
  51. package/src/pages/auth/lib/index.ts +2 -0
  52. package/src/pages/auth/lib/types.ts +5 -0
  53. package/src/pages/cart/cart-item.tsx +1 -7
  54. package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +14 -3
  55. package/src/pages/checkout/checkout-alt-form/steps/address/address-card.tsx +5 -3
  56. package/src/pages/checkout/checkout-alt-form/steps/address/delivery-address.tsx +20 -6
  57. package/src/pages/checkout/checkout-alt-form/steps/address/new-address-form.tsx +1 -1
  58. package/src/pages/checkout/checkout-alt-form/steps/customer-info/customer-information.tsx +1 -1
  59. package/src/pages/confirmation/ordered-products.tsx +3 -1
  60. package/src/pages/product-details/bopis/find-in-store-modal.tsx +4 -4
  61. package/src/pages/product-details/bopis/pickup-option-select.tsx +2 -2
  62. package/src/pages/product-details/bopis/search-header.tsx +2 -2
  63. package/src/pages/product-details/product-intro/compound/context.ts +8 -3
  64. package/src/pages/product-details/product-intro/compound/product-info.tsx +25 -19
  65. package/src/pages/product-list/product-list-view.tsx +2 -1
  66. package/src/providers/nav-provider/index.tsx +1 -1
  67. package/src/providers/nav-provider/utils/createLink.ts +1 -1
  68. package/tsconfig.json +20 -15
  69. package/__mocks__/countries.ts +0 -11
  70. package/__mocks__/create-mutation.ts +0 -68
  71. package/__mocks__/create-query.ts +0 -94
  72. package/__mocks__/data/categories.json +0 -795
  73. package/__mocks__/get-product-by-id.ts +0 -6
  74. package/__mocks__/next-auth-react.ts +0 -9
  75. package/__mocks__/next-font.js +0 -4
  76. package/__mocks__/next-headers.js +0 -13
  77. package/__mocks__/next-image.tsx +0 -18
  78. package/__mocks__/next-link.js +0 -19
  79. package/__mocks__/next-navigation.ts +0 -29
  80. package/__mocks__/product-dimensions.ts +0 -635
  81. package/__mocks__/products.ts +0 -533
  82. package/__mocks__/query-client.ts +0 -3
  83. package/__mocks__/wishlists.json +0 -408
  84. package/src/auth/msal.ts +0 -68
  85. package/src/cms/blog.ts +0 -229
  86. package/src/cms/interfaces/navigation.ts +0 -52
  87. package/src/cms/interfaces/product-data.ts +0 -83
  88. package/src/cms/pages.ts +0 -149
  89. package/src/pages/account/profile/profile-button.test.tsx +0 -59
  90. package/src/pages/account/profile/profile-button.tsx +0 -51
@@ -1,19 +1,231 @@
1
+ import type { Auth, BetterAuthOptions } from "better-auth";
1
2
  import { betterAuth } from "better-auth";
2
3
  import { nextCookies } from "better-auth/next-js";
4
+ import { decryptOAuthToken, setTokenUtil } from "better-auth/oauth2";
3
5
  import { customSession, genericOAuth } from "better-auth/plugins";
6
+ import { Pool } from "pg";
7
+
8
+ function toValidDate(dateLike: unknown): Date | null {
9
+ if (dateLike instanceof Date) {
10
+ return Number.isNaN(dateLike.getTime()) ? null : dateLike;
11
+ }
12
+
13
+ if (typeof dateLike === "string" || typeof dateLike === "number") {
14
+ const date = new Date(dateLike);
15
+ return Number.isNaN(date.getTime()) ? null : date;
16
+ }
17
+
18
+ return null;
19
+ }
20
+
21
+ /**
22
+ * Check if a date/timestamp has already passed (expired).
23
+ */
24
+ function isPast(dateLike: unknown): boolean {
25
+ if (!dateLike) return false;
26
+ const date = toValidDate(dateLike);
27
+ if (!date) return false;
28
+ return date.getTime() <= Date.now();
29
+ }
30
+
31
+ /**
32
+ * Check if a date/timestamp is within a specified number of milliseconds from now.
33
+ * Useful for detecting tokens approaching expiry.
34
+ */
35
+ function isWithinMs(dateLike: unknown, windowMs: number): boolean {
36
+ if (!dateLike) return false;
37
+ const date = toValidDate(dateLike);
38
+ if (!date) return false;
39
+ return date.getTime() - Date.now() <= windowMs;
40
+ }
41
+
42
+ /**
43
+ * Proactive refresh window: refresh access tokens 5 minutes before expiry.
44
+ * Prevents users from being signed out when their token expires.
45
+ * Better Auth's default is only ~5 seconds, which is too late.
46
+ */
47
+ const ACCESS_TOKEN_REFRESH_WINDOW_MS = 5 * 60 * 1000;
48
+
49
+ type EntraAccount = {
50
+ id?: string;
51
+ providerId?: string;
52
+ idToken?: string | null;
53
+ accessToken?: string | null;
54
+ refreshToken?: string | null;
55
+ accessTokenExpiresAt?: Date | null;
56
+ refreshTokenExpiresAt?: Date | null;
57
+ scope?: string | null;
58
+ };
59
+
60
+ async function decryptStoredOAuthToken(
61
+ token: string | null | undefined,
62
+ context: Parameters<typeof decryptOAuthToken>[1],
63
+ ): Promise<string> {
64
+ if (!token) return "";
65
+
66
+ try {
67
+ return await decryptOAuthToken(token, context);
68
+ } catch {
69
+ return "";
70
+ }
71
+ }
72
+
73
+ async function resolveEntraRefreshProvider(
74
+ socialProvidersSource: unknown,
75
+ ): Promise<{
76
+ id?: string;
77
+ refreshAccessToken?: (token: string) => Promise<{
78
+ accessToken?: string;
79
+ refreshToken?: string;
80
+ accessTokenExpiresAt?: Date | null;
81
+ refreshTokenExpiresAt?: Date | null;
82
+ idToken?: string;
83
+ scopes?: string[];
84
+ } | null>;
85
+ } | null> {
86
+ const socialProviders =
87
+ typeof socialProvidersSource === "function"
88
+ ? await socialProvidersSource()
89
+ : await Promise.resolve(socialProvidersSource);
90
+
91
+ if (Array.isArray(socialProviders)) {
92
+ return (
93
+ socialProviders.find(
94
+ (provider: { id?: string }) => provider?.id === "entra-external-id",
95
+ ) || null
96
+ );
97
+ }
98
+
99
+ if (socialProviders && typeof socialProviders === "object") {
100
+ return (
101
+ (
102
+ socialProviders as Record<
103
+ string,
104
+ {
105
+ id?: string;
106
+ refreshAccessToken?: (token: string) => Promise<{
107
+ accessToken?: string;
108
+ refreshToken?: string;
109
+ accessTokenExpiresAt?: Date | null;
110
+ refreshTokenExpiresAt?: Date | null;
111
+ idToken?: string;
112
+ scopes?: string[];
113
+ } | null>;
114
+ }
115
+ >
116
+ )["entra-external-id"] || null
117
+ );
118
+ }
119
+
120
+ return null;
121
+ }
122
+
123
+ async function updateStoredEntraTokens(
124
+ account: EntraAccount,
125
+ refreshed: {
126
+ accessToken?: string;
127
+ refreshToken?: string;
128
+ accessTokenExpiresAt?: Date | null;
129
+ refreshTokenExpiresAt?: Date | null;
130
+ idToken?: string;
131
+ scopes?: string[];
132
+ } | null,
133
+ context: Parameters<typeof decryptOAuthToken>[1] & {
134
+ internalAdapter: {
135
+ updateAccount: (
136
+ accountId: string,
137
+ data: Record<string, unknown>,
138
+ ) => Promise<unknown>;
139
+ };
140
+ },
141
+ ): Promise<void> {
142
+ if (!account.id || !refreshed) return;
143
+
144
+ const updatedAccessToken = refreshed.accessToken
145
+ ? await setTokenUtil(refreshed.accessToken, context)
146
+ : account.accessToken;
147
+ const updatedRefreshToken = refreshed.refreshToken
148
+ ? await setTokenUtil(refreshed.refreshToken, context)
149
+ : account.refreshToken;
150
+
151
+ await context.internalAdapter.updateAccount(account.id, {
152
+ accessToken: updatedAccessToken,
153
+ refreshToken: updatedRefreshToken,
154
+ accessTokenExpiresAt:
155
+ refreshed.accessTokenExpiresAt || account.accessTokenExpiresAt,
156
+ refreshTokenExpiresAt:
157
+ refreshed.refreshTokenExpiresAt || account.refreshTokenExpiresAt,
158
+ idToken: refreshed.idToken || account.idToken,
159
+ scope: refreshed.scopes?.join(",") || account.scope,
160
+ });
161
+ }
162
+
163
+ async function refreshEntraTokens(
164
+ account: EntraAccount,
165
+ context: Parameters<typeof decryptOAuthToken>[1] & {
166
+ socialProviders: unknown;
167
+ internalAdapter: {
168
+ updateAccount: (
169
+ accountId: string,
170
+ data: Record<string, unknown>,
171
+ ) => Promise<unknown>;
172
+ };
173
+ },
174
+ ): Promise<{
175
+ accessToken?: string;
176
+ refreshToken?: string;
177
+ accessTokenExpiresAt?: Date | null;
178
+ refreshTokenExpiresAt?: Date | null;
179
+ idToken?: string;
180
+ scopes?: string[];
181
+ } | null> {
182
+ const provider = await resolveEntraRefreshProvider(context.socialProviders);
183
+ if (!provider?.refreshAccessToken) {
184
+ console.warn(
185
+ "[better-auth] proactive refresh skipped: provider not resolvable",
186
+ );
187
+ return null;
188
+ }
189
+
190
+ const refreshToken = await decryptOAuthToken(
191
+ account.refreshToken || "",
192
+ context,
193
+ );
194
+ const refreshed = await provider.refreshAccessToken(refreshToken);
195
+ await updateStoredEntraTokens(account, refreshed, context);
196
+ return refreshed;
197
+ }
4
198
 
5
199
  const tenantName = process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_TENANT_NAME || "";
6
200
  const loginAuthority = `https://${tenantName}.ciamlogin.com/${tenantName}.onmicrosoft.com`;
7
201
 
8
- export const auth = betterAuth({
202
+ const globalForAuthDb = globalThis as typeof globalThis & {
203
+ storefrontAuthDbPool?: Pool;
204
+ };
205
+
206
+ const authDbPool =
207
+ globalForAuthDb.storefrontAuthDbPool ||
208
+ new Pool({
209
+ connectionString: process.env.AUTH_DB_URL,
210
+ });
211
+
212
+ if (process.env.NODE_ENV !== "production") {
213
+ globalForAuthDb.storefrontAuthDbPool = authDbPool;
214
+ }
215
+
216
+ const authConfig = {
9
217
  secret: process.env.BETTER_AUTH_SECRET,
10
- baseURL: process.env.BETTER_AUTH_URL || process.env.NEXTAUTH_URL,
218
+ baseURL: process.env.BETTER_AUTH_URL,
219
+ database: authDbPool,
220
+ // Required for native screen login: Entra ROPC validates the password,
221
+ // Better Auth uses email/password internally only to create a session.
222
+ // The entra-external-id account with real OAuth tokens is upserted separately.
223
+ emailAndPassword: {
224
+ enabled: true,
225
+ },
11
226
  session: {
12
227
  cookieCache: {
13
- enabled: true,
14
- maxAge: 60 * 60, // 1 hour
15
- strategy: "jwe",
16
- refreshCache: true,
228
+ enabled: false,
17
229
  },
18
230
  },
19
231
  account: {
@@ -24,10 +236,10 @@ export const auth = betterAuth({
24
236
  genericOAuth({
25
237
  config: [
26
238
  {
239
+ // Entra External ID is configured as public client.
240
+ // No client secret is sent during refresh (Entra rejects it).
27
241
  providerId: "entra-external-id",
28
242
  clientId: process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_CLIENT_ID || "",
29
- clientSecret:
30
- process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_CLIENT_SECRET || "",
31
243
  discoveryUrl: `${loginAuthority}/v2.0/.well-known/openid-configuration`,
32
244
  scopes: (
33
245
  process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_CUSTOM_SCOPES ||
@@ -60,30 +272,85 @@ export const auth = betterAuth({
60
272
  },
61
273
  ],
62
274
  }),
275
+ // Custom session plugin that:
276
+ // 1. Reads Entra account tokens from public.account table
277
+ // 2. Attempts proactive refresh 5 minutes before expiry
278
+ // 3. Prevents unexpected signout by keeping current token until truly expired
63
279
  customSession(async ({ user, session }, ctx) => {
64
280
  let accessToken = "";
281
+ let graphAccessToken = "";
65
282
  try {
66
- const accounts = await ctx.context.internalAdapter.findAccounts(
283
+ const accounts = (await ctx.context.internalAdapter.findAccounts(
67
284
  user.id,
68
- );
285
+ )) as EntraAccount[] | undefined;
69
286
  const account = accounts?.find(
70
- (a) => a.providerId === "entra-external-id",
287
+ (storedAccount) => storedAccount.providerId === "entra-external-id",
71
288
  );
72
- // For Entra External ID, use id_token as the access token
73
- accessToken = account?.idToken || account?.accessToken || "";
289
+ if (!account) {
290
+ return {
291
+ user,
292
+ session: {
293
+ ...session,
294
+ accessToken,
295
+ graphAccessToken,
296
+ },
297
+ };
298
+ }
299
+
300
+ const tokenExpired = isPast(account.accessTokenExpiresAt);
301
+ const shouldRefreshUpfront = isWithinMs(
302
+ account.accessTokenExpiresAt,
303
+ ACCESS_TOKEN_REFRESH_WINDOW_MS,
304
+ );
305
+ const currentIdToken = account.idToken || "";
306
+ const currentGraphToken = await decryptStoredOAuthToken(
307
+ account.accessToken,
308
+ ctx.context,
309
+ );
310
+
311
+ // Keep ID token for D365 and access token for Graph while valid.
312
+ accessToken = tokenExpired ? "" : currentIdToken;
313
+ graphAccessToken = tokenExpired ? "" : currentGraphToken;
314
+
315
+ // Attempt proactive refresh if token is within 5-minute window and a refresh token exists.
316
+ // This ensures users stay signed in without waiting until token expires.
317
+ if (account.refreshToken && shouldRefreshUpfront) {
318
+ try {
319
+ const refreshed = await refreshEntraTokens(account, ctx.context);
320
+ if (refreshed) {
321
+ // Keep ID token for D365 and access token for Graph APIs.
322
+ accessToken = refreshed.idToken || account.idToken || accessToken;
323
+ graphAccessToken = refreshed.accessToken || graphAccessToken;
324
+ }
325
+ } catch (error) {
326
+ // Preserve the current token and avoid signing out early on refresh failures.
327
+ console.warn(
328
+ "[better-auth] proactive refresh failed for entra-external-id",
329
+ error,
330
+ );
331
+ }
332
+ }
74
333
  } catch {
75
- // Account data may not be accessible in all modes
334
+ // Account data may not be accessible in all modes.
335
+ // Keep existing empty default if we cannot read account data.
76
336
  }
77
337
  return {
78
338
  user,
79
339
  session: {
80
340
  ...session,
81
341
  accessToken,
342
+ graphAccessToken,
82
343
  },
83
344
  };
84
345
  }),
85
346
  nextCookies(), // must be last plugin
86
347
  ],
87
- });
348
+ } satisfies Parameters<typeof betterAuth>[0];
349
+
350
+ const auth: Auth<BetterAuthOptions> = betterAuth(
351
+ authConfig as BetterAuthOptions,
352
+ );
353
+
354
+ export { auth };
88
355
 
89
356
  export type Session = typeof auth.$Infer.Session;
@@ -41,7 +41,7 @@ const componentMap = {
41
41
  const getBlockComponent: <B extends Block>(
42
42
  block: B,
43
43
  idx: number,
44
- ) => JSX.Element | null = (block: Block) => {
44
+ ) => React.JSX.Element | null = (block: Block) => {
45
45
  const Component = componentMap[
46
46
  block.__component as keyof typeof componentMap
47
47
  ] as React.ComponentType<Block> | undefined;
@@ -31,7 +31,9 @@ export default async function ProductSectionFullWidth(
31
31
 
32
32
  const skus = data?.ProductWrapper?.map((element) => element?.product?.sku);
33
33
 
34
- const e4Products = await getProductByIds.fetchData(queryClient, { id: skus });
34
+ const e4Products = (await getProductByIds.fetchData(queryClient, {
35
+ id: skus,
36
+ })) as SimpleProduct[] | undefined;
35
37
 
36
38
  if (!e4Products?.length) {
37
39
  return null;
@@ -1,5 +1,3 @@
1
- import type { Formats } from "@/cms/interfaces/product-data";
2
-
3
1
  const apiKeys = {
4
2
  pages: "/cms/pages",
5
3
  blogs: "/cms/blogs",
@@ -42,6 +40,13 @@ export const extendCmsEndpoints = (
42
40
 
43
41
  export type sizes = "small" | "medium" | "large" | "thumbnail";
44
42
 
43
+ interface Formats {
44
+ small?: { url: string };
45
+ medium?: { url: string };
46
+ large?: { url: string };
47
+ thumbnail?: { url: string };
48
+ }
49
+
45
50
  /**
46
51
  * @param formats formats array from CMS data
47
52
  * @param size size of the image
@@ -34,7 +34,7 @@ export default function BazaarMenu({
34
34
  {handler ? handler(handleClick) : null}
35
35
 
36
36
  <Menu
37
- elevation={0}
37
+ slotProps={{ paper: { elevation: 0 } }}
38
38
  anchorEl={anchorEl}
39
39
  onClose={handleClose}
40
40
  open={open ?? Boolean(anchorEl)}
@@ -6,9 +6,11 @@ import clsx from "clsx";
6
6
 
7
7
  // ==============================================================
8
8
  type Ellipsis = { ellipsis: number };
9
- export interface EllipsisProps extends BoxProps {
9
+ export type EllipsisProps = BoxProps & {
10
10
  ellipsis?: boolean;
11
- }
11
+ children?: React.ReactNode;
12
+ className?: string;
13
+ };
12
14
  // ==============================================================
13
15
 
14
16
  const StyledBox = styled(Box, {
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from "@storybook/react";
1
+ import type { Meta, StoryObj } from "@storybook/nextjs";
2
2
  import CarouselCard1 from "./index";
3
3
 
4
4
  const meta: Meta<typeof CarouselCard1> = {
@@ -40,7 +40,12 @@ const CategoryList = ({
40
40
  children,
41
41
  }: RootProps) => {
42
42
  const { data: fetchedCategories } = getCategories.useSuspenseData();
43
- const cats = categories ?? fetchedCategories ?? [];
43
+ let cats: typeof categories = [];
44
+ if (Array.isArray(categories)) {
45
+ cats = categories;
46
+ } else if (Array.isArray(fetchedCategories)) {
47
+ cats = fetchedCategories;
48
+ }
44
49
 
45
50
  const megaMenuRenderers: Record<
46
51
  MegaMenuType,
@@ -69,7 +74,8 @@ CategoryList.Item = ({ category }: ItemProps) => {
69
74
  if (!ctx) return null;
70
75
  const { megaMenuRenderers } = ctx;
71
76
  const { RecordId, Name, Url, Children } = category;
72
- const hasChildren = !!Children?.length;
77
+ const childCategories = Array.isArray(Children) ? Children : [];
78
+ const hasChildren = childCategories.length > 0;
73
79
 
74
80
  // Sub menu can also be "categories/mega-menu/mega-menu-1" or "mega-menu-2",
75
81
  // Mega menus have submenu items listed in columns and also have option to show banners(offers).
@@ -83,9 +89,7 @@ CategoryList.Item = ({ category }: ItemProps) => {
83
89
  title={Name ?? ""}
84
90
  caret={hasChildren}
85
91
  render={
86
- Children && Children.length > 0
87
- ? megaMenuRenderers[menuType]?.(Children)
88
- : null
92
+ hasChildren ? megaMenuRenderers[menuType]?.(childCategories) : null
89
93
  }
90
94
  />
91
95
  );
@@ -24,7 +24,7 @@ const Wrapper = styled("div", {
24
24
  type Props = {
25
25
  render: (
26
26
  handler: (e: React.MouseEvent<HTMLButtonElement>) => void,
27
- ) => JSX.Element;
27
+ ) => React.ReactElement;
28
28
  };
29
29
 
30
30
  // CategoryMenu is acting purely as a configuration/wiring component,
@@ -57,6 +57,10 @@ export default function CountryInput({
57
57
  fullWidth
58
58
  value={selectedCountry}
59
59
  options={countries}
60
+ getOptionKey={(option) => option.CountryRegionId}
61
+ isOptionEqualToValue={(option, value) =>
62
+ option.CountryRegionId === value?.CountryRegionId
63
+ }
60
64
  onChange={(_event, newValue) => {
61
65
  setSelectedCountry(newValue);
62
66
  }}
@@ -1,16 +1,13 @@
1
1
  import signOut from "@evenicanpm/storefront-core/src/auth/signout";
2
2
  import Header from "@evenicanpm/storefront-core/src/components/header/header";
3
- import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
4
  import { NextIntlClientProvider } from "next-intl";
5
5
  import { vi } from "vitest";
6
6
 
7
- const { mockMutate, mockHandleRedirectPromise, mockUseSession } = vi.hoisted(
8
- () => ({
9
- mockMutate: vi.fn().mockResolvedValue({}),
10
- mockHandleRedirectPromise: vi.fn().mockResolvedValue(null),
11
- mockUseSession: vi.fn(),
12
- }),
13
- );
7
+ const { mockUseSession } = vi.hoisted(() => ({
8
+ mockMutate: vi.fn().mockResolvedValue({}),
9
+ mockUseSession: vi.fn(),
10
+ }));
14
11
 
15
12
  vi.mock(
16
13
  "@evenicanpm/storefront-core/src/api-manager/services/session/mutations/session-init",
@@ -19,13 +16,6 @@ vi.mock(
19
16
  }),
20
17
  );
21
18
 
22
- vi.mock(
23
- "@evenicanpm/storefront-core/src/api-manager/services/user/mutations/update-user",
24
- () => ({
25
- default: () => ({ mutateAsync: mockMutate }),
26
- }),
27
- );
28
-
29
19
  vi.mock(
30
20
  "@evenicanpm/storefront-core/src/api-manager/services/session/mutations/session-logout",
31
21
  () => ({
@@ -53,13 +43,6 @@ vi.mock("better-auth/client/plugins", () => ({
53
43
  genericOAuthClient: () => ({}),
54
44
  }));
55
45
 
56
- vi.mock("@evenicanpm/storefront-core/src/auth/msal", () => ({
57
- getMsalInstance: () => ({
58
- handleRedirectPromise: mockHandleRedirectPromise,
59
- }),
60
- initializeMsal: vi.fn().mockResolvedValue(undefined),
61
- }));
62
-
63
46
  vi.mock("@evenicanpm/storefront-core/src/auth/signout", () => ({
64
47
  default: vi.fn(),
65
48
  }));
@@ -149,89 +132,4 @@ describe.skip("User Component", async () => {
149
132
  fireEvent.click(logoutButton);
150
133
  expect(mockSignOut).toHaveBeenCalled();
151
134
  });
152
-
153
- it("handles MSAL redirect and updates user on success", async () => {
154
- mockUseSession.mockReturnValue({
155
- data: {
156
- user: { email: "test@example.com" },
157
- session: { accessToken: "mockAccessToken" },
158
- },
159
- isPending: false,
160
- });
161
-
162
- vi.mocked(mockHandleRedirectPromise).mockResolvedValue({
163
- authority: "https://login.microsoftonline.com/some_authority",
164
- uniqueId: "some_unique_id",
165
- tenantId: "some_tenant_id",
166
- tokenType: "id_token",
167
- correlationId: "some_correlation_id",
168
- idToken: "some_id_token",
169
- accessToken: "some_access_token",
170
- fromCache: false,
171
- expiresOn: new Date(Date.now() + 3600000),
172
- scopes: ["some_scope"],
173
- account: {
174
- homeAccountId: "some_home_account_id",
175
- username: "some_username",
176
- localAccountId: "some_local_account_id",
177
- environment: "some_environment",
178
- tenantId: "some_tenant_id",
179
- },
180
- idTokenClaims: {
181
- given_name: "John",
182
- family_name: "Doe",
183
- },
184
- });
185
-
186
- render(
187
- <NextIntlClientProvider locale="en" messages={messages}>
188
- <Header>
189
- <Header.User />
190
- </Header>
191
- </NextIntlClientProvider>,
192
- );
193
-
194
- await waitFor(() =>
195
- expect(mockMutate).toHaveBeenCalledWith({
196
- AccountNumber: "",
197
- FirstName: "John",
198
- LastName: "Doe",
199
- }),
200
- );
201
- });
202
-
203
- it("handles error in MSAL redirect", async () => {
204
- const consoleErrorSpy = vi
205
- .spyOn(console, "error")
206
- .mockImplementation(vi.fn());
207
-
208
- mockUseSession.mockReturnValue({
209
- data: {
210
- user: { email: "test@example.com" },
211
- session: { accessToken: "mockAccessToken" },
212
- },
213
- isPending: false,
214
- });
215
-
216
- vi.mocked(mockHandleRedirectPromise).mockRejectedValue(
217
- new Error("MSAL Error"),
218
- );
219
-
220
- render(
221
- <NextIntlClientProvider locale="en" messages={messages}>
222
- <Header>
223
- <Header.User />
224
- </Header>
225
- </NextIntlClientProvider>,
226
- );
227
-
228
- await waitFor(() =>
229
- expect(consoleErrorSpy).toHaveBeenCalledWith(
230
- "MSAL redirect error:",
231
- expect.any(Error),
232
- ),
233
- );
234
-
235
- consoleErrorSpy.mockRestore();
236
- });
237
135
  });