From 577118ca029ca4e80931eea672912fdf2bddf4c2 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 19 Jan 2024 20:55:03 +0530 Subject: [PATCH] chore: new sign-in, sign-up and forgot password workflows (#3415) * chore: sign up workflow updated * chore: sign in workflow updated * refactor: folder structure * chore: forgot password workflow * refactor: form component props * chore: forgot password popover for instances with smtp unconfigured * chore: updated UX copy * chore: update reset password link * chore: update email placeholder --- .../plane/bgtasks/forgot_password_task.py | 2 +- packages/types/src/app.d.ts | 10 +- .../sign-in-forms/create-password.tsx | 2 +- .../accounts/sign-in-forms/email-form.tsx | 2 +- .../sign-in-forms/optional-set-password.tsx | 2 +- .../accounts/sign-in-forms/password.tsx | 2 +- .../sign-in-forms/self-hosted-sign-in.tsx | 2 +- .../sign-in-forms/set-password-link.tsx | 2 +- .../accounts/sign-in-forms/unique-code.tsx | 2 +- space/pages/accounts/password.tsx | 2 +- web/components/account/email-signup-form.tsx | 137 ----------- web/components/account/index.ts | 5 +- .../account/{ => o-auth}/github-sign-in.tsx | 5 +- .../account/{ => o-auth}/google-sign-in.tsx | 7 +- web/components/account/o-auth/index.ts | 3 + .../o-auth-options.tsx | 11 +- .../account/sign-in-forms/email.tsx | 110 +++++++++ .../sign-in-forms/forgot-password-popover.tsx | 54 +++++ web/components/account/sign-in-forms/index.ts | 7 +- .../sign-in-forms/optional-set-password.tsx | 113 ++++++--- .../account/sign-in-forms/password.tsx | 127 ++++------- web/components/account/sign-in-forms/root.tsx | 121 +++++----- .../sign-in-forms/set-password-link.tsx | 103 --------- .../account/sign-in-forms/unique-code.tsx | 93 ++------ .../email.tsx} | 36 +-- web/components/account/sign-up-forms/index.ts | 5 + .../optional-set-password.tsx} | 110 +++++---- .../password.tsx} | 44 ++-- web/components/account/sign-up-forms/root.tsx | 97 ++++++++ .../account/sign-up-forms/unique-code.tsx | 215 ++++++++++++++++++ .../instance/setup-form/sign-in-form.tsx | 2 +- web/components/page-views/signin.tsx | 26 +-- web/pages/accounts/forgot-password.tsx | 138 +++++++++++ .../{password.tsx => reset-password.tsx} | 86 +++---- web/pages/accounts/sign-up.tsx | 99 +++----- web/store/application/app-config.store.ts | 3 +- 36 files changed, 1022 insertions(+), 763 deletions(-) delete mode 100644 web/components/account/email-signup-form.tsx rename web/components/account/{ => o-auth}/github-sign-in.tsx (90%) rename web/components/account/{ => o-auth}/google-sign-in.tsx (87%) create mode 100644 web/components/account/o-auth/index.ts rename web/components/account/{sign-in-forms => o-auth}/o-auth-options.tsx (85%) create mode 100644 web/components/account/sign-in-forms/email.tsx create mode 100644 web/components/account/sign-in-forms/forgot-password-popover.tsx delete mode 100644 web/components/account/sign-in-forms/set-password-link.tsx rename web/components/account/{sign-in-forms/email-form.tsx => sign-up-forms/email.tsx} (74%) create mode 100644 web/components/account/sign-up-forms/index.ts rename web/components/account/{sign-in-forms/create-password.tsx => sign-up-forms/optional-set-password.tsx} (51%) rename web/components/account/{sign-in-forms/self-hosted-sign-in.tsx => sign-up-forms/password.tsx} (79%) create mode 100644 web/components/account/sign-up-forms/root.tsx create mode 100644 web/components/account/sign-up-forms/unique-code.tsx create mode 100644 web/pages/accounts/forgot-password.tsx rename web/pages/accounts/{password.tsx => reset-password.tsx} (58%) diff --git a/apiserver/plane/bgtasks/forgot_password_task.py b/apiserver/plane/bgtasks/forgot_password_task.py index 6c966f342..a2ac62927 100644 --- a/apiserver/plane/bgtasks/forgot_password_task.py +++ b/apiserver/plane/bgtasks/forgot_password_task.py @@ -21,7 +21,7 @@ from plane.license.utils.instance_value import get_email_configuration def forgot_password(first_name, email, uidb64, token, current_site): try: relative_link = ( - f"/accounts/password/?uidb64={uidb64}&token={token}&email={email}" + f"/accounts/reset-password/?uidb64={uidb64}&token={token}&email={email}" ) abs_url = str(current_site) + relative_link diff --git a/packages/types/src/app.d.ts b/packages/types/src/app.d.ts index 92b304e17..06a433ddd 100644 --- a/packages/types/src/app.d.ts +++ b/packages/types/src/app.d.ts @@ -1,14 +1,14 @@ export interface IAppConfig { email_password_login: boolean; file_size_limit: number; - google_client_id: string | null; github_app_name: string | null; github_client_id: string | null; - magic_login: boolean; - slack_client_id: string | null; - posthog_api_key: string | null; - posthog_host: string | null; + google_client_id: string | null; has_openai_configured: boolean; has_unsplash_configured: boolean; is_smtp_configured: boolean; + magic_login: boolean; + posthog_api_key: string | null; + posthog_host: string | null; + slack_client_id: string | null; } diff --git a/space/components/accounts/sign-in-forms/create-password.tsx b/space/components/accounts/sign-in-forms/create-password.tsx index cb7326b75..55205e707 100644 --- a/space/components/accounts/sign-in-forms/create-password.tsx +++ b/space/components/accounts/sign-in-forms/create-password.tsx @@ -101,7 +101,7 @@ export const CreatePasswordForm: React.FC = (props) => { onChange={onChange} ref={ref} hasError={Boolean(errors.email)} - placeholder="orville.wright@frstflt.com" + placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400" disabled /> diff --git a/space/components/accounts/sign-in-forms/email-form.tsx b/space/components/accounts/sign-in-forms/email-form.tsx index 43fd4df31..4f8ed4294 100644 --- a/space/components/accounts/sign-in-forms/email-form.tsx +++ b/space/components/accounts/sign-in-forms/email-form.tsx @@ -100,7 +100,7 @@ export const EmailForm: React.FC = (props) => { onChange={onChange} ref={ref} hasError={Boolean(errors.email)} - placeholder="orville.wright@frstflt.com" + placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400" /> {value.length > 0 && ( diff --git a/space/components/accounts/sign-in-forms/optional-set-password.tsx b/space/components/accounts/sign-in-forms/optional-set-password.tsx index 686848570..219971759 100644 --- a/space/components/accounts/sign-in-forms/optional-set-password.tsx +++ b/space/components/accounts/sign-in-forms/optional-set-password.tsx @@ -61,7 +61,7 @@ export const OptionalSetPasswordForm: React.FC = (props) => { onChange={onChange} ref={ref} hasError={Boolean(errors.email)} - placeholder="orville.wright@frstflt.com" + placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 pr-12 text-onboarding-text-400" disabled /> diff --git a/space/components/accounts/sign-in-forms/password.tsx b/space/components/accounts/sign-in-forms/password.tsx index d080ff639..f909f16c5 100644 --- a/space/components/accounts/sign-in-forms/password.tsx +++ b/space/components/accounts/sign-in-forms/password.tsx @@ -155,7 +155,7 @@ export const PasswordForm: React.FC = (props) => { value={value} onChange={onChange} hasError={Boolean(errors.email)} - placeholder="orville.wright@frstflt.com" + placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400" /> {value.length > 0 && ( diff --git a/space/components/accounts/sign-in-forms/self-hosted-sign-in.tsx b/space/components/accounts/sign-in-forms/self-hosted-sign-in.tsx index 6ebc05490..af1e5d68f 100644 --- a/space/components/accounts/sign-in-forms/self-hosted-sign-in.tsx +++ b/space/components/accounts/sign-in-forms/self-hosted-sign-in.tsx @@ -97,7 +97,7 @@ export const SelfHostedSignInForm: React.FC = (props) => { value={value} onChange={onChange} hasError={Boolean(errors.email)} - placeholder="orville.wright@frstflt.com" + placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400" /> {value.length > 0 && ( diff --git a/space/components/accounts/sign-in-forms/set-password-link.tsx b/space/components/accounts/sign-in-forms/set-password-link.tsx index b0e5f69d3..0b5ad21d9 100644 --- a/space/components/accounts/sign-in-forms/set-password-link.tsx +++ b/space/components/accounts/sign-in-forms/set-password-link.tsx @@ -87,7 +87,7 @@ export const SetPasswordLink: React.FC = (props) => { value={value} onChange={onChange} hasError={Boolean(errors.email)} - placeholder="orville.wright@frstflt.com" + placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400" disabled /> diff --git a/space/components/accounts/sign-in-forms/unique-code.tsx b/space/components/accounts/sign-in-forms/unique-code.tsx index 6b45bc429..4c61fa151 100644 --- a/space/components/accounts/sign-in-forms/unique-code.tsx +++ b/space/components/accounts/sign-in-forms/unique-code.tsx @@ -182,7 +182,7 @@ export const UniqueCodeForm: React.FC = (props) => { }} ref={ref} hasError={Boolean(errors.email)} - placeholder="orville.wright@frstflt.com" + placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400" /> {value.length > 0 && ( diff --git a/space/pages/accounts/password.tsx b/space/pages/accounts/password.tsx index a3fabdda9..85da11290 100644 --- a/space/pages/accounts/password.tsx +++ b/space/pages/accounts/password.tsx @@ -104,7 +104,7 @@ const HomePage: NextPage = () => { onChange={onChange} ref={ref} hasError={Boolean(errors.email)} - placeholder="orville.wright@frstflt.com" + placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400" disabled /> diff --git a/web/components/account/email-signup-form.tsx b/web/components/account/email-signup-form.tsx deleted file mode 100644 index 8bbf859a4..000000000 --- a/web/components/account/email-signup-form.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import React from "react"; -import Link from "next/link"; -import { Controller, useForm } from "react-hook-form"; -// ui -import { Button, Input } from "@plane/ui"; -// types -type EmailPasswordFormValues = { - email: string; - password?: string; - confirm_password: string; - medium?: string; -}; - -type Props = { - onSubmit: (formData: EmailPasswordFormValues) => Promise; -}; - -export const EmailSignUpForm: React.FC = (props) => { - const { onSubmit } = props; - - const { - handleSubmit, - control, - watch, - formState: { errors, isSubmitting, isValid, isDirty }, - } = useForm({ - defaultValues: { - email: "", - password: "", - confirm_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, ref } }) => ( - - )} - /> -
-
- ( - - )} - /> -
-
- { - if (watch("password") != val) { - return "Your passwords do no match"; - } - }, - }} - render={({ field: { value, onChange, ref } }) => ( - - )} - /> -
-
- - - Already have an account? Sign in. - - -
-
- -
-
- - ); -}; diff --git a/web/components/account/index.ts b/web/components/account/index.ts index 275f7ff08..0d1cffbc6 100644 --- a/web/components/account/index.ts +++ b/web/components/account/index.ts @@ -1,5 +1,4 @@ +export * from "./o-auth"; export * from "./sign-in-forms"; +export * from "./sign-up-forms"; export * from "./deactivate-account-modal"; -export * from "./github-sign-in"; -export * from "./google-sign-in"; -export * from "./email-signup-form"; diff --git a/web/components/account/github-sign-in.tsx b/web/components/account/o-auth/github-sign-in.tsx similarity index 90% rename from web/components/account/github-sign-in.tsx rename to web/components/account/o-auth/github-sign-in.tsx index 27a8bf01c..74bfd6d94 100644 --- a/web/components/account/github-sign-in.tsx +++ b/web/components/account/o-auth/github-sign-in.tsx @@ -12,10 +12,11 @@ import githubDarkModeImage from "/public/logos/github-dark.svg"; type Props = { handleSignIn: React.Dispatch; clientId: string; + type: "sign_in" | "sign_up"; }; export const GitHubSignInButton: FC = (props) => { - const { handleSignIn, clientId } = props; + const { handleSignIn, clientId, type } = props; // states const [loginCallBackURL, setLoginCallBackURL] = useState(undefined); const [gitCode, setGitCode] = useState(null); @@ -53,7 +54,7 @@ export const GitHubSignInButton: FC = (props) => { width={20} alt="GitHub Logo" /> - Sign-in with GitHub + {type === "sign_in" ? "Sign-in" : "Sign-up"} with GitHub diff --git a/web/components/account/google-sign-in.tsx b/web/components/account/o-auth/google-sign-in.tsx similarity index 87% rename from web/components/account/google-sign-in.tsx rename to web/components/account/o-auth/google-sign-in.tsx index 48488e07e..93958bbd2 100644 --- a/web/components/account/google-sign-in.tsx +++ b/web/components/account/o-auth/google-sign-in.tsx @@ -4,10 +4,11 @@ import Script from "next/script"; type Props = { handleSignIn: React.Dispatch; clientId: string; + type: "sign_in" | "sign_up"; }; export const GoogleSignInButton: FC = (props) => { - const { handleSignIn, clientId } = props; + const { handleSignIn, clientId, type } = props; // refs const googleSignInButton = useRef(null); // states @@ -29,7 +30,7 @@ export const GoogleSignInButton: FC = (props) => { theme: "outline", size: "large", logo_alignment: "center", - text: "signin_with", + text: type === "sign_in" ? "signin_with" : "signup_with", width: 384, } as GsiButtonConfiguration // customization attributes ); @@ -40,7 +41,7 @@ export const GoogleSignInButton: FC = (props) => { window?.google?.accounts.id.prompt(); // also display the One Tap dialog setGsiScriptLoaded(true); - }, [handleSignIn, gsiScriptLoaded, clientId]); + }, [handleSignIn, gsiScriptLoaded, clientId, type]); useEffect(() => { if (window?.google?.accounts?.id) { diff --git a/web/components/account/o-auth/index.ts b/web/components/account/o-auth/index.ts new file mode 100644 index 000000000..4cea6ce5b --- /dev/null +++ b/web/components/account/o-auth/index.ts @@ -0,0 +1,3 @@ +export * from "./github-sign-in"; +export * from "./google-sign-in"; +export * from "./o-auth-options"; diff --git a/web/components/account/sign-in-forms/o-auth-options.tsx b/web/components/account/o-auth/o-auth-options.tsx similarity index 85% rename from web/components/account/sign-in-forms/o-auth-options.tsx rename to web/components/account/o-auth/o-auth-options.tsx index 9ed4e7e5f..3ad1c2fa1 100644 --- a/web/components/account/sign-in-forms/o-auth-options.tsx +++ b/web/components/account/o-auth/o-auth-options.tsx @@ -9,13 +9,14 @@ import { GitHubSignInButton, GoogleSignInButton } from "components/account"; type Props = { handleSignInRedirection: () => Promise; + type: "sign_in" | "sign_up"; }; // services const authService = new AuthService(); export const OAuthOptions: React.FC = observer((props) => { - const { handleSignInRedirection } = props; + const { handleSignInRedirection, type } = props; // toast alert const { setToastAlert } = useToast(); // mobx store @@ -72,12 +73,14 @@ export const OAuthOptions: React.FC = observer((props) => {

Or continue with


-
+
{envConfig?.google_client_id && ( - +
+ +
)} {envConfig?.github_client_id && ( - + )}
diff --git a/web/components/account/sign-in-forms/email.tsx b/web/components/account/sign-in-forms/email.tsx new file mode 100644 index 000000000..67ef720fe --- /dev/null +++ b/web/components/account/sign-in-forms/email.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { Controller, useForm } from "react-hook-form"; +import { XCircle } from "lucide-react"; +import { observer } from "mobx-react-lite"; +// 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 "@plane/types"; + +type Props = { + onSubmit: (isPasswordAutoset: boolean) => void; + updateEmail: (email: string) => void; +}; + +type TEmailFormValues = { + email: string; +}; + +const authService = new AuthService(); + +export const SignInEmailForm: React.FC = observer((props) => { + const { onSubmit, updateEmail } = props; + // hooks + const { setToastAlert } = useToast(); + const { + control, + formState: { errors, isSubmitting, isValid }, + handleSubmit, + } = useForm({ + defaultValues: { + email: "", + }, + mode: "onChange", + reValidateMode: "onChange", + }); + + const handleFormSubmit = async (data: TEmailFormValues) => { + const payload: IEmailCheckData = { + email: data.email, + }; + + // update the global email state + updateEmail(data.email); + + await authService + .emailCheck(payload) + .then((res) => onSubmit(res.is_password_autoset)) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: err?.error ?? "Something went wrong. Please try again.", + }) + ); + }; + + return ( + <> +

+ Welcome back, let{"'"}s get you on board +

+

+ Get back to your issues, projects and workspaces. +

+ +
+
+ checkEmailValidity(value) || "Email is invalid", + }} + render={({ field: { value, onChange } }) => ( +
+ + {value.length > 0 && ( + onChange("")} + /> + )} +
+ )} + /> +
+ +
+ + ); +}); diff --git a/web/components/account/sign-in-forms/forgot-password-popover.tsx b/web/components/account/sign-in-forms/forgot-password-popover.tsx new file mode 100644 index 000000000..d652e51f1 --- /dev/null +++ b/web/components/account/sign-in-forms/forgot-password-popover.tsx @@ -0,0 +1,54 @@ +import { Fragment, useState } from "react"; +import { usePopper } from "react-popper"; +import { Popover } from "@headlessui/react"; +import { X } from "lucide-react"; + +export const ForgotPasswordPopover = () => { + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: "right-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + + return ( + + + + + + {({ close }) => ( +
+ 🤥 +

+ We see that your god hasn{"'"}t enabled SMTP, we will not be able to send a password reset link +

+ +
+ )} +
+
+ ); +}; diff --git a/web/components/account/sign-in-forms/index.ts b/web/components/account/sign-in-forms/index.ts index 1150a071c..8e44f490b 100644 --- a/web/components/account/sign-in-forms/index.ts +++ b/web/components/account/sign-in-forms/index.ts @@ -1,9 +1,6 @@ -export * from "./create-password"; -export * from "./email-form"; -export * from "./o-auth-options"; +export * from "./email"; +export * from "./forgot-password-popover"; export * from "./optional-set-password"; export * from "./password"; export * from "./root"; -export * from "./self-hosted-sign-in"; -export * from "./set-password-link"; export * from "./unique-code"; diff --git a/web/components/account/sign-in-forms/optional-set-password.tsx b/web/components/account/sign-in-forms/optional-set-password.tsx index ead9b9c9a..1669811cb 100644 --- a/web/components/account/sign-in-forms/optional-set-password.tsx +++ b/web/components/account/sign-in-forms/optional-set-password.tsx @@ -1,36 +1,76 @@ import React, { useState } from "react"; -import Link from "next/link"; 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; - isOnboarded: boolean; }; -export const OptionalSetPasswordForm: React.FC = (props) => { - const { email, handleStepChange, handleSignInRedirection, isOnboarded } = props; +type TCreatePasswordFormValues = { + email: string; + password: string; +}; + +const defaultValues: TCreatePasswordFormValues = { + email: "", + password: "", +}; + +// services +const authService = new AuthService(); + +export const SignInOptionalSetPasswordForm: React.FC = (props) => { + const { email, handleSignInRedirection } = props; // states const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false); + // toast alert + const { setToastAlert } = useToast(); // form info const { control, - formState: { errors, isValid }, - } = useForm({ + 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.", + }) + ); + }; + const handleGoToWorkspace = async () => { setIsGoingToWorkspace(true); @@ -39,12 +79,11 @@ export const OptionalSetPasswordForm: React.FC = (props) => { return ( <> -

Set a password

-

+

Set your password

+

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

- -
+ = (props) => { onChange={onChange} ref={ref} hasError={Boolean(errors.email)} - placeholder="orville.wright@frstflt.com" - className="h-[46px] w-full border border-onboarding-border-100 pr-12 text-onboarding-text-400" + placeholder="name@company.com" + className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400" disabled /> )} /> -
+
+ ( + + )} + /> +

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

+
+
-

- When you click{" "} - {isOnboarded ? "Go to workspace" : "Set up workspace"} above, - you agree with our{" "} - - terms and conditions of service. - -

); diff --git a/web/components/account/sign-in-forms/password.tsx b/web/components/account/sign-in-forms/password.tsx index c2eb358f2..fd4ccbf40 100644 --- a/web/components/account/sign-in-forms/password.tsx +++ b/web/components/account/sign-in-forms/password.tsx @@ -1,5 +1,6 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import Link from "next/link"; +import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; import { XCircle } from "lucide-react"; // services @@ -7,22 +8,20 @@ import { AuthService } from "services/auth.service"; // hooks import useToast from "hooks/use-toast"; import { useApplication } from "hooks/store"; +// components +import { ESignInSteps, ForgotPasswordPopover } from "components/account"; // ui import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types import { IPasswordSignInData } from "@plane/types"; -// constants -import { ESignInSteps } from "components/account"; -import { observer } from "mobx-react-lite"; type Props = { email: string; - updateEmail: (email: string) => void; handleStepChange: (step: ESignInSteps) => void; - handleSignInRedirection: () => Promise; handleEmailClear: () => void; + onSubmit: () => Promise; }; type TPasswordFormValues = { @@ -37,24 +36,24 @@ const defaultValues: TPasswordFormValues = { const authService = new AuthService(); -export const PasswordForm: React.FC = observer((props) => { - const { email, updateEmail, handleStepChange, handleSignInRedirection, handleEmailClear } = props; +export const SignInPasswordForm: React.FC = observer((props) => { + const { email, handleStepChange, handleEmailClear, onSubmit } = props; // states const [isSendingUniqueCode, setIsSendingUniqueCode] = useState(false); - const [isSendingResetPasswordLink, setIsSendingResetPasswordLink] = useState(false); // toast alert const { setToastAlert } = useToast(); const { config: { envConfig }, } = useApplication(); + // derived values + const isSmtpConfigured = envConfig?.is_smtp_configured; // form info const { control, - formState: { dirtyFields, errors, isSubmitting, isValid }, + formState: { errors, isSubmitting, isValid }, getValues, handleSubmit, setError, - setFocus, } = useForm({ defaultValues: { ...defaultValues, @@ -65,8 +64,6 @@ export const PasswordForm: React.FC = observer((props) => { }); const handleFormSubmit = async (formData: TPasswordFormValues) => { - updateEmail(formData.email); - const payload: IPasswordSignInData = { email: formData.email, password: formData.password, @@ -74,7 +71,7 @@ export const PasswordForm: React.FC = observer((props) => { await authService .passwordSignIn(payload) - .then(async () => await handleSignInRedirection()) + .then(async () => await onSubmit()) .catch((err) => setToastAlert({ type: "error", @@ -84,31 +81,6 @@ export const PasswordForm: React.FC = observer((props) => { ); }; - const handleForgotPassword = async () => { - const emailFormValue = getValues("email"); - - const isEmailValid = checkEmailValidity(emailFormValue); - - if (!isEmailValid) { - setError("email", { message: "Email is invalid" }); - return; - } - - setIsSendingResetPasswordLink(true); - - 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.", - }) - ) - .finally(() => setIsSendingResetPasswordLink(false)); - }; - const handleSendUniqueCode = async () => { const emailFormValue = getValues("email"); @@ -134,16 +106,15 @@ export const PasswordForm: React.FC = observer((props) => { .finally(() => setIsSendingUniqueCode(false)); }; - useEffect(() => { - setFocus("password"); - }, [setFocus]); - return ( <> -

