chore: handled signin workflow

This commit is contained in:
gurusainath 2024-04-30 19:30:42 +05:30
parent e8711dc4cc
commit 6c84b4f490
12 changed files with 275 additions and 399 deletions

View File

@ -240,13 +240,15 @@ export const InstanceSignUpForm: FC = (props) => {
</div> </div>
<div className="relative flex items-center pt-2 gap-2"> <div className="relative flex items-center pt-2 gap-2">
<Checkbox <div>
id="is_telemetry_enabled" <Checkbox
name="is_telemetry_enabled" id="is_telemetry_enabled"
value={formData.is_telemetry_enabled ? "True" : "False"} name="is_telemetry_enabled"
onChange={() => handleFormChange("is_telemetry_enabled", !formData.is_telemetry_enabled)} value={formData.is_telemetry_enabled ? "True" : "False"}
checked={formData.is_telemetry_enabled} onChange={() => handleFormChange("is_telemetry_enabled", !formData.is_telemetry_enabled)}
/> checked={formData.is_telemetry_enabled}
/>
</div>
<label className="text-sm text-custom-text-300 font-medium cursor-pointer" htmlFor="is_telemetry_enabled"> <label className="text-sm text-custom-text-300 font-medium cursor-pointer" htmlFor="is_telemetry_enabled">
Allow Plane to anonymously collect usage events. Allow Plane to anonymously collect usage events.
</label> </label>

View File

@ -2,7 +2,6 @@
import { FC, ReactNode } from "react"; import { FC, ReactNode } from "react";
import Image from "next/image"; import Image from "next/image";
import { usePathname } from "next/navigation";
// logo // logo
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
@ -12,9 +11,6 @@ type TDefaultLayout = {
export const DefaultLayout: FC<TDefaultLayout> = (props) => { export const DefaultLayout: FC<TDefaultLayout> = (props) => {
const { children } = props; const { children } = props;
const pathname = usePathname();
console.log("pathname", pathname);
return ( return (
<div className="relative h-screen max-h-max w-full overflow-hidden overflow-y-auto flex flex-col"> <div className="relative h-screen max-h-max w-full overflow-hidden overflow-y-auto flex flex-col">

View File

@ -13,8 +13,10 @@ export const AuthBanner: FC<TAuthBanner> = (props) => {
if (!bannerData) return <></>; if (!bannerData) return <></>;
return ( return (
<div className="relative inline-flex items-center p-3 rounded-md gap-3 border border-custom-primary-100/50 bg-custom-primary-100/10"> <div className="relative inline-flex items-center p-2 rounded-md gap-2 border border-custom-primary-100/50 bg-custom-primary-100/10">
<Info className="w-5 h-5 flex-shrink-0 text-custom-primary-100" /> <div className="w-4 h-4 flex-shrink-0 relative flex justify-center items-center">
<Info size={16} className="text-custom-primary-100" />
</div>
<div className="w-full text-sm font-medium text-custom-primary-100">{bannerData?.message}</div> <div className="w-full text-sm font-medium text-custom-primary-100">{bannerData?.message}</div>
<div <div
className="relative ml-auto w-6 h-6 rounded-sm flex justify-center items-center transition-all cursor-pointer hover:bg-custom-primary-100/20 text-custom-primary-100/80" className="relative ml-auto w-6 h-6 rounded-sm flex justify-center items-center transition-all cursor-pointer hover:bg-custom-primary-100/20 text-custom-primary-100/80"

View File

@ -6,10 +6,11 @@ import { Eye, EyeOff, XCircle } from "lucide-react";
// ui // ui
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
// components // components
import { EAuthModes, EAuthSteps, ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/account"; import { ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/account";
// constants // constants
import { FORGOT_PASSWORD } from "@/constants/event-tracker"; import { FORGOT_PASSWORD } from "@/constants/event-tracker";
// helpers // helpers
import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper";
import { API_BASE_URL } from "@/helpers/common.helper"; import { API_BASE_URL } from "@/helpers/common.helper";
import { getPasswordStrength } from "@/helpers/password.helper"; import { getPasswordStrength } from "@/helpers/password.helper";
// hooks // hooks
@ -94,51 +95,78 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
); );
return ( return (
<> <form
<form className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96"
className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96" method="POST"
method="POST" action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "sign-in" : "sign-up"}/`}
action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "sign-in" : "sign-up"}/`} >
> <input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} /> <div className="space-y-1">
<div className="space-y-1"> <label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email"> Email
Email </label>
</label> <div className="relative flex items-center rounded-md bg-onboarding-background-200">
<div className="relative flex items-center rounded-md bg-onboarding-background-200"> <Input
<Input id="email"
id="email" name="email"
name="email" type="email"
type="email" value={passwordFormData.email}
value={passwordFormData.email} onChange={(e) => handleFormChange("email", e.target.value)}
onChange={(e) => handleFormChange("email", e.target.value)} // hasError={Boolean(errors.email)}
// hasError={Boolean(errors.email)} placeholder="name@company.com"
placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400" />
{passwordFormData.email.length > 0 && (
<XCircle
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={handleEmailClear}
/> />
{passwordFormData.email.length > 0 && ( )}
<XCircle
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={handleEmailClear}
/>
)}
</div>
</div> </div>
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
{mode === EAuthModes.SIGN_IN ? "Password" : "Set a password"}
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
name="password"
value={passwordFormData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoFocus
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(false)}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)}
/>
)}
</div>
{passwordSupport}
</div>
{mode === EAuthModes.SIGN_UP && (
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password"> <label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
{mode === EAuthModes.SIGN_IN ? "Password" : "Set a password"} Confirm password
</label> </label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200"> <div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input <Input
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
name="password" name="confirm_password"
value={passwordFormData.password} value={passwordFormData.confirm_password}
onChange={(e) => handleFormChange("password", e.target.value)} onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder="Enter password" placeholder="Confirm password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoFocus
/> />
{showPassword ? ( {showPassword ? (
<EyeOff <EyeOff
@ -152,64 +180,35 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
/> />
)} )}
</div> </div>
{passwordSupport} {!!passwordFormData.confirm_password && passwordFormData.password !== passwordFormData.confirm_password && (
</div> <span className="text-sm text-red-500">Passwords don{"'"}t match</span>
{mode === EAuthModes.SIGN_UP && (
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
Confirm password
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
name="confirm_password"
value={passwordFormData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder="Confirm password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(false)}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)}
/>
)}
</div>
{!!passwordFormData.confirm_password && passwordFormData.password !== passwordFormData.confirm_password && (
<span className="text-sm text-red-500">Passwords don{"'"}t match</span>
)}
</div>
)}
<div className="space-y-2.5">
{mode === EAuthModes.SIGN_IN ? (
<>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
{isSmtpConfigured ? "Continue" : "Go to workspace"}
</Button>
{instance && isSmtpConfigured && (
<Button
type="button"
onClick={redirectToUniqueCodeLogin}
variant="outline-primary"
className="w-full"
size="lg"
>
Sign in with unique code
</Button>
)}
</>
) : (
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
Create account
</Button>
)} )}
</div> </div>
</form> )}
</> <div className="space-y-2.5">
{mode === EAuthModes.SIGN_IN ? (
<>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
{isSmtpConfigured ? "Continue" : "Go to workspace"}
</Button>
{instance && isSmtpConfigured && (
<Button
type="button"
onClick={redirectToUniqueCodeLogin}
variant="outline-primary"
className="w-full"
size="lg"
>
Sign in with unique code
</Button>
)}
</>
) : (
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
Create account
</Button>
)}
</div>
</form>
); );
}); });

