@evenicanpm/storefront-core 2.4.0 → 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 (36) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/package.json +15 -2
  3. package/src/api-manager/datasources/d365/d365-address.datasource.ts +142 -66
  4. package/src/api-manager/datasources/d365/d365-user.datasource.ts +109 -37
  5. package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +44 -10
  6. package/src/api-manager/lib/get-graphql-client.ts +35 -7
  7. package/src/api-manager/services/create-query.ts +36 -3
  8. package/src/api-manager/services/get-query-client.ts +10 -0
  9. package/src/auth/better-auth.ts +282 -15
  10. package/src/cms/blocks/components/product-section-fullwidth/index.tsx +3 -1
  11. package/src/components/categories/category-list/category-list.tsx +9 -5
  12. package/src/components/header/__tests__/user.test.tsx +5 -107
  13. package/src/components/header/components/user.tsx +72 -45
  14. package/src/hooks/use-nextauth-session.ts +0 -33
  15. package/src/lib/auth-client.ts +14 -0
  16. package/src/lib/auth.ts +4 -0
  17. package/src/lib/entra-native-auth.ts +138 -0
  18. package/src/lib/native-session.ts +434 -0
  19. package/src/pages/account/profile/__tests__/profile-form.test.tsx +173 -0
  20. package/src/pages/account/profile/profile-form.tsx +285 -0
  21. package/src/pages/account/profile/profile-validation.ts +52 -0
  22. package/src/pages/auth/auth-activate-page.tsx +509 -0
  23. package/src/pages/auth/auth-layout.tsx +73 -0
  24. package/src/pages/auth/auth-login-page.tsx +241 -0
  25. package/src/pages/auth/auth-reset-password-page.tsx +487 -0
  26. package/src/pages/auth/compound/auth-form.tsx +182 -0
  27. package/src/pages/auth/compound/auth-pages.tsx +21 -0
  28. package/src/pages/auth/lib/friendly-error.ts +70 -0
  29. package/src/pages/auth/lib/index.ts +2 -0
  30. package/src/pages/auth/lib/types.ts +5 -0
  31. package/src/pages/checkout/checkout-alt-form/steps/address/delivery-address.tsx +20 -6
  32. package/src/pages/checkout/checkout-alt-form/steps/customer-info/customer-information.tsx +1 -1
  33. package/tsconfig.json +20 -14
  34. package/src/auth/msal.ts +0 -65
  35. package/src/pages/account/profile/profile-button.test.tsx +0 -59
  36. package/src/pages/account/profile/profile-button.tsx +0 -51