- Get on your flight deck +

+ Welcome back, let{"'"}s get you on board

-
+

+ Get back to your issues, projects and workspaces. +

+
= observer((props) => { value={value} onChange={onChange} hasError={Boolean(errors.email)} - placeholder="orville.wright@frstflt.com" + placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400" - disabled + disabled={isSmtpConfigured} /> {value.length > 0 && ( { + if (isSmtpConfigured) handleEmailClear(); + else onChange(""); + }} /> )}
@@ -180,7 +154,7 @@ export const PasswordForm: React.FC = observer((props) => { control={control} name="password" rules={{ - required: dirtyFields.email ? false : "Password is required", + required: "Password is required", }} render={({ field: { value, onChange } }) => ( = observer((props) => { hasError={Boolean(errors.password)} placeholder="Enter password" className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" + autoFocus /> )} /> -
- +
+ {isSmtpConfigured ? ( + + Forgot your password? + + ) : ( + + )}
-
+
+ {envConfig && envConfig.is_smtp_configured && ( )} -
-

- When you click Go to workspace above, you agree with our{" "} - - terms and conditions of service. - -

); diff --git a/web/components/account/sign-in-forms/root.tsx b/web/components/account/sign-in-forms/root.tsx index 80f46c63e..c92cd4bd4 100644 --- a/web/components/account/sign-in-forms/root.tsx +++ b/web/components/account/sign-in-forms/root.tsx @@ -1,4 +1,5 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; +import Link from "next/link"; import { observer } from "mobx-react-lite"; // hooks import { useApplication } from "hooks/store"; @@ -6,115 +7,115 @@ import useSignInRedirection from "hooks/use-sign-in-redirection"; // components import { LatestFeatureBlock } from "components/common"; import { - EmailForm, - UniqueCodeForm, - PasswordForm, - SetPasswordLink, + SignInEmailForm, + SignInUniqueCodeForm, + SignInPasswordForm, OAuthOptions, - OptionalSetPasswordForm, - CreatePasswordForm, + SignInOptionalSetPasswordForm, } 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", USE_UNIQUE_CODE_FROM_PASSWORD = "USE_UNIQUE_CODE_FROM_PASSWORD", } -const OAUTH_HIDDEN_STEPS = [ESignInSteps.OPTIONAL_SET_PASSWORD, ESignInSteps.CREATE_PASSWORD]; - export const SignInRoot = observer(() => { // states - const [signInStep, setSignInStep] = useState(ESignInSteps.EMAIL); + const [signInStep, setSignInStep] = useState(null); const [email, setEmail] = useState(""); - const [isOnboarded, setIsOnboarded] = useState(false); // sign in redirection hook const { handleRedirection } = useSignInRedirection(); // mobx store const { config: { envConfig }, } = useApplication(); + // derived values + const isSmtpConfigured = envConfig?.is_smtp_configured; + + // step 1 submit handler- email verification + const handleEmailVerification = (isPasswordAutoset: boolean) => { + if (isSmtpConfigured && isPasswordAutoset) setSignInStep(ESignInSteps.UNIQUE_CODE); + else setSignInStep(ESignInSteps.PASSWORD); + }; + + // step 2 submit handler- unique code sign in + const handleUniqueCodeSignIn = async (isPasswordAutoset: boolean) => { + if (isPasswordAutoset) setSignInStep(ESignInSteps.OPTIONAL_SET_PASSWORD); + else await handleRedirection(); + }; + + // step 3 submit handler- password sign in + const handlePasswordSignIn = async () => { + await handleRedirection(); + }; const isOAuthEnabled = envConfig && (envConfig.google_client_id || envConfig.github_client_id); + useEffect(() => { + if (isSmtpConfigured) setSignInStep(ESignInSteps.EMAIL); + else setSignInStep(ESignInSteps.PASSWORD); + }, [isSmtpConfigured]); + return ( <>
<> {signInStep === ESignInSteps.EMAIL && ( - setSignInStep(step)} - updateEmail={(newEmail) => setEmail(newEmail)} + setEmail(newEmail)} /> + )} + {signInStep === ESignInSteps.UNIQUE_CODE && ( + { + setEmail(""); + setSignInStep(ESignInSteps.EMAIL); + }} + onSubmit={handleUniqueCodeSignIn} + submitButtonText="Continue" /> )} {signInStep === ESignInSteps.PASSWORD && ( - setEmail(newEmail)} - handleStepChange={(step) => setSignInStep(step)} handleEmailClear={() => { setEmail(""); setSignInStep(ESignInSteps.EMAIL); }} - handleSignInRedirection={handleRedirection} + onSubmit={handlePasswordSignIn} + handleStepChange={(step) => setSignInStep(step)} /> )} - {signInStep === ESignInSteps.SET_PASSWORD_LINK && ( - setEmail(newEmail)} /> - )} {signInStep === ESignInSteps.USE_UNIQUE_CODE_FROM_PASSWORD && ( - setEmail(newEmail)} - handleStepChange={(step) => setSignInStep(step)} - handleSignInRedirection={handleRedirection} - submitButtonLabel="Go to workspace" - showTermsAndConditions - updateUserOnboardingStatus={(value) => setIsOnboarded(value)} - handleEmailClear={() => { - setEmail(""); - setSignInStep(ESignInSteps.EMAIL); - }} - /> - )} - {signInStep === ESignInSteps.UNIQUE_CODE && ( - setEmail(newEmail)} - handleStepChange={(step) => setSignInStep(step)} - handleSignInRedirection={handleRedirection} - updateUserOnboardingStatus={(value) => setIsOnboarded(value)} handleEmailClear={() => { setEmail(""); setSignInStep(ESignInSteps.EMAIL); }} + onSubmit={handleUniqueCodeSignIn} + submitButtonText="Go to workspace" /> )} {signInStep === ESignInSteps.OPTIONAL_SET_PASSWORD && ( - setSignInStep(step)} - handleSignInRedirection={handleRedirection} - isOnboarded={isOnboarded} - /> - )} - {signInStep === ESignInSteps.CREATE_PASSWORD && ( - setSignInStep(step)} - handleSignInRedirection={handleRedirection} - isOnboarded={isOnboarded} - /> + )}
- {isOAuthEnabled && !OAUTH_HIDDEN_STEPS.includes(signInStep) && ( - - )} + {isOAuthEnabled && + (signInStep === ESignInSteps.EMAIL || (!isSmtpConfigured && signInStep === ESignInSteps.PASSWORD)) && ( + <> + +

+ Don{"'"}t have an account?{" "} + + Sign up + +

+ + )} ); diff --git a/web/components/account/sign-in-forms/set-password-link.tsx b/web/components/account/sign-in-forms/set-password-link.tsx deleted file mode 100644 index 788142d80..000000000 --- a/web/components/account/sign-in-forms/set-password-link.tsx +++ /dev/null @@ -1,103 +0,0 @@ -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"; -// types -import { IEmailCheckData } from "@plane/types"; - -type Props = { - email: string; - updateEmail: (email: string) => void; -}; - -const authService = new AuthService(); - -export const SetPasswordLink: React.FC = (props) => { - const { email, updateEmail } = props; - - const { setToastAlert } = useToast(); - - const { - control, - formState: { errors, isSubmitting, isValid }, - handleSubmit, - } = useForm({ - defaultValues: { - email, - }, - mode: "onChange", - reValidateMode: "onChange", - }); - - const handleSendNewLink = async (formData: { email: string }) => { - updateEmail(formData.email); - - const payload: IEmailCheckData = { - email: formData.email, - }; - - await authService - .sendResetPasswordLink(payload) - .then(() => - setToastAlert({ - type: "success", - title: "Success!", - message: "We have sent a new link to your email.", - }) - ) - .catch((err) => - setToastAlert({ - type: "error", - title: "Error!", - message: err?.error ?? "Something went wrong. Please try again.", - }) - ); - }; - - 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 } }) => ( - - )} - /> -
- -
- - ); -}; diff --git a/web/components/account/sign-in-forms/unique-code.tsx b/web/components/account/sign-in-forms/unique-code.tsx index 3ab75831d..6e0ae3745 100644 --- a/web/components/account/sign-in-forms/unique-code.tsx +++ b/web/components/account/sign-in-forms/unique-code.tsx @@ -1,7 +1,6 @@ -import React, { useEffect, useState } from "react"; -import Link from "next/link"; +import React, { useState } from "react"; import { Controller, useForm } from "react-hook-form"; -import { CornerDownLeft, XCircle } from "lucide-react"; +import { XCircle } from "lucide-react"; // services import { AuthService } from "services/auth.service"; import { UserService } from "services/user.service"; @@ -14,18 +13,12 @@ import { Button, Input } from "@plane/ui"; import { checkEmailValidity } from "helpers/string.helper"; // types import { IEmailCheckData, IMagicSignInData } from "@plane/types"; -// constants -import { ESignInSteps } from "components/account"; type Props = { email: string; - updateEmail: (email: string) => void; - handleStepChange: (step: ESignInSteps) => void; - handleSignInRedirection: () => Promise; - submitButtonLabel?: string; - showTermsAndConditions?: boolean; - updateUserOnboardingStatus: (value: boolean) => void; + onSubmit: (isPasswordAutoset: boolean) => Promise; handleEmailClear: () => void; + submitButtonText: string; }; type TUniqueCodeFormValues = { @@ -42,17 +35,8 @@ const defaultValues: TUniqueCodeFormValues = { const authService = new AuthService(); const userService = new UserService(); -export const UniqueCodeForm: React.FC = (props) => { - const { - email, - updateEmail, - handleStepChange, - handleSignInRedirection, - submitButtonLabel = "Continue", - showTermsAndConditions = false, - updateUserOnboardingStatus, - handleEmailClear, - } = props; +export const SignInUniqueCodeForm: React.FC = (props) => { + const { email, onSubmit, handleEmailClear, submitButtonText } = props; // states const [isRequestingNewCode, setIsRequestingNewCode] = useState(false); // toast alert @@ -62,11 +46,10 @@ export const UniqueCodeForm: React.FC = (props) => { // form info const { control, - formState: { dirtyFields, errors, isSubmitting, isValid }, + formState: { errors, isSubmitting, isValid }, getValues, handleSubmit, reset, - setFocus, } = useForm({ defaultValues: { ...defaultValues, @@ -88,10 +71,7 @@ export const UniqueCodeForm: React.FC = (props) => { .then(async () => { const currentUser = await userService.currentUser(); - updateUserOnboardingStatus(currentUser.is_onboarded); - - if (currentUser.is_password_autoset) handleStepChange(ESignInSteps.OPTIONAL_SET_PASSWORD); - else await handleSignInRedirection(); + await onSubmit(currentUser.is_password_autoset); }) .catch((err) => setToastAlert({ @@ -131,13 +111,6 @@ export const UniqueCodeForm: React.FC = (props) => { ); }; - const handleFormSubmit = async (formData: TUniqueCodeFormValues) => { - updateEmail(formData.email); - - if (dirtyFields.email) await handleSendNewCode(formData); - else await handleUniqueCodeSignIn(formData); - }; - const handleRequestNewCode = async () => { setIsRequestingNewCode(true); @@ -147,21 +120,16 @@ export const UniqueCodeForm: React.FC = (props) => { }; const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0; - const hasEmailChanged = dirtyFields.email; - useEffect(() => { - setFocus("token"); - }, [setFocus]); return ( <> -

