@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
@@ -0,0 +1,241 @@
1
+ "use client";
2
+
3
+ import AuthForm from "@evenicanpm/storefront-core/src/pages/auth/compound/auth-form";
4
+ import { Box, Divider, TextField, Typography } from "@mui/material";
5
+ import Link from "next/link";
6
+ import { useSearchParams } from "next/navigation";
7
+ import { useTranslations } from "next-intl";
8
+ import {
9
+ type ChangeEvent,
10
+ createContext,
11
+ type FormEvent,
12
+ type ReactNode,
13
+ useContext,
14
+ useMemo,
15
+ useState,
16
+ } from "react";
17
+
18
+ type LoginContextValue = {
19
+ t: ReturnType<typeof useTranslations>;
20
+ tErrors: ReturnType<typeof useTranslations>;
21
+ callbackUrl: string;
22
+ email: string;
23
+ password: string;
24
+ loading: boolean;
25
+ error: string | null;
26
+ handleEmailChange: (event: ChangeEvent<HTMLInputElement>) => void;
27
+ handlePasswordChange: (event: ChangeEvent<HTMLInputElement>) => void;
28
+ handleSubmit: (event: FormEvent<HTMLFormElement>) => Promise<void>;
29
+ };
30
+
31
+ const AuthLoginPageContext = createContext<LoginContextValue | null>(null);
32
+
33
+ const useAuthLoginPage = () => {
34
+ const context = useContext(AuthLoginPageContext);
35
+ if (!context) {
36
+ throw new Error(
37
+ "AuthLoginPage compound components must be used within AuthLoginPage",
38
+ );
39
+ }
40
+ return context;
41
+ };
42
+
43
+ function AuthLoginHeader() {
44
+ const { t } = useAuthLoginPage();
45
+
46
+ return (
47
+ <>
48
+ <Typography variant="h5" fontWeight={700} gutterBottom>
49
+ {t("title")}
50
+ </Typography>
51
+ <Typography variant="body2" color="text.secondary" mb={3}>
52
+ {t("subtitle")}
53
+ </Typography>
54
+ <Divider sx={{ mb: 3 }} />
55
+ </>
56
+ );
57
+ }
58
+
59
+ function AuthLoginError() {
60
+ const { error } = useAuthLoginPage();
61
+ return <AuthForm.Error message={error} />;
62
+ }
63
+
64
+ function AuthLoginCredentialsForm() {
65
+ const {
66
+ t,
67
+ email,
68
+ password,
69
+ loading,
70
+ handleEmailChange,
71
+ handlePasswordChange,
72
+ handleSubmit,
73
+ } = useAuthLoginPage();
74
+
75
+ return (
76
+ <AuthForm onSubmit={handleSubmit}>
77
+ <TextField
78
+ label={t("emailLabel")}
79
+ type="email"
80
+ value={email}
81
+ onChange={handleEmailChange}
82
+ fullWidth
83
+ required
84
+ autoComplete="email"
85
+ autoFocus
86
+ sx={{ mb: 2 }}
87
+ disabled={loading}
88
+ />
89
+ <TextField
90
+ label={t("passwordLabel")}
91
+ type="password"
92
+ value={password}
93
+ onChange={handlePasswordChange}
94
+ fullWidth
95
+ required
96
+ autoComplete="current-password"
97
+ sx={{ mb: 1 }}
98
+ disabled={loading}
99
+ />
100
+ <Box sx={{ textAlign: "right", mb: 2 }}>
101
+ <Link href="/reset-password" style={{ fontSize: "0.8125rem" }}>
102
+ {t("forgotPassword")}
103
+ </Link>
104
+ </Box>
105
+ <AuthForm.SubmitButton
106
+ label={t("submit")}
107
+ loading={loading}
108
+ disabled={!email || !password}
109
+ />
110
+ </AuthForm>
111
+ );
112
+ }
113
+
114
+ function AuthLoginFooter() {
115
+ const { t } = useAuthLoginPage();
116
+
117
+ return (
118
+ <Box sx={{ mt: 3, textAlign: "center" }}>
119
+ <Link href="/activate" style={{ fontSize: "0.875rem" }}>
120
+ {t("activateAccount")}
121
+ </Link>
122
+ </Box>
123
+ );
124
+ }
125
+
126
+ function AuthLoginDefaultView() {
127
+ return (
128
+ <>
129
+ <AuthLoginPage.Header />
130
+ <AuthLoginPage.Error />
131
+ <AuthLoginPage.Form />
132
+ <AuthLoginPage.Footer />
133
+ </>
134
+ );
135
+ }
136
+
137
+ function AuthLoginPageRoot({ children }: Readonly<{ children?: ReactNode }>) {
138
+ const t = useTranslations("Auth.Login");
139
+ const tErrors = useTranslations("Auth.Errors");
140
+
141
+ const searchParams = useSearchParams();
142
+ const callbackUrl =
143
+ searchParams.get("callbackUrl") ?? searchParams.get("redirect") ?? "/";
144
+
145
+ const [email, setEmail] = useState("");
146
+ const [password, setPassword] = useState("");
147
+ const [loading, setLoading] = useState(false);
148
+ const [error, setError] = useState<string | null>(null);
149
+
150
+ const handleEmailChange = (event: ChangeEvent<HTMLInputElement>) => {
151
+ setEmail(event.target.value);
152
+ };
153
+
154
+ const handlePasswordChange = (event: ChangeEvent<HTMLInputElement>) => {
155
+ setPassword(event.target.value);
156
+ };
157
+
158
+ const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
159
+ event.preventDefault();
160
+ setLoading(true);
161
+ setError(null);
162
+
163
+ try {
164
+ const response = await fetch("/api/auth/native-login", {
165
+ method: "POST",
166
+ headers: { "Content-Type": "application/json" },
167
+ body: JSON.stringify({ email, password }),
168
+ });
169
+
170
+ if (!response.ok) {
171
+ const data = (await response.json().catch(() => null)) as {
172
+ error_description?: string;
173
+ error?: string;
174
+ } | null;
175
+ setError(
176
+ data?.error_description ||
177
+ data?.error ||
178
+ tErrors("invalidCredentials"),
179
+ );
180
+ setLoading(false);
181
+ return;
182
+ }
183
+
184
+ globalThis.location.href = callbackUrl;
185
+ } catch {
186
+ setError(tErrors("unableToSignIn"));
187
+ setLoading(false);
188
+ }
189
+ };
190
+
191
+ const contextValue = useMemo(
192
+ () => ({
193
+ t,
194
+ tErrors,
195
+ callbackUrl,
196
+ email,
197
+ password,
198
+ loading,
199
+ error,
200
+ handleEmailChange,
201
+ handlePasswordChange,
202
+ handleSubmit,
203
+ }),
204
+ [
205
+ t,
206
+ tErrors,
207
+ callbackUrl,
208
+ email,
209
+ password,
210
+ loading,
211
+ error,
212
+ handleEmailChange,
213
+ handlePasswordChange,
214
+ handleSubmit,
215
+ ],
216
+ );
217
+
218
+ return (
219
+ <AuthLoginPageContext.Provider value={contextValue}>
220
+ {children ?? <AuthLoginPage.Default />}
221
+ </AuthLoginPageContext.Provider>
222
+ );
223
+ }
224
+
225
+ type AuthLoginPageComponent = typeof AuthLoginPageRoot & {
226
+ Header: typeof AuthLoginHeader;
227
+ Error: typeof AuthLoginError;
228
+ Form: typeof AuthLoginCredentialsForm;
229
+ Footer: typeof AuthLoginFooter;
230
+ Default: typeof AuthLoginDefaultView;
231
+ };
232
+
233
+ const AuthLoginPage = AuthLoginPageRoot as AuthLoginPageComponent;
234
+
235
+ AuthLoginPage.Header = AuthLoginHeader;
236
+ AuthLoginPage.Error = AuthLoginError;
237
+ AuthLoginPage.Form = AuthLoginCredentialsForm;
238
+ AuthLoginPage.Footer = AuthLoginFooter;
239
+ AuthLoginPage.Default = AuthLoginDefaultView;
240
+
241
+ export default AuthLoginPage;
@@ -0,0 +1,487 @@
1
+ "use client";
2
+
3
+ import AuthForm from "@evenicanpm/storefront-core/src/pages/auth/compound/auth-form";
4
+ import type { OtpMeta } from "@evenicanpm/storefront-core/src/pages/auth/lib";
5
+ import { friendlyError } from "@evenicanpm/storefront-core/src/pages/auth/lib";
6
+ import { Box, Button, Divider, TextField, Typography } from "@mui/material";
7
+ import Link from "next/link";
8
+ import { useTranslations } from "next-intl";
9
+ import {
10
+ type ChangeEvent,
11
+ createContext,
12
+ type FormEvent,
13
+ type ReactNode,
14
+ useContext,
15
+ useMemo,
16
+ useState,
17
+ } from "react";
18
+
19
+ type Step = "email" | "otp" | "password" | "success";
20
+
21
+ type ResetPasswordContextValue = {
22
+ t: ReturnType<typeof useTranslations>;
23
+ tErrors: ReturnType<typeof useTranslations>;
24
+ tCommon: ReturnType<typeof useTranslations>;
25
+ step: Step;
26
+ error: string | null;
27
+ email: string;
28
+ emailLoading: boolean;
29
+ otpMeta: OtpMeta | null;
30
+ otp: string;
31
+ otpLoading: boolean;
32
+ passwordToken: string;
33
+ newPassword: string;
34
+ confirmPassword: string;
35
+ passwordLoading: boolean;
36
+ passwordMismatch: boolean;
37
+ setStep: (step: Step) => void;
38
+ setError: (message: string | null) => void;
39
+ setEmail: (value: string) => void;
40
+ setOtp: (value: string) => void;
41
+ setNewPassword: (value: string) => void;
42
+ setConfirmPassword: (value: string) => void;
43
+ handleEmail: (event: FormEvent<HTMLFormElement>) => Promise<void>;
44
+ handleOtp: (event: FormEvent<HTMLFormElement>) => Promise<void>;
45
+ handlePassword: (event: FormEvent<HTMLFormElement>) => Promise<void>;
46
+ };
47
+
48
+ const AuthResetPasswordPageContext =
49
+ createContext<ResetPasswordContextValue | null>(null);
50
+
51
+ function getStringValue(value: unknown): string {
52
+ return typeof value === "string" ? value : "";
53
+ }
54
+
55
+ const useAuthResetPasswordPage = () => {
56
+ const context = useContext(AuthResetPasswordPageContext);
57
+ if (!context) {
58
+ throw new Error(
59
+ "AuthResetPasswordPage compound components must be used within AuthResetPasswordPage",
60
+ );
61
+ }
62
+ return context;
63
+ };
64
+
65
+ function AuthResetPasswordHeader() {
66
+ const { t, tCommon, step, otpMeta } = useAuthResetPasswordPage();
67
+
68
+ return (
69
+ <>
70
+ <Typography variant="h5" fontWeight={700} gutterBottom>
71
+ {step === "success" ? t("titleSuccess") : t("title")}
72
+ </Typography>
73
+ <Typography variant="body2" color="text.secondary" mb={3}>
74
+ {step === "email" && t("subtitleEmail")}
75
+ {step === "otp" &&
76
+ t("subtitleOtp", {
77
+ codeLength: otpMeta?.code_length ?? 8,
78
+ target: otpMeta?.challenge_target_label || tCommon("yourEmail"),
79
+ })}
80
+ {step === "password" && t("subtitlePassword")}
81
+ {step === "success" && t("subtitleSuccess")}
82
+ </Typography>
83
+ <Divider sx={{ mb: 3 }} />
84
+ </>
85
+ );
86
+ }
87
+
88
+ function AuthResetPasswordError() {
89
+ const { error, step } = useAuthResetPasswordPage();
90
+
91
+ if (step === "success") {
92
+ return null;
93
+ }
94
+
95
+ return <AuthForm.Error message={error} />;
96
+ }
97
+
98
+ function AuthResetPasswordEmailForm() {
99
+ const { t, step, email, emailLoading, setEmail, handleEmail } =
100
+ useAuthResetPasswordPage();
101
+
102
+ if (step !== "email") {
103
+ return null;
104
+ }
105
+
106
+ return (
107
+ <AuthForm onSubmit={handleEmail}>
108
+ <TextField
109
+ label={t("emailLabel")}
110
+ type="email"
111
+ value={email}
112
+ onChange={(
113
+ event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
114
+ ) => setEmail(event.target.value)}
115
+ fullWidth
116
+ required
117
+ autoComplete="email"
118
+ autoFocus
119
+ sx={{ mb: 3 }}
120
+ disabled={emailLoading}
121
+ />
122
+ <AuthForm.SubmitButton
123
+ label={t("sendCode")}
124
+ loading={emailLoading}
125
+ disabled={!email}
126
+ />
127
+ </AuthForm>
128
+ );
129
+ }
130
+
131
+ function AuthResetPasswordOtpForm() {
132
+ const {
133
+ t,
134
+ step,
135
+ otp,
136
+ otpMeta,
137
+ otpLoading,
138
+ setStep,
139
+ setOtp,
140
+ setError,
141
+ handleOtp,
142
+ } = useAuthResetPasswordPage();
143
+
144
+ if (step !== "otp") {
145
+ return null;
146
+ }
147
+
148
+ return (
149
+ <AuthForm onSubmit={handleOtp}>
150
+ <AuthForm.OtpField
151
+ value={otp}
152
+ onChange={setOtp}
153
+ codeLength={otpMeta?.code_length ?? 8}
154
+ disabled={otpLoading}
155
+ label={t("verificationCodeLabel")}
156
+ />
157
+ <AuthForm.SubmitButton
158
+ label={t("verifyCode")}
159
+ loading={otpLoading}
160
+ disabled={otp.length < (otpMeta?.code_length ?? 8)}
161
+ />
162
+ <Button
163
+ variant="text"
164
+ color="secondary"
165
+ fullWidth
166
+ sx={{ mt: 1 }}
167
+ onClick={() => {
168
+ setStep("email");
169
+ setOtp("");
170
+ setError(null);
171
+ }}
172
+ >
173
+ {t("sendAgain")}
174
+ </Button>
175
+ </AuthForm>
176
+ );
177
+ }
178
+
179
+ function AuthResetPasswordPasswordForm() {
180
+ const {
181
+ t,
182
+ tCommon,
183
+ step,
184
+ newPassword,
185
+ confirmPassword,
186
+ passwordLoading,
187
+ passwordMismatch,
188
+ setNewPassword,
189
+ setConfirmPassword,
190
+ handlePassword,
191
+ } = useAuthResetPasswordPage();
192
+
193
+ if (step !== "password") {
194
+ return null;
195
+ }
196
+
197
+ return (
198
+ <AuthForm onSubmit={handlePassword}>
199
+ <AuthForm.PasswordFields
200
+ password={newPassword}
201
+ confirmPassword={confirmPassword}
202
+ onPasswordChange={setNewPassword}
203
+ onConfirmChange={setConfirmPassword}
204
+ disabled={passwordLoading}
205
+ autoFocus
206
+ label={t("newPasswordLabel")}
207
+ confirmLabel={t("confirmNewPasswordLabel")}
208
+ mismatchHelperText={tCommon("passwordsDoNotMatch")}
209
+ />
210
+ <AuthForm.SubmitButton
211
+ label={t("resetPassword")}
212
+ loading={passwordLoading}
213
+ disabled={passwordMismatch || !newPassword || !confirmPassword}
214
+ />
215
+ </AuthForm>
216
+ );
217
+ }
218
+
219
+ function AuthResetPasswordSuccessAction() {
220
+ const { t, step } = useAuthResetPasswordPage();
221
+
222
+ if (step !== "success") {
223
+ return null;
224
+ }
225
+
226
+ return (
227
+ <Button
228
+ component={Link}
229
+ href="/login"
230
+ variant="contained"
231
+ color="primary"
232
+ fullWidth
233
+ size="large"
234
+ sx={{ borderRadius: 2, py: 1.5 }}
235
+ >
236
+ {t("returnToSignIn")}
237
+ </Button>
238
+ );
239
+ }
240
+
241
+ function AuthResetPasswordFooter() {
242
+ const { t, step } = useAuthResetPasswordPage();
243
+
244
+ if (step === "success") {
245
+ return null;
246
+ }
247
+
248
+ return (
249
+ <Box sx={{ mt: 3, textAlign: "center" }}>
250
+ <Link href="/login" style={{ fontSize: "0.875rem" }}>
251
+ {t("backToSignIn")}
252
+ </Link>
253
+ </Box>
254
+ );
255
+ }
256
+
257
+ function AuthResetPasswordDefaultView() {
258
+ return (
259
+ <>
260
+ <AuthResetPasswordPage.Header />
261
+ <AuthResetPasswordPage.Error />
262
+ <AuthResetPasswordPage.EmailForm />
263
+ <AuthResetPasswordPage.OtpForm />
264
+ <AuthResetPasswordPage.PasswordForm />
265
+ <AuthResetPasswordPage.SuccessAction />
266
+ <AuthResetPasswordPage.Footer />
267
+ </>
268
+ );
269
+ }
270
+
271
+ function AuthResetPasswordPageRoot({
272
+ children,
273
+ }: Readonly<{ children?: ReactNode }>) {
274
+ const t = useTranslations("Auth.ResetPassword");
275
+ const tErrors = useTranslations("Auth.Errors");
276
+ const tCommon = useTranslations("Auth.Common");
277
+
278
+ const [step, setStep] = useState<Step>("email");
279
+ const [error, setError] = useState<string | null>(null);
280
+
281
+ const [email, setEmail] = useState("");
282
+ const [emailLoading, setEmailLoading] = useState(false);
283
+ const [otpMeta, setOtpMeta] = useState<OtpMeta | null>(null);
284
+
285
+ const [otp, setOtp] = useState("");
286
+ const [otpLoading, setOtpLoading] = useState(false);
287
+ const [passwordToken, setPasswordToken] = useState("");
288
+
289
+ const [newPassword, setNewPassword] = useState("");
290
+ const [confirmPassword, setConfirmPassword] = useState("");
291
+ const [passwordLoading, setPasswordLoading] = useState(false);
292
+
293
+ const passwordMismatch =
294
+ confirmPassword.length > 0 && newPassword !== confirmPassword;
295
+
296
+ const handleEmail = async (event: FormEvent<HTMLFormElement>) => {
297
+ event.preventDefault();
298
+ setError(null);
299
+ setEmailLoading(true);
300
+ try {
301
+ const response = await fetch("/api/reset-password", {
302
+ method: "POST",
303
+ headers: { "Content-Type": "application/json" },
304
+ body: JSON.stringify({ email }),
305
+ });
306
+ const data = (await response.json()) as Record<string, unknown>;
307
+ if (!response.ok) {
308
+ setError(
309
+ friendlyError(
310
+ getStringValue(data.error),
311
+ getStringValue(data.suberror),
312
+ tErrors,
313
+ ),
314
+ );
315
+ return;
316
+ }
317
+ setOtpMeta({
318
+ continuation_token: getStringValue(data.continuation_token),
319
+ challenge_target_label:
320
+ getStringValue(data.challenge_target_label) || email,
321
+ code_length: Number(data.code_length || 8),
322
+ });
323
+ setStep("otp");
324
+ } catch {
325
+ setError(tErrors("networkError"));
326
+ } finally {
327
+ setEmailLoading(false);
328
+ }
329
+ };
330
+
331
+ const handleOtp = async (event: FormEvent<HTMLFormElement>) => {
332
+ event.preventDefault();
333
+ if (!otpMeta) return;
334
+ setError(null);
335
+ setOtpLoading(true);
336
+ try {
337
+ const response = await fetch("/api/reset-password/verify", {
338
+ method: "POST",
339
+ headers: { "Content-Type": "application/json" },
340
+ body: JSON.stringify({
341
+ otp,
342
+ continuation_token: otpMeta.continuation_token,
343
+ }),
344
+ });
345
+ const data = (await response.json()) as Record<string, unknown>;
346
+ if (!response.ok) {
347
+ setError(
348
+ friendlyError(
349
+ getStringValue(data.error),
350
+ getStringValue(data.suberror),
351
+ tErrors,
352
+ ),
353
+ );
354
+ if (getStringValue(data.suberror) === "invalid_oob_value") setOtp("");
355
+ return;
356
+ }
357
+ setPasswordToken(getStringValue(data.continuation_token));
358
+ setStep("password");
359
+ } catch {
360
+ setError(tErrors("networkError"));
361
+ } finally {
362
+ setOtpLoading(false);
363
+ }
364
+ };
365
+
366
+ const handlePassword = async (event: FormEvent<HTMLFormElement>) => {
367
+ event.preventDefault();
368
+ if (newPassword !== confirmPassword) {
369
+ setError(tCommon("passwordsDoNotMatch"));
370
+ return;
371
+ }
372
+ setError(null);
373
+ setPasswordLoading(true);
374
+ try {
375
+ const response = await fetch("/api/reset-password/submit", {
376
+ method: "POST",
377
+ headers: { "Content-Type": "application/json" },
378
+ body: JSON.stringify({
379
+ new_password: newPassword,
380
+ continuation_token: passwordToken,
381
+ }),
382
+ });
383
+ const data = (await response.json()) as Record<string, unknown>;
384
+ if (!response.ok) {
385
+ setError(
386
+ friendlyError(
387
+ getStringValue(data.error),
388
+ getStringValue(data.suberror),
389
+ tErrors,
390
+ ),
391
+ );
392
+ return;
393
+ }
394
+ setStep("success");
395
+ } catch {
396
+ setError(tErrors("networkError"));
397
+ } finally {
398
+ setPasswordLoading(false);
399
+ }
400
+ };
401
+
402
+ const contextValue = useMemo(
403
+ () => ({
404
+ t,
405
+ tErrors,
406
+ tCommon,
407
+ step,
408
+ error,
409
+ email,
410
+ emailLoading,
411
+ otpMeta,
412
+ otp,
413
+ otpLoading,
414
+ passwordToken,
415
+ newPassword,
416
+ confirmPassword,
417
+ passwordLoading,
418
+ passwordMismatch,
419
+ setStep,
420
+ setError,
421
+ setEmail,
422
+ setOtp,
423
+ setNewPassword,
424
+ setConfirmPassword,
425
+ handleEmail,
426
+ handleOtp,
427
+ handlePassword,
428
+ }),
429
+ [
430
+ t,
431
+ tErrors,
432
+ tCommon,
433
+ step,
434
+ error,
435
+ email,
436
+ emailLoading,
437
+ otpMeta,
438
+ otp,
439
+ otpLoading,
440
+ passwordToken,
441
+ newPassword,
442
+ confirmPassword,
443
+ passwordLoading,
444
+ passwordMismatch,
445
+ setStep,
446
+ setError,
447
+ setEmail,
448
+ setOtp,
449
+ setNewPassword,
450
+ setConfirmPassword,
451
+ handleEmail,
452
+ handleOtp,
453
+ handlePassword,
454
+ ],
455
+ );
456
+
457
+ return (
458
+ <AuthResetPasswordPageContext.Provider value={contextValue}>
459
+ {children ?? <AuthResetPasswordPage.Default />}
460
+ </AuthResetPasswordPageContext.Provider>
461
+ );
462
+ }
463
+
464
+ type AuthResetPasswordPageComponent = typeof AuthResetPasswordPageRoot & {
465
+ Header: typeof AuthResetPasswordHeader;
466
+ Error: typeof AuthResetPasswordError;
467
+ EmailForm: typeof AuthResetPasswordEmailForm;
468
+ OtpForm: typeof AuthResetPasswordOtpForm;
469
+ PasswordForm: typeof AuthResetPasswordPasswordForm;
470
+ SuccessAction: typeof AuthResetPasswordSuccessAction;
471
+ Footer: typeof AuthResetPasswordFooter;
472
+ Default: typeof AuthResetPasswordDefaultView;
473
+ };
474
+
475
+ const AuthResetPasswordPage =
476
+ AuthResetPasswordPageRoot as AuthResetPasswordPageComponent;
477
+
478
+ AuthResetPasswordPage.Header = AuthResetPasswordHeader;
479
+ AuthResetPasswordPage.Error = AuthResetPasswordError;
480
+ AuthResetPasswordPage.EmailForm = AuthResetPasswordEmailForm;
481
+ AuthResetPasswordPage.OtpForm = AuthResetPasswordOtpForm;
482
+ AuthResetPasswordPage.PasswordForm = AuthResetPasswordPasswordForm;
483
+ AuthResetPasswordPage.SuccessAction = AuthResetPasswordSuccessAction;
484
+ AuthResetPasswordPage.Footer = AuthResetPasswordFooter;
485
+ AuthResetPasswordPage.Default = AuthResetPasswordDefaultView;
486
+
487
+ export default AuthResetPasswordPage;