@@ -0,0 +1,285 @@
1
+ "use client";
2
+
3
+ import type { User } from "@evenicanpm/storefront-core/src/api-manager/schemas/user.schema";
4
+ import useUpdateUser from "@evenicanpm/storefront-core/src/api-manager/services/user/mutations/update-user";
5
+ import {
6
+ createProfileValidationSchema,
7
+ type ProfileFormValues,
8
+ } from "@evenicanpm/storefront-core/src/pages/account/profile/profile-validation";
9
+ import { useNotification } from "@evenicanpm/storefront-core/src/providers/notifications/use-notification";
10
+ import { Button, CircularProgress, TextField, Typography } from "@mui/material";
11
+ import Grid from "@mui/material/Grid2";
12
+ import { useQueryClient } from "@tanstack/react-query";
13
+ import { useRouter } from "next/navigation";
14
+ import { useTranslations } from "next-intl";
15
+ import {
16
+ type ChangeEvent,
17
+ createContext,
18
+ type PropsWithChildren,
19
+ type ReactNode,
20
+ type SyntheticEvent,
21
+ useContext,
22
+ useEffect,
23
+ useMemo,
24
+ useState,
25
+ } from "react";
26
+
27
+ interface ProfileFormContextValue {
28
+ values: ProfileFormValues;
29
+ touched: Partial<Record<keyof ProfileFormValues, boolean>>;
30
+ errors: Partial<Record<keyof ProfileFormValues, string>>;
31
+ isSubmitting: boolean;
32
+ handleChange: (name: keyof ProfileFormValues, value: string) => void;
33
+ handleBlur: (name: keyof ProfileFormValues) => void;
34
+ submitError: string | null;
35
+ setSubmitError: (value: string | null) => void;
36
+ handleSubmit: (event: SyntheticEvent<HTMLFormElement>) => Promise<void>;
37
+ }
38
+
39
+ const ProfileFormContext = createContext<ProfileFormContextValue | null>(null);
40
+
41
+ type ProfileFormProps = Readonly<
42
+ PropsWithChildren<{
43
+ user: User;
44
+ onBeforeSave?: (values: ProfileFormValues) => Promise<void>;
45
+ onSuccess?: () => void;
46
+ }>
47
+ >;
48
+
49
+ const ProfileForm = ({
50
+ user,
51
+ onBeforeSave,
52
+ onSuccess,
53
+ children,
54
+ }: ProfileFormProps) => {
55
+ const t = useTranslations("Account.Profile");
56
+ const router = useRouter();
57
+ const queryClient = useQueryClient();
58
+ const { setNotification } = useNotification();
59
+ const { mutateAsync: updateUser } = useUpdateUser();
60
+ const [submitError, setSubmitError] = useState<string | null>(null);
61
+ const [isSubmitting, setIsSubmitting] = useState(false);
62
+ const [values, setValues] = useState<ProfileFormValues>({
63
+ firstName: user?.FirstName ?? "",
64
+ lastName: user?.LastName ?? "",
65
+ });
66
+ const [touched, setTouched] = useState<
67
+ Partial<Record<keyof ProfileFormValues, boolean>>
68
+ >({});
69
+ const [errors, setErrors] = useState<
70
+ Partial<Record<keyof ProfileFormValues, string>>
71
+ >({});
72
+
73
+ useEffect(() => {
74
+ setValues({
75
+ firstName: user?.FirstName ?? "",
76
+ lastName: user?.LastName ?? "",
77
+ });
78
+ setTouched({});
79
+ setErrors({});
80
+ setSubmitError(null);
81
+ }, [user?.FirstName, user?.LastName]);
82
+
83
+ const validationSchema = createProfileValidationSchema({
84
+ requiredFirstName: t("validationRequired", { field: t("fieldFirstName") }),
85
+ requiredLastName: t("validationRequired", { field: t("fieldLastName") }),
86
+ invalidCharacters: t("validationNameInvalidCharacters"),
87
+ tooLong: t("validationNameTooLong"),
88
+ });
89
+
90
+ const mapZodErrors = (input: ProfileFormValues) => {
91
+ const result = validationSchema.safeParse(input);
92
+ if (result.success)
93
+ return {} as Partial<Record<keyof ProfileFormValues, string>>;
94
+
95
+ return {
96
+ firstName: result.error.flatten().fieldErrors.firstName?.[0],
97
+ lastName: result.error.flatten().fieldErrors.lastName?.[0],
98
+ };
99
+ };
100
+
101
+ const handleChange = (name: keyof ProfileFormValues, value: string) => {
102
+ setSubmitError(null);
103
+ setValues((prev: ProfileFormValues) => {
104
+ const next = { ...prev, [name]: value };
105
+ if (touched[name]) {
106
+ const nextErrors = mapZodErrors(next);
107
+ setErrors(nextErrors);
108
+ }
109
+ return next;
110
+ });
111
+ };
112
+
113
+ const handleBlur = (name: keyof ProfileFormValues) => {
114
+ setTouched((prev: Partial<Record<keyof ProfileFormValues, boolean>>) => ({
115
+ ...prev,
116
+ [name]: true,
117
+ }));
118
+ setErrors(mapZodErrors(values));
119
+ };
120
+
121
+ const handleSubmit = async (event: SyntheticEvent<HTMLFormElement>) => {
122
+ event.preventDefault();
123
+ setSubmitError(null);
124
+
125
+ const payload: ProfileFormValues = {
126
+ firstName: values.firstName.trim(),
127
+ lastName: values.lastName.trim(),
128
+ };
129
+
130
+ const validationErrors = mapZodErrors(payload);
131
+ setTouched({ firstName: true, lastName: true });
132
+ setErrors(validationErrors);
133
+ if (validationErrors.firstName || validationErrors.lastName) {
134
+ return;
135
+ }
136
+
137
+ try {
138
+ setIsSubmitting(true);
139
+ if (onBeforeSave) {
140
+ await onBeforeSave(payload);
141
+ }
142
+
143
+ await updateUser({
144
+ AccountNumber: "",
145
+ Email: user?.Email || undefined,
146
+ FirstName: payload.firstName,
147
+ LastName: payload.lastName,
148
+ });
149
+
150
+ await queryClient.invalidateQueries({ queryKey: ["me"] });
151
+ if (onSuccess) onSuccess();
152
+ else router.push("/account/profile");
153
+ } catch {
154
+ setSubmitError(null);
155
+ setNotification({
156
+ message: t("errorUpdateFailed"),
157
+ severity: "error",
158
+ });
159
+ } finally {
160
+ setIsSubmitting(false);
161
+ }
162
+ };
163
+
164
+ const contextValue = useMemo<ProfileFormContextValue>(
165
+ () => ({
166
+ values,
167
+ touched,
168
+ errors,
169
+ isSubmitting,
170
+ handleChange,
171
+ handleBlur,
172
+ submitError,
173
+ setSubmitError,
174
+ handleSubmit,
175
+ }),
176
+ [
177
+ values,
178
+ touched,
179
+ errors,
180
+ isSubmitting,
181
+ handleChange,
182
+ handleBlur,
183
+ submitError,
184
+ handleSubmit,
185
+ ],
186
+ );
187
+
188
+ return (
189
+ <ProfileFormContext.Provider value={contextValue}>
190
+ <form onSubmit={handleSubmit} noValidate>
191
+ {children}
192
+ </form>
193
+ </ProfileFormContext.Provider>
194
+ );
195
+ };
196
+
197
+ ProfileForm.Fields = ({ children }: Readonly<{ children?: ReactNode }>) => {
198
+ return (
199
+ <Grid container spacing={3}>
200
+ {children}
201
+ </Grid>
202
+ );
203
+ };
204
+
205
+ function ProfileFormField({
206
+ name,
207
+ label,
208
+ required,
209
+ autoComplete,
210
+ }: Readonly<{
211
+ name: keyof ProfileFormValues;
212
+ label: string;
213
+ required?: boolean;
214
+ autoComplete?: string;
215
+ }>) {
216
+ const ctx = useContext(ProfileFormContext);
217
+ if (!ctx) return null;
218
+ const { values, touched, errors, handleChange, handleBlur } = ctx;
219
+
220
+ return (
221
+ <Grid size={{ sm: 6, xs: 12 }}>
222
+ <TextField
223
+ name={name}
224
+ label={label}
225
+ fullWidth
226
+ required={required}
227
+ autoComplete={autoComplete}
228
+ value={values[name] ?? ""}
229
+ onChange={(event: ChangeEvent<HTMLInputElement>) => {
230
+ handleChange(name, event.target.value);
231
+ }}
232
+ onBlur={() => handleBlur(name)}
233
+ error={touched[name] && Boolean(errors[name])}
234
+ helperText={
235
+ touched[name] && typeof errors[name] === "string"
236
+ ? errors[name]
237
+ : undefined
238
+ }
239
+ />
240
+ </Grid>
241
+ );
242
+ }
243
+ ProfileForm.Field = ProfileFormField;
244
+
245
+ function ProfileFormError() {
246
+ const ctx = useContext(ProfileFormContext);
247
+ if (!ctx?.submitError) return null;
248
+
249
+ return <Typography color="error.main">{ctx.submitError}</Typography>;
250
+ }
251
+ ProfileForm.Error = ProfileFormError;
252
+
253
+ ProfileForm.Actions = ({ children }: Readonly<PropsWithChildren>) => {
254
+ return (
255
+ <Grid size={{ sm: 6, xs: 12 }} sx={{ mt: 3 }}>
256
+ {children}
257
+ </Grid>
258
+ );
259
+ };
260
+
261
+ function ProfileFormSubmitButton() {
262
+ const t = useTranslations("Account.Profile");
263
+ const ctx = useContext(ProfileFormContext);
264
+ if (!ctx) return null;
265
+ const { isSubmitting } = ctx;
266
+
267
+ return (
268
+ <Button
269
+ type="submit"
270
+ variant="contained"
271
+ color="primary"
272
+ disabled={isSubmitting}
273
+ startIcon={
274
+ isSubmitting ? (
275
+ <CircularProgress size={20} color="inherit" />
276
+ ) : undefined
277
+ }
278
+ >
279
+ {t("btnSaveProfile")}
280
+ </Button>
281
+ );
282
+ }
283
+ ProfileForm.SubmitButton = ProfileFormSubmitButton;
284
+
285
+ export default ProfileForm;
@@ -0,0 +1,52 @@
1
+ import z from "zod";
2
+
3
+ const namePattern = /^[\p{L}\p{M}][\p{L}\p{M}'’. -]*$/u;
4
+
5
+ export type ProfileValidationMessages = {
6
+ requiredFirstName: string;
7
+ requiredLastName: string;
8
+ invalidCharacters: string;
9
+ tooLong: string;
10
+ };
11
+
12
+ const createNameSchema = (
13
+ requiredMessage: string,
14
+ messages: ProfileValidationMessages,
15
+ ) =>
16
+ z
17
+ .string()
18
+ .trim()
19
+ .min(1, requiredMessage)
20
+ .max(50, messages.tooLong)
21
+ .refine(
22
+ (value) => value.length === 0 || namePattern.test(value),
23
+ messages.invalidCharacters,
24
+ );
25
+
26
+ export const createProfileValidationSchema = (
27
+ messages?: Partial<ProfileValidationMessages>,
28
+ ) => {
29
+ const resolvedMessages: ProfileValidationMessages = {
30
+ requiredFirstName: "First Name is required",
31
+ requiredLastName: "Last Name is required",
32
+ invalidCharacters:
33
+ "Names can only contain letters, spaces, apostrophes, periods, and hyphens.",
34
+ tooLong: "Names must be 50 characters or fewer.",
35
+ ...messages,
36
+ };
37
+
38
+ return z.object({
39
+ firstName: createNameSchema(
40
+ resolvedMessages.requiredFirstName,
41
+ resolvedMessages,
42
+ ),
43
+ lastName: createNameSchema(
44
+ resolvedMessages.requiredLastName,
45
+ resolvedMessages,
46
+ ),
47
+ });
48
+ };
49
+
50
+ export type ProfileFormValues = z.infer<
51
+ ReturnType<typeof createProfileValidationSchema>
52
+ >;