- Get on your flight deck -

+

Moving to the runway

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

- -
+
= (props) => { type="email" value={value} onChange={onChange} - onBlur={() => { - if (hasEmailChanged) handleSendNewCode(getValues()); - }} ref={ref} hasError={Boolean(errors.email)} - placeholder="orville.wright@frstflt.com" + placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400" disabled /> @@ -196,21 +161,13 @@ export const UniqueCodeForm: React.FC = (props) => {
)} /> - {hasEmailChanged && ( - - )}
( = (props) => { hasError={Boolean(errors.token)} placeholder="gets-sets-flys" className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" + autoFocus /> )} /> @@ -241,24 +199,9 @@ export const UniqueCodeForm: React.FC = (props) => {
- - {showTermsAndConditions && ( -

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

- )} ); diff --git a/web/components/account/sign-in-forms/email-form.tsx b/web/components/account/sign-up-forms/email.tsx similarity index 74% rename from web/components/account/sign-in-forms/email-form.tsx rename to web/components/account/sign-up-forms/email.tsx index 7642c3b99..0d5861b4e 100644 --- a/web/components/account/sign-in-forms/email-form.tsx +++ b/web/components/account/sign-up-forms/email.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React from "react"; import { Controller, useForm } from "react-hook-form"; import { XCircle } from "lucide-react"; import { observer } from "mobx-react-lite"; @@ -6,18 +6,15 @@ import { observer } from "mobx-react-lite"; import { AuthService } from "services/auth.service"; // hooks import useToast from "hooks/use-toast"; -import { useApplication } from "hooks/store"; // ui import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types import { IEmailCheckData } from "@plane/types"; -// constants -import { ESignInSteps } from "components/account"; type Props = { - handleStepChange: (step: ESignInSteps) => void; + onSubmit: () => void; updateEmail: (email: string) => void; }; @@ -27,18 +24,14 @@ type TEmailFormValues = { const authService = new AuthService(); -export const EmailForm: React.FC = observer((props) => { - const { handleStepChange, updateEmail } = props; +export const SignUpEmailForm: React.FC = observer((props) => { + const { onSubmit, updateEmail } = props; // hooks const { setToastAlert } = useToast(); - const { - config: { envConfig }, - } = useApplication(); const { control, formState: { errors, isSubmitting, isValid }, handleSubmit, - setFocus, } = useForm({ defaultValues: { email: "", @@ -57,14 +50,7 @@ export const EmailForm: React.FC = observer((props) => { await authService .emailCheck(payload) - .then((res) => { - // if the password has been auto set, send the user to magic sign-in - if (res.is_password_autoset && envConfig?.is_smtp_configured) { - handleStepChange(ESignInSteps.UNIQUE_CODE); - } - // if the password has not been auto set, send them to password sign-in - else handleStepChange(ESignInSteps.PASSWORD); - }) + .then(() => onSubmit()) .catch((err) => setToastAlert({ type: "error", @@ -74,10 +60,6 @@ export const EmailForm: React.FC = observer((props) => { ); }; - useEffect(() => { - setFocus("email"); - }, [setFocus]); - return ( <>

@@ -96,7 +78,7 @@ export const EmailForm: React.FC = observer((props) => { required: "Email is required", validate: (value) => checkEmailValidity(value) || "Email is invalid", }} - render={({ field: { value, onChange, ref } }) => ( + render={({ field: { value, onChange } }) => (
= observer((props) => { type="email" value={value} onChange={onChange} - ref={ref} hasError={Boolean(errors.email)} - placeholder="orville.wright@frstflt.com" + placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400" + autoFocus /> {value.length > 0 && ( = observer((props) => { />
diff --git a/web/components/account/sign-up-forms/index.ts b/web/components/account/sign-up-forms/index.ts new file mode 100644 index 000000000..f84d41abc --- /dev/null +++ b/web/components/account/sign-up-forms/index.ts @@ -0,0 +1,5 @@ +export * from "./email"; +export * from "./optional-set-password"; +export * from "./password"; +export * from "./root"; +export * from "./unique-code"; diff --git a/web/components/account/sign-in-forms/create-password.tsx b/web/components/account/sign-up-forms/optional-set-password.tsx similarity index 51% rename from web/components/account/sign-in-forms/create-password.tsx rename to web/components/account/sign-up-forms/optional-set-password.tsx index cf53078be..38fdaeca1 100644 --- a/web/components/account/sign-in-forms/create-password.tsx +++ b/web/components/account/sign-up-forms/optional-set-password.tsx @@ -1,5 +1,4 @@ -import React, { useEffect } from "react"; -import Link from "next/link"; +import React, { useState } from "react"; import { Controller, useForm } from "react-hook-form"; // services import { AuthService } from "services/auth.service"; @@ -10,13 +9,12 @@ import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // constants -import { ESignInSteps } from "components/account"; +import { ESignUpSteps } from "components/account"; type Props = { email: string; - handleStepChange: (step: ESignInSteps) => void; + handleStepChange: (step: ESignUpSteps) => void; handleSignInRedirection: () => Promise; - isOnboarded: boolean; }; type TCreatePasswordFormValues = { @@ -32,8 +30,10 @@ const defaultValues: TCreatePasswordFormValues = { // services const authService = new AuthService(); -export const CreatePasswordForm: React.FC = (props) => { - const { email, handleSignInRedirection, isOnboarded } = props; +export const SignUpOptionalSetPasswordForm: React.FC = (props) => { + const { email, handleSignInRedirection } = props; + // states + const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false); // toast alert const { setToastAlert } = useToast(); // form info @@ -41,7 +41,6 @@ export const CreatePasswordForm: React.FC = (props) => { control, formState: { errors, isSubmitting, isValid }, handleSubmit, - setFocus, } = useForm({ defaultValues: { ...defaultValues, @@ -75,16 +74,21 @@ export const CreatePasswordForm: React.FC = (props) => { ); }; - useEffect(() => { - setFocus("password"); - }, [setFocus]); + const handleGoToWorkspace = async () => { + setIsGoingToWorkspace(true); + + await handleSignInRedirection().finally(() => setIsGoingToWorkspace(false)); + }; return ( <> -

- Get on your flight deck -

-
+

Moving to the runway

+

+ Let{"'"}s set a password so +
+ you can do away with codes. +

+ = (props) => { onChange={onChange} ref={ref} hasError={Boolean(errors.email)} - placeholder="orville.wright@frstflt.com" + placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400" disabled /> )} /> - ( - - )} - /> - -

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

+
+ ( + + )} + /> +

+ This password will continue to be your account{"'"}s password. +

+
+
+ + +
); diff --git a/web/components/account/sign-in-forms/self-hosted-sign-in.tsx b/web/components/account/sign-up-forms/password.tsx similarity index 79% rename from web/components/account/sign-in-forms/self-hosted-sign-in.tsx rename to web/components/account/sign-up-forms/password.tsx index bcecef20a..6ff6753df 100644 --- a/web/components/account/sign-in-forms/self-hosted-sign-in.tsx +++ b/web/components/account/sign-up-forms/password.tsx @@ -1,5 +1,6 @@ -import React, { useEffect } from "react"; +import React from "react"; import Link from "next/link"; +import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; import { XCircle } from "lucide-react"; // services @@ -14,9 +15,7 @@ import { checkEmailValidity } from "helpers/string.helper"; import { IPasswordSignInData } from "@plane/types"; type Props = { - email: string; - updateEmail: (email: string) => void; - handleSignInRedirection: () => Promise; + onSubmit: () => Promise; }; type TPasswordFormValues = { @@ -31,20 +30,18 @@ const defaultValues: TPasswordFormValues = { const authService = new AuthService(); -export const SelfHostedSignInForm: React.FC = (props) => { - const { email, updateEmail, handleSignInRedirection } = props; +export const SignUpPasswordForm: React.FC = observer((props) => { + const { onSubmit } = props; // toast alert const { setToastAlert } = useToast(); // form info const { control, - formState: { dirtyFields, errors, isSubmitting }, + formState: { errors, isSubmitting, isValid }, handleSubmit, - setFocus, } = useForm({ defaultValues: { ...defaultValues, - email, }, mode: "onChange", reValidateMode: "onChange", @@ -56,11 +53,9 @@ export const SelfHostedSignInForm: React.FC = (props) => { password: formData.password, }; - updateEmail(formData.email); - await authService .passwordSignIn(payload) - .then(async () => await handleSignInRedirection()) + .then(async () => await onSubmit()) .catch((err) => setToastAlert({ type: "error", @@ -70,16 +65,15 @@ export const SelfHostedSignInForm: React.FC = (props) => { ); }; - useEffect(() => { - setFocus("email"); - }, [setFocus]); - return ( <> -

+

Get on your flight deck

-
+

+ Create or join a workspace. Start with your e-mail. +

+
= (props) => { value={value} onChange={onChange} hasError={Boolean(errors.email)} - placeholder="orville.wright@frstflt.com" + placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400" /> {value.length > 0 && ( @@ -115,7 +109,7 @@ export const SelfHostedSignInForm: React.FC = (props) => { control={control} name="password" rules={{ - required: dirtyFields.email ? false : "Password is required", + required: "Password is required", }} render={({ field: { value, onChange } }) => ( = (props) => { hasError={Boolean(errors.password)} placeholder="Enter password" className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" + autoFocus /> )} /> +

+ This password will continue to be your account{"'"}s password. +

-

When you click the button above, you agree with our{" "} @@ -141,4 +139,4 @@ export const SelfHostedSignInForm: React.FC = (props) => { ); -}; +}); diff --git a/web/components/account/sign-up-forms/root.tsx b/web/components/account/sign-up-forms/root.tsx new file mode 100644 index 000000000..da9d7d79a --- /dev/null +++ b/web/components/account/sign-up-forms/root.tsx @@ -0,0 +1,97 @@ +import React, { useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +// hooks +import { useApplication } from "hooks/store"; +import useSignInRedirection from "hooks/use-sign-in-redirection"; +// components +import { + OAuthOptions, + SignUpEmailForm, + SignUpOptionalSetPasswordForm, + SignUpPasswordForm, + SignUpUniqueCodeForm, +} from "components/account"; +import Link from "next/link"; + +export enum ESignUpSteps { + EMAIL = "EMAIL", + UNIQUE_CODE = "UNIQUE_CODE", + PASSWORD = "PASSWORD", + OPTIONAL_SET_PASSWORD = "OPTIONAL_SET_PASSWORD", +} + +const OAUTH_ENABLED_STEPS = [ESignUpSteps.EMAIL]; + +export const SignUpRoot = observer(() => { + // states + const [signInStep, setSignInStep] = useState(null); + const [email, setEmail] = useState(""); + // sign in redirection hook + const { handleRedirection } = useSignInRedirection(); + // mobx store + const { + config: { envConfig }, + } = useApplication(); + + // step 1 submit handler- email verification + const handleEmailVerification = () => setSignInStep(ESignUpSteps.UNIQUE_CODE); + + // step 2 submit handler- unique code sign in + const handleUniqueCodeSignIn = async (isPasswordAutoset: boolean) => { + if (isPasswordAutoset) setSignInStep(ESignUpSteps.OPTIONAL_SET_PASSWORD); + else await handleRedirection(); + }; + + // step 3 submit handler- password sign in + const handlePasswordSignIn = async () => { + await handleRedirection(); + }; + + const isOAuthEnabled = envConfig && (envConfig.google_client_id || envConfig.github_client_id); + + useEffect(() => { + if (envConfig?.is_smtp_configured) setSignInStep(ESignUpSteps.EMAIL); + else setSignInStep(ESignUpSteps.PASSWORD); + }, [envConfig?.is_smtp_configured]); + + return ( + <> +

+ <> + {signInStep === ESignUpSteps.EMAIL && ( + setEmail(newEmail)} /> + )} + {signInStep === ESignUpSteps.UNIQUE_CODE && ( + { + setEmail(""); + setSignInStep(ESignUpSteps.EMAIL); + }} + onSubmit={handleUniqueCodeSignIn} + /> + )} + {signInStep === ESignUpSteps.PASSWORD && } + {signInStep === ESignUpSteps.OPTIONAL_SET_PASSWORD && ( + setSignInStep(step)} + /> + )} + +
+ {isOAuthEnabled && signInStep && OAUTH_ENABLED_STEPS.includes(signInStep) && ( + <> + +

+ Already using Plane?{" "} + + Sign in + +

+ + )} + + ); +}); diff --git a/web/components/account/sign-up-forms/unique-code.tsx b/web/components/account/sign-up-forms/unique-code.tsx new file mode 100644 index 000000000..7764b627e --- /dev/null +++ b/web/components/account/sign-up-forms/unique-code.tsx @@ -0,0 +1,215 @@ +import React, { useState } from "react"; +import Link from "next/link"; +import { Controller, useForm } from "react-hook-form"; +import { 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 "@plane/types"; + +type Props = { + email: string; + handleEmailClear: () => void; + onSubmit: (isPasswordAutoset: boolean) => Promise; +}; + +type TUniqueCodeFormValues = { + email: string; + token: string; +}; + +const defaultValues: TUniqueCodeFormValues = { + email: "", + token: "", +}; + +// services +const authService = new AuthService(); +const userService = new UserService(); + +export const SignUpUniqueCodeForm: React.FC = (props) => { + const { email, handleEmailClear, onSubmit } = props; + // states + const [isRequestingNewCode, setIsRequestingNewCode] = useState(false); + // toast alert + const { setToastAlert } = useToast(); + // timer + const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30); + // form info + const { + control, + formState: { 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(); + + await onSubmit(currentUser.is_password_autoset); + }) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: err?.error ?? "Something went wrong. Please try again.", + }) + ); + }; + + const handleSendNewCode = async (formData: TUniqueCodeFormValues) => { + const payload: IEmailCheckData = { + email: formData.email, + }; + + await authService + .generateUniqueCode(payload) + .then(() => { + setResendCodeTimer(30); + 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 handleRequestNewCode = async () => { + setIsRequestingNewCode(true); + + await handleSendNewCode(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 } }) => ( +
+ + {value.length > 0 && ( + + )} +
+ )} + /> +
+
+ ( + + )} + /> +
+ +
+
+ +

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

