diff --git a/src/App.tsx b/src/App.tsx index 12004605..dddb9f81 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,14 +2,18 @@ import * as Sentry from '@sentry/react'; import { Route, BrowserRouter as Router, Routes } from 'react-router-dom'; import ErrorFallback from './components/ErrorFallback'; +import ForgotPassword from './components/ForgotPassword'; import MagicLinkSuccessContent from './components/MagicLinkSuccessContent'; import MobileAuth from './components/MobileAuth'; import Redirection from './components/Redirection'; +import ResetPassword from './components/ResetPassword'; import SignIn from './components/SignIn'; import SignUp from './components/SignUp'; import { + FORGOT_PASSWORD_PATH, HOME_PATH, MOBILE_AUTH_PATH, + RESET_PASSWORD_PATH, SIGN_IN_MAGIC_LINK_SUCCESS_PATH, SIGN_IN_PATH, SIGN_UP_PATH, @@ -26,6 +30,8 @@ const App = () => ( element={} /> } /> + } /> + } /> } /> } /> diff --git a/src/components/ForgotPassword.tsx b/src/components/ForgotPassword.tsx new file mode 100644 index 00000000..fd445bb6 --- /dev/null +++ b/src/components/ForgotPassword.tsx @@ -0,0 +1,64 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; + +import { Button, GraaspLogo } from '@graasp/ui'; + +import { Stack, useTheme } from '@mui/material'; +import FormControl from '@mui/material/FormControl'; +import Typography from '@mui/material/Typography'; + +import { useAuthTranslation } from '../config/i18n'; +import { SIGN_IN_PATH } from '../config/paths'; +import { AUTH } from '../langs/constants'; +import EmailInput from './EmailInput'; +import FullscreenContainer from './FullscreenContainer'; + +const ForgotPassword = () => { + const { t } = useAuthTranslation(); + const theme = useTheme(); + + // enable validation after first click + const [shouldValidate, setShouldValidate] = useState(false); + const [email, setEmail] = useState(''); + + const resetPassword = () => { + setShouldValidate(true); + }; + + const handleKeypress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + resetPassword(); + } + }; + + return ( + + { + + + + + {t(AUTH.FORGOT_PASSWORD_TITLE)} + + + + + + + + + {t(AUTH.FORGOT_PASSWORD_BACK_BUTTON)} + + } + + ); +}; + +export default ForgotPassword; diff --git a/src/components/ResetPassword.tsx b/src/components/ResetPassword.tsx new file mode 100644 index 00000000..0bdd96c0 --- /dev/null +++ b/src/components/ResetPassword.tsx @@ -0,0 +1,123 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; + +import { Button, GraaspLogo } from '@graasp/ui'; + +import { Stack, useTheme } from '@mui/material'; +import FormControl from '@mui/material/FormControl'; +import Typography from '@mui/material/Typography'; + +import { useAuthTranslation } from '../config/i18n'; +import { SIGN_IN_PATH } from '../config/paths'; +import { AUTH } from '../langs/constants'; +import { passwordValidator } from '../utils/validation'; +import FullscreenContainer from './FullscreenContainer'; +import StyledTextField from './StyledTextField'; + +const ResetPassword = () => { + const { t } = useAuthTranslation(); + const theme = useTheme(); + + // enable validation after first click + const [passwordError, setPasswordError] = useState(null); + const [confirmPasswordError, setConfirmPasswordError] = useState< + string | null + >(null); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + + const resetPassword = () => { + const checkingPassword = passwordValidator(password); + const checkingConfirmPassword = passwordValidator(confirmPassword); + if (checkingPassword || checkingConfirmPassword) { + if (checkingPassword) { + setPasswordError(checkingPassword); + } + if (checkingConfirmPassword) { + setConfirmPasswordError(checkingConfirmPassword); + } + } else { + setPasswordError(null); + setConfirmPasswordError(null); + // const token = await executeCaptcha( + // isMobile + // ? RecaptchaAction.SignInWithPasswordMobile + // : RecaptchaAction.SignInWithPassword, + // ); + // const result = await (isMobile + // ? mobileSignInWithPassword({ + // email: lowercaseEmail, + // password, + // captcha: token, + // challenge, + // }) + // : signInWithPassword({ + // email: lowercaseEmail, + // password, + // captcha: token, + // url: redirect.url, + // })); + // // successful redirect + // if (result && result.resource) { + // window.location.href = result.resource; + // } + } + }; + + const handleKeypress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + resetPassword(); + } + }; + + return ( + + { + + + + + {t(AUTH.RESET_PASSWORD_TITLE)} + + + + + { + setPassword(e.target.value); + }} + type="password" + onKeyDown={handleKeypress} + /> + { + setConfirmPassword(e.target.value); + }} + type="password" + onKeyDown={handleKeypress} + /> + + + + {t(AUTH.FORGOT_PASSWORD_BACK_BUTTON)} + + } + + ); +}; + +export default ResetPassword; diff --git a/src/components/SignInPasswordForm.tsx b/src/components/SignInPasswordForm.tsx index 3c2d343e..57bf67ca 100644 --- a/src/components/SignInPasswordForm.tsx +++ b/src/components/SignInPasswordForm.tsx @@ -1,10 +1,12 @@ import React, { useState } from 'react'; +import { Link } from 'react-router-dom'; import { RecaptchaAction } from '@graasp/sdk'; import { Alert, LoadingButton } from '@mui/lab'; import { useAuthTranslation } from '../config/i18n'; +import { FORGOT_PASSWORD_PATH } from '../config/paths'; import { mutations } from '../config/queryClient'; import { EMAIL_SIGN_IN_FIELD_ID, @@ -131,6 +133,7 @@ const SignInPasswordForm = () => { > {t(SIGN_IN_BUTTON)} + {t(AUTH.FORGOT_PASSWORD_LINK)} {(signInWithPasswordSuccess || mobileSignInWithPasswordSuccess) && ( {t(AUTH.PASSWORD_SUCCESS_ALERT)} diff --git a/src/config/paths.ts b/src/config/paths.ts index ae04b62a..ca846f73 100644 --- a/src/config/paths.ts +++ b/src/config/paths.ts @@ -3,3 +3,5 @@ export const SIGN_IN_MAGIC_LINK_SUCCESS_PATH = '/signin/success'; export const SIGN_UP_PATH = '/signup'; export const HOME_PATH = '/'; export const MOBILE_AUTH_PATH = '/auth'; +export const FORGOT_PASSWORD_PATH = '/password/forgot'; +export const RESET_PASSWORD_PATH = '/password/reset'; diff --git a/src/langs/constants.ts b/src/langs/constants.ts index b6313a20..3d42db4c 100644 --- a/src/langs/constants.ts +++ b/src/langs/constants.ts @@ -41,4 +41,10 @@ export const AUTH = { EMPTY_EMAIL_ERROR: 'EMPTY_EMAIL_ERROR', PASSWORD_EMPTY_ERROR: 'PASSWORD_EMPTY_ERROR', PASSWORD_SUCCESS_ALERT: 'PASSWORD_SUCCESS_ALERT', + FORGOT_PASSWORD_LINK: 'FORGOT_PASSWORD_LINK', + FORGOT_PASSWORD_TITLE: 'FORGOT_PASSWORD_TITLE', + FORGOT_PASSWORD_BUTTON: 'FORGOT_PASSWORD_BUTTON', + FORGOT_PASSWORD_BACK_BUTTON: 'FORGOT_PASSWORD_BACK_BUTTON', + RESET_PASSWORD_TITLE: 'RESET_PASSWORD_TITLE', + CONFIRM_PASSWORD_FIELD_LABEL: 'CONFIRM_PASSWORD_FIELD_LABEL', }; diff --git a/src/langs/en.json b/src/langs/en.json index a16183a2..855ca059 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -39,5 +39,11 @@ "INVALID_EMAIL_ERROR": "This does not look like a valid email address", "EMPTY_EMAIL_ERROR": "An email address is required, this field can not be empty", "PASSWORD_EMPTY_ERROR": "The password can not be empty", - "PASSWORD_SUCCESS_ALERT": "You successfully signed in. You will be redirected soon." + "PASSWORD_SUCCESS_ALERT": "You successfully signed in. You will be redirected soon.", + "FORGOT_PASSWORD_LINK": "Forgot your password? Click here to reset", + "FORGOT_PASSWORD_TITLE": "Request new password", + "FORGOT_PASSWORD_BUTTON": "Reset", + "FORGOT_PASSWORD_BACK_BUTTON": "Back to sign in", + "RESET_PASSWORD_TITLE": "Reset your password", + "CONFIRM_PASSWORD_FIELD_LABEL": "Confirm password" }