diff --git a/packages/ui/src/button/helper.tsx b/packages/ui/src/button/helper.tsx index 48b1fc94a..5e6ff6a51 100644 --- a/packages/ui/src/button/helper.tsx +++ b/packages/ui/src/button/helper.tsx @@ -49,9 +49,9 @@ export const buttonStyling: IButtonStyling = { disabled: `cursor-not-allowed !text-custom-primary-60`, }, "outline-primary": { - default: `text-custom-primary-100 bg-custom-background-100 border border-custom-primary-100`, - hover: `hover:border-custom-primary-80 hover:bg-custom-primary-10`, - pressed: `focus:text-custom-primary-80 focus:bg-custom-primary-10 focus:border-custom-primary-80`, + default: `text-custom-primary-100 bg-transparent border border-custom-primary-100`, + hover: `hover:bg-custom-primary-100/20`, + pressed: `focus:text-custom-primary-100 focus:bg-custom-primary-100/30`, disabled: `cursor-not-allowed !text-custom-primary-60 !border-custom-primary-60 `, }, "neutral-primary": { @@ -80,7 +80,7 @@ export const buttonStyling: IButtonStyling = { disabled: `cursor-not-allowed !text-red-300`, }, "outline-danger": { - default: `text-red-500 bg-custom-background-100 border border-red-500`, + default: `text-red-500 bg-transparent border border-red-500`, hover: `hover:text-red-400 hover:border-red-400`, pressed: `focus:text-red-400 focus:border-red-400`, disabled: `cursor-not-allowed !text-red-300 !border-red-300`, diff --git a/web/components/account/email-code-form.tsx b/web/components/account/email-code-form.tsx deleted file mode 100644 index b71a92756..000000000 --- a/web/components/account/email-code-form.tsx +++ /dev/null @@ -1,314 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { XCircle } from "lucide-react"; -// ui -import { Button, Input } from "@plane/ui"; -// components -import { AuthType } from "components/page-views"; -// services -import { AuthService } from "services/auth.service"; -// hooks -import useToast from "hooks/use-toast"; -import useTimer from "hooks/use-timer"; - -type EmailCodeFormValues = { - email: string; - key?: string; - token?: string; -}; - -const authService = new AuthService(); - -type Props = { - handleSignIn: any; - authType: AuthType; -}; - -export const EmailCodeForm: React.FC = (Props) => { - const { handleSignIn, authType } = Props; - // states - const [codeSent, setCodeSent] = useState(false); - const [codeResent, setCodeResent] = useState(false); - const [isCodeResending, setIsCodeResending] = useState(false); - const [errorResendingCode, setErrorResendingCode] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [sentEmail, setSentEmail] = useState(""); - - const { setToastAlert } = useToast(); - const { timer: resendCodeTimer, setTimer: setResendCodeTimer } = useTimer(); - - const { - handleSubmit, - control, - setError, - setValue, - getValues, - formState: { errors, isSubmitting, isValid, isDirty }, - } = useForm({ - defaultValues: { - email: "", - key: "", - token: "", - }, - mode: "onChange", - reValidateMode: "onChange", - }); - - const isResendDisabled = resendCodeTimer > 0 || isCodeResending || isSubmitting; - - const onSubmit = async ({ email }: EmailCodeFormValues) => { - setErrorResendingCode(false); - await authService - .emailCode({ email }) - .then((res) => { - setSentEmail(email); - setValue("key", res.key); - setCodeSent(true); - }) - .catch((err) => { - setErrorResendingCode(true); - setToastAlert({ - title: "Oops!", - type: "error", - message: err?.error, - }); - }); - }; - - const handleSignin = async (formData: EmailCodeFormValues) => { - setIsLoading(true); - await authService - .magicSignIn(formData) - .then((response) => { - handleSignIn(response); - }) - .catch((error) => { - setIsLoading(false); - setToastAlert({ - title: "Oops!", - type: "error", - message: error?.response?.data?.error ?? "Enter the correct code to sign in", - }); - setError("token" as keyof EmailCodeFormValues, { - type: "manual", - message: error?.error, - }); - }); - }; - - const emailOld = getValues("email"); - - useEffect(() => { - const submitForm = (e: KeyboardEvent) => { - if (!codeSent && e.key === "Enter") { - e.preventDefault(); - handleSubmit(onSubmit)().then(() => { - setResendCodeTimer(30); - }); - } else if ( - codeSent && - sentEmail != getValues("email") && - getValues("email").length > 0 && - (e.key === "Enter" || e.key === "Tab") - ) { - e.preventDefault(); - console.log("resend"); - onSubmit({ email: getValues("email") }).then(() => { - setCodeResent(true); - }); - } - }; - - window.addEventListener("keydown", submitForm); - - return () => { - window.removeEventListener("keydown", submitForm); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [handleSubmit, codeSent, sentEmail]); - - return ( - <> - {codeSent || codeResent ? ( - <> -

- Moving to the runway -

-
-

Paste the code you got at

- {sentEmail} - below. -
- - ) : ( - <> -

- {authType === "sign-in" ? "Get on your flight deck!" : "Let’s get you prepped!"} -

- {authType == "sign-up" ? ( -
-

- This whole thing will take less than two minutes. -

-

Promise!

-
- ) : ( -

- Sign in with the email you used to sign up for Plane -

- )} - - )} - -
-
- - /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( - value - ) || "Email address is not valid", - }} - render={({ field: { value, onChange, ref } }) => ( -
- - {value.length > 0 && ( - setValue("email", "")} - /> - )} -
- )} - /> -
- - {codeSent && ( - <> -
- {codeResent && sentEmail === getValues("email") ? ( -
- You got a new code at {sentEmail}. -
- ) : sentEmail != getValues("email") && getValues("email").length > 0 ? ( -
- Hit enter - or Tab to get a new code -
- ) : ( -
- )} -
-
- ( - - )} - /> - {resendCodeTimer <= 0 && !isResendDisabled && ( - - )} -
-
- {resendCodeTimer > 0 ? ( - Request new code in {resendCodeTimer}s - ) : isCodeResending ? ( - "Sending new code..." - ) : null} -
- - )} - {codeSent ? ( -
- {" "} - -
-

- When you click the button above, you agree with our{" "} - - terms and conditions of service. - {" "} -

-
-
- ) : ( - - )} - - - ); -}; diff --git a/web/components/account/email-forgot-password-form.tsx b/web/components/account/email-forgot-password-form.tsx deleted file mode 100644 index a78efec5d..000000000 --- a/web/components/account/email-forgot-password-form.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { FC } from "react"; -import { useRouter } from "next/router"; -import { useForm, Controller } from "react-hook-form"; -// ui -import { Input, Button } from "@plane/ui"; - -export interface EmailForgotPasswordFormValues { - email: string; -} - -export interface IEmailForgotPasswordForm { - onSubmit: (formValues: any) => Promise; -} - -export const EmailForgotPasswordForm: FC = (props) => { - const { onSubmit } = props; - // router - const router = useRouter(); - // form data - const { - control, - handleSubmit, - formState: { errors, isSubmitting }, - } = useForm({ - defaultValues: { - email: "", - }, - mode: "onChange", - reValidateMode: "onChange", - }); - - return ( -
-
- - /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( - value - ) || "Email address is not valid", - }} - render={({ field: { value, onChange } }) => ( - - )} - /> -
-
- - -
-
- ); -}; diff --git a/web/components/account/email-password-form.tsx b/web/components/account/email-password-form.tsx deleted file mode 100644 index fe8d2d51c..000000000 --- a/web/components/account/email-password-form.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React from "react"; -import { useForm, Controller } from "react-hook-form"; -import { useRouter } from "next/router"; -// ui -import { Input, Button } from "@plane/ui"; - -export interface EmailPasswordFormValues { - email: string; - password?: string; - medium?: string; -} - -export interface IEmailPasswordForm { - onSubmit: (formData: EmailPasswordFormValues) => Promise; -} - -export const EmailPasswordForm: React.FC = (props) => { - const { onSubmit } = props; - // router - const router = useRouter(); - // form info - const { - control, - handleSubmit, - formState: { errors, isSubmitting, isValid, isDirty }, - } = useForm({ - defaultValues: { - email: "", - password: "", - medium: "email", - }, - mode: "onChange", - reValidateMode: "onChange", - }); - - return ( - <> -
-
- - /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( - value - ) || "Email address is not valid", - }} - render={({ field: { value, onChange } }) => ( - - )} - /> -
-
- ( - - )} - /> -
-
- -
-
- -
-
- -
-
- - ); -}; diff --git a/web/components/account/github-login-button.tsx b/web/components/account/github-login-button.tsx index 63d7056b6..b49d06f2b 100644 --- a/web/components/account/github-login-button.tsx +++ b/web/components/account/github-login-button.tsx @@ -8,16 +8,14 @@ import { useTheme } from "next-themes"; // images import githubLightModeImage from "/public/logos/github-black.png"; import githubDarkModeImage from "/public/logos/github-dark.svg"; -import { AuthType } from "components/page-views"; export interface GithubLoginButtonProps { handleSignIn: React.Dispatch; clientId: string; - authType: AuthType; } export const GithubLoginButton: FC = (props) => { - const { handleSignIn, clientId, authType } = props; + const { handleSignIn, clientId } = props; // states const [loginCallBackURL, setLoginCallBackURL] = useState(undefined); const [gitCode, setGitCode] = useState(null); @@ -55,7 +53,7 @@ export const GithubLoginButton: FC = (props) => { width={20} alt="GitHub Logo" /> - {authType == "sign-in" ? "Sign-in" : "Sign-up"} with GitHub + Sign-in with GitHub
diff --git a/web/components/account/index.ts b/web/components/account/index.ts index 4633d91b3..1184c4d2f 100644 --- a/web/components/account/index.ts +++ b/web/components/account/index.ts @@ -1,7 +1,5 @@ +export * from "./sign-in-forms"; export * from "./deactivate-account-modal"; -export * from "./email-code-form"; -export * from "./email-password-form"; -export * from "./email-forgot-password-form"; export * from "./github-login-button"; export * from "./google-login"; export * from "./email-signup-form"; diff --git a/web/components/account/sign-in-forms/create-password.tsx b/web/components/account/sign-in-forms/create-password.tsx new file mode 100644 index 000000000..9494d1518 --- /dev/null +++ b/web/components/account/sign-in-forms/create-password.tsx @@ -0,0 +1,133 @@ +import React from "react"; +import { Controller, useForm } from "react-hook-form"; +// services +import { AuthService } from "services/auth.service"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { Button, Input } from "@plane/ui"; +// helpers +import { checkEmailValidity } from "helpers/string.helper"; +// constants +import { ESignInSteps } from "components/account"; + +type Props = { + email: string; + handleStepChange: (step: ESignInSteps) => void; + handleSignInRedirection: () => Promise; +}; + +type TCreatePasswordFormValues = { + email: string; + password: string; +}; + +const defaultValues: TCreatePasswordFormValues = { + email: "", + password: "", +}; + +// services +const authService = new AuthService(); + +export const CreatePasswordForm: React.FC = (props) => { + const { email, handleSignInRedirection } = props; + // toast alert + const { setToastAlert } = useToast(); + // form info + const { + control, + formState: { errors, isSubmitting, isValid }, + handleSubmit, + } = useForm({ + defaultValues: { + ...defaultValues, + email, + }, + mode: "onChange", + reValidateMode: "onChange", + }); + + const handleCreatePassword = async (formData: TCreatePasswordFormValues) => { + const payload = { + password: formData.password, + }; + + await authService + .setPassword(payload) + .then(async () => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Password created successfully.", + }); + await handleSignInRedirection(); + }) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: err?.error ?? "Something went wrong. Please try again.", + }) + ); + }; + + return ( + <> +

