@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,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
|
+
>;
|