+
+ + ); +}; diff --git a/web/components/instance/setup-form/sign-in-form.tsx b/web/components/instance/setup-form/sign-in-form.tsx index 5a7a22987..f34d807c9 100644 --- a/web/components/instance/setup-form/sign-in-form.tsx +++ b/web/components/instance/setup-form/sign-in-form.tsx @@ -88,7 +88,7 @@ export const InstanceSetupSignInForm: FC = (props) => { type="email" value={value} onChange={onChange} - placeholder="orville.wright@frstflt.com" + placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400" /> {value.length > 0 && ( diff --git a/web/components/page-views/signin.tsx b/web/components/page-views/signin.tsx index e4810ec2d..89bca5d62 100644 --- a/web/components/page-views/signin.tsx +++ b/web/components/page-views/signin.tsx @@ -7,7 +7,7 @@ import useSignInRedirection from "hooks/use-sign-in-redirection"; // components import { SignInRoot } from "components/account"; // ui -import { Loader, Spinner } from "@plane/ui"; +import { Spinner } from "@plane/ui"; // images import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; @@ -26,7 +26,7 @@ export const SignInView = observer(() => { handleRedirection(); }, [handleRedirection]); - if (isRedirecting || currentUser) + if (isRedirecting || currentUser || !envConfig) return (
@@ -35,32 +35,16 @@ export const SignInView = observer(() => { return (
-
+
Plane Logo Plane
-
+
- {!envConfig ? ( -
-
- - - - - - - - - -
-
- ) : ( - - )} +
diff --git a/web/pages/accounts/forgot-password.tsx b/web/pages/accounts/forgot-password.tsx new file mode 100644 index 000000000..8d3c4cd28 --- /dev/null +++ b/web/pages/accounts/forgot-password.tsx @@ -0,0 +1,138 @@ +import { ReactElement } from "react"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { Controller, useForm } from "react-hook-form"; +// services +import { AuthService } from "services/auth.service"; +// hooks +import useToast from "hooks/use-toast"; +import useTimer from "hooks/use-timer"; +// layouts +import DefaultLayout from "layouts/default-layout"; +// components +import { LatestFeatureBlock } from "components/common"; +// ui +import { Button, Input } from "@plane/ui"; +// images +import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; +// helpers +import { checkEmailValidity } from "helpers/string.helper"; +// type +import { NextPageWithLayout } from "lib/types"; + +type TForgotPasswordFormValues = { + email: string; +}; + +const defaultValues: TForgotPasswordFormValues = { + email: "", +}; + +// services +const authService = new AuthService(); + +const ForgotPasswordPage: NextPageWithLayout = () => { + // router + const router = useRouter(); + const { email } = router.query; + // toast + const { setToastAlert } = useToast(); + // timer + const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(0); + // form info + const { + control, + formState: { errors, isSubmitting, isValid }, + handleSubmit, + } = useForm({ + defaultValues: { + ...defaultValues, + email: email?.toString() ?? "", + }, + }); + + const handleForgotPassword = async (formData: TForgotPasswordFormValues) => { + await authService + .sendResetPasswordLink({ + email: formData.email, + }) + .then(() => { + setToastAlert({ + type: "success", + title: "Email sent", + message: + "Check your inbox for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder.", + }); + setResendCodeTimer(30); + }) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: err?.error ?? "Something went wrong. Please try again.", + }) + ); + }; + + return ( +
+
+
+ Plane Logo + Plane +
+
+ +
+
+
+

+ Get on your flight deck +

+

Get a link to reset your password

+
+ checkEmailValidity(value) || "Email is invalid", + }} + render={({ field: { value, onChange, ref } }) => ( + + )} + /> + + +
+ +
+
+
+ ); +}; + +ForgotPasswordPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default ForgotPasswordPage; diff --git a/web/pages/accounts/password.tsx b/web/pages/accounts/reset-password.tsx similarity index 58% rename from web/pages/accounts/password.tsx rename to web/pages/accounts/reset-password.tsx index 364c92711..2b893d665 100644 --- a/web/pages/accounts/password.tsx +++ b/web/pages/accounts/reset-password.tsx @@ -1,9 +1,6 @@ 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"; @@ -12,11 +9,12 @@ import useToast from "hooks/use-toast"; import useSignInRedirection from "hooks/use-sign-in-redirection"; // layouts import DefaultLayout from "layouts/default-layout"; +// components +import { LatestFeatureBlock } from "components/common"; // ui import { Button, Input } from "@plane/ui"; // images import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; -import latestFeatures from "public/onboarding/onboarding-pages.svg"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // type @@ -35,12 +33,10 @@ const defaultValues: TResetPasswordFormValues = { // services const authService = new AuthService(); -const HomePage: NextPageWithLayout = () => { +const ResetPasswordPage: NextPageWithLayout = () => { // router const router = useRouter(); const { uidb64, token, email } = router.query; - // next-themes - const { resolvedTheme } = useTheme(); // toast const { setToastAlert } = useToast(); // sign in redirection hook @@ -108,35 +104,30 @@ const HomePage: NextPageWithLayout = () => { onChange={onChange} ref={ref} hasError={Boolean(errors.email)} - placeholder="orville.wright@frstflt.com" + placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400" disabled /> )} /> -
- ( - - )} - /> -