+ Let{"'"}s get a new password +

+ +
+ checkEmailValidity(value) || "Email is invalid", + }} + render={({ field: { value, onChange, ref } }) => ( + + )} + /> +
+ ( + + )} + /> +

+ Whatever you choose now will be your account{"'"}s password until you change it. +

+
+ + + + ); +}; diff --git a/web/components/account/sign-in-forms/email-form.tsx b/web/components/account/sign-in-forms/email-form.tsx new file mode 100644 index 000000000..c8e138a49 --- /dev/null +++ b/web/components/account/sign-in-forms/email-form.tsx @@ -0,0 +1,152 @@ +import React, { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { XCircle } from "lucide-react"; +// services +import { AuthService } from "services/auth.service"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { Button, Input } from "@plane/ui"; +// helpers +import { checkEmailValidity } from "helpers/string.helper"; +// types +import { IEmailCheckData, TEmailCheckTypes } from "types/auth"; +// constants +import { ESignInSteps } from "components/account"; + +type Props = { + handleStepChange: (step: ESignInSteps) => void; + updateEmail: (email: string) => void; +}; + +type TEmailCodeFormValues = { + email: string; +}; + +const authService = new AuthService(); + +export const EmailForm: React.FC = (props) => { + const { handleStepChange, updateEmail } = props; + // states + const [isCheckingEmail, setIsCheckingEmail] = useState(null); + + const { setToastAlert } = useToast(); + + const { + control, + formState: { errors, isValid }, + handleSubmit, + watch, + } = useForm({ + defaultValues: { + email: "", + }, + mode: "onChange", + reValidateMode: "onChange", + }); + + const handleEmailCheck = async (type: TEmailCheckTypes) => { + setIsCheckingEmail(type); + + const email = watch("email"); + + const payload: IEmailCheckData = { + email, + type, + }; + + // update the global email state + updateEmail(email); + + await authService + .emailCheck(payload) + .then((res) => { + // if type is magic_code, send the user to magic sign in + if (type === "magic_code") handleStepChange(ESignInSteps.UNIQUE_CODE); + // if type is password, check if the user has a password set + if (type === "password") { + // if password is autoset, send them to set new password link + if (res.is_password_autoset) handleStepChange(ESignInSteps.SET_PASSWORD_LINK); + // if password is not autoset, send them to password form + else handleStepChange(ESignInSteps.PASSWORD); + } + }) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: err?.error ?? "Something went wrong. Please try again.", + }) + ) + .finally(() => setIsCheckingEmail(null)); + }; + + return ( + <> +

+ Get on your flight deck! +

+

+ Sign in with the email you used to sign up for Plane +

+ +
{})} className="mt-5 sm:w-96 mx-auto"> +
+ checkEmailValidity(value) || "Email is invalid", + }} + render={({ field: { value, onChange, ref } }) => ( +
+ + {value.length > 0 && ( + onChange("")} + /> + )} +
+ )} + /> +
+
+ + +
+
+ + ); +}; diff --git a/web/components/account/sign-in-forms/index.ts b/web/components/account/sign-in-forms/index.ts new file mode 100644 index 000000000..694fb3ef0 --- /dev/null +++ b/web/components/account/sign-in-forms/index.ts @@ -0,0 +1,8 @@ +export * from "./create-password"; +export * from "./email-form"; +export * from "./o-auth-options"; +export * from "./optional-set-password"; +export * from "./unique-code"; +export * from "./password"; +export * from "./root"; +export * from "./set-password-link"; diff --git a/web/components/account/sign-in-forms/o-auth-options.tsx b/web/components/account/sign-in-forms/o-auth-options.tsx new file mode 100644 index 000000000..690dc7697 --- /dev/null +++ b/web/components/account/sign-in-forms/o-auth-options.tsx @@ -0,0 +1,104 @@ +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// services +import { AuthService } from "services/auth.service"; +import { UserService } from "services/user.service"; +// hooks +import useToast from "hooks/use-toast"; +// components +import { ESignInSteps, GithubLoginButton, GoogleLoginButton } from "components/account"; + +type Props = { + updateEmail: (email: string) => void; + handleStepChange: (step: ESignInSteps) => void; + handleSignInRedirection: () => Promise; +}; + +// services +const authService = new AuthService(); +const userService = new UserService(); + +export const OAuthOptions: React.FC = observer((props) => { + const { updateEmail, handleStepChange, handleSignInRedirection } = props; + // toast alert + const { setToastAlert } = useToast(); + // mobx store + const { + appConfig: { envConfig }, + } = useMobxStore(); + + const handleGoogleSignIn = async ({ clientId, credential }: any) => { + try { + if (clientId && credential) { + const socialAuthPayload = { + medium: "google", + credential, + clientId, + }; + const response = await authService.socialAuth(socialAuthPayload); + + if (response) { + const currentUser = await userService.currentUser(); + + updateEmail(currentUser.email); + + if (currentUser.is_password_autoset) handleStepChange(ESignInSteps.OPTIONAL_SET_PASSWORD); + else handleSignInRedirection(); + } + } else throw Error("Cant find credentials"); + } catch (err: any) { + setToastAlert({ + title: "Error signing in!", + type: "error", + message: err?.error || "Something went wrong. Please try again later or contact the support team.", + }); + } + }; + + const handleGitHubSignIn = async (credential: string) => { + try { + if (envConfig && envConfig.github_client_id && credential) { + const socialAuthPayload = { + medium: "github", + credential, + clientId: envConfig.github_client_id, + }; + const response = await authService.socialAuth(socialAuthPayload); + + if (response) { + const currentUser = await userService.currentUser(); + + updateEmail(currentUser.email); + + if (currentUser.is_password_autoset) handleStepChange(ESignInSteps.OPTIONAL_SET_PASSWORD); + else handleSignInRedirection(); + } + } else throw Error("Cant find credentials"); + } catch (err: any) { + setToastAlert({ + title: "Error signing in!", + type: "error", + message: err?.error || "Something went wrong. Please try again later or contact the support team.", + }); + } + }; + + return ( + <> +
+
+

