mirror of
https://github.com/spacebarchat/client.git
synced 2024-11-22 10:22:30 +01:00
feat: implement full login flow
This commit is contained in:
parent
5557d2c74c
commit
2bdd6e4ffd
@ -18,7 +18,7 @@
|
||||
"@hcaptcha/react-hcaptcha": "^1.8.0",
|
||||
"@mattjennings/react-modal-stack": "^1.0.4",
|
||||
"@mui/material": "^5.11.13",
|
||||
"@spacebarchat/spacebar-api-types": "^0.37.44",
|
||||
"@spacebarchat/spacebar-api-types": "^0.37.46",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
|
@ -20,8 +20,8 @@ dependencies:
|
||||
specifier: ^5.11.13
|
||||
version: 5.11.13(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@spacebarchat/spacebar-api-types':
|
||||
specifier: ^0.37.44
|
||||
version: 0.37.44
|
||||
specifier: ^0.37.46
|
||||
version: 0.37.46
|
||||
'@testing-library/jest-dom':
|
||||
specifier: ^5.16.5
|
||||
version: 5.16.5
|
||||
@ -2899,8 +2899,8 @@ packages:
|
||||
dependencies:
|
||||
'@sinonjs/commons': 1.8.6
|
||||
|
||||
/@spacebarchat/spacebar-api-types@0.37.44:
|
||||
resolution: {integrity: sha512-Kudf7A/vRCvpgi+JW3xkh+jAHHDYYHUdhN4gFBlJtNT+BNOVxuFIVpVWMuUdgNvQLaK41MmPMbLrDvTLUory2g==}
|
||||
/@spacebarchat/spacebar-api-types@0.37.46:
|
||||
resolution: {integrity: sha512-fuNZIMBWKezpkyvZHvAfQ1boCfEoncP+odg+bqr824WiGldiiAqhBuye1v/TMYkAm6A4R2p9/ifkCqmFQgemKA==}
|
||||
dev: false
|
||||
|
||||
/@surma/rollup-plugin-off-main-thread@2.2.3:
|
||||
|
@ -27,7 +27,6 @@ function App() {
|
||||
reaction(
|
||||
() => app.token,
|
||||
(value) => {
|
||||
console.log(value);
|
||||
if (value) {
|
||||
app.rest.setToken(value);
|
||||
if (app.gateway.readyState === WebSocket.CLOSED) {
|
||||
@ -60,6 +59,11 @@ function App() {
|
||||
path="/"
|
||||
element={<AuthenticationGuard component={RootPage} />}
|
||||
/>
|
||||
<Route
|
||||
index
|
||||
path="/app"
|
||||
element={<AuthenticationGuard component={RootPage} />}
|
||||
/>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/register" element={<RegistrationPage />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
|
87
src/components/HCaptcha.tsx
Normal file
87
src/components/HCaptcha.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import HCaptchaLib from "@hcaptcha/react-hcaptcha";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import Container from "./Container";
|
||||
|
||||
export const Wrapper = styled(Container)`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: var(--secondary);
|
||||
`;
|
||||
|
||||
export const AuthBox = styled(Container)`
|
||||
background-color: var(--primary-alt);
|
||||
padding: 32px;
|
||||
font-size: 18px;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
width: 480px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const HeaderContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const Header = styled.h1`
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
font-size: 24px;
|
||||
color: var(--text);
|
||||
`;
|
||||
|
||||
export const SubHeader = styled.h2`
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
margin-bottom: 40px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
sitekey: string;
|
||||
captchaRef: React.RefObject<HCaptchaLib>;
|
||||
onLoad?: () => void;
|
||||
onChalExpired?: () => void;
|
||||
onError?: (e: any) => void;
|
||||
onExpire?: () => void;
|
||||
onVerify?: (token: string) => void;
|
||||
}
|
||||
|
||||
function HCaptcha(props: Props) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<AuthBox>
|
||||
<HeaderContainer>
|
||||
<Header>Welcome Back!</Header>
|
||||
<SubHeader>Beep boop. Boop beep?</SubHeader>
|
||||
|
||||
<HCaptchaLib
|
||||
sitekey={props.sitekey}
|
||||
ref={props.captchaRef}
|
||||
theme="dark" // TODO: make this dynamically change based on theme
|
||||
onVerify={props.onVerify}
|
||||
onLoad={props.onLoad}
|
||||
onChalExpired={props.onChalExpired}
|
||||
onError={props.onError}
|
||||
onExpire={props.onExpire}
|
||||
/>
|
||||
</HeaderContainer>
|
||||
</AuthBox>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default HCaptcha;
|
303
src/components/MFA.tsx
Normal file
303
src/components/MFA.tsx
Normal file
@ -0,0 +1,303 @@
|
||||
import { Routes } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import {
|
||||
IAPIError,
|
||||
IAPILoginResponseMFARequired,
|
||||
IAPILoginResponseSuccess,
|
||||
IAPITOTPRequest,
|
||||
} from "../utils/interfaces/api";
|
||||
import { messageFromFieldError } from "../utils/messageFromFieldError";
|
||||
import Button from "./Button";
|
||||
import Container from "./Container";
|
||||
|
||||
export const Wrapper = styled(Container)`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: var(--secondary);
|
||||
`;
|
||||
|
||||
export const AuthBox = styled(Container)`
|
||||
background-color: var(--primary-alt);
|
||||
padding: 32px;
|
||||
font-size: 18px;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
width: 480px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const HeaderContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const Header = styled.h1`
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
font-size: 24px;
|
||||
color: var(--text);
|
||||
`;
|
||||
|
||||
export const SubHeader = styled.h2`
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
margin-bottom: 40px;
|
||||
`;
|
||||
|
||||
export const FormContainer = styled.form`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const InputContainer = styled.h1<{ marginBottom: boolean }>`
|
||||
margin-bottom: ${(props) => (props.marginBottom ? "20px" : "0")};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
`;
|
||||
|
||||
export const LabelWrapper = styled.div<{ error?: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 8px;
|
||||
color: ${(props) => (props.error ? "var(--error)" : "#b1b5bc")};
|
||||
`;
|
||||
|
||||
export const InputErrorText = styled.label`
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
`;
|
||||
|
||||
export const InputLabel = styled.label`
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
export const InputWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const Input = styled.input<{ error?: boolean }>`
|
||||
outline: none;
|
||||
background: var(--secondary);
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
flex: 1;
|
||||
border-radius: 12px;
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
border: none;
|
||||
aria-invalid: ${(props) => (props.error ? "true" : "false")};
|
||||
`;
|
||||
|
||||
export const Link = styled.button`
|
||||
margin-bottom: 20px;
|
||||
margin-top: 4px;
|
||||
padding: 2px 0;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
color: var(--text-link);
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
export const LoginButton = styled(Button)`
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
min-width: 130px;
|
||||
min-height: 44px;
|
||||
`;
|
||||
|
||||
export const RegisterContainer = styled.div`
|
||||
margin-top: 4px;
|
||||
text-align: initial;
|
||||
`;
|
||||
|
||||
export const RegisterLabel = styled.label`
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
export const RegisterLink = styled.button`
|
||||
font-size: 14px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-link);
|
||||
|
||||
@media (max-width: 480px) {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Divider = styled.span`
|
||||
padding: 0 4px;
|
||||
`;
|
||||
|
||||
type FormValues = {
|
||||
code: string;
|
||||
};
|
||||
|
||||
function MFA(props: IAPILoginResponseMFARequired) {
|
||||
const app = useAppStore();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setError,
|
||||
} = useForm<FormValues>();
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
setLoading(true);
|
||||
|
||||
app.rest
|
||||
.post<IAPITOTPRequest, IAPILoginResponseSuccess>(Routes.mfaTotp(), {
|
||||
...data,
|
||||
ticket: props.ticket,
|
||||
})
|
||||
.then((r) => {
|
||||
app.setToken(r.token);
|
||||
navigate("/app", { replace: true });
|
||||
})
|
||||
.catch((r: IAPIError) => {
|
||||
if ("message" in r) {
|
||||
// error
|
||||
if (r.errors) {
|
||||
const t = messageFromFieldError(r.errors);
|
||||
if (t) {
|
||||
setError(t.field as keyof FormValues, {
|
||||
type: "manual",
|
||||
message: t.error,
|
||||
});
|
||||
} else {
|
||||
setError("code", {
|
||||
type: "manual",
|
||||
message: r.message,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setError("code", {
|
||||
type: "manual",
|
||||
message: r.message,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// unknown error
|
||||
console.error(r);
|
||||
setError("code", {
|
||||
type: "manual",
|
||||
message: "Unknown Error",
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
});
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<AuthBox>
|
||||
<HeaderContainer>
|
||||
<Header>Two-factor authentication</Header>
|
||||
<SubHeader>
|
||||
You can use a backup code or your two-factor
|
||||
authentication mobile app.
|
||||
</SubHeader>
|
||||
|
||||
<FormContainer onSubmit={onSubmit}>
|
||||
<InputContainer
|
||||
marginBottom={true}
|
||||
style={{ marginTop: 0 }}
|
||||
>
|
||||
<LabelWrapper error={!!errors.code}>
|
||||
<InputLabel>
|
||||
Enter Spacebar Auth/Backup Code
|
||||
</InputLabel>
|
||||
{errors.code && (
|
||||
<InputErrorText>
|
||||
<>
|
||||
<Divider>-</Divider>
|
||||
{errors.code.message}
|
||||
</>
|
||||
</InputErrorText>
|
||||
)}
|
||||
</LabelWrapper>
|
||||
<InputWrapper>
|
||||
<Input
|
||||
type="text"
|
||||
autoFocus
|
||||
{...register("code", { required: true })}
|
||||
error={!!errors.code}
|
||||
disabled={loading}
|
||||
placeholder="6-digit authentication code/8-digit backup code"
|
||||
/>
|
||||
</InputWrapper>
|
||||
</InputContainer>
|
||||
|
||||
<LoginButton
|
||||
variant="primary"
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
>
|
||||
Log In
|
||||
</LoginButton>
|
||||
|
||||
{/* <Link
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://youtu.be/dQw4w9WgXcQ",
|
||||
"_blank",
|
||||
);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Recieve auth code from SMS
|
||||
</Link> */}
|
||||
|
||||
<Link
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://youtu.be/dQw4w9WgXcQ",
|
||||
"_blank",
|
||||
);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Go Back to Login
|
||||
</Link>
|
||||
</FormContainer>
|
||||
</HeaderContainer>
|
||||
</AuthBox>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default MFA;
|
@ -1,11 +1,21 @@
|
||||
import HCaptchaLib from "@hcaptcha/react-hcaptcha";
|
||||
import { Routes } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import Button from "../components/Button";
|
||||
import Container from "../components/Container";
|
||||
import HCaptcha from "../components/HCaptcha";
|
||||
import MFA from "../components/MFA";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import {
|
||||
IAPILoginRequest,
|
||||
IAPILoginResponse,
|
||||
IAPILoginResponseError,
|
||||
IAPILoginResponseMFARequired,
|
||||
} from "../utils/interfaces/api";
|
||||
import { messageFromFieldError } from "../utils/messageFromFieldError";
|
||||
|
||||
export const Wrapper = styled(Container)`
|
||||
display: flex;
|
||||
@ -15,7 +25,7 @@ export const Wrapper = styled(Container)`
|
||||
background-color: var(--secondary);
|
||||
`;
|
||||
|
||||
export const LoginBox = styled(Container)`
|
||||
export const AuthBox = styled(Container)`
|
||||
background-color: var(--primary-alt);
|
||||
padding: 32px;
|
||||
font-size: 18px;
|
||||
@ -152,7 +162,7 @@ export const Divider = styled.span`
|
||||
padding: 0 4px;
|
||||
`;
|
||||
|
||||
type LoginFormValues = {
|
||||
type FormValues = {
|
||||
login: string;
|
||||
password: string;
|
||||
captcha_key?: string;
|
||||
@ -163,6 +173,8 @@ function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [captchaSiteKey, setCaptchaSiteKey] = React.useState<string>();
|
||||
const [mfaData, setMfaData] =
|
||||
React.useState<IAPILoginResponseMFARequired>();
|
||||
const captchaRef = React.useRef<HCaptchaLib>(null);
|
||||
|
||||
const {
|
||||
@ -171,32 +183,94 @@ function LoginPage() {
|
||||
formState: { errors },
|
||||
setError,
|
||||
setValue,
|
||||
} = useForm<LoginFormValues>();
|
||||
} = useForm<FormValues>();
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
setLoading(true);
|
||||
// app.api
|
||||
// .login(data)
|
||||
// .catch((e) => {
|
||||
// if (e instanceof MFAError) {
|
||||
// console.log("MFA Required", e);
|
||||
// } else if (e instanceof CaptchaError) {
|
||||
// // TODO: other captcha services
|
||||
// setCaptchaSiteKey(e.captcha_sitekey);
|
||||
// captchaRef.current?.execute();
|
||||
// } else if (e instanceof APIError) {
|
||||
// console.log("APIError", e.message, e.code, e.fieldErrors);
|
||||
// e.fieldErrors.forEach((fieldError) => {
|
||||
// setError(fieldError.field as any, {
|
||||
// type: "manual",
|
||||
// message: fieldError.error,
|
||||
// });
|
||||
// });
|
||||
// } else {
|
||||
// console.log("General Error", e);
|
||||
// }
|
||||
// })
|
||||
// .finally(() => setLoading(false));
|
||||
setCaptchaSiteKey(undefined);
|
||||
setMfaData(undefined);
|
||||
|
||||
app.rest
|
||||
.post<IAPILoginRequest, IAPILoginResponse>(Routes.login(), {
|
||||
...data,
|
||||
undelete: false,
|
||||
})
|
||||
.then((r) => {
|
||||
if ("token" in r && "settings" in r) {
|
||||
// success
|
||||
app.setToken(r.token);
|
||||
return;
|
||||
} else if ("ticket" in r) {
|
||||
// mfa
|
||||
console.log("MFA Required", r);
|
||||
setMfaData(r);
|
||||
return;
|
||||
} else {
|
||||
// unknown error
|
||||
console.error(r);
|
||||
setError("login", {
|
||||
type: "manual",
|
||||
message: "Unknown Error",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((r: IAPILoginResponseError) => {
|
||||
if ("captcha_key" in r) {
|
||||
// catcha required
|
||||
if (r.captcha_key[0] !== "captcha-required") {
|
||||
// some kind of captcha error
|
||||
setError("login", {
|
||||
type: "manual",
|
||||
message: `Captcha Error: ${r.captcha_key[0]}`,
|
||||
});
|
||||
} else if (r.captcha_service !== "hcaptcha") {
|
||||
// recaptcha or something else
|
||||
setError("login", {
|
||||
type: "manual",
|
||||
message: `Unsupported captcha service: ${r.captcha_service}`,
|
||||
});
|
||||
} else {
|
||||
// hcaptcha
|
||||
setCaptchaSiteKey(r.captcha_sitekey);
|
||||
captchaRef.current?.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
captchaRef.current?.resetCaptcha();
|
||||
} else if ("message" in r) {
|
||||
// error
|
||||
if (r.errors) {
|
||||
const t = messageFromFieldError(r.errors);
|
||||
if (t) {
|
||||
setError(t.field as keyof FormValues, {
|
||||
type: "manual",
|
||||
message: t.error,
|
||||
});
|
||||
} else {
|
||||
setError("login", {
|
||||
type: "manual",
|
||||
message: r.message,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setError("login", {
|
||||
type: "manual",
|
||||
message: r.message,
|
||||
});
|
||||
}
|
||||
|
||||
captchaRef.current?.resetCaptcha();
|
||||
} else {
|
||||
// unknown error
|
||||
console.error(r);
|
||||
setError("login", {
|
||||
type: "manual",
|
||||
message: "Unknown Error",
|
||||
});
|
||||
captchaRef.current?.resetCaptcha();
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
});
|
||||
|
||||
const onCaptchaVerify = (token: string) => {
|
||||
@ -204,9 +278,23 @@ function LoginPage() {
|
||||
onSubmit();
|
||||
};
|
||||
|
||||
if (captchaSiteKey) {
|
||||
return (
|
||||
<HCaptcha
|
||||
captchaRef={captchaRef}
|
||||
sitekey={captchaSiteKey}
|
||||
onVerify={onCaptchaVerify}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (mfaData) {
|
||||
return <MFA {...mfaData} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<LoginBox>
|
||||
<AuthBox>
|
||||
<HeaderContainer>
|
||||
<Header>Welcome Back!</Header>
|
||||
<SubHeader>We're so excited to see you again!</SubHeader>
|
||||
@ -272,25 +360,7 @@ function LoginPage() {
|
||||
>
|
||||
Forgot your password?
|
||||
</PasswordResetLink>
|
||||
{captchaSiteKey && (
|
||||
<HCaptchaLib
|
||||
sitekey={captchaSiteKey}
|
||||
onVerify={onCaptchaVerify}
|
||||
ref={captchaRef}
|
||||
onChalExpired={() => {
|
||||
console.log("Captcha challenge Expired");
|
||||
// TODO: red outline?
|
||||
}}
|
||||
onError={(e) => {
|
||||
console.log("Captcha Error", e);
|
||||
// TODO: red outline?
|
||||
}}
|
||||
onExpire={() => {
|
||||
console.log("Captcha Expired");
|
||||
// TODO: red outline?
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<LoginButton
|
||||
variant="primary"
|
||||
type="submit"
|
||||
@ -313,7 +383,7 @@ function LoginPage() {
|
||||
</RegisterLink>
|
||||
</RegisterContainer>
|
||||
</FormContainer>
|
||||
</LoginBox>
|
||||
</AuthBox>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ const Wrapper = styled(Container)`
|
||||
background-color: var(--secondary);
|
||||
`;
|
||||
|
||||
const LoginBox = styled(Container)`
|
||||
const AuthBox = styled(Container)`
|
||||
background-color: var(--primary-alt);
|
||||
padding: 32px;
|
||||
font-size: 18px;
|
||||
@ -129,7 +129,7 @@ const Divider = styled.span`
|
||||
padding: 0 4px;
|
||||
`;
|
||||
|
||||
type RegisterFormValues = {
|
||||
type FormValues = {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
@ -147,7 +147,7 @@ function RegistrationPage() {
|
||||
formState: { errors },
|
||||
setError,
|
||||
clearErrors,
|
||||
} = useForm<RegisterFormValues>();
|
||||
} = useForm<FormValues>();
|
||||
|
||||
const dobRegister = register("date_of_birth", {
|
||||
required: true,
|
||||
@ -183,7 +183,7 @@ function RegistrationPage() {
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<LoginBox>
|
||||
<AuthBox>
|
||||
<HeaderContainer>
|
||||
<Header>Create an account</Header>
|
||||
{/* <SubHeader>We're so excited to see you again!</SubHeader> */}
|
||||
@ -313,7 +313,7 @@ function RegistrationPage() {
|
||||
Already have an account?
|
||||
</LoginLink>
|
||||
</FormContainer>
|
||||
</LoginBox>
|
||||
</AuthBox>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
32
src/utils/messageFromFieldError.ts
Normal file
32
src/utils/messageFromFieldError.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export function messageFromFieldError(
|
||||
e:
|
||||
| {
|
||||
[key: string]: {
|
||||
_errors: {
|
||||
code: string;
|
||||
message: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
[key: string]: {
|
||||
code: string;
|
||||
message: string;
|
||||
}[];
|
||||
},
|
||||
prevKey?: string,
|
||||
): { field: string | undefined; error: string } | null {
|
||||
for (const key in e) {
|
||||
const obj = e[key];
|
||||
if (obj) {
|
||||
if (key === "_errors" && Array.isArray(obj)) {
|
||||
const r = obj[0];
|
||||
return r ? { field: prevKey, error: r.message } : null;
|
||||
}
|
||||
if (typeof obj === "object") {
|
||||
return messageFromFieldError(obj as any, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
Loading…
Reference in New Issue
Block a user