mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: handled signin workflow
This commit is contained in:
parent
e8711dc4cc
commit
6c84b4f490
@ -240,13 +240,15 @@ export const InstanceSignUpForm: FC = (props) => {
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-center pt-2 gap-2">
|
||||
<Checkbox
|
||||
id="is_telemetry_enabled"
|
||||
name="is_telemetry_enabled"
|
||||
value={formData.is_telemetry_enabled ? "True" : "False"}
|
||||
onChange={() => handleFormChange("is_telemetry_enabled", !formData.is_telemetry_enabled)}
|
||||
checked={formData.is_telemetry_enabled}
|
||||
/>
|
||||
<div>
|
||||
<Checkbox
|
||||
id="is_telemetry_enabled"
|
||||
name="is_telemetry_enabled"
|
||||
value={formData.is_telemetry_enabled ? "True" : "False"}
|
||||
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">
|
||||
Allow Plane to anonymously collect usage events.
|
||||
</label>
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
import { FC, ReactNode } from "react";
|
||||
import Image from "next/image";
|
||||
import { usePathname } from "next/navigation";
|
||||
// logo
|
||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||
|
||||
@ -12,9 +11,6 @@ type TDefaultLayout = {
|
||||
|
||||
export const DefaultLayout: FC<TDefaultLayout> = (props) => {
|
||||
const { children } = props;
|
||||
const pathname = usePathname();
|
||||
|
||||
console.log("pathname", pathname);
|
||||
|
||||
return (
|
||||
<div className="relative h-screen max-h-max w-full overflow-hidden overflow-y-auto flex flex-col">
|
||||
|
@ -13,8 +13,10 @@ export const AuthBanner: FC<TAuthBanner> = (props) => {
|
||||
|
||||
if (!bannerData) 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">
|
||||
<Info className="w-5 h-5 flex-shrink-0 text-custom-primary-100" />
|
||||
<div className="relative inline-flex items-center p-2 rounded-md gap-2 border border-custom-primary-100/50 bg-custom-primary-100/10">
|
||||
<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="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"
|
||||
|
@ -6,10 +6,11 @@ import { Eye, EyeOff, XCircle } from "lucide-react";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// components
|
||||
import { EAuthModes, EAuthSteps, ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/account";
|
||||
import { ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/account";
|
||||
// constants
|
||||
import { FORGOT_PASSWORD } from "@/constants/event-tracker";
|
||||
// helpers
|
||||
import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { getPasswordStrength } from "@/helpers/password.helper";
|
||||
// hooks
|
||||
@ -94,51 +95,78 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96"
|
||||
method="POST"
|
||||
action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "sign-in" : "sign-up"}/`}
|
||||
>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={passwordFormData.email}
|
||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||
// hasError={Boolean(errors.email)}
|
||||
placeholder="name@company.com"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
|
||||
<form
|
||||
className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96"
|
||||
method="POST"
|
||||
action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "sign-in" : "sign-up"}/`}
|
||||
>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={passwordFormData.email}
|
||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||
// hasError={Boolean(errors.email)}
|
||||
placeholder="name@company.com"
|
||||
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 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">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
|
||||
{mode === EAuthModes.SIGN_IN ? "Password" : "Set a password"}
|
||||
<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="password"
|
||||
value={passwordFormData.password}
|
||||
onChange={(e) => handleFormChange("password", e.target.value)}
|
||||
placeholder="Enter 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"
|
||||
onFocus={() => setIsPasswordInputFocused(true)}
|
||||
onBlur={() => setIsPasswordInputFocused(false)}
|
||||
autoFocus
|
||||
/>
|
||||
{showPassword ? (
|
||||
<EyeOff
|
||||
@ -152,64 +180,35 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{passwordSupport}
|
||||
</div>
|
||||
{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>
|
||||
{!!passwordFormData.confirm_password && passwordFormData.password !== passwordFormData.confirm_password && (
|
||||
<span className="text-sm text-red-500">Passwords don{"'"}t match</span>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
});
|
||||
|
@ -1,175 +1,68 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
// types
|
||||
import { IEmailCheckData, IWorkspaceMemberInvitation } from "@plane/types";
|
||||
// ui
|
||||
import { IEmailCheckData } from "@plane/types";
|
||||
import { Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
AuthHeader,
|
||||
AuthBanner,
|
||||
AuthEmailForm,
|
||||
AuthPasswordForm,
|
||||
OAuthOptions,
|
||||
TermsAndConditions,
|
||||
UniqueCodeForm,
|
||||
} 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";
|
||||
// services
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
|
||||
const authService = new AuthService();
|
||||
const workSpaceService = new WorkspaceService();
|
||||
|
||||
export enum EAuthModes {
|
||||
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;
|
||||
export const SignInAuthRoot = observer(() => {
|
||||
//router
|
||||
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
|
||||
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
|
||||
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
|
||||
const [invitation, setInvitation] = useState<IWorkspaceMemberInvitation | undefined>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
|
||||
// hooks
|
||||
const { instance } = useInstance();
|
||||
// derived values
|
||||
const isSmtpConfigured = instance?.config?.is_smtp_configured;
|
||||
|
||||
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 } });
|
||||
};
|
||||
const authMode = EAuthModes.SIGN_IN;
|
||||
|
||||
useEffect(() => {
|
||||
if (invitation_id && workspaceSlug) {
|
||||
setIsLoading(true);
|
||||
workSpaceService
|
||||
.getWorkspaceInvitation(workspaceSlug.toString(), invitation_id.toString())
|
||||
.then((res) => {
|
||||
setInvitation(res);
|
||||
})
|
||||
.catch(() => {
|
||||
setInvitation(undefined);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
} else {
|
||||
setInvitation(undefined);
|
||||
if (error_code && error_message) {
|
||||
const errorhandler = authErrorHandler(
|
||||
error_code?.toString() as EAuthenticationErrorCodes,
|
||||
error_message?.toString()
|
||||
);
|
||||
if (errorhandler) setErrorInfo(errorhandler);
|
||||
}
|
||||
}, [invitation_id, workspaceSlug]);
|
||||
|
||||
const { header, subHeader } = getHeaderSubHeader(authStep, mode, invitation, email);
|
||||
}, [error_code, error_message]);
|
||||
|
||||
// step 1 submit handler- email verification
|
||||
const handleEmailVerification = async (data: IEmailCheckData) => {
|
||||
setEmail(data.email);
|
||||
|
||||
const emailCheck = mode === EAuthModes.SIGN_UP ? authService.signUpEmailCheck : authService.signInEmailCheck;
|
||||
|
||||
await emailCheck(data)
|
||||
.then((res) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
await authService
|
||||
.signInEmailCheck(data)
|
||||
.then(() => {
|
||||
setAuthStep(EAuthSteps.PASSWORD);
|
||||
})
|
||||
.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({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
@ -190,13 +83,19 @@ export const SignInAuthRoot = observer((props: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto flex flex-col">
|
||||
<div className="text-center space-y-1 py-4 mx-auto sm:w-96">
|
||||
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">{header}</h3>
|
||||
<p className="font-medium text-onboarding-text-400">{subHeader}</p>
|
||||
</div>
|
||||
<div className="relative max-w-lg mx-auto flex flex-col space-y-6">
|
||||
<AuthHeader
|
||||
workspaceSlug={workspaceSlug?.toString() || undefined}
|
||||
invitationId={invitation_id?.toString() || undefined}
|
||||
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.UNIQUE_CODE && (
|
||||
<UniqueCodeForm
|
||||
email={email}
|
||||
@ -205,10 +104,9 @@ export const SignInAuthRoot = observer((props: Props) => {
|
||||
setAuthStep(EAuthSteps.EMAIL);
|
||||
}}
|
||||
submitButtonText="Continue"
|
||||
mode={mode}
|
||||
mode={authMode}
|
||||
/>
|
||||
)}
|
||||
|
||||
{authStep === EAuthSteps.PASSWORD && (
|
||||
<AuthPasswordForm
|
||||
email={email}
|
||||
@ -217,12 +115,12 @@ export const SignInAuthRoot = observer((props: Props) => {
|
||||
setAuthStep(EAuthSteps.EMAIL);
|
||||
}}
|
||||
handleStepChange={(step) => setAuthStep(step)}
|
||||
mode={mode}
|
||||
mode={authMode}
|
||||
/>
|
||||
)}
|
||||
{isOAuthEnabled && <OAuthOptions />}
|
||||
<TermsAndConditions isSignUp={false} />
|
||||
</div>
|
||||
{isOAuthEnabled && <OAuthOptions />}
|
||||
<TermsAndConditions isSignUp={mode === EAuthModes.SIGN_UP} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { FC, useState } from "react";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
// types
|
||||
@ -17,7 +16,14 @@ import {
|
||||
UniqueCodeForm,
|
||||
} from "@/components/account";
|
||||
// helpers
|
||||
import { EAuthModes, EAuthSteps, EErrorAlertType, TAuthErrorInfo } from "@/helpers/authentication.helper";
|
||||
import {
|
||||
EAuthModes,
|
||||
EAuthSteps,
|
||||
EAuthenticationErrorCodes,
|
||||
EErrorAlertType,
|
||||
TAuthErrorInfo,
|
||||
authErrorHandler,
|
||||
} from "@/helpers/authentication.helper";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// services
|
||||
@ -29,7 +35,7 @@ const authService = new AuthService();
|
||||
export const SignUpAuthRoot: FC = observer(() => {
|
||||
//router
|
||||
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
|
||||
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
|
||||
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
|
||||
@ -41,12 +47,17 @@ export const SignUpAuthRoot: FC = observer(() => {
|
||||
const authMode = EAuthModes.SIGN_UP;
|
||||
const isSmtpConfigured = instance?.config?.is_smtp_configured;
|
||||
|
||||
// const redirectToSignUp = (email: string) => {
|
||||
// if (isEmpty(email)) router.push({ pathname: "/", query: router.query });
|
||||
// else router.push({ pathname: "/", query: { ...router.query, email: email } });
|
||||
// };
|
||||
useEffect(() => {
|
||||
if (error_code && error_message) {
|
||||
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) => {
|
||||
setEmail(data.email);
|
||||
await authService
|
||||
@ -55,12 +66,16 @@ export const SignUpAuthRoot: FC = observer(() => {
|
||||
if (isSmtpConfigured) setAuthStep(EAuthSteps.UNIQUE_CODE);
|
||||
else setAuthStep(EAuthSteps.PASSWORD);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("error", err);
|
||||
.catch((error) => {
|
||||
const errorhandler = authErrorHandler(error?.error_code, error?.error_message);
|
||||
if (errorhandler) {
|
||||
setErrorInfo(errorhandler);
|
||||
return;
|
||||
}
|
||||
setToast({
|
||||
type: TOAST_TYPE.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 (
|
||||
<>
|
||||
<div className="relative max-w-lg mx-auto flex flex-col space-y-6">
|
||||
<AuthHeader
|
||||
workspaceSlug={workspaceSlug?.toString() || undefined}
|
||||
invitationId={invitation_id?.toString() || undefined}
|
||||
invitationEmail={emailParam?.toString() || undefined}
|
||||
authMode={EAuthModes.SIGN_UP}
|
||||
currentAuthStep={authStep}
|
||||
handleLoader={setIsLoading}
|
||||
<div className="relative max-w-lg mx-auto flex flex-col space-y-6">
|
||||
<AuthHeader
|
||||
workspaceSlug={workspaceSlug?.toString() || undefined}
|
||||
invitationId={invitation_id?.toString() || undefined}
|
||||
invitationEmail={email || undefined}
|
||||
authMode={EAuthModes.SIGN_UP}
|
||||
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.UNIQUE_CODE && (
|
||||
<UniqueCodeForm
|
||||
email={email}
|
||||
handleEmailClear={() => {
|
||||
setEmail("");
|
||||
setAuthStep(EAuthSteps.EMAIL);
|
||||
}}
|
||||
submitButtonText="Continue"
|
||||
mode={authMode}
|
||||
/>
|
||||
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
{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>
|
||||
);
|
||||
});
|
||||
|
@ -1,17 +1,14 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { CircleCheck, XCircle } from "lucide-react";
|
||||
// types
|
||||
import { IEmailCheckData } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
// helpers
|
||||
import { EAuthModes } from "@/helpers/authentication.helper";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import useTimer from "@/hooks/use-timer";
|
||||
// services
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
import { EAuthModes } from "./sign-up-root";
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
|
@ -1,45 +1,43 @@
|
||||
import { FC } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useTheme } from "next-themes";
|
||||
// icons
|
||||
import { UserCog2 } from "lucide-react";
|
||||
// ui
|
||||
import { getButtonStyling } from "@plane/ui";
|
||||
import { Button } from "@plane/ui";
|
||||
// images
|
||||
import instanceNotReady from "public/instance/plane-instance-not-ready.webp";
|
||||
import PlaneBlackLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
|
||||
import PlaneWhiteLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
|
||||
|
||||
type TInstanceNotReady = {
|
||||
isGodModeEnabled: boolean;
|
||||
handleGodModeStateChange?: (state: boolean) => void;
|
||||
};
|
||||
|
||||
export const InstanceNotReady: FC<TInstanceNotReady> = () => {
|
||||
// const { isGodModeEnabled, handleGodModeStateChange } = props;
|
||||
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 PlaneTakeOffImage from "@/public/plane-takeoff.png";
|
||||
|
||||
export const InstanceNotReady: FC = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const planeLogo = resolvedTheme === "dark" ? PlaneWhiteLogo : PlaneBlackLogo;
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full overflow-y-auto bg-onboarding-gradient-100">
|
||||
<div className="h-full w-full pt-24">
|
||||
<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 rounded-t-md bg-onboarding-gradient-200 px-7 sm:px-0">
|
||||
<div className="flex items-center justify-center py-10">
|
||||
<Image src={planeLogo} className="h-[44px] w-full" alt="Plane logo" />
|
||||
<div className="relative h-screen max-h-max w-full overflow-hidden overflow-y-auto flex flex-col">
|
||||
<div className="flex-shrink-0 h-[120px]">
|
||||
<div className="relative h-full container mx-auto px-5 lg:px-0 flex items-center justify-between gap-5 z-50">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Image src={planeLogo} className="h-[24px] 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 className="mt-20">
|
||||
<Image src={instanceNotReady} className="w-full" alt="Instance not ready" />
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-center gap-5 py-12 pb-20">
|
||||
<h3 className="text-2xl font-medium">Your Plane instance isn{"'"}t ready yet</h3>
|
||||
<p className="text-sm">Ask your Instance Admin to complete set-up first.</p>
|
||||
<a href="/god-mode" className={`${getButtonStyling("primary", "md")} mt-4`}>
|
||||
<UserCog2 className="h-3.5 w-3.5" />
|
||||
Get started
|
||||
</a>
|
||||
<div>
|
||||
<Link href={"/god-mode/"}>
|
||||
<Button size="lg" className="w-full">
|
||||
Get started
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -46,7 +46,10 @@ export enum EErrorAlertType {
|
||||
|
||||
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 = [
|
||||
EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED,
|
||||
@ -66,43 +69,41 @@ export const errorHandler = (errorType: EAuthenticationErrorCodes, errorMessage:
|
||||
const inlineEmailCodeErrorCodes = [EAuthenticationErrorCodes.INLINE_EMAIL_CODE];
|
||||
const inlinePasswordErrorCodes = [EAuthenticationErrorCodes.INLINE_PASSWORD];
|
||||
|
||||
let errorPayload: TAuthErrorInfo | undefined = undefined;
|
||||
|
||||
if (toastAlertErrorCodes.includes(errorType))
|
||||
errorPayload = {
|
||||
if (toastAlertErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.TOAST_ALERT,
|
||||
message: errorMessage || "Something went wrong",
|
||||
message: errorMessage || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
if (bannerAlertErrorCodes.includes(errorType))
|
||||
errorPayload = {
|
||||
if (bannerAlertErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.BANNER_ALERT,
|
||||
message: errorMessage || "Something went wrong",
|
||||
message: errorMessage || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
if (inlineFirstNameErrorCodes.includes(errorType))
|
||||
errorPayload = {
|
||||
if (inlineFirstNameErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.INLINE_FIRST_NAME,
|
||||
message: errorMessage || "Something went wrong",
|
||||
message: errorMessage || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
if (inlineEmailErrorCodes.includes(errorType))
|
||||
errorPayload = {
|
||||
if (inlineEmailErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.INLINE_EMAIL,
|
||||
message: errorMessage || "Something went wrong",
|
||||
message: errorMessage || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
if (inlinePasswordErrorCodes.includes(errorType))
|
||||
errorPayload = {
|
||||
if (inlinePasswordErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.INLINE_PASSWORD,
|
||||
message: errorMessage || "Something went wrong",
|
||||
message: errorMessage || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
if (inlineEmailCodeErrorCodes.includes(errorType))
|
||||
errorPayload = {
|
||||
if (inlineEmailCodeErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.INLINE_EMAIL_CODE,
|
||||
message: errorMessage || "Something went wrong",
|
||||
message: errorMessage || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
return errorPayload;
|
||||
return undefined;
|
||||
};
|
||||
|
@ -37,11 +37,8 @@ const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
|
||||
</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
|
||||
if (instance?.instance?.is_setup_done === false) return <InstanceNotReady isGodModeEnabled />;
|
||||
if (instance?.instance?.is_setup_done === false) return <InstanceNotReady />;
|
||||
|
||||
return <>{children}</>;
|
||||
});
|
||||
|
BIN
web/public/plane-takeoff.png
Normal file
BIN
web/public/plane-takeoff.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
@ -16,7 +16,6 @@ type TError = {
|
||||
export interface IInstanceStore {
|
||||
// issues
|
||||
isLoading: boolean;
|
||||
instanceNotReady: any | undefined;
|
||||
instance: IInstance | undefined;
|
||||
error: TError | undefined;
|
||||
// action
|
||||
@ -25,7 +24,6 @@ export interface IInstanceStore {
|
||||
|
||||
export class InstanceStore implements IInstanceStore {
|
||||
isLoading: boolean = true;
|
||||
instanceNotReady: any | undefined = undefined;
|
||||
instance: IInstance | undefined = undefined;
|
||||
error: TError | undefined = undefined;
|
||||
// services
|
||||
@ -49,33 +47,13 @@ export class InstanceStore implements IInstanceStore {
|
||||
*/
|
||||
fetchInstanceInfo = async () => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.isLoading = true;
|
||||
this.error = undefined;
|
||||
});
|
||||
|
||||
this.isLoading = true;
|
||||
this.error = undefined;
|
||||
const instance = await this.instanceService.getInstanceInfo();
|
||||
|
||||
const isInstanceNotSetup = (instance: IInstance) => "is_activated" in instance && "is_setup_done" in 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;
|
||||
});
|
||||
}
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
this.instance = instance;
|
||||
});
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
|
Loading…
Reference in New Issue
Block a user