diff --git a/src/langs/fa.json b/src/langs/fa.json index ff1909d..9817035 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -35,6 +35,14 @@ "enter_password_step3": "رمز عبور را وارد کنید", "enter_your_password": "رمز عبور خود را وارد کنید.", "forgot_password": "فراموشی رمز عبور", + "forgot_password_description": "ایمیل خود را وارد کنید تا کد بازیابی برای شما ارسال شود.", + "reset_password": "بازیابی رمز عبور", + "new_password": "رمز عبور جدید", + "confirm_password": "تکرار رمز عبور", + "password_mismatch": "رمز عبور و تکرار آن یکسان نیستند", + "change_email": "تغییر ایمیل", + "resend_code": "ارسال مجدد کد", + "back_to_login": "بازگشت به صفحه ورود", "login_with_otp": "ورود با رمز عبور یکبار مصرف", "otp_sent": "رمز یک بار مصرف برای شما ارسال شد" }, diff --git a/src/pages/auth/ForgotPassword.tsx b/src/pages/auth/ForgotPassword.tsx new file mode 100644 index 0000000..1a25783 --- /dev/null +++ b/src/pages/auth/ForgotPassword.tsx @@ -0,0 +1,50 @@ +import { FC, useState } from 'react' +import LogoImage from '../../assets/images/logo.svg' +import LogoSmallImage from '../../assets/images/logo-small.svg' +import { Link } from 'react-router-dom' +import { Pages } from '../../config/Pages' +import { useTranslation } from 'react-i18next' +import ForgotPasswordStep1 from './components/ForgotPasswordStep1' +import ForgotPasswordStep2 from './components/ForgotPasswordStep2' + +const ForgotPassword: FC = () => { + + const { t } = useTranslation('global') + const [step, setStep] = useState(1) + + return ( +
+
+
+
+ + +
+ { + step === 1 ? + setStep(2)} /> + : + setStep(1)} /> + } + +
+

+ {t('auth.back_to_login')} +

