Forgotten password

The forgotten password page exists to allow users to request a password reset email when they have forgotten their password. We therefore need the user to enter their email and then we'll use the members API to send them a password reset email or if that fails display a generic error.

This page is helpful to the user, but doesn't add any value, so we'll need to make it as quick and easy to complete as possible, by only asking for their email address.

User's are likely to navigate between the registration, login, and forgotten password pages as they often can't remember if they have an account or what the password was. For this reason we can reduce their effort by persisting any email address they've typed in across these pages and offer links between the pages.

The following code to do all this should be placed in frontend/src/pages/ForgottenPassword.tsx,

import Box from "@material-ui/core/Box";
import Typography from "@material-ui/core/Typography";
import axios from "axios";
import { Form, Formik } from "formik";
import React, { useContext } from "react";
import { useTranslation } from "react-i18next";
import { useHistory, useLocation } from "react-router";
import * as yup from "yup";

import EmailField from "src/components/EmailField";
import SecondaryButton from "src/components/SecondaryButton";
import SubmitButton from "src/components/SubmitButton";
import { useMutation } from "src/query";
import { ToastContext } from "src/ToastContext";

interface IForm {
  email: string;
}

interface ILocationState {
  email?: string;
}

const ForgottenPassword = () => {
  const { t } = useTranslation();
  const history = useHistory();
  const location = useLocation<ILocationState>();
  const { addToast } = useContext(ToastContext);

  const { mutateAsync: forgottenPassword } = useMutation(
    async (data: IForm) => {
      await axios.post("/members/forgotten-password/", data);
    },
  );

  const validationSchema = yup.object({
    email: yup
      .string()
      .email(t("generic.emailRequired"))
      .required(t("generic.required")),
  });

  const onSubmit = async (data: IForm) => {
    try {
      await forgottenPassword(data);
      addToast({
        category: "success",
        message: t("ForgottenPassword.success"),
      });
      history.push("/login/");
    } catch {
      addToast({ category: "error", message: t("generic.tryAgainError") });
    }
  };

  return (
    <>
      <Box mb={1} mt={2}>
        <Typography component="h1" variant="h5">
          {t("ForgottenPassword.lead")}
        </Typography>
      </Box>
      <Formik<IForm>
        initialValues={{
          email: location.state?.email ?? "",
        }}
        onSubmit={onSubmit}
        validationSchema={validationSchema}
      >
        {({ isSubmitting, values }) => (
          <Form>
            <EmailField
              fullWidth={true}
              label={t("generic.email")}
              name="email"
              required={true}
            />
            <SubmitButton
              label={t("ForgottenPassword.submit")}
              submitting={isSubmitting}
            />
           <SecondaryButton
              label={t("generic.register")}
              to={{
                pathname: "/register/",
                state: { email: values.email },
              }}
            />
            <SecondaryButton
              label={t("generic.login")}
              to={{
                pathname: "/login/",
                state: { email: values.email },
              }}
            />
          </Form>
        )}
      </Formik>
    </>
  );
};

export default ForgottenPassword;

Then we can add the page to the routing by adding the following to frontend/src/Router.tsx,

import ForgottenPassword from "src/pages/ForgottenPassword";
...

const Router = () => (
  <BrowserRouter>
    ...
    <Route exact={true} path="/forgotten-password/">
      <ForgottenPassword />
    </Route>
  </BrowserRouter>
);