From de7da6f56b5c089f3e705ce8708bcbbfc31965ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Wed, 23 Mar 2022 13:14:52 +0100 Subject: [PATCH 1/6] feat(dashboard-v2): implement account recovery flow --- .../src/components/forms/RecoveryForm.js | 57 +++++++++++++++ .../src/components/forms/ResetPasswordForm.js | 72 +++++++++++++++++++ .../src/pages/auth/reset-password.js | 50 +++++++++++++ .../dashboard-v2/src/pages/user/recover.js | 56 +++++++++++++++ 4 files changed, 235 insertions(+) create mode 100644 packages/dashboard-v2/src/components/forms/RecoveryForm.js create mode 100644 packages/dashboard-v2/src/components/forms/ResetPasswordForm.js create mode 100644 packages/dashboard-v2/src/pages/auth/reset-password.js create mode 100644 packages/dashboard-v2/src/pages/user/recover.js diff --git a/packages/dashboard-v2/src/components/forms/RecoveryForm.js b/packages/dashboard-v2/src/components/forms/RecoveryForm.js new file mode 100644 index 00000000..50564b34 --- /dev/null +++ b/packages/dashboard-v2/src/components/forms/RecoveryForm.js @@ -0,0 +1,57 @@ +import PropTypes from "prop-types"; +import { Formik, Form } from "formik"; +import * as Yup from "yup"; + +import { TextField } from "../Form/TextField"; +import { Button } from "../Button"; + +import accountsService from "../../services/accountsService"; + +const recoverySchema = Yup.object().shape({ + email: Yup.string().required("Email is required").email("Please provide a valid email address"), +}); + +export const RecoveryForm = ({ onSuccess, onFailure }) => ( + { + try { + await accountsService.post("user/recover/request", { + json: values, + }); + + onSuccess(); + } catch { + onFailure(); + } + }} + > + {({ errors, touched }) => ( +
+

Request account recovery

+ + +
+ +
+ + )} +
+); + +RecoveryForm.propTypes = { + onFailure: PropTypes.func.isRequired, + onSuccess: PropTypes.func.isRequired, +}; diff --git a/packages/dashboard-v2/src/components/forms/ResetPasswordForm.js b/packages/dashboard-v2/src/components/forms/ResetPasswordForm.js new file mode 100644 index 00000000..5f3fbf0b --- /dev/null +++ b/packages/dashboard-v2/src/components/forms/ResetPasswordForm.js @@ -0,0 +1,72 @@ +import PropTypes from "prop-types"; +import { Formik, Form } from "formik"; +import * as Yup from "yup"; + +import { TextField } from "../Form/TextField"; +import { Button } from "../Button"; + +import accountsService from "../../services/accountsService"; + +const resetPasswordSchema = Yup.object().shape({ + password: Yup.string().required("Password is required").min(6, "Password has to be at least 6 characters long"), + confirmPassword: Yup.string().oneOf([Yup.ref("password"), null], "Passwords must match"), +}); + +export const ResetPasswordForm = ({ token, onSuccess, onFailure }) => ( + { + try { + await accountsService.post("user/recover", { + json: { + token, + password, + confirmPassword, + }, + }); + + onSuccess(); + } catch { + onFailure(); + } + }} + > + {({ errors, touched }) => ( +
+

Set your new password

+ + + +
+ +
+ + )} +
+); + +ResetPasswordForm.propTypes = { + token: PropTypes.string.isRequired, + onFailure: PropTypes.func.isRequired, + onSuccess: PropTypes.func.isRequired, +}; diff --git a/packages/dashboard-v2/src/pages/auth/reset-password.js b/packages/dashboard-v2/src/pages/auth/reset-password.js new file mode 100644 index 00000000..9151f8b4 --- /dev/null +++ b/packages/dashboard-v2/src/pages/auth/reset-password.js @@ -0,0 +1,50 @@ +import { useState } from "react"; + +import AuthLayout from "../../layouts/AuthLayout"; + +import { RecoveryForm } from "../../components/forms/RecoveryForm"; +import HighlightedLink from "../../components/HighlightedLink"; + +const State = { + Pure: "PURE", + Success: "SUCCESS", + Failure: "FAILURE", +}; + +const ResetPasswordPage = () => { + const [state, setState] = useState(State.Pure); + + return ( +
+
+ Skynet +
+ {state !== State.Success && ( + setState(State.Success)} onFailure={() => setState(State.Failure)} /> + )} + + {state === State.Success && ( +

+ Please check your email inbox for further instructions. +

+ )} + + {state === State.Failure && ( +

Something went wrong, please try again later.

+ )} + +
+

+ Suddenly remembered your password? Sign in +

+

+ Don't actually have an account? Create one! +

+
+
+ ); +}; + +ResetPasswordPage.Layout = AuthLayout; + +export default ResetPasswordPage; diff --git a/packages/dashboard-v2/src/pages/user/recover.js b/packages/dashboard-v2/src/pages/user/recover.js new file mode 100644 index 00000000..6f419fdb --- /dev/null +++ b/packages/dashboard-v2/src/pages/user/recover.js @@ -0,0 +1,56 @@ +import { useState } from "react"; +import { navigate } from "gatsby"; + +import AuthLayout from "../../layouts/AuthLayout"; + +import { ResetPasswordForm } from "../../components/forms/ResetPasswordForm"; +import HighlightedLink from "../../components/HighlightedLink"; + +const State = { + Pure: "PURE", + Success: "SUCCESS", + Failure: "FAILURE", +}; + +const RecoverPage = ({ location }) => { + const query = new URLSearchParams(location.search); + const token = query.get("token"); + + const [state, setState] = useState(State.Pure); + + return ( +
+
+ Skynet +
+ {state !== State.Success && ( + { + setState(State.Success); + navigate("/"); + }} + onFailure={() => setState(State.Failure)} + /> + )} + + {state === State.Success && ( +

+ All done! You will be redirected to your dashboard shortly. +

+ )} + + {state === State.Failure && ( +

Something went wrong, please try again later.

+ )} + +

+ Suddenly remembered your old password? Sign in +

+
+ ); +}; + +RecoverPage.Layout = AuthLayout; + +export default RecoverPage; From 202450e9d876c57210a6bfedcf28b1b91d5a3e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Wed, 23 Mar 2022 14:32:35 +0100 Subject: [PATCH 2/6] feat(dashboard-v2): implement account registration flow --- packages/dashboard-v2/gatsby-config.js | 2 +- .../src/components/forms/SignUpForm.js | 109 ++++++++++++++++++ .../dashboard-v2/src/layouts/AuthLayout.js | 49 ++++---- packages/dashboard-v2/src/lib/swrConfig.js | 4 + .../dashboard-v2/src/pages/auth/signup.js | 56 +++++++++ .../dashboard-v2/src/pages/user/confirm.js | 68 +++++++++++ 6 files changed, 265 insertions(+), 23 deletions(-) create mode 100644 packages/dashboard-v2/src/components/forms/SignUpForm.js create mode 100644 packages/dashboard-v2/src/pages/auth/signup.js create mode 100644 packages/dashboard-v2/src/pages/user/confirm.js diff --git a/packages/dashboard-v2/gatsby-config.js b/packages/dashboard-v2/gatsby-config.js index 017a4dfc..160a7784 100644 --- a/packages/dashboard-v2/gatsby-config.js +++ b/packages/dashboard-v2/gatsby-config.js @@ -26,7 +26,7 @@ module.exports = { app.use( "/api/", createProxyMiddleware({ - target: "https://account.siasky.net", + target: "https://account.skynetpro.net", secure: false, // Do not reject self-signed certificates. changeOrigin: true, }) diff --git a/packages/dashboard-v2/src/components/forms/SignUpForm.js b/packages/dashboard-v2/src/components/forms/SignUpForm.js new file mode 100644 index 00000000..7ff7ba87 --- /dev/null +++ b/packages/dashboard-v2/src/components/forms/SignUpForm.js @@ -0,0 +1,109 @@ +import PropTypes from "prop-types"; +import { Formik, Form } from "formik"; +import * as Yup from "yup"; + +import { TextField } from "../Form/TextField"; +import { Button } from "../Button"; + +import accountsService from "../../services/accountsService"; + +// TODO: better password strength validation +const registrationSchema = Yup.object().shape({ + email: Yup.string().required("Email is required").email("Please provide a valid email address"), + password: Yup.string().required("Password is required").min(6, "Password has to be at least 6 characters long"), + confirmPassword: Yup.string().oneOf([Yup.ref("password"), null], "Passwords must match"), +}); + +const USER_EXISTS_ERROR = "identity already belongs to an existing user"; + +export const SignUpForm = ({ onSuccess, onFailure }) => ( + { + try { + await accountsService.post("user", { + json: { + email, + password, + }, + }); + + onSuccess(); + } catch (err) { + let isFormErrorSet = false; + + if (err.response) { + const data = await err.response.json(); + + // If it's a user error, let's capture it and handle within the form. + if (USER_EXISTS_ERROR === data.message) { + setErrors({ email: "This email address already in use." }); + isFormErrorSet = true; + } + } + + if (!isFormErrorSet) { + onFailure(); + } + } + }} + > + {({ errors, touched }) => ( +
+
+

Create your free account

+

Includes 100 GB storage at basic speed

+
+ + + + +
+
    +
  • At least 6 characters long
  • +
  • Significantly different from the email
  • +
+
+ + + +
+ +
+ + )} +
+); + +SignUpForm.propTypes = { + onFailure: PropTypes.func.isRequired, + onSuccess: PropTypes.func.isRequired, +}; diff --git a/packages/dashboard-v2/src/layouts/AuthLayout.js b/packages/dashboard-v2/src/layouts/AuthLayout.js index 9706c83b..8a803fed 100644 --- a/packages/dashboard-v2/src/layouts/AuthLayout.js +++ b/packages/dashboard-v2/src/layouts/AuthLayout.js @@ -3,7 +3,7 @@ import styled from "styled-components"; import { SWRConfig } from "swr"; import { UserProvider } from "../contexts/user"; -import { guestsOnly } from "../lib/swrConfig"; +import { guestsOnly, allUsers } from "../lib/swrConfig"; const Layout = styled.div.attrs({ className: "h-screen w-screen bg-black flex", @@ -21,25 +21,30 @@ const Content = styled.div.attrs({ className: "w-full md:w-5/12 md:max-w-[680px] shrink-0", })``; -const AuthLayout = ({ children }) => { - return ( - <> - - - - -
-

- The decentralized revolution starts with decentralized storage -

-
-
- {children} -
-
-
- - ); -}; +const AuthLayout = + (swrConfig) => + ({ children }) => { + return ( + <> + + + + +
+

+ The decentralized revolution starts with decentralized storage +

+
+
+ {children} +
+
+
+ + ); + }; -export default AuthLayout; +// Some pages (e.g. email confirmation) need to be accessible to both logged-in and guest users. +export const AllUsersAuthLayout = AuthLayout(allUsers); + +export default AuthLayout(guestsOnly); diff --git a/packages/dashboard-v2/src/lib/swrConfig.js b/packages/dashboard-v2/src/lib/swrConfig.js index 3e5f730a..058b5ead 100644 --- a/packages/dashboard-v2/src/lib/swrConfig.js +++ b/packages/dashboard-v2/src/lib/swrConfig.js @@ -23,6 +23,10 @@ const redirectAuthenticated = (key) => return response.json(); }); +export const allUsers = { + fetcher: (key) => fetch(`${baseUrl}/${key}`).then((response) => response.json()), +}; + export const authenticatedOnly = { fetcher: redirectUnauthenticated, }; diff --git a/packages/dashboard-v2/src/pages/auth/signup.js b/packages/dashboard-v2/src/pages/auth/signup.js new file mode 100644 index 00000000..d3df874d --- /dev/null +++ b/packages/dashboard-v2/src/pages/auth/signup.js @@ -0,0 +1,56 @@ +import { useEffect, useState } from "react"; + +import AuthLayout from "../../layouts/AuthLayout"; + +import HighlightedLink from "../../components/HighlightedLink"; +import { SignUpForm } from "../../components/forms/SignUpForm"; +import { navigate } from "gatsby"; + +const State = { + Pure: "PURE", + Success: "SUCCESS", + Failure: "FAILURE", +}; + +const SignUpPage = () => { + const [state, setState] = useState(State.Pure); + + useEffect(() => { + if (state === State.Success) { + const timer = setTimeout(() => navigate("/"), 3000); + + return () => clearTimeout(timer); + } + }, [state]); + + return ( +
+
+ Skynet +
+ {state !== State.Success && ( + setState(State.Success)} onFailure={() => setState(State.Failure)} /> + )} + + {state === State.Success && ( +
+

Please check your email inbox and confirm your email address.

+

You will be redirected to your dashboard shortly.

+ Click here to go there now. +
+ )} + + {state === State.Failure && ( +

Something went wrong, please try again later.

+ )} + +

+ Already have an account? Sign in +

+
+ ); +}; + +SignUpPage.Layout = AuthLayout; + +export default SignUpPage; diff --git a/packages/dashboard-v2/src/pages/user/confirm.js b/packages/dashboard-v2/src/pages/user/confirm.js new file mode 100644 index 00000000..a66fc368 --- /dev/null +++ b/packages/dashboard-v2/src/pages/user/confirm.js @@ -0,0 +1,68 @@ +import { useEffect, useState } from "react"; +import { navigate } from "gatsby"; + +import { AllUsersAuthLayout } from "../../layouts/AuthLayout"; + +import HighlightedLink from "../../components/HighlightedLink"; +import accountsService from "../../services/accountsService"; + +const State = { + Pure: "PURE", + Success: "SUCCESS", + Failure: "FAILURE", +}; + +const EmailConfirmationPage = ({ location }) => { + const query = new URLSearchParams(location.search); + const token = query.get("token"); + + const [state, setState] = useState(State.Pure); + + useEffect(() => { + let timer; + + async function confirm(token) { + try { + await accountsService.get("user/confirm", { searchParams: { token } }); + + timer = setTimeout(() => { + navigate("/"); + }, 3000); + setState(State.Success); + } catch { + setState(State.Failure); + } + } + + if (token) { + confirm(token); + } + + return () => clearTimeout(timer); + }, [token]); + + return ( +
+
+ Skynet +
+
+ {state === State.Pure &&

Please wait while we verify your account...

} + + {state === State.Success && ( + <> +

All done!

+

You will be redirected to your dashboard shortly.

+ Redirect now. + + )} + + {state === State.Failure &&

Something went wrong, please try again later.

} +
+
+ ); +}; + +EmailConfirmationPage.Layout = AllUsersAuthLayout; + +export default EmailConfirmationPage; From 2664a3c4c45e3b5caf3af3a0af41b1d49bba23b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Thu, 24 Mar 2022 09:44:58 +0100 Subject: [PATCH 3/6] fix(dashboard-v2): styling fixes for mobile view --- packages/dashboard-v2/src/layouts/AuthLayout.js | 2 +- packages/dashboard-v2/src/pages/auth/login.js | 4 ++-- packages/dashboard-v2/src/pages/auth/reset-password.js | 4 ++-- packages/dashboard-v2/src/pages/auth/signup.js | 4 ++-- packages/dashboard-v2/src/pages/user/confirm.js | 4 ++-- packages/dashboard-v2/src/pages/user/recover.js | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/dashboard-v2/src/layouts/AuthLayout.js b/packages/dashboard-v2/src/layouts/AuthLayout.js index 8a803fed..85604321 100644 --- a/packages/dashboard-v2/src/layouts/AuthLayout.js +++ b/packages/dashboard-v2/src/layouts/AuthLayout.js @@ -6,7 +6,7 @@ import { UserProvider } from "../contexts/user"; import { guestsOnly, allUsers } from "../lib/swrConfig"; const Layout = styled.div.attrs({ - className: "h-screen w-screen bg-black flex", + className: "min-h-screen w-screen bg-black flex", })` background-image: url(/images/auth-bg.svg); background-repeat: no-repeat; diff --git a/packages/dashboard-v2/src/pages/auth/login.js b/packages/dashboard-v2/src/pages/auth/login.js index 077dda61..e3b5240b 100644 --- a/packages/dashboard-v2/src/pages/auth/login.js +++ b/packages/dashboard-v2/src/pages/auth/login.js @@ -9,8 +9,8 @@ const LoginPage = ({ location }) => { const redirectTo = query.get("return_to"); return ( -
-
+
+
Skynet
navigate(redirectTo || "/")} /> diff --git a/packages/dashboard-v2/src/pages/auth/reset-password.js b/packages/dashboard-v2/src/pages/auth/reset-password.js index 9151f8b4..a7761327 100644 --- a/packages/dashboard-v2/src/pages/auth/reset-password.js +++ b/packages/dashboard-v2/src/pages/auth/reset-password.js @@ -15,8 +15,8 @@ const ResetPasswordPage = () => { const [state, setState] = useState(State.Pure); return ( -
-
+
+
Skynet
{state !== State.Success && ( diff --git a/packages/dashboard-v2/src/pages/auth/signup.js b/packages/dashboard-v2/src/pages/auth/signup.js index d3df874d..210abab8 100644 --- a/packages/dashboard-v2/src/pages/auth/signup.js +++ b/packages/dashboard-v2/src/pages/auth/signup.js @@ -24,8 +24,8 @@ const SignUpPage = () => { }, [state]); return ( -
-
+
+
Skynet
{state !== State.Success && ( diff --git a/packages/dashboard-v2/src/pages/user/confirm.js b/packages/dashboard-v2/src/pages/user/confirm.js index a66fc368..e0eb9778 100644 --- a/packages/dashboard-v2/src/pages/user/confirm.js +++ b/packages/dashboard-v2/src/pages/user/confirm.js @@ -42,8 +42,8 @@ const EmailConfirmationPage = ({ location }) => { }, [token]); return ( -
-
+
+
Skynet
diff --git a/packages/dashboard-v2/src/pages/user/recover.js b/packages/dashboard-v2/src/pages/user/recover.js index 6f419fdb..686e677a 100644 --- a/packages/dashboard-v2/src/pages/user/recover.js +++ b/packages/dashboard-v2/src/pages/user/recover.js @@ -19,8 +19,8 @@ const RecoverPage = ({ location }) => { const [state, setState] = useState(State.Pure); return ( -
-
+
+
Skynet
{state !== State.Success && ( From 6b733ab739ca84077580983f1efca4ecc6344ab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 25 Mar 2022 10:52:28 +0100 Subject: [PATCH 4/6] style(dashboard-v2): unify password schemas --- .../src/components/forms/ResetPasswordForm.js | 9 ++------ .../src/components/forms/SignUpForm.js | 22 +++++++++---------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/packages/dashboard-v2/src/components/forms/ResetPasswordForm.js b/packages/dashboard-v2/src/components/forms/ResetPasswordForm.js index 5f3fbf0b..68e8480f 100644 --- a/packages/dashboard-v2/src/components/forms/ResetPasswordForm.js +++ b/packages/dashboard-v2/src/components/forms/ResetPasswordForm.js @@ -1,24 +1,19 @@ import PropTypes from "prop-types"; import { Formik, Form } from "formik"; -import * as Yup from "yup"; import { TextField } from "../Form/TextField"; import { Button } from "../Button"; +import { passwordSchema } from "./SignUpForm"; import accountsService from "../../services/accountsService"; -const resetPasswordSchema = Yup.object().shape({ - password: Yup.string().required("Password is required").min(6, "Password has to be at least 6 characters long"), - confirmPassword: Yup.string().oneOf([Yup.ref("password"), null], "Passwords must match"), -}); - export const ResetPasswordForm = ({ token, onSuccess, onFailure }) => ( { try { await accountsService.post("user/recover", { diff --git a/packages/dashboard-v2/src/components/forms/SignUpForm.js b/packages/dashboard-v2/src/components/forms/SignUpForm.js index 7ff7ba87..976c0c74 100644 --- a/packages/dashboard-v2/src/components/forms/SignUpForm.js +++ b/packages/dashboard-v2/src/components/forms/SignUpForm.js @@ -7,13 +7,19 @@ import { Button } from "../Button"; import accountsService from "../../services/accountsService"; -// TODO: better password strength validation -const registrationSchema = Yup.object().shape({ - email: Yup.string().required("Email is required").email("Please provide a valid email address"), - password: Yup.string().required("Password is required").min(6, "Password has to be at least 6 characters long"), - confirmPassword: Yup.string().oneOf([Yup.ref("password"), null], "Passwords must match"), +export const passwordSchema = Yup.object().shape({ + password: Yup.string().required("Password is required"), + confirmPassword: Yup.string() + .oneOf([Yup.ref("password"), null], "Passwords must match") + .required("Please re-type your password"), }); +const registrationSchema = Yup.object() + .shape({ + email: Yup.string().required("Email is required").email("Please provide a valid email address"), + }) + .concat(passwordSchema); + const USER_EXISTS_ERROR = "identity already belongs to an existing user"; export const SignUpForm = ({ onSuccess, onFailure }) => ( @@ -77,12 +83,6 @@ export const SignUpForm = ({ onSuccess, onFailure }) => ( error={errors.password} touched={touched.password} /> -
-
    -
  • At least 6 characters long
  • -
  • Significantly different from the email
  • -
-
Date: Fri, 25 Mar 2022 10:52:59 +0100 Subject: [PATCH 5/6] style(dashboard-v2): unify wording of messages --- packages/dashboard-v2/src/components/forms/LoginForm.js | 2 +- packages/dashboard-v2/src/pages/auth/reset-password.js | 6 ++---- packages/dashboard-v2/src/pages/auth/signup.js | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/dashboard-v2/src/components/forms/LoginForm.js b/packages/dashboard-v2/src/components/forms/LoginForm.js index 61973215..a5a1deeb 100644 --- a/packages/dashboard-v2/src/components/forms/LoginForm.js +++ b/packages/dashboard-v2/src/components/forms/LoginForm.js @@ -70,7 +70,7 @@ export const LoginForm = ({ onSuccess }) => { touched={touched.password} />
- + Forgot your password?
diff --git a/packages/dashboard-v2/src/pages/auth/reset-password.js b/packages/dashboard-v2/src/pages/auth/reset-password.js index a7761327..8ecbfeaa 100644 --- a/packages/dashboard-v2/src/pages/auth/reset-password.js +++ b/packages/dashboard-v2/src/pages/auth/reset-password.js @@ -24,9 +24,7 @@ const ResetPasswordPage = () => { )} {state === State.Success && ( -

- Please check your email inbox for further instructions. -

+

Please check your inbox for further instructions.

)} {state === State.Failure && ( @@ -38,7 +36,7 @@ const ResetPasswordPage = () => { Suddenly remembered your password? Sign in

- Don't actually have an account? Create one! + Don't actually have an account? Create one!

diff --git a/packages/dashboard-v2/src/pages/auth/signup.js b/packages/dashboard-v2/src/pages/auth/signup.js index 210abab8..4acbae85 100644 --- a/packages/dashboard-v2/src/pages/auth/signup.js +++ b/packages/dashboard-v2/src/pages/auth/signup.js @@ -34,7 +34,7 @@ const SignUpPage = () => { {state === State.Success && (
-

Please check your email inbox and confirm your email address.

+

Please check your inbox and confirm your email address.

You will be redirected to your dashboard shortly.

Click here to go there now.
From be82050b869b8b7eeadb1e93a0cf310eab097749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 25 Mar 2022 10:53:16 +0100 Subject: [PATCH 6/6] fix(dashboard-v2): fix possible race condition --- .../dashboard-v2/src/pages/user/confirm.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/dashboard-v2/src/pages/user/confirm.js b/packages/dashboard-v2/src/pages/user/confirm.js index e0eb9778..9e95e3e3 100644 --- a/packages/dashboard-v2/src/pages/user/confirm.js +++ b/packages/dashboard-v2/src/pages/user/confirm.js @@ -19,18 +19,25 @@ const EmailConfirmationPage = ({ location }) => { const [state, setState] = useState(State.Pure); useEffect(() => { + const controller = new AbortController(); let timer; async function confirm(token) { try { - await accountsService.get("user/confirm", { searchParams: { token } }); + await accountsService.get("user/confirm", { + signal: controller.signal, + searchParams: { token }, + }); timer = setTimeout(() => { navigate("/"); }, 3000); setState(State.Success); - } catch { - setState(State.Failure); + } catch (err) { + // Don't show an error message if request was aborted due to `token` changing. + if (err.code !== DOMException.ABORT_ERR) { + setState(State.Failure); + } } } @@ -38,7 +45,10 @@ const EmailConfirmationPage = ({ location }) => { confirm(token); } - return () => clearTimeout(timer); + return () => { + controller.abort(); + clearTimeout(timer); + }; }, [token]); return (