Or continue with

+
+
+
+ {envConfig?.google_client_id && ( + + )} + {envConfig?.github_client_id && ( + + )} +
+ + ); +}); diff --git a/web/components/account/sign-in-forms/optional-set-password.tsx b/web/components/account/sign-in-forms/optional-set-password.tsx new file mode 100644 index 000000000..372dc3947 --- /dev/null +++ b/web/components/account/sign-in-forms/optional-set-password.tsx @@ -0,0 +1,94 @@ +import React, { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +// ui +import { Button, Input } from "@plane/ui"; +// helpers +import { checkEmailValidity } from "helpers/string.helper"; +// constants +import { ESignInSteps } from "components/account"; + +type Props = { + email: string; + handleStepChange: (step: ESignInSteps) => void; + handleSignInRedirection: () => Promise; +}; + +export const OptionalSetPasswordForm: React.FC = (props) => { + const { email, handleStepChange, handleSignInRedirection } = props; + // states + const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false); + + const { + control, + formState: { errors, isValid }, + } = useForm({ + defaultValues: { + email, + }, + mode: "onChange", + reValidateMode: "onChange", + }); + + const handleGoToWorkspace = async () => { + setIsGoingToWorkspace(true); + + await handleSignInRedirection().finally(() => setIsGoingToWorkspace(false)); + }; + + return ( + <> +

Set a password

+

+ If you{"'"}d to do away with codes, set a password here. +

+ +
+ checkEmailValidity(value) || "Email is invalid", + }} + render={({ field: { value, onChange, ref } }) => ( + + )} + /> +
+ + +
+ + + ); +}; diff --git a/web/components/account/sign-in-forms/password.tsx b/web/components/account/sign-in-forms/password.tsx new file mode 100644 index 000000000..bdaa34b22 --- /dev/null +++ b/web/components/account/sign-in-forms/password.tsx @@ -0,0 +1,201 @@ +import React from "react"; +import { Controller, useForm } from "react-hook-form"; +import { XCircle } from "lucide-react"; +// services +import { AuthService } from "services/auth.service"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { Button, Input } from "@plane/ui"; +// helpers +import { checkEmailValidity } from "helpers/string.helper"; +// types +import { IEmailCheckData, IPasswordSignInData } from "types/auth"; +// constants +import { ESignInSteps } from "components/account"; + +type Props = { + email: string; + updateEmail: (email: string) => void; + handleStepChange: (step: ESignInSteps) => void; + handleSignInRedirection: () => Promise; +}; + +type TPasswordFormValues = { + email: string; + password: string; +}; + +const defaultValues: TPasswordFormValues = { + email: "", + password: "", +}; + +const authService = new AuthService(); + +export const PasswordForm: React.FC = (props) => { + const { email, updateEmail, handleStepChange, handleSignInRedirection } = props; + // toast alert + const { setToastAlert } = useToast(); + // form info + const { + control, + formState: { dirtyFields, errors, isSubmitting, isValid }, + getValues, + handleSubmit, + reset, + setError, + } = useForm({ + defaultValues: { + ...defaultValues, + email, + }, + mode: "onChange", + reValidateMode: "onChange", + }); + + const handlePasswordSignIn = async (formData: TPasswordFormValues) => { + const payload: IPasswordSignInData = { + email: formData.email, + password: formData.password, + }; + + await authService + .passwordSignIn(payload) + .then(async () => await handleSignInRedirection()) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: err?.error ?? "Something went wrong. Please try again.", + }) + ); + }; + + const handleEmailCheck = async (formData: TPasswordFormValues) => { + const payload: IEmailCheckData = { + email: formData.email, + type: "password", + }; + + await authService + .emailCheck(payload) + .then((res) => { + if (res.is_password_autoset) handleStepChange(ESignInSteps.SET_PASSWORD_LINK); + else + reset({ + email: formData.email, + password: "", + }); + }) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: err?.error ?? "Something went wrong. Please try again.", + }) + ); + }; + + const handleFormSubmit = async (formData: TPasswordFormValues) => { + if (dirtyFields.email) await handleEmailCheck(formData); + else await handlePasswordSignIn(formData); + }; + + const handleForgotPassword = async () => { + const emailFormValue = getValues("email"); + + const isEmailValid = checkEmailValidity(emailFormValue); + + if (!isEmailValid) { + setError("email", { message: "Email is invalid" }); + return; + } + + authService + .sendResetPasswordLink({ email: emailFormValue }) + .then(() => handleStepChange(ESignInSteps.SET_PASSWORD_LINK)) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: err?.error ?? "Something went wrong. Please try again.", + }) + ); + }; + + return ( + <> +

