@evenicanpm/storefront-core 2.4.0 → 2.4.2
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 +18 -0
- package/package.json +90 -77
- package/src/api-manager/datasources/d365/d365-address.datasource.ts +142 -66
- package/src/api-manager/datasources/d365/d365-user.datasource.ts +109 -37
- package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +44 -10
- package/src/api-manager/lib/get-graphql-client.ts +35 -7
- package/src/api-manager/services/create-query.ts +36 -3
- package/src/api-manager/services/get-query-client.ts +10 -0
- package/src/auth/better-auth.ts +282 -15
- package/src/cms/blocks/components/footer/data/index.ts +0 -0
- package/src/cms/blocks/components/footer/index.tsx +0 -0
- package/src/cms/blocks/components/footer/sections/footer-app-store.tsx +0 -0
- package/src/cms/blocks/components/footer/sections/footer-contact.tsx +0 -0
- package/src/cms/blocks/components/footer/sections/footer-links.tsx +0 -0
- package/src/cms/blocks/components/footer/sections/footer-logo.tsx +0 -0
- package/src/cms/blocks/components/footer/sections/footer-social-links.tsx +0 -0
- package/src/cms/blocks/components/footer/styles/index.ts +0 -0
- package/src/cms/blocks/components/hero-carousel/index.tsx +0 -0
- package/src/cms/blocks/components/product-carousel/index.tsx +0 -0
- package/src/cms/blocks/components/product-section-fullwidth/index.tsx +3 -1
- package/src/cms/blocks/components/shared/featured-product-card.tsx +0 -0
- package/src/cms/blocks/components/shared/product-category-item.tsx +0 -0
- package/src/cms/blocks/components/shared/product-grid.tsx +0 -0
- package/src/cms/blocks/components/shared/top-categories-card.tsx +0 -0
- package/src/cms/blocks/components/shared/top-rating-product-card.tsx +0 -0
- package/src/cms/blocks/icons/components/category.tsx +0 -0
- package/src/cms/blocks/icons/components/dotted-star.tsx +0 -0
- package/src/cms/blocks/icons/components/gift-box.tsx +0 -0
- package/src/cms/blocks/icons/components/light.tsx +0 -0
- package/src/cms/blocks/icons/components/new-arrival.tsx +0 -0
- package/src/cms/blocks/icons/components/rank-badge.tsx +0 -0
- package/src/components/categories/category-list/category-list.tsx +9 -5
- package/src/components/header/__tests__/user.test.tsx +5 -107
- package/src/components/header/components/user.tsx +72 -45
- package/src/hooks/use-nextauth-session.ts +0 -33
- package/src/lib/auth-client.ts +14 -0
- package/src/lib/auth.ts +4 -0
- package/src/lib/entra-native-auth.ts +138 -0
- package/src/lib/graphqlRequestSdk.ts +0 -0
- package/src/lib/native-session.ts +434 -0
- package/src/pages/account/profile/__tests__/profile-form.test.tsx +173 -0
- package/src/pages/account/profile/profile-form.tsx +285 -0
- package/src/pages/account/profile/profile-validation.ts +52 -0
- package/src/pages/auth/auth-activate-page.tsx +509 -0
- package/src/pages/auth/auth-layout.tsx +73 -0
- package/src/pages/auth/auth-login-page.tsx +241 -0
- package/src/pages/auth/auth-reset-password-page.tsx +487 -0
- package/src/pages/auth/compound/auth-form.tsx +182 -0
- package/src/pages/auth/compound/auth-pages.tsx +21 -0
- package/src/pages/auth/lib/friendly-error.ts +70 -0
- package/src/pages/auth/lib/index.ts +2 -0
- package/src/pages/auth/lib/types.ts +5 -0
- package/src/pages/checkout/checkout-alt-form/steps/address/delivery-address.tsx +20 -6
- package/src/pages/checkout/checkout-alt-form/steps/customer-info/customer-information.tsx +1 -1
- package/tsconfig.json +20 -14
- package/src/auth/msal.ts +0 -65
- package/src/pages/account/profile/profile-button.test.tsx +0 -59
- package/src/pages/account/profile/profile-button.tsx +0 -51
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Box,
|
|
5
|
+
type BoxProps,
|
|
6
|
+
Button,
|
|
7
|
+
CircularProgress,
|
|
8
|
+
TextField,
|
|
9
|
+
} from "@mui/material";
|
|
10
|
+
import type { ChangeEvent, FC } from "react";
|
|
11
|
+
|
|
12
|
+
interface AuthFormSubmitButtonProps {
|
|
13
|
+
label: string;
|
|
14
|
+
loading: boolean;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
color?: "primary" | "secondary";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface AuthFormPasswordFieldsProps {
|
|
20
|
+
password: string;
|
|
21
|
+
confirmPassword: string;
|
|
22
|
+
onPasswordChange: (value: string) => void;
|
|
23
|
+
onConfirmChange: (value: string) => void;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
autoFocus?: boolean;
|
|
26
|
+
label?: string;
|
|
27
|
+
confirmLabel?: string;
|
|
28
|
+
mismatchHelperText?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface AuthFormOtpFieldProps {
|
|
32
|
+
value: string;
|
|
33
|
+
onChange: (value: string) => void;
|
|
34
|
+
codeLength: number;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
autoFocus?: boolean;
|
|
37
|
+
label?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface AuthFormErrorProps {
|
|
41
|
+
message: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const AuthFormError = ({ message }: AuthFormErrorProps) => {
|
|
45
|
+
if (!message) return null;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Box
|
|
49
|
+
role="alert"
|
|
50
|
+
sx={{
|
|
51
|
+
mb: 2,
|
|
52
|
+
color: "error.main",
|
|
53
|
+
fontSize: "0.875rem",
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
{message}
|
|
57
|
+
</Box>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const AuthFormSubmitButton = ({
|
|
62
|
+
label,
|
|
63
|
+
loading,
|
|
64
|
+
disabled = false,
|
|
65
|
+
color = "primary",
|
|
66
|
+
}: AuthFormSubmitButtonProps) => {
|
|
67
|
+
return (
|
|
68
|
+
<Button
|
|
69
|
+
type="submit"
|
|
70
|
+
variant="contained"
|
|
71
|
+
color={color}
|
|
72
|
+
fullWidth
|
|
73
|
+
size="large"
|
|
74
|
+
disabled={loading || disabled}
|
|
75
|
+
sx={{ borderRadius: 2, py: 1.4 }}
|
|
76
|
+
>
|
|
77
|
+
{loading ? <CircularProgress size={20} color="inherit" /> : label}
|
|
78
|
+
</Button>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const AuthFormPasswordFields = ({
|
|
83
|
+
password,
|
|
84
|
+
confirmPassword,
|
|
85
|
+
onPasswordChange,
|
|
86
|
+
onConfirmChange,
|
|
87
|
+
disabled = false,
|
|
88
|
+
autoFocus = false,
|
|
89
|
+
label = "Password",
|
|
90
|
+
confirmLabel = "Confirm password",
|
|
91
|
+
mismatchHelperText = "Passwords do not match",
|
|
92
|
+
}: AuthFormPasswordFieldsProps) => {
|
|
93
|
+
const mismatch = confirmPassword.length > 0 && password !== confirmPassword;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<>
|
|
97
|
+
<TextField
|
|
98
|
+
label={label}
|
|
99
|
+
type="password"
|
|
100
|
+
value={password}
|
|
101
|
+
onChange={(
|
|
102
|
+
event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
|
103
|
+
) => onPasswordChange(event.target.value)}
|
|
104
|
+
fullWidth
|
|
105
|
+
required
|
|
106
|
+
autoComplete="new-password"
|
|
107
|
+
autoFocus={autoFocus}
|
|
108
|
+
sx={{ mb: 2 }}
|
|
109
|
+
disabled={disabled}
|
|
110
|
+
/>
|
|
111
|
+
<TextField
|
|
112
|
+
label={confirmLabel}
|
|
113
|
+
type="password"
|
|
114
|
+
value={confirmPassword}
|
|
115
|
+
onChange={(
|
|
116
|
+
event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
|
117
|
+
) => onConfirmChange(event.target.value)}
|
|
118
|
+
fullWidth
|
|
119
|
+
required
|
|
120
|
+
autoComplete="new-password"
|
|
121
|
+
error={mismatch}
|
|
122
|
+
helperText={mismatch ? mismatchHelperText : undefined}
|
|
123
|
+
sx={{ mb: 3 }}
|
|
124
|
+
disabled={disabled}
|
|
125
|
+
/>
|
|
126
|
+
</>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const AuthFormOtpField = ({
|
|
131
|
+
value,
|
|
132
|
+
onChange,
|
|
133
|
+
codeLength,
|
|
134
|
+
disabled = false,
|
|
135
|
+
autoFocus = true,
|
|
136
|
+
label = "Verification code",
|
|
137
|
+
}: AuthFormOtpFieldProps) => {
|
|
138
|
+
return (
|
|
139
|
+
<TextField
|
|
140
|
+
label={label}
|
|
141
|
+
type="text"
|
|
142
|
+
value={value}
|
|
143
|
+
onChange={(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
|
|
144
|
+
onChange(event.target.value.replace(/\D/g, "").slice(0, codeLength))
|
|
145
|
+
}
|
|
146
|
+
fullWidth
|
|
147
|
+
required
|
|
148
|
+
autoFocus={autoFocus}
|
|
149
|
+
autoComplete="one-time-code"
|
|
150
|
+
inputProps={{
|
|
151
|
+
maxLength: codeLength,
|
|
152
|
+
style: {
|
|
153
|
+
textAlign: "center",
|
|
154
|
+
letterSpacing: "0.4em",
|
|
155
|
+
fontSize: "1.3rem",
|
|
156
|
+
},
|
|
157
|
+
}}
|
|
158
|
+
sx={{ mb: 3 }}
|
|
159
|
+
disabled={disabled}
|
|
160
|
+
/>
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
type AuthFormProps = BoxProps<"form">;
|
|
165
|
+
|
|
166
|
+
type AuthFormComponent = FC<AuthFormProps> & {
|
|
167
|
+
Error: typeof AuthFormError;
|
|
168
|
+
SubmitButton: typeof AuthFormSubmitButton;
|
|
169
|
+
PasswordFields: typeof AuthFormPasswordFields;
|
|
170
|
+
OtpField: typeof AuthFormOtpField;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const AuthForm = ((props: AuthFormProps) => (
|
|
174
|
+
<Box component="form" noValidate {...props} />
|
|
175
|
+
)) as AuthFormComponent;
|
|
176
|
+
|
|
177
|
+
AuthForm.Error = AuthFormError;
|
|
178
|
+
AuthForm.SubmitButton = AuthFormSubmitButton;
|
|
179
|
+
AuthForm.PasswordFields = AuthFormPasswordFields;
|
|
180
|
+
AuthForm.OtpField = AuthFormOtpField;
|
|
181
|
+
|
|
182
|
+
export default AuthForm;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import AuthActivatePage from "@evenicanpm/storefront-core/src/pages/auth/auth-activate-page";
|
|
4
|
+
import AuthLoginPage from "@evenicanpm/storefront-core/src/pages/auth/auth-login-page";
|
|
5
|
+
import AuthResetPasswordPage from "@evenicanpm/storefront-core/src/pages/auth/auth-reset-password-page";
|
|
6
|
+
|
|
7
|
+
type AuthPagesComponent = ((props: { children?: unknown }) => unknown) & {
|
|
8
|
+
Login: typeof AuthLoginPage;
|
|
9
|
+
Activate: typeof AuthActivatePage;
|
|
10
|
+
ResetPassword: typeof AuthResetPasswordPage;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const AuthPages = (({ children }: { children?: unknown }) => (
|
|
14
|
+
<>{children}</>
|
|
15
|
+
)) as AuthPagesComponent;
|
|
16
|
+
|
|
17
|
+
AuthPages.Login = AuthLoginPage;
|
|
18
|
+
AuthPages.Activate = AuthActivatePage;
|
|
19
|
+
AuthPages.ResetPassword = AuthResetPasswordPage;
|
|
20
|
+
|
|
21
|
+
export default AuthPages;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
type Translate = (key: string) => string;
|
|
2
|
+
const genericMessage = "Something went wrong. Please try again.";
|
|
3
|
+
|
|
4
|
+
const defaultMessages: Record<string, string> = {
|
|
5
|
+
"backend.passwordTooWeak": "Password is too weak.",
|
|
6
|
+
"backend.passwordTooShort": "Password is too short.",
|
|
7
|
+
"backend.passwordTooLong": "Password is too long.",
|
|
8
|
+
"backend.passwordRecentlyUsed": "Password was recently used.",
|
|
9
|
+
"backend.passwordBanned": "Password is not allowed.",
|
|
10
|
+
"backend.passwordInvalid": "Password is invalid.",
|
|
11
|
+
"backend.invalidVerificationCode": "Verification code is invalid.",
|
|
12
|
+
"backend.userAlreadyExists": "An account already exists.",
|
|
13
|
+
"backend.userNotFound": "No account found for this email.",
|
|
14
|
+
"backend.redirectRequired":
|
|
15
|
+
"This tenant requires browser-based authentication.",
|
|
16
|
+
"backend.resetFailed": "Password reset failed.",
|
|
17
|
+
"backend.timeout": "Request timed out. Please try again.",
|
|
18
|
+
generic: genericMessage,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const translateOrDefault = (t: Translate | undefined, key: string): string => {
|
|
22
|
+
if (t) return t(key);
|
|
23
|
+
|
|
24
|
+
return defaultMessages[key] ?? genericMessage;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function friendlyError(
|
|
28
|
+
error: string,
|
|
29
|
+
suberror?: string,
|
|
30
|
+
t?: Translate,
|
|
31
|
+
): string {
|
|
32
|
+
if (suberror === "password_too_weak") {
|
|
33
|
+
return translateOrDefault(t, "backend.passwordTooWeak");
|
|
34
|
+
}
|
|
35
|
+
if (suberror === "password_too_short") {
|
|
36
|
+
return translateOrDefault(t, "backend.passwordTooShort");
|
|
37
|
+
}
|
|
38
|
+
if (suberror === "password_too_long") {
|
|
39
|
+
return translateOrDefault(t, "backend.passwordTooLong");
|
|
40
|
+
}
|
|
41
|
+
if (suberror === "password_recently_used") {
|
|
42
|
+
return translateOrDefault(t, "backend.passwordRecentlyUsed");
|
|
43
|
+
}
|
|
44
|
+
if (suberror === "password_banned") {
|
|
45
|
+
return translateOrDefault(t, "backend.passwordBanned");
|
|
46
|
+
}
|
|
47
|
+
if (suberror === "password_is_invalid") {
|
|
48
|
+
return translateOrDefault(t, "backend.passwordInvalid");
|
|
49
|
+
}
|
|
50
|
+
if (suberror === "invalid_oob_value") {
|
|
51
|
+
return translateOrDefault(t, "backend.invalidVerificationCode");
|
|
52
|
+
}
|
|
53
|
+
if (error === "user_already_exists") {
|
|
54
|
+
return translateOrDefault(t, "backend.userAlreadyExists");
|
|
55
|
+
}
|
|
56
|
+
if (error === "user_not_found") {
|
|
57
|
+
return translateOrDefault(t, "backend.userNotFound");
|
|
58
|
+
}
|
|
59
|
+
if (error === "redirect_required") {
|
|
60
|
+
return translateOrDefault(t, "backend.redirectRequired");
|
|
61
|
+
}
|
|
62
|
+
if (error === "reset_failed") {
|
|
63
|
+
return translateOrDefault(t, "backend.resetFailed");
|
|
64
|
+
}
|
|
65
|
+
if (error === "timeout") {
|
|
66
|
+
return translateOrDefault(t, "backend.timeout");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return translateOrDefault(t, "generic");
|
|
70
|
+
}
|
|
@@ -105,7 +105,8 @@ const DeliveryAddress: DeliveryAddressCompound = ({
|
|
|
105
105
|
|
|
106
106
|
// user + guest logic
|
|
107
107
|
const { data: user, isLoading: userLoading } = getUser.useData();
|
|
108
|
-
const
|
|
108
|
+
const isAuthenticatedUser = !!user;
|
|
109
|
+
const isGuest = !userLoading && !isAuthenticatedUser;
|
|
109
110
|
|
|
110
111
|
// dropdown visibility
|
|
111
112
|
const showDropDown = useDropDown && !isGuest;
|
|
@@ -165,9 +166,22 @@ const DeliveryAddress: DeliveryAddressCompound = ({
|
|
|
165
166
|
|
|
166
167
|
const handleAuthFlow = async (address: Address) => {
|
|
167
168
|
const isNew = !address.Id;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
let saved: Address;
|
|
170
|
+
if (isNew) {
|
|
171
|
+
try {
|
|
172
|
+
saved = await createAddress(address);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
175
|
+
// Newly created async customers may not be ready for address persistence yet.
|
|
176
|
+
if (message.includes("CreatingAsyncAddressesNotSupported")) {
|
|
177
|
+
saved = address;
|
|
178
|
+
} else {
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
saved = await updateAddress(address);
|
|
184
|
+
}
|
|
171
185
|
|
|
172
186
|
// update local list
|
|
173
187
|
if (isNew) {
|
|
@@ -184,12 +198,12 @@ const DeliveryAddress: DeliveryAddressCompound = ({
|
|
|
184
198
|
};
|
|
185
199
|
|
|
186
200
|
const handleSaveAddress = async (address: Address) => {
|
|
187
|
-
if (
|
|
201
|
+
if (!isAuthenticatedUser || userLoading) return handleGuestFlow(address);
|
|
188
202
|
return handleAuthFlow(address);
|
|
189
203
|
};
|
|
190
204
|
|
|
191
205
|
const handleDelete = async (address: Address) => {
|
|
192
|
-
if (
|
|
206
|
+
if (isAuthenticatedUser && address.Id) {
|
|
193
207
|
await deleteAddress(address.Id);
|
|
194
208
|
}
|
|
195
209
|
setAddressList((prev) => prev.filter((a) => a.Id !== address.Id));
|
|
@@ -39,7 +39,7 @@ const CustomerInformation = (
|
|
|
39
39
|
} = props;
|
|
40
40
|
|
|
41
41
|
const { data: user, isLoading: userLoading } = getUser.useData();
|
|
42
|
-
const isGuest = !userLoading && !user
|
|
42
|
+
const isGuest = !userLoading && !user;
|
|
43
43
|
|
|
44
44
|
const resolveEmail = () => {
|
|
45
45
|
if (isGuest) return cartEmail;
|
package/tsconfig.json
CHANGED
|
@@ -7,25 +7,31 @@
|
|
|
7
7
|
"esModuleInterop": true,
|
|
8
8
|
"skipLibCheck": true,
|
|
9
9
|
"declaration": true,
|
|
10
|
-
"moduleResolution": "
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
11
|
"resolveJsonModule": true,
|
|
12
12
|
"outDir": "./dist",
|
|
13
|
+
"rootDir": "./src",
|
|
13
14
|
"jsx": "preserve",
|
|
14
|
-
"baseUrl": ".",
|
|
15
15
|
"paths": {
|
|
16
|
+
"@evenicanpm/cms/src/*": ["../cms/src/*"],
|
|
16
17
|
"@evenicanpm/storefront-core/src/*": ["./src/*"],
|
|
17
|
-
"@evenicanpm/storefront-core/src/lib/*": ["src/lib/*"],
|
|
18
|
-
"@evenicanpm/storefront-core/src/api-manager/*": ["src/api-manager/*"],
|
|
19
|
-
"@evenicanpm/storefront-core/src/components/*": ["src/components/*"],
|
|
20
|
-
"@/auth/*": ["src/auth/*"],
|
|
21
|
-
"@/_components/*": ["src/components/_components/*"],
|
|
22
|
-
"@/components/*": ["src/components/*"],
|
|
23
|
-
"@/cms/*": ["src/cms/*"],
|
|
24
|
-
"@/schemas/*": ["src/api-manager/schemas/*"],
|
|
25
|
-
"@/services/*": ["src/api-manager/services/*"],
|
|
26
|
-
"@evenicanpm/storefront-core/src/hooks/*": ["src/hooks/*"],
|
|
27
|
-
"@config": ["src/config"],
|
|
28
|
-
"@messages/*": ["../../../../storefront/messages/*"]
|
|
18
|
+
"@evenicanpm/storefront-core/src/lib/*": ["./src/lib/*"],
|
|
19
|
+
"@evenicanpm/storefront-core/src/api-manager/*": ["./src/api-manager/*"],
|
|
20
|
+
"@evenicanpm/storefront-core/src/components/*": ["./src/components/*"],
|
|
21
|
+
"@/auth/*": ["./src/auth/*"],
|
|
22
|
+
"@/_components/*": ["./src/components/_components/*"],
|
|
23
|
+
"@/components/*": ["./src/components/*"],
|
|
24
|
+
"@/cms/*": ["./src/cms/*"],
|
|
25
|
+
"@/schemas/*": ["./src/api-manager/schemas/*"],
|
|
26
|
+
"@/services/*": ["./src/api-manager/services/*"],
|
|
27
|
+
"@evenicanpm/storefront-core/src/hooks/*": ["./src/hooks/*"],
|
|
28
|
+
"@config": ["./src/config"],
|
|
29
|
+
"@messages/*": ["../../../../storefront/messages/*"],
|
|
30
|
+
"*": [
|
|
31
|
+
"../../node_modules/.pnpm/node_modules/*",
|
|
32
|
+
"./node_modules/*",
|
|
33
|
+
"../../apps/storefront/node_modules/*"
|
|
34
|
+
]
|
|
29
35
|
}
|
|
30
36
|
},
|
|
31
37
|
"include": ["src"]
|
package/src/auth/msal.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type Configuration,
|
|
3
|
-
PublicClientApplication,
|
|
4
|
-
} from "@azure/msal-browser";
|
|
5
|
-
|
|
6
|
-
const createMsalConfig = (): Configuration => {
|
|
7
|
-
return {
|
|
8
|
-
auth: {
|
|
9
|
-
clientId: process.env.NEXT_PUBLIC_AZURE_AD_B2C_CLIENTID || "",
|
|
10
|
-
authority: process.env.NEXT_PUBLIC_AZURE_AD_B2C_LOGIN_AUTHORITY || "",
|
|
11
|
-
redirectUri: process.env.NEXT_PUBLIC_AZURE_AD_B2C_REDIRECT || "/",
|
|
12
|
-
postLogoutRedirectUri:
|
|
13
|
-
process.env.NEXT_PUBLIC_AZURE_AD_B2C_POST_LOGOUT_REDIRECT || "/",
|
|
14
|
-
knownAuthorities: [
|
|
15
|
-
process.env.NEXT_PUBLIC_AZURE_AD_B2C_KNOWN_AUTHORITY || "",
|
|
16
|
-
],
|
|
17
|
-
},
|
|
18
|
-
system: {},
|
|
19
|
-
};
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// create module singleton (to pass the env variables)
|
|
23
|
-
// const msalInstance = new PublicClientApplication(createMsalConfig());
|
|
24
|
-
let msalInstance: PublicClientApplication | null = null;
|
|
25
|
-
|
|
26
|
-
const initializeMsal = async () => {
|
|
27
|
-
try {
|
|
28
|
-
const config = createMsalConfig();
|
|
29
|
-
msalInstance = new PublicClientApplication(config);
|
|
30
|
-
await msalInstance.initialize();
|
|
31
|
-
|
|
32
|
-
const accounts = msalInstance.getAllAccounts();
|
|
33
|
-
if (accounts.length > 0) {
|
|
34
|
-
msalInstance.setActiveAccount(accounts[0]);
|
|
35
|
-
}
|
|
36
|
-
} catch (error) {
|
|
37
|
-
console.error("MSAL Initialization Error:", error);
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const getMsalInstance = () => {
|
|
42
|
-
if (!msalInstance) throw new Error("msalInstance not initialized");
|
|
43
|
-
return msalInstance;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const getRequestConfig = (requestType?: string) => {
|
|
47
|
-
const editAuthority = process.env.NEXT_PUBLIC_AZURE_AD_B2C_EDIT_AUTHORITY;
|
|
48
|
-
const resetAuthority = process.env.NEXT_PUBLIC_AZURE_AD_B2C_RESET_AUTHORITY;
|
|
49
|
-
|
|
50
|
-
const baseRequest = {
|
|
51
|
-
scopes: [process.env.NEXT_PUBLIC_AZURE_AD_B2C_API_PERMISSION],
|
|
52
|
-
authority: createMsalConfig().auth.authority,
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
switch (requestType) {
|
|
56
|
-
case "edit":
|
|
57
|
-
return { ...baseRequest, authority: editAuthority };
|
|
58
|
-
case "reset":
|
|
59
|
-
return { ...baseRequest, authority: resetAuthority };
|
|
60
|
-
default:
|
|
61
|
-
return baseRequest;
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export { getMsalInstance, getRequestConfig, initializeMsal };
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import ProfileButton from "@evenicanpm/storefront-core/src/pages/account/profile/profile-button";
|
|
2
|
-
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
-
import { vi } from "vitest";
|
|
4
|
-
import { getMsalInstance, getRequestConfig, initializeMsal } from "@/auth/msal";
|
|
5
|
-
|
|
6
|
-
vi.mock("@/auth/msal", () => ({
|
|
7
|
-
initializeMsal: vi.fn(),
|
|
8
|
-
getMsalInstance: vi.fn(),
|
|
9
|
-
getRequestConfig: vi.fn(),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
vi.mock("@evenicanpm/storefront-core/src/components/T/index", () => ({
|
|
13
|
-
default: ({ path }: { path: string }) => <span>{path}</span>,
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
describe("ProfileButton Component", () => {
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
vi.restoreAllMocks();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("renders disabled button initially (MSAL not ready)", () => {
|
|
22
|
-
render(<ProfileButton type="edit" path="Account.Profile.btnEditProfile" />);
|
|
23
|
-
const button = screen.getByRole("button");
|
|
24
|
-
expect(button).toBeDisabled();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("initializes MSAL on mount", async () => {
|
|
28
|
-
render(<ProfileButton type="edit" path="Account.Profile.btnEditProfile" />);
|
|
29
|
-
await waitFor(() => {
|
|
30
|
-
expect(initializeMsal).toHaveBeenCalled();
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("calls MSAL loginRedirect when clicked", async () => {
|
|
35
|
-
(initializeMsal as ReturnType<typeof vi.fn>).mockImplementation(() =>
|
|
36
|
-
Promise.resolve(),
|
|
37
|
-
);
|
|
38
|
-
(getRequestConfig as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
39
|
-
scopes: ["user.read"],
|
|
40
|
-
});
|
|
41
|
-
(getMsalInstance as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
42
|
-
loginRedirect: vi.fn(),
|
|
43
|
-
});
|
|
44
|
-
render(<ProfileButton type="edit" path="Account.Profile.btnEditProfile" />);
|
|
45
|
-
|
|
46
|
-
await waitFor(() => {
|
|
47
|
-
expect(initializeMsal).toHaveBeenCalled();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const button = screen.getByRole("button");
|
|
51
|
-
fireEvent.click(button);
|
|
52
|
-
|
|
53
|
-
await waitFor(() => {
|
|
54
|
-
expect(getMsalInstance().loginRedirect).toHaveBeenCalledWith({
|
|
55
|
-
scopes: ["user.read"],
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
});
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import type { RedirectRequest } from "@azure/msal-browser";
|
|
4
|
-
import T from "@evenicanpm/storefront-core/src/components/T/index";
|
|
5
|
-
import { Button } from "@mui/material";
|
|
6
|
-
import { useEffect, useState } from "react";
|
|
7
|
-
import { getMsalInstance, getRequestConfig, initializeMsal } from "@/auth/msal";
|
|
8
|
-
|
|
9
|
-
interface Props {
|
|
10
|
-
type: string;
|
|
11
|
-
path: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const ProfileButton = ({ type, path }: Props) => {
|
|
15
|
-
const [isMsalReady, setIsMsalReady] = useState(false);
|
|
16
|
-
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
const initMsal = async () => {
|
|
19
|
-
await initializeMsal();
|
|
20
|
-
setIsMsalReady(true);
|
|
21
|
-
};
|
|
22
|
-
initMsal();
|
|
23
|
-
}, []);
|
|
24
|
-
|
|
25
|
-
const handleProfile = async () => {
|
|
26
|
-
if (!isMsalReady) {
|
|
27
|
-
console.error("MSAL is not initialized yet.");
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
const req = getRequestConfig(type);
|
|
33
|
-
await getMsalInstance().loginRedirect(req as RedirectRequest); // "handleRedirectPromise" is in user component.
|
|
34
|
-
} catch (error) {
|
|
35
|
-
console.error("Profile update through MSAL failed", error);
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<Button
|
|
41
|
-
onClick={() => handleProfile()}
|
|
42
|
-
disabled={!isMsalReady}
|
|
43
|
-
color="primary"
|
|
44
|
-
sx={{ bgcolor: "primary.light", px: 4 }}
|
|
45
|
-
>
|
|
46
|
-
<T path={path} />
|
|
47
|
-
</Button>
|
|
48
|
-
);
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export default ProfileButton;
|