View File

@ -1,175 +1,68 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import isEmpty from "lodash/isEmpty";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// types import { IEmailCheckData } from "@plane/types";
import { IEmailCheckData, IWorkspaceMemberInvitation } from "@plane/types";
// ui
import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; import { Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// components // components
import { import {
AuthHeader,
AuthBanner,
AuthEmailForm, AuthEmailForm,
AuthPasswordForm, AuthPasswordForm,
OAuthOptions, OAuthOptions,
TermsAndConditions, TermsAndConditions,
UniqueCodeForm, UniqueCodeForm,
} from "@/components/account"; } from "@/components/account";
import { WorkspaceLogo } from "@/components/workspace/logo"; // helpers
import {
EAuthModes,
EAuthSteps,
EAuthenticationErrorCodes,
EErrorAlertType,
TAuthErrorInfo,
authErrorHandler,
} from "@/helpers/authentication.helper";
// hooks
import { useInstance } from "@/hooks/store"; import { useInstance } from "@/hooks/store";
// services // services
import { AuthService } from "@/services/auth.service"; import { AuthService } from "@/services/auth.service";
import { WorkspaceService } from "@/services/workspace.service";
const authService = new AuthService(); const authService = new AuthService();
const workSpaceService = new WorkspaceService();
export enum EAuthModes { export const SignInAuthRoot = observer(() => {
SIGN_IN = "SIGN_IN",
SIGN_UP = "SIGN_UP",
}
export enum EAuthSteps {
EMAIL = "EMAIL",
PASSWORD = "PASSWORD",
UNIQUE_CODE = "UNIQUE_CODE",
}
type Props = {
mode: EAuthModes;
};
const Titles = {
[EAuthModes.SIGN_IN]: {
[EAuthSteps.EMAIL]: {
header: "Sign in to Plane",
subHeader: "Get back to your projects and make progress",
},
[EAuthSteps.PASSWORD]: {
header: "Sign in to Plane",
subHeader: "Get back to your projects and make progress",
},
[EAuthSteps.UNIQUE_CODE]: {
header: "Sign in to Plane",
subHeader: "Get back to your projects and make progress",
},
},
[EAuthModes.SIGN_UP]: {
[EAuthSteps.EMAIL]: {
header: "Create your account",
subHeader: "Start tracking your projects with Plane",
},
[EAuthSteps.PASSWORD]: {
header: "Create your account",
subHeader: "Progress, visualize, and measure work how it works best for you.",
},
[EAuthSteps.UNIQUE_CODE]: {
header: "Create your account",
subHeader: "Progress, visualize, and measure work how it works best for you.",
},
},
};
const getHeaderSubHeader = (
step: EAuthSteps,
mode: EAuthModes,
invitation?: IWorkspaceMemberInvitation | undefined,
email?: string
) => {
if (invitation && email && invitation.email === email && invitation.workspace) {
const workspace = invitation.workspace;
return {
header: (
<>
Join <WorkspaceLogo logo={workspace?.logo} name={workspace?.name} classNames="w-8 h-9" /> {workspace.name}
</>
),
subHeader: `${
mode == EAuthModes.SIGN_UP ? "Create an account" : "Sign in"
} to start managing work with your team.`,
};
}
return Titles[mode][step];
};
export const SignInAuthRoot = observer((props: Props) => {
const { mode } = props;
//router //router
const router = useRouter(); const router = useRouter();
const { email: emailParam, invitation_id, slug: workspaceSlug } = router.query; const { email: emailParam, invitation_id, slug: workspaceSlug, error_code, error_message } = router.query;
// states // states
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL); const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
const [email, setEmail] = useState(emailParam ? emailParam.toString() : ""); const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
const [invitation, setInvitation] = useState<IWorkspaceMemberInvitation | undefined>(undefined);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
// hooks // hooks
const { instance } = useInstance(); const { instance } = useInstance();
// derived values // derived values
const isSmtpConfigured = instance?.config?.is_smtp_configured; const authMode = EAuthModes.SIGN_IN;
const redirectToSignUp = (email: string) => {
if (isEmpty(email)) router.push({ pathname: "/", query: router.query });
else router.push({ pathname: "/", query: { ...router.query, email: email } });
};
const redirectToSignIn = (email: string) => {
if (isEmpty(email)) router.push({ pathname: "/accounts/sign-in", query: router.query });
else router.push({ pathname: "/accounts/sign-in", query: { ...router.query, email: email } });
};
useEffect(() => { useEffect(() => {
if (invitation_id && workspaceSlug) { if (error_code && error_message) {
setIsLoading(true); const errorhandler = authErrorHandler(
workSpaceService error_code?.toString() as EAuthenticationErrorCodes,
.getWorkspaceInvitation(workspaceSlug.toString(), invitation_id.toString()) error_message?.toString()
.then((res) => { );
setInvitation(res); if (errorhandler) setErrorInfo(errorhandler);
})
.catch(() => {
setInvitation(undefined);
})
.finally(() => setIsLoading(false));
} else {
setInvitation(undefined);
} }
}, [invitation_id, workspaceSlug]); }, [error_code, error_message]);
const { header, subHeader } = getHeaderSubHeader(authStep, mode, invitation, email);
// step 1 submit handler- email verification // step 1 submit handler- email verification
const handleEmailVerification = async (data: IEmailCheckData) => { const handleEmailVerification = async (data: IEmailCheckData) => {
setEmail(data.email); setEmail(data.email);
const emailCheck = mode === EAuthModes.SIGN_UP ? authService.signUpEmailCheck : authService.signInEmailCheck; await authService
.signInEmailCheck(data)
await emailCheck(data) .then(() => {
.then((res) => { setAuthStep(EAuthSteps.PASSWORD);
if (mode === EAuthModes.SIGN_IN && !res.is_password_autoset) {
setAuthStep(EAuthSteps.PASSWORD);
} else {
if (isSmtpConfigured) {
setAuthStep(EAuthSteps.UNIQUE_CODE);
} else {
if (mode === EAuthModes.SIGN_IN) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Unable to process request please contact Administrator to reset password",
});
} else {
setAuthStep(EAuthSteps.PASSWORD);
}
}
}
}) })
.catch((err) => { .catch((err) => {
if (err?.error_code === "USER_DOES_NOT_EXIST") {
redirectToSignUp(data.email);
return;
} else if (err?.error_code === "USER_ALREADY_EXIST") {
redirectToSignIn(data.email);
return;
}
setToast({ setToast({
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
title: "Error!", title: "Error!",
@ -190,13 +83,19 @@ export const SignInAuthRoot = observer((props: Props) => {
return ( return (
<> <>
<div className="mx-auto flex flex-col"> <div className="relative max-w-lg mx-auto flex flex-col space-y-6">
<div className="text-center space-y-1 py-4 mx-auto sm:w-96"> <AuthHeader
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">{header}</h3> workspaceSlug={workspaceSlug?.toString() || undefined}
<p className="font-medium text-onboarding-text-400">{subHeader}</p> invitationId={invitation_id?.toString() || undefined}
</div> invitationEmail={email || undefined}
authMode={EAuthModes.SIGN_IN}
currentAuthStep={authStep}
handleLoader={setIsLoading}
/>
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
)}
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />} {authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
{authStep === EAuthSteps.UNIQUE_CODE && ( {authStep === EAuthSteps.UNIQUE_CODE && (
<UniqueCodeForm <UniqueCodeForm
email={email} email={email}
@ -205,10 +104,9 @@ export const SignInAuthRoot = observer((props: Props) => {
setAuthStep(EAuthSteps.EMAIL); setAuthStep(EAuthSteps.EMAIL);
}} }}
submitButtonText="Continue" submitButtonText="Continue"
mode={mode} mode={authMode}
/> />
)} )}
{authStep === EAuthSteps.PASSWORD && ( {authStep === EAuthSteps.PASSWORD && (
<AuthPasswordForm <AuthPasswordForm
email={email} email={email}
@ -217,12 +115,12 @@ export const SignInAuthRoot = observer((props: Props) => {
setAuthStep(EAuthSteps.EMAIL); setAuthStep(EAuthSteps.EMAIL);
}} }}
handleStepChange={(step) => setAuthStep(step)} handleStepChange={(step) => setAuthStep(step)}
mode={mode} mode={authMode}
/> />
)} )}
{isOAuthEnabled && <OAuthOptions />}
<TermsAndConditions isSignUp={false} />
</div> </div>
{isOAuthEnabled && <OAuthOptions />}
<TermsAndConditions isSignUp={mode === EAuthModes.SIGN_UP} />
</> </>
); );
}); });

View File

@ -1,5 +1,4 @@
import { FC, useState } from "react"; import { FC, useEffect, useState } from "react";
import isEmpty from "lodash/isEmpty";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// types // types
@ -17,7 +16,14 @@ import {
UniqueCodeForm, UniqueCodeForm,
} from "@/components/account"; } from "@/components/account";
// helpers // helpers
import { EAuthModes, EAuthSteps, EErrorAlertType, TAuthErrorInfo } from "@/helpers/authentication.helper"; import {
EAuthModes,
EAuthSteps,
EAuthenticationErrorCodes,
EErrorAlertType,
TAuthErrorInfo,
authErrorHandler,
} from "@/helpers/authentication.helper";
// hooks // hooks
import { useInstance } from "@/hooks/store"; import { useInstance } from "@/hooks/store";
// services // services
@ -29,7 +35,7 @@ const authService = new AuthService();
export const SignUpAuthRoot: FC = observer(() => { export const SignUpAuthRoot: FC = observer(() => {
//router //router
const router = useRouter(); const router = useRouter();
const { email: emailParam, invitation_id, slug: workspaceSlug } = router.query; const { email: emailParam, invitation_id, slug: workspaceSlug, error_code, error_message } = router.query;
// states // states
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL); const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
const [email, setEmail] = useState(emailParam ? emailParam.toString() : ""); const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
@ -41,12 +47,17 @@ export const SignUpAuthRoot: FC = observer(() => {
const authMode = EAuthModes.SIGN_UP; const authMode = EAuthModes.SIGN_UP;
const isSmtpConfigured = instance?.config?.is_smtp_configured; const isSmtpConfigured = instance?.config?.is_smtp_configured;
// const redirectToSignUp = (email: string) => { useEffect(() => {
// if (isEmpty(email)) router.push({ pathname: "/", query: router.query }); if (error_code && error_message) {
// else router.push({ pathname: "/", query: { ...router.query, email: email } }); const errorhandler = authErrorHandler(
// }; error_code?.toString() as EAuthenticationErrorCodes,
error_message?.toString()
);
if (errorhandler) setErrorInfo(errorhandler);
}
}, [error_code, error_message]);
// step 1 - email verification // email verification
const handleEmailVerification = async (data: IEmailCheckData) => { const handleEmailVerification = async (data: IEmailCheckData) => {
setEmail(data.email); setEmail(data.email);
await authService await authService
@ -55,12 +66,16 @@ export const SignUpAuthRoot: FC = observer(() => {
if (isSmtpConfigured) setAuthStep(EAuthSteps.UNIQUE_CODE); if (isSmtpConfigured) setAuthStep(EAuthSteps.UNIQUE_CODE);
else setAuthStep(EAuthSteps.PASSWORD); else setAuthStep(EAuthSteps.PASSWORD);
}) })
.catch((err) => { .catch((error) => {
console.log("error", err); const errorhandler = authErrorHandler(error?.error_code, error?.error_message);
if (errorhandler) {
setErrorInfo(errorhandler);
return;
}
setToast({ setToast({
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
title: "Error!", title: "Error!",
message: err?.error_message ?? "Something went wrong. Please try again.", message: error?.error_message ?? "Something went wrong. Please try again.",
}); });
}); });
}; };
@ -76,50 +91,43 @@ export const SignUpAuthRoot: FC = observer(() => {
); );
return ( return (
<> <div className="relative max-w-lg mx-auto flex flex-col space-y-6">
<div className="relative max-w-lg mx-auto flex flex-col space-y-6"> <AuthHeader
<AuthHeader workspaceSlug={workspaceSlug?.toString() || undefined}
workspaceSlug={workspaceSlug?.toString() || undefined} invitationId={invitation_id?.toString() || undefined}
invitationId={invitation_id?.toString() || undefined} invitationEmail={email || undefined}
invitationEmail={emailParam?.toString() || undefined} authMode={EAuthModes.SIGN_UP}
authMode={EAuthModes.SIGN_UP} currentAuthStep={authStep}
currentAuthStep={authStep} handleLoader={setIsLoading}
handleLoader={setIsLoading} />
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
)}
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
{authStep === EAuthSteps.UNIQUE_CODE && (
<UniqueCodeForm
email={email}
handleEmailClear={() => {
setEmail("");
setAuthStep(EAuthSteps.EMAIL);
}}
submitButtonText="Continue"
mode={authMode}
/> />
)}
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && ( {authStep === EAuthSteps.PASSWORD && (
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} /> <AuthPasswordForm
)} email={email}
handleEmailClear={() => {
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />} setEmail("");
setAuthStep(EAuthSteps.EMAIL);
{authStep === EAuthSteps.UNIQUE_CODE && ( }}
<UniqueCodeForm handleStepChange={(step) => setAuthStep(step)}
email={email} mode={authMode}
handleEmailClear={() => { />
setEmail(""); )}
setAuthStep(EAuthSteps.EMAIL); {isOAuthEnabled && <OAuthOptions />}
}} <TermsAndConditions isSignUp={authMode === EAuthModes.SIGN_UP} />
submitButtonText="Continue" </div>
mode={authMode}
/>
)}
{authStep === EAuthSteps.PASSWORD && (
<AuthPasswordForm
email={email}
handleEmailClear={() => {
setEmail("");
setAuthStep(EAuthSteps.EMAIL);
}}
handleStepChange={(step) => setAuthStep(step)}
mode={authMode}
/>
)}
{isOAuthEnabled && <OAuthOptions />}
<TermsAndConditions isSignUp={authMode === EAuthModes.SIGN_UP} />
</div>
</>
); );
}); });