+ Get on your flight deck +

+
+
+ checkEmailValidity(value) || "Email is invalid", + }} + render={({ field: { value, onChange } }) => ( +
+ { + updateEmail(e.target.value); + onChange(e.target.value); + }} + onBlur={() => { + if (dirtyFields.email) handleEmailCheck(getValues()); + }} + hasError={Boolean(errors.email)} + placeholder="orville.wright@firstflight.com" + className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12" + /> + {value.length > 0 && ( + onChange("")} + /> + )} +
+ )} + /> +
+
+ ( + + )} + /> + +
+ +
+ + ); +}; diff --git a/web/components/account/sign-in-forms/root.tsx b/web/components/account/sign-in-forms/root.tsx new file mode 100644 index 000000000..49089b5bd --- /dev/null +++ b/web/components/account/sign-in-forms/root.tsx @@ -0,0 +1,84 @@ +import React, { useState } from "react"; +// components +import { + EmailForm, + UniqueCodeForm, + PasswordForm, + SetPasswordLink, + OAuthOptions, + OptionalSetPasswordForm, + CreatePasswordForm, +} from "components/account"; + +export enum ESignInSteps { + EMAIL = "EMAIL", + PASSWORD = "PASSWORD", + SET_PASSWORD_LINK = "SET_PASSWORD_LINK", + UNIQUE_CODE = "UNIQUE_CODE", + OPTIONAL_SET_PASSWORD = "OPTIONAL_SET_PASSWORD", + CREATE_PASSWORD = "CREATE_PASSWORD", +} + +type Props = { + handleSignInRedirection: () => Promise; +}; + +export const SignInRoot: React.FC = (props) => { + const { handleSignInRedirection } = props; + // states + const [signInStep, setSignInStep] = useState(ESignInSteps.EMAIL); + const [email, setEmail] = useState(""); + + return ( + <> +
+ {signInStep === ESignInSteps.EMAIL && ( + setSignInStep(step)} + updateEmail={(newEmail) => setEmail(newEmail)} + /> + )} + {signInStep === ESignInSteps.PASSWORD && ( + setEmail(newEmail)} + handleStepChange={(step: ESignInSteps) => setSignInStep(step)} + handleSignInRedirection={handleSignInRedirection} + /> + )} + {signInStep === ESignInSteps.SET_PASSWORD_LINK && ( + setEmail(newEmail)} /> + )} + {signInStep === ESignInSteps.UNIQUE_CODE && ( + setEmail(newEmail)} + handleStepChange={(step: ESignInSteps) => setSignInStep(step)} + handleSignInRedirection={handleSignInRedirection} + /> + )} + {signInStep === ESignInSteps.OPTIONAL_SET_PASSWORD && ( + setSignInStep(step)} + handleSignInRedirection={handleSignInRedirection} + /> + )} + {signInStep === ESignInSteps.CREATE_PASSWORD && ( + setSignInStep(step)} + handleSignInRedirection={handleSignInRedirection} + /> + )} +
+ {signInStep !== ESignInSteps.OPTIONAL_SET_PASSWORD && ( + setEmail(newEmail)} + handleStepChange={(step: ESignInSteps) => setSignInStep(step)} + handleSignInRedirection={handleSignInRedirection} + /> + )} + + ); +}; diff --git a/web/components/account/sign-in-forms/set-password-link.tsx b/web/components/account/sign-in-forms/set-password-link.tsx new file mode 100644 index 000000000..34e643b92 --- /dev/null +++ b/web/components/account/sign-in-forms/set-password-link.tsx @@ -0,0 +1,122 @@ +import React, { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { XCircle } from "lucide-react"; +// services +import { AuthService } from "services/auth.service"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { Button, Input } from "@plane/ui"; +// helpers +import { checkEmailValidity } from "helpers/string.helper"; +// types +import { IEmailCheckData } from "types/auth"; + +type Props = { + email: string; + updateEmail: (email: string) => void; +}; + +const authService = new AuthService(); + +export const SetPasswordLink: React.FC = (props) => { + const { email, updateEmail } = props; + // states + const [isSendingNewLink, setIsSendingNewLink] = useState(false); + + const { setToastAlert } = useToast(); + + const { + control, + formState: { errors, isValid }, + watch, + } = useForm({ + defaultValues: { + email, + }, + mode: "onChange", + reValidateMode: "onChange", + }); + + const handleSendNewLink = async () => { + setIsSendingNewLink(true); + + const payload: IEmailCheckData = { + email: watch("email"), + type: "password", + }; + + await authService + .emailCheck(payload) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: err?.error ?? "Something went wrong. Please try again.", + }) + ) + .finally(() => setIsSendingNewLink(false)); + }; + + return ( + <> +

+ Get on your flight deck! +

+

+ We have sent a link to {email}, so you can set a + password +

+ +
+
+ checkEmailValidity(value) || "Email is invalid", + }} + render={({ field: { value, onChange, ref } }) => ( +
+ { + updateEmail(e.target.value); + onChange(e.target.value); + }} + ref={ref} + hasError={Boolean(errors.email)} + placeholder="orville.wright@firstflight.com" + className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12" + /> + {value.length > 0 && ( + onChange("")} + /> + )} +
+ )} + /> +
+
+ +
+
+ + ); +}; diff --git a/web/components/account/sign-in-forms/unique-code.tsx b/web/components/account/sign-in-forms/unique-code.tsx new file mode 100644 index 000000000..71ed93f0c --- /dev/null +++ b/web/components/account/sign-in-forms/unique-code.tsx @@ -0,0 +1,232 @@ +import React, { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { CornerDownLeft, XCircle } from "lucide-react"; +// services +import { AuthService } from "services/auth.service"; +import { UserService } from "services/user.service"; +// hooks +import useToast from "hooks/use-toast"; +import useTimer from "hooks/use-timer"; +// ui +import { Button, Input } from "@plane/ui"; +// helpers +import { checkEmailValidity } from "helpers/string.helper"; +// types +import { IEmailCheckData, IMagicSignInData } from "types/auth"; +// constants +import { ESignInSteps } from "components/account"; + +type Props = { + email: string; + updateEmail: (email: string) => void; + handleStepChange: (step: ESignInSteps) => void; + handleSignInRedirection: () => Promise; +}; + +type TUniqueCodeFormValues = { + email: string; + token: string; +}; + +const defaultValues: TUniqueCodeFormValues = { + email: "", + token: "", +}; + +// services +const authService = new AuthService(); +const userService = new UserService(); + +export const UniqueCodeForm: React.FC = (props) => { + const { email, updateEmail, handleStepChange, handleSignInRedirection } = props; + // states + const [isRequestingNewCode, setIsRequestingNewCode] = useState(false); + // toast alert + const { setToastAlert } = useToast(); + // timer + const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(); + // form info + const { + control, + formState: { dirtyFields, errors, isSubmitting, isValid }, + getValues, + handleSubmit, + reset, + } = useForm({ + defaultValues: { + ...defaultValues, + email, + }, + mode: "onChange", + reValidateMode: "onChange", + }); + + const handleUniqueCodeSignIn = async (formData: TUniqueCodeFormValues) => { + const payload: IMagicSignInData = { + email: formData.email, + key: `magic_${formData.email}`, + token: formData.token, + }; + + await authService + .magicSignIn(payload) + .then(async () => { + const currentUser = await userService.currentUser(); + + if (currentUser.is_password_autoset) handleStepChange(ESignInSteps.OPTIONAL_SET_PASSWORD); + else await handleSignInRedirection(); + }) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: err?.error ?? "Something went wrong. Please try again.", + }) + ); + }; + + const handleSendNewLink = async (formData: TUniqueCodeFormValues) => { + const payload: IEmailCheckData = { + email: formData.email, + type: "magic_code", + }; + + await authService + .emailCheck(payload) + .then(() => { + setToastAlert({ + type: "success", + title: "Success!", + message: "A new unique code has been sent to your email.", + }); + + reset({ + email: formData.email, + token: "", + }); + }) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: err?.error ?? "Something went wrong. Please try again.", + }) + ); + }; + + const handleFormSubmit = async (formData: TUniqueCodeFormValues) => { + if (dirtyFields.email) await handleSendNewLink(formData); + else await handleUniqueCodeSignIn(formData); + }; + + const handleRequestNewCode = async () => { + setIsRequestingNewCode(true); + + await handleSendNewLink(getValues()) + .then(() => setResendCodeTimer(30)) + .finally(() => setIsRequestingNewCode(false)); + }; + + const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0; + + return ( + <> +

+ Moving to the runway +

+

+ Paste the code you got at {email} below. +

+ +
+
+ checkEmailValidity(value) || "Email is invalid", + }} + render={({ field: { value, onChange, ref } }) => ( +
+ { + updateEmail(e.target.value); + onChange(e.target.value); + }} + onBlur={() => { + if (dirtyFields.email) handleSendNewLink(getValues()); + }} + ref={ref} + hasError={Boolean(errors.email)} + placeholder="orville.wright@firstflight.com" + className="w-full h-[46px] placeholder:text-onboarding-text-400 border border-onboarding-border-100 pr-12" + /> + {value.length > 0 && ( + onChange("")} + /> + )} +
+ )} + /> + {dirtyFields.email && ( + + )} +
+
+ ( + + )} + /> + +
+ +
+ + ); +}; diff --git a/web/components/page-views/signin.tsx b/web/components/page-views/signin.tsx index d4cbfeaae..bf4db894a 100644 --- a/web/components/page-views/signin.tsx +++ b/web/components/page-views/signin.tsx @@ -1,22 +1,14 @@ import { useState, useEffect, useCallback } from "react"; +import Link from "next/link"; import { observer } from "mobx-react-lite"; import Image from "next/image"; import { useTheme } from "next-themes"; import { useRouter } from "next/router"; import { Lightbulb } from "lucide-react"; -// hooks -import useToast from "hooks/use-toast"; +// mobx store import { useMobxStore } from "lib/mobx/store-provider"; -// services -import { AuthService } from "services/auth.service"; // components -import { - GoogleLoginButton, - GithubLoginButton, - EmailCodeForm, - EmailPasswordForm, - EmailPasswordFormValues, -} from "components/account"; +import { SignInRoot } from "components/account"; // ui import { Loader, Spinner } from "@plane/ui"; // images @@ -27,8 +19,6 @@ import { IUser, IUserSettings } from "types"; export type AuthType = "sign-in" | "sign-up"; -const authService = new AuthService(); - export const SignInView = observer(() => { // store const { @@ -37,27 +27,14 @@ export const SignInView = observer(() => { } = useMobxStore(); // router const router = useRouter(); - const { next: next_url } = router.query as { next: string }; + const { next: next_url } = router.query; // states const [isLoading, setLoading] = useState(false); - const [authType, setAuthType] = useState("sign-in"); - // toast - const { setToastAlert } = useToast(); + // next-themes const { resolvedTheme } = useTheme(); - // computed. - const enableEmailPassword = - envConfig && - (envConfig?.email_password_login || - !( - envConfig?.email_password_login || - envConfig?.magic_login || - envConfig?.google_client_id || - envConfig?.github_client_id - )); - - const handleLoginRedirection = useCallback( - (user: IUser) => { + const handleSignInRedirection = useCallback( + async (user: IUser) => { // if the user is not onboarded, redirect them to the onboarding page if (!user.is_onboarded) { router.push("/onboarding"); @@ -65,122 +42,33 @@ export const SignInView = observer(() => { } // if next_url is provided, redirect the user to that url if (next_url) { - router.push(next_url); + router.push(next_url.toString()); return; } // if the user is onboarded, fetch their last workspace details - fetchCurrentUserSettings() + await fetchCurrentUserSettings() .then((userSettings: IUserSettings) => { const workspaceSlug = userSettings?.workspace?.last_workspace_slug || userSettings?.workspace?.fallback_workspace_slug; if (workspaceSlug) router.push(`/${workspaceSlug}`); else router.push("/profile"); }) - .catch(() => { - setLoading(false); - }); + .catch(() => setLoading(false)); }, [fetchCurrentUserSettings, router, next_url] ); - const mutateUserInfo = useCallback(() => { - fetchCurrentUser().then((user) => { - handleLoginRedirection(user); + const mutateUserInfo = useCallback(async () => { + await fetchCurrentUser().then(async (user) => { + await handleSignInRedirection(user); }); - }, [fetchCurrentUser, handleLoginRedirection]); + }, [fetchCurrentUser, handleSignInRedirection]); useEffect(() => { mutateUserInfo(); }, [mutateUserInfo]); - const handleGoogleSignIn = async ({ clientId, credential }: any) => { - try { - setLoading(true); - if (clientId && credential) { - const socialAuthPayload = { - medium: "google", - credential, - clientId, - }; - const response = await authService.socialAuth(socialAuthPayload); - if (response) { - mutateUserInfo(); - } - } else { - setLoading(false); - throw Error("Cant find credentials"); - } - } catch (err: any) { - setLoading(false); - setToastAlert({ - title: "Error signing in!", - type: "error", - message: err?.error || "Something went wrong. Please try again later or contact the support team.", - }); - } - }; - - const handleGitHubSignIn = async (credential: string) => { - try { - setLoading(true); - if (envConfig && envConfig.github_client_id && credential) { - const socialAuthPayload = { - medium: "github", - credential, - clientId: envConfig.github_client_id, - }; - const response = await authService.socialAuth(socialAuthPayload); - if (response) { - mutateUserInfo(); - } - } else { - setLoading(false); - throw Error("Cant find credentials"); - } - } catch (err: any) { - setLoading(false); - setToastAlert({ - title: "Error signing in!", - type: "error", - message: err?.error || "Something went wrong. Please try again later or contact the support team.", - }); - } - }; - - const handlePasswordSignIn = (formData: EmailPasswordFormValues) => { - setLoading(true); - return authService - .emailLogin(formData) - .then(() => { - mutateUserInfo(); - }) - .catch((err) => { - setLoading(false); - setToastAlert({ - type: "error", - title: "Error!", - message: err?.error || "Something went wrong. Please try again later or contact the support team.", - }); - }); - }; - - const handleEmailCodeSignIn = async (response: any) => { - try { - setLoading(true); - if (response) { - mutateUserInfo(); - } - } catch (err: any) { - setLoading(false); - setToastAlert({ - type: "error", - title: "Error!", - message: err?.error || "Something went wrong. Please try again later or contact the support team.", - }); - } - }; - return ( <> {isLoading ? ( @@ -194,26 +82,10 @@ export const SignInView = observer(() => { Plane Logo Plane - - {/*
- {authType === "sign-in" && ( -
- New to Plane?{" "} -

{ - setAuthType("sign-up"); - }} - > - Create a new account -

-
- )} -
*/}
-
+
{!envConfig ? (
@@ -230,57 +102,21 @@ export const SignInView = observer(() => {
) : ( <> - <> - {enableEmailPassword && } - {envConfig?.magic_login && ( -
-
- -
-
- )} -
-
-

- Or continue with -

-
-
-
- {envConfig?.google_client_id && ( - - )} - {envConfig?.github_client_id && ( - - )} -
- {/* {authType === "sign-up" && ( -
- Already using Plane?{" "} - { - setAuthType("sign-in"); - }} - > - Sign in - -
- )} */} - -
+ + +
-

- Try the latest features, like Tiptap editor, to write compelling responses.{" "} - {}}> - See new features - +

+ Pages gets a facelift! Write anything and use Galileo to help you start.{" "} + + + Learn more + +

diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts index d402de873..d8f549626 100644 --- a/web/helpers/string.helper.ts +++ b/web/helpers/string.helper.ts @@ -206,3 +206,21 @@ export const substringMatch = (text: string, searchQuery: string): boolean => { return false; } }; + +/** + * @returns {boolean} true if email is valid, false otherwise + * @description Returns true if email is valid, false otherwise + * @param {string} email string to check if it is a valid email + * @example checkEmailIsValid("hello world") => false + * @example checkEmailIsValid("example@plane.so") => true + */ +export const checkEmailValidity = (email: string): boolean => { + if (!email) return false; + + const isEmailValid = + /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( + email + ); + + return isEmailValid; +}; diff --git a/web/pages/accounts/forgot-password.tsx b/web/pages/accounts/forgot-password.tsx deleted file mode 100644 index c18694393..000000000 --- a/web/pages/accounts/forgot-password.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { ReactElement } from "react"; -import Image from "next/image"; -// components -import { EmailForgotPasswordForm, EmailForgotPasswordFormValues } from "components/account"; -// layouts -import DefaultLayout from "layouts/default-layout"; -// services -import { UserService } from "services/user.service"; -// hooks -import useToast from "hooks/use-toast"; -// images -import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; -// types -import { NextPageWithLayout } from "types/app"; - -const userService = new UserService(); - -const ForgotPasswordPage: NextPageWithLayout = () => { - // toast - const { setToastAlert } = useToast(); - - const handleForgotPassword = (formData: EmailForgotPasswordFormValues) => { - const payload = { - email: formData.email, - }; - - return userService - .forgotPassword(payload) - .then(() => - setToastAlert({ - type: "success", - title: "Success!", - message: "Password reset link has been sent to your email address.", - }) - ) - .catch((err) => { - if (err.status === 400) - setToastAlert({ - type: "error", - title: "Error!", - message: "Please check the Email ID entered.", - }); - else - setToastAlert({ - type: "error", - title: "Error!", - message: "Something went wrong. Please try again.", - }); - }); - }; - return ( - <> -
-
-
-
- Plane Logo -
-
-
-
-
-

Forgot Password

- -
-
- - ); -}; - -ForgotPasswordPage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default ForgotPasswordPage; diff --git a/web/pages/accounts/magic-sign-in.tsx b/web/pages/accounts/magic-sign-in.tsx deleted file mode 100644 index fc698fef1..000000000 --- a/web/pages/accounts/magic-sign-in.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useState, useEffect, ReactElement } from "react"; -import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; -// layouts -import DefaultLayout from "layouts/default-layout"; -// services -import { AuthService } from "services/auth.service"; -// hooks -import useUserAuth from "hooks/use-user-auth"; -import useToast from "hooks/use-toast"; -// types -import { NextPageWithLayout } from "types/app"; - -const authService = new AuthService(); - -const MagicSignInPage: NextPageWithLayout = () => { - const router = useRouter(); - const { password, key } = router.query; - - const { setToastAlert } = useToast(); - - const { setTheme } = useTheme(); - - const { mutateUser } = useUserAuth("sign-in"); - - const [isSigningIn, setIsSigningIn] = useState(false); - const [errorSigningIn, setErrorSignIn] = useState(); - - useEffect(() => { - setTheme("system"); - }, [setTheme]); - - useEffect(() => { - setIsSigningIn(() => false); - setErrorSignIn(() => undefined); - if (!password || !key) { - setErrorSignIn("URL is invalid"); - return; - } else { - setIsSigningIn(() => true); - authService - .magicSignIn({ token: password, key }) - .then(async () => { - setIsSigningIn(false); - await mutateUser(); - }) - .catch((err) => { - setErrorSignIn(err.response.data.error); - setIsSigningIn(false); - }); - } - }, [password, key, mutateUser, router]); - - return ( -
- {isSigningIn ? ( -
-

Signing you in...

-

Please wait while we are preparing your take off.

-
- ) : errorSigningIn ? ( -
-

Error

-
-
{errorSigningIn}.
- { - authService - .emailCode({ email: (key as string).split("_")[1] }) - .then(() => { - setToastAlert({ - type: "success", - title: "Email sent", - message: "A new link/code has been send to you.", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error", - message: "Unable to send email.", - }); - }); - }} - > - Send link again? - -
-
- ) : ( -
-

Success

-

Redirecting you to the app...

-
- )} -
- ); -}; - -MagicSignInPage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default MagicSignInPage; diff --git a/web/pages/accounts/password.tsx b/web/pages/accounts/password.tsx new file mode 100644 index 000000000..47c74f784 --- /dev/null +++ b/web/pages/accounts/password.tsx @@ -0,0 +1,185 @@ +import { ReactElement } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useTheme } from "next-themes"; +import { Lightbulb } from "lucide-react"; +import { Controller, useForm } from "react-hook-form"; +// services +import { AuthService } from "services/auth.service"; +// hooks +import useToast from "hooks/use-toast"; +// layouts +import DefaultLayout from "layouts/default-layout"; +// ui +import { Button, Input } from "@plane/ui"; +// images +import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; +import signInIssues from "public/onboarding/onboarding-issues.svg"; +// helpers +import { checkEmailValidity } from "helpers/string.helper"; +// type +import { NextPageWithLayout } from "types/app"; + +type TResetPasswordFormValues = { + email: string; + password: string; +}; + +const defaultValues: TResetPasswordFormValues = { + email: "", + password: "", +}; + +// services +const authService = new AuthService(); + +const HomePage: NextPageWithLayout = () => { + // router + const router = useRouter(); + const { uidb64, token, email } = router.query; + // next-themes + const { resolvedTheme } = useTheme(); + // toast + const { setToastAlert } = useToast(); + // form info + const { + control, + formState: { errors, isSubmitting, isValid }, + handleSubmit, + } = useForm({ + defaultValues: { + ...defaultValues, + email: email?.toString() ?? "", + }, + }); + + const handleResetPassword = async (formData: TResetPasswordFormValues) => { + if (!uidb64 || !token || !email) return; + + const payload = { + new_password: formData.password, + }; + + await authService.resetPassword(uidb64.toString(), token.toString(), payload).catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: err?.error ?? "Something went wrong. Please try again.", + }) + ); + }; + + return ( +
+
+
+ Plane Logo + Plane +
+
+ +
+
+
+

+ Let{"'"}s get a new password +

+
+ checkEmailValidity(value) || "Email is invalid", + }} + render={({ field: { value, onChange, ref } }) => ( + + )} + /> +
+ ( + + )} + /> +