+ +
{t('auth.login')}
+ +
+
+
+
+ +
+ +
+
+
+ ) +} + +export default ForgotPassword diff --git a/src/pages/auth/components/ForgotPasswordStep1.tsx b/src/pages/auth/components/ForgotPasswordStep1.tsx new file mode 100644 index 0000000..2dcfdb6 --- /dev/null +++ b/src/pages/auth/components/ForgotPasswordStep1.tsx @@ -0,0 +1,88 @@ +import { FC } from 'react' +import Input from '../../../components/Input' +import { useTranslation } from 'react-i18next' +import { ForgotPasswordType } from '../types/AuthTypes' +import { useFormik } from 'formik' +import * as Yup from 'yup' +import { useAuthStore } from '../store/AuthStore' +import Error from '../../../components/Error' +import Button from '../../../components/Button' +import { useForgotPassword } from '../hooks/useAuthData' +import { toast } from '../../../components/Toast'; +import { ErrorType } from '../../../helpers/types' +import { isEmail } from '../../../config/func' + +type Props = { + onSuccess: () => void +} + +const ForgotPasswordStep1: FC = ({ onSuccess }) => { + + const { t } = useTranslation('global') + const { email, setEmail } = useAuthStore() + const forgotPassword = useForgotPassword() + + const formik = useFormik({ + initialValues: { + email: email, + }, + validationSchema: Yup.object({ + email: Yup.string() + .required(t('errors.required')) + .test('is-email', t('errors.required'), (value) => !!value && isEmail(value)), + }), + onSubmit(values) { + setEmail(values.email) + forgotPassword.mutate({ email: values.email }, { + onSuccess() { + toast(t('auth.otp_sent'), 'success') + onSuccess() + }, + onError(error: ErrorType) { + toast(error?.response?.data?.error?.message[0], 'error') + }, + }) + }, + }) + + return ( +
+
+

+ {t('auth.forgot_password')} +

+

+ {t('auth.forgot_password_description')} +

+
+ +
+ + + { + formik.touched.email && formik.errors.email && + + } +
+ +
+ ) +} + +export default ForgotPasswordStep1 diff --git a/src/pages/auth/components/ForgotPasswordStep2.tsx b/src/pages/auth/components/ForgotPasswordStep2.tsx new file mode 100644 index 0000000..3dfe4f0 --- /dev/null +++ b/src/pages/auth/components/ForgotPasswordStep2.tsx @@ -0,0 +1,172 @@ +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { ResetPasswordType } from '../types/AuthTypes' +import { useFormik } from 'formik' +import * as Yup from 'yup' +import { useAuthStore } from '../store/AuthStore' +import Button from '../../../components/Button' +import Input from '../../../components/Input' +import Error from '../../../components/Error' +import OTPInput from 'react-otp-input' +import { useCountDown } from '../../../hooks/useCountDown' +import { Pages } from '../../../config/Pages' +import { useForgotPassword, useResetPassword } from '../hooks/useAuthData' +import { ErrorType } from '../../../helpers/types' +import { toast } from '../../../components/Toast'; +import { useNavigate } from 'react-router-dom' + +type Props = { + onBack: () => void +} + +const ForgotPasswordStep2: FC = ({ onBack }) => { + + const { t } = useTranslation('global') + const navigate = useNavigate() + const { email } = useAuthStore() + const { value, reset } = useCountDown(2, true) + const resetPassword = useResetPassword() + const forgotPassword = useForgotPassword() + + const formik = useFormik({ + initialValues: { + email: email, + code: '', + newPassword: '', + confirmPassword: '', + }, + validationSchema: Yup.object({ + code: Yup.string() + .required(t('errors.required')), + newPassword: Yup.string() + .required(t('errors.required')), + confirmPassword: Yup.string() + .required(t('errors.required')) + .oneOf([Yup.ref('newPassword')], t('auth.password_mismatch')), + }), + onSubmit(values) { + resetPassword.mutate({ + email, + code: values.code, + newPassword: values.newPassword, + }, { + onSuccess() { + toast(t('success'), 'success') + navigate(Pages.auth.login) + }, + onError(error: ErrorType) { + toast(error?.response?.data?.error?.message[0], 'error') + }, + }) + }, + }) + + const handleResend = () => { + forgotPassword.mutate({ email }, { + onSuccess() { + reset() + toast(t('auth.otp_sent'), 'success') + }, + onError(error: ErrorType) { + toast(error?.response?.data?.error?.message[0], 'error') + }, + }) + } + + return ( +
+
+

+ {t('auth.reset_password')} +

+

+

+
{t('auth.verfify_code_text_1')}
+
{email}
+
{t('auth.verfify_code_text_2')}
+
+
+ {t('auth.change_email')} +
+

+
+ +
+
+ {t('auth.verify_code')} +
+
+ formik.setFieldValue('code', otp)} + renderInput={(props) => } + /> +
+ + { + formik.touched.code && formik.errors.code && +
{formik.errors.code}
+ } + +
+
+ {t('auth.resend_code')} +
+
+
{value}
+
{t('auth.counter_otp')}
+
+
+
+ +
+ + { + formik.touched.newPassword && formik.errors.newPassword && + + } +
+ +
+ + { + formik.touched.confirmPassword && formik.errors.confirmPassword && + + } +
+ +
+ ) +} + +export default ForgotPasswordStep2 diff --git a/src/pages/auth/hooks/useAuthData.tsx b/src/pages/auth/hooks/useAuthData.tsx index 0c480e9..6ea1549 100644 --- a/src/pages/auth/hooks/useAuthData.tsx +++ b/src/pages/auth/hooks/useAuthData.tsx @@ -1,6 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import * as api from '../service/AuthService' -import { CheckHasAccountPhoneType, CheckHasAccountType, LoginWithOtpType, LoginWithPasswordType, OtpVerifyType, RegisterType } from '../types/AuthTypes'; +import { CheckHasAccountPhoneType, CheckHasAccountType, ForgotPasswordType, LoginWithOtpType, LoginWithPasswordType, OtpVerifyType, RegisterType, ResetPasswordType } from '../types/AuthTypes'; export const useLoginWithPassword = () => { return useMutation({ @@ -36,4 +36,16 @@ export const useRegister = () => { return useMutation({ mutationFn: (variables: RegisterType) => api.register(variables), }); +}; + +export const useForgotPassword = () => { + return useMutation({ + mutationFn: (variables: ForgotPasswordType) => api.forgotPassword(variables), + }); +}; + +export const useResetPassword = () => { + return useMutation({ + mutationFn: (variables: ResetPasswordType) => api.resetPassword(variables), + }); }; \ No newline at end of file diff --git a/src/pages/auth/service/AuthService.ts b/src/pages/auth/service/AuthService.ts index 41f9f20..d25bdbc 100644 --- a/src/pages/auth/service/AuthService.ts +++ b/src/pages/auth/service/AuthService.ts @@ -2,11 +2,13 @@ import axios from "../../../config/axios"; import { CheckHasAccountPhoneType, CheckHasAccountType, + ForgotPasswordType, LoginWithOtpType, LoginWithPasswordType, OtpVerifyType, RefreshTokenType, RegisterType, + ResetPasswordType, } from "../types/AuthTypes"; export const loginWithPassword = async (params: LoginWithPasswordType) => { @@ -43,3 +45,13 @@ export const refreshToken = async (params: RefreshTokenType) => { const { data } = await axios.post(`/auth/refresh`, params); return data; }; + +export const forgotPassword = async (params: ForgotPasswordType) => { + const { data } = await axios.post(`/auth/forgot-password`, params); + return data; +}; + +export const resetPassword = async (params: ResetPasswordType) => { + const { data } = await axios.post(`/auth/reset-password`, params); + return data; +}; diff --git a/src/pages/auth/types/AuthTypes.ts b/src/pages/auth/types/AuthTypes.ts index a9c1019..8259d32 100644 --- a/src/pages/auth/types/AuthTypes.ts +++ b/src/pages/auth/types/AuthTypes.ts @@ -52,3 +52,13 @@ export type RegisterType = { export type RefreshTokenType = { refreshToken: string; }; + +export type ForgotPasswordType = { + email: string; +}; + +export type ResetPasswordType = { + email: string; + code: string; + newPassword: string; +}; diff --git a/src/router/Auth.tsx b/src/router/Auth.tsx index 12f7a30..b2c4b75 100644 --- a/src/router/Auth.tsx +++ b/src/router/Auth.tsx @@ -2,13 +2,15 @@ import { FC } from 'react' import { Route, Routes } from 'react-router-dom' import { Pages } from '../config/Pages' import Login from '../pages/auth/Login' +import ForgotPassword from '../pages/auth/ForgotPassword' const AuthRouter: FC = () => { return ( } /> + } /> ) } -export default AuthRouter \ No newline at end of file +export default AuthRouter