View File

@ -1,17 +1,14 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { CircleCheck, XCircle } from "lucide-react"; import { CircleCheck, XCircle } from "lucide-react";
// types
import { IEmailCheckData } from "@plane/types"; import { IEmailCheckData } from "@plane/types";
// ui
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
// constants
// helpers // helpers
import { EAuthModes } from "@/helpers/authentication.helper";
import { API_BASE_URL } from "@/helpers/common.helper"; import { API_BASE_URL } from "@/helpers/common.helper";
// hooks // hooks
import useTimer from "@/hooks/use-timer"; import useTimer from "@/hooks/use-timer";
// services // services
import { AuthService } from "@/services/auth.service"; import { AuthService } from "@/services/auth.service";
import { EAuthModes } from "./sign-up-root";
type Props = { type Props = {
email: string; email: string;

View File

@ -1,45 +1,43 @@
import { FC } from "react"; import { FC } from "react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// icons import { Button } from "@plane/ui";
import { UserCog2 } from "lucide-react";
// ui
import { getButtonStyling } from "@plane/ui";
// images // images
import instanceNotReady from "public/instance/plane-instance-not-ready.webp"; import PlaneBlackLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.svg";
import PlaneBlackLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; import PlaneWhiteLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.svg";
import PlaneWhiteLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; import PlaneTakeOffImage from "@/public/plane-takeoff.png";
type TInstanceNotReady = {
isGodModeEnabled: boolean;
handleGodModeStateChange?: (state: boolean) => void;
};
export const InstanceNotReady: FC<TInstanceNotReady> = () => {
// const { isGodModeEnabled, handleGodModeStateChange } = props;
export const InstanceNotReady: FC = () => {
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const planeLogo = resolvedTheme === "dark" ? PlaneWhiteLogo : PlaneBlackLogo; const planeLogo = resolvedTheme === "dark" ? PlaneWhiteLogo : PlaneBlackLogo;
return ( return (
<div className="h-screen w-full overflow-y-auto bg-onboarding-gradient-100"> <div className="relative h-screen max-h-max w-full overflow-hidden overflow-y-auto flex flex-col">
<div className="h-full w-full pt-24"> <div className="flex-shrink-0 h-[120px]">
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-100 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3"> <div className="relative h-full container mx-auto px-5 lg:px-0 flex items-center justify-between gap-5 z-50">
<div className="relative h-full rounded-t-md bg-onboarding-gradient-200 px-7 sm:px-0"> <div className="flex items-center gap-x-2">
<div className="flex items-center justify-center py-10"> <Image src={planeLogo} className="h-[24px] w-full" alt="Plane logo" />
<Image src={planeLogo} className="h-[44px] w-full" alt="Plane logo" /> </div>
</div>
</div>
<div className="w-full flex-grow">
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center">
<div className="w-auto max-w-2xl relative space-y-8 py-10">
<div className="relative flex flex-col justify-center items-center space-y-4">
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
<Image src={PlaneTakeOffImage} alt="Plane Logo" />
<p className="font-medium text-base text-custom-text-400">
Get started by setting up your instance and workspace
</p>
</div> </div>
<div className="mt-20"> <div>
<Image src={instanceNotReady} className="w-full" alt="Instance not ready" /> <Link href={"/god-mode/"}>
</div> <Button size="lg" className="w-full">
<div className="flex w-full flex-col items-center gap-5 py-12 pb-20"> Get started
<h3 className="text-2xl font-medium">Your Plane instance isn{"'"}t ready yet</h3> </Button>
<p className="text-sm">Ask your Instance Admin to complete set-up first.</p> </Link>
<a href="/god-mode" className={`${getButtonStyling("primary", "md")} mt-4`}>
<UserCog2 className="h-3.5 w-3.5" />
Get started
</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -46,7 +46,10 @@ export enum EErrorAlertType {
export type TAuthErrorInfo = { type: EErrorAlertType; message: string }; export type TAuthErrorInfo = { type: EErrorAlertType; message: string };
export const errorHandler = (errorType: EAuthenticationErrorCodes, errorMessage: string | undefined) => { export const authErrorHandler = (
errorCode: EAuthenticationErrorCodes,
errorMessage: string | undefined
): TAuthErrorInfo | undefined => {
const toastAlertErrorCodes = [ const toastAlertErrorCodes = [
EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED, EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED,
EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED, EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED,
@ -66,43 +69,41 @@ export const errorHandler = (errorType: EAuthenticationErrorCodes, errorMessage:
const inlineEmailCodeErrorCodes = [EAuthenticationErrorCodes.INLINE_EMAIL_CODE]; const inlineEmailCodeErrorCodes = [EAuthenticationErrorCodes.INLINE_EMAIL_CODE];
const inlinePasswordErrorCodes = [EAuthenticationErrorCodes.INLINE_PASSWORD]; const inlinePasswordErrorCodes = [EAuthenticationErrorCodes.INLINE_PASSWORD];
let errorPayload: TAuthErrorInfo | undefined = undefined; if (toastAlertErrorCodes.includes(errorCode))
return {
if (toastAlertErrorCodes.includes(errorType))
errorPayload = {
type: EErrorAlertType.TOAST_ALERT, type: EErrorAlertType.TOAST_ALERT,
message: errorMessage || "Something went wrong", message: errorMessage || "Something went wrong. Please try again.",
}; };
if (bannerAlertErrorCodes.includes(errorType)) if (bannerAlertErrorCodes.includes(errorCode))
errorPayload = { return {
type: EErrorAlertType.BANNER_ALERT, type: EErrorAlertType.BANNER_ALERT,
message: errorMessage || "Something went wrong", message: errorMessage || "Something went wrong. Please try again.",
}; };
if (inlineFirstNameErrorCodes.includes(errorType)) if (inlineFirstNameErrorCodes.includes(errorCode))
errorPayload = { return {
type: EErrorAlertType.INLINE_FIRST_NAME, type: EErrorAlertType.INLINE_FIRST_NAME,
message: errorMessage || "Something went wrong", message: errorMessage || "Something went wrong. Please try again.",
}; };
if (inlineEmailErrorCodes.includes(errorType)) if (inlineEmailErrorCodes.includes(errorCode))
errorPayload = { return {
type: EErrorAlertType.INLINE_EMAIL, type: EErrorAlertType.INLINE_EMAIL,
message: errorMessage || "Something went wrong", message: errorMessage || "Something went wrong. Please try again.",
}; };
if (inlinePasswordErrorCodes.includes(errorType)) if (inlinePasswordErrorCodes.includes(errorCode))
errorPayload = { return {
type: EErrorAlertType.INLINE_PASSWORD, type: EErrorAlertType.INLINE_PASSWORD,
message: errorMessage || "Something went wrong", message: errorMessage || "Something went wrong. Please try again.",
}; };
if (inlineEmailCodeErrorCodes.includes(errorType)) if (inlineEmailCodeErrorCodes.includes(errorCode))
errorPayload = { return {
type: EErrorAlertType.INLINE_EMAIL_CODE, type: EErrorAlertType.INLINE_EMAIL_CODE,
message: errorMessage || "Something went wrong", message: errorMessage || "Something went wrong. Please try again.",
}; };
return errorPayload; return undefined;
}; };

View File

@ -37,11 +37,8 @@ const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
</div> </div>
); );
// checking if the instance is activated or not
if (error && !error?.data?.is_activated) return <InstanceNotReady isGodModeEnabled={false} />;
// instance is not ready and setup is not done // instance is not ready and setup is not done
if (instance?.instance?.is_setup_done === false) return <InstanceNotReady isGodModeEnabled />; if (instance?.instance?.is_setup_done === false) return <InstanceNotReady />;
return <>{children}</>; return <>{children}</>;
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -16,7 +16,6 @@ type TError = {
export interface IInstanceStore { export interface IInstanceStore {
// issues // issues
isLoading: boolean; isLoading: boolean;
instanceNotReady: any | undefined;
instance: IInstance | undefined; instance: IInstance | undefined;
error: TError | undefined; error: TError | undefined;
// action // action
@ -25,7 +24,6 @@ export interface IInstanceStore {
export class InstanceStore implements IInstanceStore { export class InstanceStore implements IInstanceStore {
isLoading: boolean = true; isLoading: boolean = true;
instanceNotReady: any | undefined = undefined;
instance: IInstance | undefined = undefined; instance: IInstance | undefined = undefined;
error: TError | undefined = undefined; error: TError | undefined = undefined;
// services // services
@ -49,33 +47,13 @@ export class InstanceStore implements IInstanceStore {
*/ */
fetchInstanceInfo = async () => { fetchInstanceInfo = async () => {
try { try {
runInAction(() => { this.isLoading = true;
this.isLoading = true; this.error = undefined;
this.error = undefined;
});
const instance = await this.instanceService.getInstanceInfo(); const instance = await this.instanceService.getInstanceInfo();
runInAction(() => {
const isInstanceNotSetup = (instance: IInstance) => "is_activated" in instance && "is_setup_done" in instance; this.isLoading = false;
this.instance = instance;
if (isInstanceNotSetup(instance)) { });
runInAction(() => {
this.isLoading = false;
this.error = {
status: "success",
message: "Instance is not created in the backend",
data: {
is_activated: instance?.instance?.is_activated,
is_setup_done: instance?.instance?.is_setup_done,
},
};
});
} else {
runInAction(() => {
this.isLoading = false;
this.instance = instance;
});
}
} catch (error) { } catch (error) {
runInAction(() => { runInAction(() => {
this.isLoading = false; this.isLoading = false;