+ Whatever you choose now will be your account{"'"}s password until you change it. +

+
+ +

+ When you click the button above, you agree with our{" "} + + terms and conditions of service. + +

+ +
+
+ +

+ Try the latest features, like Tiptap editor, to write compelling responses.{" "} + + + See new features + + +

+
+
+ Plane Issues +
+
+
+
+ ); +}; + +HomePage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default HomePage; diff --git a/web/pages/accounts/reset-password.tsx b/web/pages/accounts/reset-password.tsx deleted file mode 100644 index a817509d1..000000000 --- a/web/pages/accounts/reset-password.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import React, { useEffect, useState, ReactElement } from "react"; -import { useRouter } from "next/router"; -import Image from "next/image"; -import { useTheme } from "next-themes"; -import { Controller, useForm } from "react-hook-form"; -// hooks -import useToast from "hooks/use-toast"; -// services -import { UserService } from "services/user.service"; -// layouts -import DefaultLayout from "layouts/default-layout"; -// ui -import { Button, Input, Spinner } from "@plane/ui"; -// images -import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; -// types -import { NextPageWithLayout } from "types/app"; - -type FormData = { - password: string; - confirmPassword: string; -}; - -// services -const userService = new UserService(); - -const ResetPasswordPage: NextPageWithLayout = () => { - const [isLoading, setIsLoading] = useState(true); - - const router = useRouter(); - const { uidb64, token } = router.query; - - const { setToastAlert } = useToast(); - - const { setTheme } = useTheme(); - - const { - handleSubmit, - control, - formState: { errors, isSubmitting }, - } = useForm(); - - const onSubmit = async (formData: FormData) => { - if (!uidb64 || !token) return; - - if (formData.password !== formData.confirmPassword) { - setToastAlert({ - type: "error", - title: "Error!", - message: "Passwords do not match.", - }); - - return; - } - - const payload = { - new_password: formData.password, - confirm_password: formData.confirmPassword, - }; - - await userService - .resetPassword(uidb64.toString(), token.toString(), payload) - .then(() => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Password reset successfully. You can now login with your new password.", - }); - router.push("/"); - }) - .catch((err) => - setToastAlert({ - type: "error", - title: "Error!", - message: err?.error || "Something went wrong. Please try again later or contact the support team.", - }) - ); - }; - - useEffect(() => { - setTheme("system"); - }, [setTheme]); - - useEffect(() => { - if (parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0")) router.push("/"); - else setIsLoading(false); - }, [router]); - - if (isLoading) - return ( -
- -
- ); - - return ( - <> -
-
-
-
- Plane Logo -
-
-
-
-
-

Reset your password

-
-
- ( - - )} - /> -
-
- ( - - )} - /> -
- -
-
-
- - ); -}; - -ResetPasswordPage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default ResetPasswordPage; diff --git a/web/services/auth.service.ts b/web/services/auth.service.ts index f7dd9e94c..76d2cedc8 100644 --- a/web/services/auth.service.ts +++ b/web/services/auth.service.ts @@ -2,18 +2,25 @@ import { APIService } from "services/api.service"; // helpers import { API_BASE_URL } from "helpers/common.helper"; - -export interface ILoginTokenResponse { - access_token: string; - refresh_toke: string; -} +// types +import { IEmailCheckData, ILoginTokenResponse, IMagicSignInData, IPasswordSignInData } from "types/auth"; export class AuthService extends APIService { constructor() { super(API_BASE_URL); } - async emailLogin(data: any): Promise { + async emailCheck(data: IEmailCheckData): Promise<{ + is_password_autoset: boolean; + }> { + return this.post("/api/email-check/", data, { headers: {} }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async passwordSignIn(data: IPasswordSignInData): Promise { return this.post("/api/sign-in/", data, { headers: {} }) .then((response) => { this.setAccessToken(response?.data?.access_token); @@ -25,6 +32,42 @@ export class AuthService extends APIService { }); } + async sendResetPasswordLink(data: { email: string }): Promise { + return this.post(`/api/forgot-password/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async setPassword(data: { password: string }): Promise { + return this.post(`/api/users/me/set-password/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async resetPassword( + uidb64: string, + token: string, + data: { + new_password: string; + } + ): Promise { + return this.post(`/api/reset-password/${uidb64}/${token}/`, data) + .then((response) => { + if (response?.status === 200) { + this.setAccessToken(response?.data?.access_token); + this.setRefreshToken(response?.data?.refresh_token); + return response?.data; + } + }) + .catch((error) => { + throw error?.response?.data; + }); + } + async emailSignUp(data: { email: string; password: string }): Promise { return this.post("/api/sign-up/", data, { headers: {} }) .then((response) => { @@ -57,14 +100,18 @@ export class AuthService extends APIService { }); } - async magicSignIn(data: any): Promise { - const response = await this.post("/api/magic-sign-in/", data, { headers: {} }); - if (response?.status === 200) { - this.setAccessToken(response?.data?.access_token); - this.setRefreshToken(response?.data?.refresh_token); - return response?.data; - } - throw response.response.data; + async magicSignIn(data: IMagicSignInData): Promise { + return await this.post("/api/magic-sign-in/", data, { headers: {} }) + .then((response) => { + if (response?.status === 200) { + this.setAccessToken(response?.data?.access_token); + this.setRefreshToken(response?.data?.refresh_token); + return response?.data; + } + }) + .catch((error) => { + throw error?.response?.data; + }); } async signOut(): Promise { diff --git a/web/services/user.service.ts b/web/services/user.service.ts index c3dbdfd9c..f5619a2f8 100644 --- a/web/services/user.service.ts +++ b/web/services/user.service.ts @@ -117,29 +117,6 @@ export class UserService extends APIService { }); } - async forgotPassword(data: { email: string }): Promise { - return this.post(`/api/forgot-password/`, data) - .then((response) => response?.data) - .catch((error) => { - throw error?.response; - }); - } - - async resetPassword( - uidb64: string, - token: string, - data: { - new_password: string; - confirm_password: string; - } - ): Promise { - return this.post(`/api/reset-password/${uidb64}/${token}/`, data) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - async changePassword(data: { old_password: string; new_password: string; confirm_password: string }): Promise { return this.post(`/api/users/me/change-password/`, data) .then((response) => response?.data) diff --git a/web/types/auth.d.ts b/web/types/auth.d.ts new file mode 100644 index 000000000..15853802c --- /dev/null +++ b/web/types/auth.d.ts @@ -0,0 +1,22 @@ +export type TEmailCheckTypes = "magic_code" | "password"; + +export interface IEmailCheckData { + email: string; + type: TEmailCheckTypes; +} + +export interface ILoginTokenResponse { + access_token: string; + refresh_toke: string; +} + +export interface IMagicSignInData { + email: string; + key: string; + token: string; +} + +export interface IPasswordSignInData { + email: string; + password: string; +}