- 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) { +ResetPasswordPage.getLayout = function getLayout(page: ReactElement) { return {page}; }; -export default HomePage; +export default ResetPasswordPage; diff --git a/web/pages/accounts/sign-up.tsx b/web/pages/accounts/sign-up.tsx index 390d91ec9..5b5648439 100644 --- a/web/pages/accounts/sign-up.tsx +++ b/web/pages/accounts/sign-up.tsx @@ -1,97 +1,52 @@ -import React, { useEffect, ReactElement } from "react"; +import React from "react"; import Image from "next/image"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// next-themes -import { useTheme } from "next-themes"; -// services -import { AuthService } from "services/auth.service"; // hooks -import { useUser } from "hooks/store"; -import useUserAuth from "hooks/use-user-auth"; -import useToast from "hooks/use-toast"; +import { useApplication, useUser } from "hooks/store"; // layouts import DefaultLayout from "layouts/default-layout"; // components -import { EmailSignUpForm } from "components/account"; -// images +import { SignUpRoot } from "components/account"; +// ui +import { Spinner } from "@plane/ui"; +// assets import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // types import { NextPageWithLayout } from "lib/types"; -type EmailPasswordFormValues = { - email: string; - password?: string; - medium?: string; -}; - -// services -const authService = new AuthService(); - const SignUpPage: NextPageWithLayout = observer(() => { - // router - const router = useRouter(); - // toast alert - const { setToastAlert } = useToast(); - // next-themes - const { setTheme } = useTheme(); // store hooks - const { currentUser, fetchCurrentUser, currentUserLoader } = useUser(); - // custom hooks - const {} = useUserAuth({ routeAuth: "sign-in", user: currentUser, isLoading: currentUserLoader }); + const { + config: { envConfig }, + } = useApplication(); + const { currentUser } = useUser(); - const handleSignUp = async (formData: EmailPasswordFormValues) => { - const payload = { - email: formData.email, - password: formData.password ?? "", - }; - - await authService - .emailSignUp(payload) - .then(async (response) => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Account created successfully.", - }); - - if (response) await fetchCurrentUser(); - router.push("/onboarding"); - }) - .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]); + if (currentUser || !envConfig) + return ( +
+ +
+ ); return ( - <> -
-
-
-
- Plane Logo -
+
+
+
+ Plane Logo + Plane
-
-
-

SignUp on Plane

- + +
+
+
- +
); }); -SignUpPage.getLayout = function getLayout(page: ReactElement) { +SignUpPage.getLayout = function getLayout(page: React.ReactElement) { return {page}; }; diff --git a/web/store/application/app-config.store.ts b/web/store/application/app-config.store.ts index f95177f78..6faef8b69 100644 --- a/web/store/application/app-config.store.ts +++ b/web/store/application/app-config.store.ts @@ -5,8 +5,9 @@ import { IAppConfig } from "@plane/types"; import { AppConfigService } from "services/app_config.service"; export interface IAppConfigStore { + // observables envConfig: IAppConfig | null; - // action + // actions fetchAppConfig: () => Promise; }