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