feat: session auth implementation (#4411)

* feat: session authentication and god-mode implementation (#4302)

* dev: move authentication to base class for credentials

* chore: new account creation

* dev: return error as query parameter

* dev: accounts and profile endpoints for user

* fix: user store updates

* fix: store fixes

* fix: type fixes

* dev: set is_password_autoset and is_email_verifier for auth providers

* dev: move all auth configuration to different apps

* dev: fix circular imports

* dev: remove unused imports

* dev: fix imports for authentication

* dev: update endpoints to use rest framework api viewa

* fix: onboarding fixes

* dev: session model changes

* fix: session model and add check for last name first name and avatar

* dev: fix referer redirect

* dev: remove auth imports

* dev: fix imports

* dev: update migrations

* fix: instance admin login

* comflict: conflicts resolved

* dev: fix import errors and email check endpoint

* fix: error messages and redirects after login

* dev: configs api

* fix: is github enabled boolean

* dev: merge config and instance api

* conflict: merge conflict resolved

* dev: instance admin sign up endpoint

* dev: enable magic link login

* dev: configure instance variables for github and google enabled

* chore: typo fixes

* fix: god mode docker file changes

* build-error: resolved build errors

* fix: docker compose changes

* dev: add email credential check endpoint

* fix: minor package changes

* fix: docker related changes

* dev: add nginx rules in the nginx template

* dev: refactor the url patterns

* fix: docker changes

* fix: docker files for god-mode

* fix: static export

* fix: nginx conf

* dev: smtp sender refused exception

* fix: godmode fixes

* chore: god mode revamp.

* dev: add csrf secured flag

* fix: oauth redirect uri and session settings

* chore: god mode app changes.  (#3982)

* chore: send test email functionality.

* style: authentication methods page UI revamp.

* chore: create workspace popup.

* fix: user me endpoint

* dev: fix redirection after authentication

* dev: handle god mode redirection

* fix: redirections

* fix: auth related hooks

* fix: store related fixes

* dev: fix session authentication for rest apis

* fix: linting errors

* fix: removing references of useStore=

* dev: fix redirection and password validation

* dev: add useUser hook

* fix: build fixes and lint issues

* fix: removing useApplication hook

* fix: build errors

* fix: delete unused files

* fix: auth build fixes

* fix: bugfixes

* dev: alter avatar to support more than 255 chars

* dev: fix profile endpoint and increase session expiry time and update session on every request

* chore: resolved the migration

* chore: resolved merge conflicts

* dev: error codes and error messages for the auth flow

* dev: instance admin sign up and sign in endpoint

* dev: use zxcvbn to validate password strength

* dev: add extra parameters when error handling on instance god mode

* chore: auth init

* chore: signin/ signup form ui updates and password strength meter.

* chore: update password fields.

* chore: validations and error handling.

* chore: updated sign-up form

* chore: updated workflow and updated the code structure

* chore: instance empty state for god-mode.

* chore: instance and auth wrappers update

* fix: renaming godmode

* fix: docker changes

* chore: updated authentication wrappers

* chore: updated the authentication workflow and rendered all pages

* fix: build errors

* fix: docker related fixes

* fix: tailing slash added to space and admin for valid nginx locations

* chore: seperate pages for signup and login

* git-action modified for admin file changes

* feature build action updated for admin app

* self host modified

* chore: resolved build errors and handled signin and signup in a seperate route

* chore: sign-in and sign-up revamp.

* fix: migration conflicts

* dev: migrations

* chore: handled redirection

* dev: admin url

* dev: create seperate endpoint for instance admin me

* dev: instance admin endpoint

* git action fixed

* chore: handled auth wrappers

* dev: add serializer and remove print logs

* fix: build errors

* dev: fix migrations

* dev: instance folder structuring

* fix: linting errors

* chore: resolved build errors

* chore: updated store and auth workflow and updates api service types

* chore: Replaced Next Link with Anchoer tag for god-mode redirection

* add 3333 port to allowed origins

* make password login working again

* dev: fix redirection, add admin signout endpoint and fix email credential check endpoint

* fix unique code sign in

* fix small build error

* enable sign out

* dev: add google client secret variable to configure instance

* dev: add referer for redirection

* fix origin urls for oauths

* admin setup and login separation

* dev: fix user redirection and tour completed endpoint

* fix build errors

* dev: add set password endpoint

* dev: remove user creation logic for redirection

* fix unique code page

* fix forgot password

* chore: onboarding revamp.

* dev: fix workspace slug redirection in login

* chore: invited user onboarding flow update.

* chore: fix switch or delete account modal.

* fix members exception

* refactor auth flows and add invitations to auth flow

* fix sig in sign up url

* fix action url

* fix build errors

* dev: fix user set password when logging in

* dev: reset password endpoint

* chore: confirm password validation for signup and onboarding.

* enable reset password

* fix build error

* chore: minor UI updates.

* chore: forgot and reset password UI revamp.

* fix authentication re directions

* dev: auth redirections

* change url paths for signup and signin

* dev: make the user logged in when changing passwords

* dev: next path redirection for web and space app

* dev: next path for magic sign in endpoint

* dev: github space endpoint

* chore: minor ui updates and fixes in web app.

* set password screen

* fix multiple unique code generation

* dev: next path base redirection

* dev: remove print logs

* dev: auth space endpoints

* fix build errors

* dev: invalidate cache on configuration update, god mode exception errors and authentication failed code

* dev: fix space endpoints and add extra endpoints

* chore: space auth revamp.

* dev: add sign up for space app

* fix: build errors.

* fix: auth redirection logic.

* chore: space app onboarding revamp.

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com>
Co-authored-by: Manish Gupta <manish@mgupta.me>
Co-authored-by: = <=>
Co-authored-by: rahulramesha <rahulramesham@gmail.com>

* chore: updated file structure for admin

* chore: updated admin-sidebar

* chore: auth error handling

* chore: onboarding UI updates and dark mode fixes.

* chore: add `user personalization` step to onboarding profile setup screen.

* chore: fix minor UI bugs

* chore: authentication workflow changes

* chore: handled signin workflow

* style: switch or delete account workflow

* chore: god mode redirection URL

* feat(dashboard): improve label readability (#4321)

change none label for all time in dashbard filters

* chore: god-mode redirection

* chore: onboarding ui updates and accept invitation workflow updates.

* chore: rename unique code auth form.

* style: space auth ux copy.

* chore: updated intance and auth wrapper logic

* chore: update default layout style.

* chore: update confirm password.

* chore: backend redirection

* style: update banner ui

* chore: minor ui updates and validation fix.

* chore: removed old auth hook

* chore: handled auth wrapper

* chore: handled store loaders in the user

* chore: handled logs

* chore: add loading spinners for all auth and onboarding form buttons.

* chore: add background pattern in admin auth forms and minor ui fixes.

* chore: UI changes and revamp components for authentication

* chore: auth UI consistency in web, space and admin.

* chore: resolved build errors

* chore: removed old auth hooks

* chore: handled lint errors in use accounts

* chore: updated authentication wrapper logic in web app

* [WEB -1149] dev: update dependencies (#4333)

* dev: upgrade dependencies remove unwanted dependency and add ruff as local dependency

* dev: add comments

* chore: authentication wrapper fetch user

* chore: updated store loader

* chore: removed old auth wrapper and replaced the imports with new auth wrapper

* chore: join workspace invitation workflow updates

* chore: build error resolved in deploy

* chore: handled onboarding step error in web app

* chore: SMTP Name and Password validation removed

* chore: handled seo and signout logic and new user popup

* chore: added redirection to plane in the sidebar

* chore: resolved build errors

* dev: admin session cookie update

* chore: updated cookie session time for admin

* dev: add start date and end date to projects (#4355)

* chore: add email security dropdown and remove SMTP username and password validation.

* chore: add tooltip to admin sidebar help-section.

* chore: add dropdown to collapsed admin sidebar.

* chore: profile themning

* chore: updated page error messages and theme in command palette

* dev: add email validation in email check apis

* dev: remove start date and end date from project

* chore: updated space folder structure and updated the store hooks

* dev: error codes for authentication

* chore: handled authentication in space and web apps

* chore: banner redirect handling the email

* dev: god mode error codes

* chore: updated error codes

* chore: updated onboarding images

* dev: signout endpoints and saving login domain while creating sessions

* feat: Self Host Data Backup (#4383)

* feat: implemented backup , support for docker-compose tool, readme updated

* minor fix in shell script

* codacy fixes

* chore: handled build errors in web

* chore: updated react, react-dom, and next versions

* chore: updated password autioset in the signin

* dev: add logo prop to views and pages

* chore: updated api service and handled the set password in store

* chore: handled build errors and code cleanup

* dev: return 401 when the session is not valid

* dev: users/me exception for api

* chore: installed lodash in space app

* dev: add auth route in nginx

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com>
Co-authored-by: Manish Gupta <manish@mgupta.me>
Co-authored-by: rahulramesha <rahulramesham@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
Co-authored-by: Daniel Alba <56451942+redrum15@users.noreply.github.com>
Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
This commit is contained in:
sriram veeraghanta 2024-05-08 23:01:20 +05:30 committed by GitHub
parent ae43d05714
commit 59335618b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
903 changed files with 25736 additions and 16041 deletions

View file

@ -0,0 +1,29 @@
import { FC } from "react";
import { Info, X } from "lucide-react";
// helpers
import { TAuthErrorInfo } from "@/helpers/authentication.helper";
type TAuthBanner = {
bannerData: TAuthErrorInfo | undefined;
handleBannerData?: (bannerData: TAuthErrorInfo | undefined) => void;
};
export const AuthBanner: FC<TAuthBanner> = (props) => {
const { bannerData, handleBannerData } = props;
if (!bannerData) return <></>;
return (
<div className="relative 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"
onClick={() => handleBannerData && handleBannerData(undefined)}
>
<X className="w-4 h-4 flex-shrink-0" />
</div>
</div>
);
};

View file

@ -0,0 +1,108 @@
import { FC, ReactNode } from "react";
import useSWR from "swr";
import { IWorkspaceMemberInvitation } from "@plane/types";
import { Spinner } from "@plane/ui";
// components
import { WorkspaceLogo } from "@/components/workspace/logo";
// helpers
import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper";
// services
import { WorkspaceService } from "@/services/workspace.service";
type TAuthHeader = {
workspaceSlug: string | undefined;
invitationId: string | undefined;
invitationEmail: string | undefined;
authMode: EAuthModes;
currentAuthStep: EAuthSteps;
children: ReactNode;
};
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 workSpaceService = new WorkspaceService();
export const AuthHeader: FC<TAuthHeader> = (props) => {
const { workspaceSlug, invitationId, invitationEmail, authMode, currentAuthStep, children } = props;
const { data: invitation, isLoading } = useSWR(
workspaceSlug && invitationId ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null,
async () => workspaceSlug && invitationId && workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId),
{
revalidateOnFocus: false,
shouldRetryOnError: false,
}
);
const getHeaderSubHeader = (
step: EAuthSteps,
mode: EAuthModes,
invitation: IWorkspaceMemberInvitation | undefined,
email: string | undefined
) => {
if (invitation && email && invitation.email === email && invitation.workspace) {
const workspace = invitation.workspace;
return {
header: (
<div className="relative inline-flex items-center gap-2">
Join <WorkspaceLogo logo={workspace?.logo} name={workspace?.name} classNames="w-8 h-9 flex-shrink-0" />{" "}
{workspace.name}
</div>
),
subHeader: `${
mode == EAuthModes.SIGN_UP ? "Create an account" : "Sign in"
} to start managing work with your team.`,
};
}
return Titles[mode][step];
};
const { header, subHeader } = getHeaderSubHeader(currentAuthStep, authMode, invitation || undefined, invitationEmail);
if (isLoading)
return (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
);
return (
<>
<div className="space-y-1 text-center">
<h3 className="text-3xl font-bold text-onboarding-text-100">{header}</h3>
<p className="font-medium text-onboarding-text-400">{subHeader}</p>
</div>
{children}
</>
);
};

View file

@ -0,0 +1,150 @@
import React, { FC, useEffect, useState } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
import { IEmailCheckData } from "@plane/types";
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import {
AuthHeader,
AuthBanner,
AuthEmailForm,
AuthPasswordForm,
OAuthOptions,
TermsAndConditions,
AuthUniqueCodeForm,
} from "@/components/account";
// 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";
const authService = new AuthService();
type TAuthRoot = {
authMode: EAuthModes;
};
export const AuthRoot: FC<TAuthRoot> = observer((props) => {
//router
const router = useRouter();
const { email: emailParam, invitation_id, slug: workspaceSlug, error_code } = router.query;
// props
const { authMode } = props;
// states
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
// hooks
const { instance } = useInstance();
useEffect(() => {
if (error_code) {
const errorhandler = authErrorHandler(error_code?.toString() as EAuthenticationErrorCodes);
if (errorhandler) {
if (
[
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN,
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP,
].includes(errorhandler.code)
)
setAuthStep(EAuthSteps.PASSWORD);
if (
[EAuthenticationErrorCodes.INVALID_MAGIC_CODE, EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE].includes(
errorhandler.code
)
)
setAuthStep(EAuthSteps.UNIQUE_CODE);
// validating weather to show alert to banner
if (errorhandler?.type === EErrorAlertType.TOAST_ALERT) {
setToast({
type: TOAST_TYPE.ERROR,
title: errorhandler?.title,
message: errorhandler?.message as string,
});
} else setErrorInfo(errorhandler);
}
}
}, [error_code, authMode]);
// step 1 submit handler- email verification
const handleEmailVerification = async (data: IEmailCheckData) => {
setEmail(data.email);
const emailCheckRequest =
authMode === EAuthModes.SIGN_IN ? authService.signInEmailCheck(data) : authService.signUpEmailCheck(data);
await emailCheckRequest
.then((response) => {
if (authMode === EAuthModes.SIGN_IN) {
if (response.is_password_autoset) setAuthStep(EAuthSteps.UNIQUE_CODE);
else setAuthStep(EAuthSteps.PASSWORD);
} else setAuthStep(EAuthSteps.PASSWORD);
})
.catch((error) => {
const errorhandler = authErrorHandler(error?.error_code.toString(), data?.email || undefined);
if (errorhandler?.type === EErrorAlertType.BANNER_ALERT) {
setErrorInfo(errorhandler);
return;
} else if (errorhandler?.type === EErrorAlertType.TOAST_ALERT)
setToast({
type: TOAST_TYPE.ERROR,
title: errorhandler?.title,
message: (errorhandler?.message as string) || "Something went wrong. Please try again.",
});
});
};
const isOAuthEnabled =
instance?.config && (instance?.config?.is_google_enabled || instance?.config?.is_github_enabled);
return (
<div className="relative flex flex-col space-y-6">
<AuthHeader
workspaceSlug={workspaceSlug?.toString() || undefined}
invitationId={invitation_id?.toString() || undefined}
invitationEmail={email || undefined}
authMode={authMode}
currentAuthStep={authStep}
>
{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 && (
<AuthUniqueCodeForm
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={false} />
</AuthHeader>
</div>
);
});

View file

@ -0,0 +1,77 @@
import { FC, FormEvent, useMemo, useState } from "react";
import { observer } from "mobx-react-lite";
// icons
import { CircleAlert, XCircle } from "lucide-react";
// types
import { IEmailCheckData } from "@plane/types";
// ui
import { Button, Input, Spinner } from "@plane/ui";
// helpers
import { checkEmailValidity } from "@/helpers/string.helper";
type TAuthEmailForm = {
defaultEmail: string;
onSubmit: (data: IEmailCheckData) => Promise<void>;
};
export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
const { onSubmit, defaultEmail } = props;
// states
const [isSubmitting, setIsSubmitting] = useState(false);
const [email, setEmail] = useState(defaultEmail);
const emailError = useMemo(
() => (email && !checkEmailValidity(email) ? { email: "Email is invalid" } : undefined),
[email]
);
const handleFormSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsSubmitting(true);
const payload: IEmailCheckData = {
email: email,
};
await onSubmit(payload);
setIsSubmitting(false);
};
const isButtonDisabled = email.length === 0 || Boolean(emailError?.email) || isSubmitting;
return (
<form onSubmit={handleFormSubmit} className="mt-8 space-y-4">
<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={email}
onChange={(e) => setEmail(e.target.value)}
hasError={Boolean(emailError?.email)}
placeholder="name@company.com"
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
autoFocus
/>
{email.length > 0 && (
<XCircle
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setEmail("")}
/>
)}
</div>
{emailError?.email && (
<p className="flex items-center gap-1 text-xs text-red-600 px-0.5">
<CircleAlert height={12} width={12} />
{emailError.email}
</p>
)}
</div>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
</Button>
</form>
);
});

View file

@ -1,6 +1,9 @@
export * from "./auth-root";
export * from "./auth-header";
export * from "./auth-banner";
export * from "./email";
export * from "./forgot-password-popover";
export * from "./optional-set-password";
export * from "./password";
export * from "./root";
export * from "./unique-code";

View file

@ -0,0 +1,224 @@
import React, { useEffect, useMemo, useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
// icons
import { Eye, EyeOff, XCircle } from "lucide-react";
// ui
import { Button, Input, Spinner } from "@plane/ui";
// components
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
import { useEventTracker, useInstance } from "@/hooks/store";
// services
import { AuthService } from "@/services/auth.service";
type Props = {
email: string;
mode: EAuthModes;
handleStepChange: (step: EAuthSteps) => void;
handleEmailClear: () => void;
};
type TPasswordFormValues = {
email: string;
password: string;
confirm_password?: string;
};
const defaultValues: TPasswordFormValues = {
email: "",
password: "",
};
const authService = new AuthService();
export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
const { email, handleStepChange, handleEmailClear, mode } = props;
// states
const [passwordFormData, setPasswordFormData] = useState<TPasswordFormValues>({ ...defaultValues, email });
const [showPassword, setShowPassword] = useState(false);
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
// hooks
const { instance } = useInstance();
const { captureEvent } = useEventTracker();
// derived values
const isSmtpConfigured = instance?.config?.is_smtp_configured;
const handleFormChange = (key: keyof TPasswordFormValues, value: string) =>
setPasswordFormData((prev) => ({ ...prev, [key]: value }));
useEffect(() => {
if (csrfToken === undefined)
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
}, [csrfToken]);
const redirectToUniqueCodeSignIn = async () => {
handleStepChange(EAuthSteps.UNIQUE_CODE);
};
const passwordSupport =
mode === EAuthModes.SIGN_IN ? (
<div className="mt-2 w-full pb-3">
{isSmtpConfigured ? (
<Link
onClick={() => captureEvent(FORGOT_PASSWORD)}
href={`/accounts/forgot-password?email=${email}`}
className="text-xs font-medium text-custom-primary-100"
>
Forgot your password?
</Link>
) : (
<ForgotPasswordPopover />
)}
</div>
) : (
isPasswordInputFocused && <PasswordStrengthMeter password={passwordFormData.password} />
);
const isButtonDisabled = useMemo(
() =>
!isSubmitting &&
!!passwordFormData.password &&
(mode === EAuthModes.SIGN_UP
? getPasswordStrength(passwordFormData.password) >= 3 &&
passwordFormData.password === passwordFormData.confirm_password
: true)
? false
: true,
[isSubmitting, mode, passwordFormData.confirm_password, passwordFormData.password]
);
return (
<form
className="mt-5 space-y-4"
method="POST"
action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "sign-in" : "sign-up"}/`}
onSubmit={() => setIsSubmitting(true)}
onError={() => setIsSubmitting(false)}
>
<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}
/>
)}
</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="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}>
{isSubmitting ? (
<Spinner height="20px" width="20px" />
) : isSmtpConfigured ? (
"Continue"
) : (
"Go to workspace"
)}
</Button>
{instance && isSmtpConfigured && (
<Button
type="button"
onClick={redirectToUniqueCodeSignIn}
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}>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Create account"}
</Button>
)}
</div>
</form>
);
});

View file

@ -0,0 +1,177 @@
import React, { useEffect, useState } from "react";
import { CircleCheck, XCircle } from "lucide-react";
import { IEmailCheckData } from "@plane/types";
import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// 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";
type Props = {
email: string;
handleEmailClear: () => void;
submitButtonText: string;
mode: EAuthModes;
};
type TUniqueCodeFormValues = {
email: string;
code: string;
};
const defaultValues: TUniqueCodeFormValues = {
email: "",
code: "",
};
// services
const authService = new AuthService();
export const AuthUniqueCodeForm: React.FC<Props> = (props) => {
const { email, handleEmailClear, submitButtonText, mode } = props;
// states
const [uniqueCodeFormData, setUniqueCodeFormData] = useState<TUniqueCodeFormValues>({ ...defaultValues, email });
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [isSubmitting, setIsSubmitting] = useState(false);
// store hooks
// const { captureEvent } = useEventTracker();
// timer
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30);
const handleFormChange = (key: keyof TUniqueCodeFormValues, value: string) =>
setUniqueCodeFormData((prev) => ({ ...prev, [key]: value }));
const handleSendNewCode = async (email: string) => {
const payload: IEmailCheckData = {
email,
};
await authService
.generateUniqueCode(payload)
.then(() => {
setResendCodeTimer(30);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "A new unique code has been sent to your email.",
});
handleFormChange("code", "");
})
.catch((err) =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong while generating unique code. Please try again.",
})
);
};
const handleRequestNewCode = async (email: string) => {
setIsRequestingNewCode(true);
await handleSendNewCode(email)
.then(() => setResendCodeTimer(30))
.finally(() => setIsRequestingNewCode(false));
};
useEffect(() => {
if (csrfToken === undefined)
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
}, [csrfToken]);
useEffect(() => {
handleRequestNewCode(email);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0;
const isButtonDisabled = isRequestingNewCode || !uniqueCodeFormData.code || isSubmitting;
return (
<form
className="mt-5 space-y-4"
method="POST"
action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "magic-sign-in" : "magic-sign-up"}/`}
onSubmit={() => setIsSubmitting(true)}
onError={() => setIsSubmitting(false)}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="space-y-1">
<label className="text-sm font-medium text-onboarding-text-300" htmlFor="email">
Email
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
id="email"
name="email"
type="email"
value={uniqueCodeFormData.email}
onChange={(e) => handleFormChange("email", e.target.value)}
// FIXME:
// 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"
/>
{uniqueCodeFormData.email.length > 0 && (
<XCircle
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={handleEmailClear}
/>
)}
</div>
</div>
<div className="space-y-1">
<label className="text-sm font-medium text-onboarding-text-300" htmlFor="code">
Unique code
</label>
<Input
name="code"
value={uniqueCodeFormData.code}
onChange={(e) => handleFormChange("code", e.target.value)}
// FIXME:
// hasError={Boolean(errors.code)}
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
/>
{/* )}
/> */}
<div className="flex w-full items-center justify-between px-1 text-xs">
<p className="flex items-center gap-1 font-medium text-green-700">
<CircleCheck height={12} width={12} />
Paste the code sent to your email
</p>
<button
type="button"
onClick={() => handleRequestNewCode(uniqueCodeFormData.email)}
className={`${
isRequestNewCodeDisabled
? "text-onboarding-text-400"
: "font-medium text-custom-primary-300 hover:text-custom-primary-200"
}`}
disabled={isRequestNewCodeDisabled}
>
{resendTimerCode > 0
? `Resend in ${resendTimerCode}s`
: isRequestingNewCode
? "Requesting new code"
: "Resend"}
</button>
</div>
</div>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
{isRequestingNewCode ? (
"Sending code"
) : isSubmitting ? (
<Spinner height="20px" width="20px" />
) : (
submitButtonText
)}
</Button>
</form>
);
};

View file

@ -46,7 +46,7 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
router.push("/");
handleClose();
})
.catch((err) =>
.catch((err: any) =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",

View file

@ -1,5 +1,5 @@
export * from "./o-auth";
export * from "./sign-in-forms";
export * from "./sign-up-forms";
export * from "./oauth";
export * from "./auth-forms";
export * from "./deactivate-account-modal";
export * from "./terms-and-conditions";
export * from "./password-strength-meter";

View file

@ -1,62 +0,0 @@
// react
import { useEffect, useState, FC } from "react";
// next
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/router";
import { useTheme } from "next-themes";
// images
import githubLightModeImage from "/public/logos/github-black.png";
import githubDarkModeImage from "/public/logos/github-dark.svg";
type Props = {
handleSignIn: React.Dispatch<string>;
clientId: string;
type: "sign_in" | "sign_up";
};
export const GitHubSignInButton: FC<Props> = (props) => {
const { handleSignIn, clientId, type } = props;
// states
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
const [gitCode, setGitCode] = useState<null | string>(null);
// router
const {
query: { code },
} = useRouter();
// theme
const { resolvedTheme } = useTheme();
useEffect(() => {
if (code && !gitCode) {
setGitCode(code.toString());
handleSignIn(code.toString());
}
}, [code, gitCode, handleSignIn]);
useEffect(() => {
const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
setLoginCallBackURL(`${origin}/` as any);
}, []);
return (
<div className="w-full">
<Link
href={`https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
>
<button
className={`flex h-[42px] w-full items-center justify-center gap-2 rounded border px-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-onboarding-background-300 ${
resolvedTheme === "dark" ? "border-[#43484F] bg-[#2F3135]" : "border-[#D9E4FF]"
}`}
>
<Image
src={resolvedTheme === "dark" ? githubDarkModeImage : githubLightModeImage}
height={20}
width={20}
alt="GitHub Logo"
/>
<span className="text-onboarding-text-200">{type === "sign_in" ? "Sign-in" : "Sign-up"} with GitHub</span>
</button>
</Link>
</div>
);
};

View file

@ -1,60 +0,0 @@
import { FC, useEffect, useRef, useCallback, useState } from "react";
import Script from "next/script";
type Props = {
handleSignIn: React.Dispatch<any>;
clientId: string;
type: "sign_in" | "sign_up";
};
export const GoogleSignInButton: FC<Props> = (props) => {
const { handleSignIn, clientId, type } = props;
// refs
const googleSignInButton = useRef<HTMLDivElement>(null);
// states
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false);
const loadScript = useCallback(() => {
if (!googleSignInButton.current || gsiScriptLoaded) return;
window?.google?.accounts.id.initialize({
client_id: clientId,
callback: handleSignIn,
});
try {
window?.google?.accounts.id.renderButton(
googleSignInButton.current,
{
type: "standard",
theme: "outline",
size: "large",
logo_alignment: "center",
text: type === "sign_in" ? "signin_with" : "signup_with",
} as GsiButtonConfiguration // customization attributes
);
} catch (err) {
console.log(err);
}
window?.google?.accounts.id.prompt(); // also display the One Tap dialog
setGsiScriptLoaded(true);
}, [handleSignIn, gsiScriptLoaded, clientId, type]);
useEffect(() => {
if (window?.google?.accounts?.id) {
loadScript();
}
return () => {
window?.google?.accounts.id.cancel();
};
}, [loadScript]);
return (
<>
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
<div className="!w-full overflow-hidden rounded" id="googleSignInButton" ref={googleSignInButton} />
</>
);
};

View file

@ -1,3 +0,0 @@
export * from "./github-sign-in";
export * from "./google-sign-in";
export * from "./o-auth-options";

View file

@ -1,89 +0,0 @@
import { observer } from "mobx-react-lite";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { GitHubSignInButton, GoogleSignInButton } from "@/components/account";
// hooks
import { useApplication } from "@/hooks/store";
// services
import { AuthService } from "@/services/auth.service";
type Props = {
handleSignInRedirection: () => Promise<void>;
type: "sign_in" | "sign_up";
};
// services
const authService = new AuthService();
export const OAuthOptions: React.FC<Props> = observer((props) => {
const { handleSignInRedirection, type } = props;
// mobx store
const {
config: { envConfig },
} = useApplication();
// derived values
const areBothOAuthEnabled = envConfig?.google_client_id && envConfig?.github_client_id;
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
try {
if (clientId && credential) {
const socialAuthPayload = {
medium: "google",
credential,
clientId,
};
const response = await authService.socialAuth(socialAuthPayload);
if (response) handleSignInRedirection();
} else throw Error("Can't find credentials");
} catch (err: any) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error signing in!",
message: err?.error || "Something went wrong. Please try again later or contact the support team.",
});
}
};
const handleGitHubSignIn = async (credential: string) => {
try {
if (envConfig && envConfig.github_client_id && credential) {
const socialAuthPayload = {
medium: "github",
credential,
clientId: envConfig.github_client_id,
};
const response = await authService.socialAuth(socialAuthPayload);
if (response) handleSignInRedirection();
} else throw Error("Can't find credentials");
} catch (err: any) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error signing in!",
message: err?.error || "Something went wrong. Please try again later or contact the support team.",
});
}
};
return (
<>
<div className="mx-auto mt-4 flex items-center sm:w-96">
<hr className="w-full border-onboarding-border-100" />
<p className="mx-3 flex-shrink-0 text-center text-sm text-onboarding-text-400">Or continue with</p>
<hr className="w-full border-onboarding-border-100" />
</div>
<div className={`mx-auto mt-7 grid gap-4 overflow-hidden sm:w-96 ${areBothOAuthEnabled ? "grid-cols-2" : ""}`}>
{envConfig?.google_client_id && (
<div className="flex h-[42px] items-center !overflow-hidden">
<GoogleSignInButton clientId={envConfig?.google_client_id} handleSignIn={handleGoogleSignIn} type={type} />
</div>
)}
{envConfig?.github_client_id && (
<GitHubSignInButton clientId={envConfig?.github_client_id} handleSignIn={handleGitHubSignIn} type={type} />
)}
</div>
</>
);
});

View file

@ -0,0 +1,39 @@
import { FC } from "react";
import Image from "next/image";
import { useTheme } from "next-themes";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// images
import githubLightModeImage from "/public/logos/github-black.png";
import githubDarkModeImage from "/public/logos/github-dark.svg";
export type GithubOAuthButtonProps = {
text: string;
};
export const GithubOAuthButton: FC<GithubOAuthButtonProps> = (props) => {
const { text } = props;
// hooks
const { resolvedTheme } = useTheme();
const handleSignIn = () => {
window.location.assign(`${API_BASE_URL}/auth/github/`);
};
return (
<button
className={`flex h-[42px] w-full items-center justify-center gap-2 rounded border px-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-onboarding-background-300 ${
resolvedTheme === "dark" ? "border-[#43484F] bg-[#2F3135]" : "border-[#D9E4FF]"
}`}
onClick={handleSignIn}
>
<Image
src={resolvedTheme === "dark" ? githubDarkModeImage : githubLightModeImage}
height={20}
width={20}
alt="GitHub Logo"
/>
<span className="text-onboarding-text-200">{text}</span>
</button>
);
};

View file

@ -0,0 +1,33 @@
import { FC } from "react";
import Image from "next/image";
import { useTheme } from "next-themes";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// images
import GoogleLogo from "/public/logos/google-logo.svg";
export type GoogleOAuthButtonProps = {
text: string;
};
export const GoogleOAuthButton: FC<GoogleOAuthButtonProps> = (props) => {
const { text } = props;
// hooks
const { resolvedTheme } = useTheme();
const handleSignIn = () => {
window.location.assign(`${API_BASE_URL}/auth/google/`);
};
return (
<button
className={`flex h-[42px] w-full items-center justify-center gap-2 rounded border px-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-onboarding-background-300 ${
resolvedTheme === "dark" ? "border-[#43484F] bg-[#2F3135]" : "border-[#D9E4FF]"
}`}
onClick={handleSignIn}
>
<Image src={GoogleLogo} height={18} width={18} alt="Google Logo" />
{text}
</button>
);
};

View file

@ -0,0 +1,3 @@
export * from "./oauth-options";
export * from "./google-button";
export * from "./github-button";

View file

@ -0,0 +1,28 @@
import { observer } from "mobx-react";
// components
import { GithubOAuthButton, GoogleOAuthButton } from "@/components/account";
// hooks
import { useInstance } from "@/hooks/store";
export const OAuthOptions: React.FC = observer(() => {
// hooks
const { instance } = useInstance();
return (
<>
<div className="mt-4 flex items-center">
<hr className="w-full border-onboarding-border-100" />
<p className="mx-3 flex-shrink-0 text-center text-sm text-onboarding-text-400">or</p>
<hr className="w-full border-onboarding-border-100" />
</div>
<div className={`mt-7 grid gap-4 overflow-hidden`}>
{instance?.config?.is_google_enabled && (
<div className="flex h-[42px] items-center !overflow-hidden">
<GoogleOAuthButton text="SignIn with Google" />
</div>
)}
{instance?.config?.is_github_enabled && <GithubOAuthButton text="SignIn with Github" />}
</div>
</>
);
});

View file

@ -0,0 +1,67 @@
// icons
import { CircleCheck } from "lucide-react";
// helpers
import { cn } from "@/helpers/common.helper";
import { getPasswordStrength } from "@/helpers/password.helper";
type Props = {
password: string;
};
export const PasswordStrengthMeter: React.FC<Props> = (props: Props) => {
const { password } = props;
const strength = getPasswordStrength(password);
let bars = [];
let text = "";
let textColor = "";
if (password.length === 0) {
bars = [`bg-[#F0F0F3]`, `bg-[#F0F0F3]`, `bg-[#F0F0F3]`];
text = "Password requirements";
} else if (password.length < 8) {
bars = [`bg-[#DC3E42]`, `bg-[#F0F0F3]`, `bg-[#F0F0F3]`];
text = "Password is too short";
textColor = `text-[#DC3E42]`;
} else if (strength < 3) {
bars = [`bg-[#FFBA18]`, `bg-[#FFBA18]`, `bg-[#F0F0F3]`];
text = "Password is weak";
textColor = `text-[#FFBA18]`;
} else {
bars = [`bg-[#3E9B4F]`, `bg-[#3E9B4F]`, `bg-[#3E9B4F]`];
text = "Password is strong";
textColor = `text-[#3E9B4F]`;
}
const criteria = [
{ label: "Min 8 characters", isValid: password.length >= 8 },
{ label: "Min 1 upper-case letter", isValid: /[A-Z]/.test(password) },
{ label: "Min 1 number", isValid: /\d/.test(password) },
{ label: "Min 1 special character", isValid: /[!@#$%^&*]/.test(password) },
];
return (
<div className="w-full p-1">
<div className="flex w-full gap-1.5">
{bars.map((color, index) => (
<div key={index} className={cn("w-full h-1 rounded-full", color)} />
))}
</div>
<p className={cn("text-xs font-medium py-1", textColor)}>{text}</p>
<div className="flex flex-wrap gap-x-4 gap-y-2">
{criteria.map((criterion, index) => (
<div
key={index}
className={cn(
"flex items-center gap-1 text-xs font-medium",
criterion.isValid ? `text-[#3E9B4F]` : "text-custom-text-400"
)}
>
<CircleCheck width={14} height={14} />
{criterion.label}
</div>
))}
</div>
</div>
);
};

View file

@ -1,107 +0,0 @@
import React from "react";
import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form";
import { XCircle } from "lucide-react";
import { IEmailCheckData } from "@plane/types";
// services
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { checkEmailValidity } from "@/helpers/string.helper";
import { AuthService } from "@/services/auth.service";
// ui
// helpers
// types
type Props = {
onSubmit: (isPasswordAutoset: boolean) => void;
updateEmail: (email: string) => void;
};
type TEmailFormValues = {
email: string;
};
const authService = new AuthService();
export const SignInEmailForm: React.FC<Props> = observer((props) => {
const { onSubmit, updateEmail } = props;
// hooks
const {
control,
formState: { errors, isSubmitting, isValid },
handleSubmit,
} = useForm<TEmailFormValues>({
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) =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
})
);
};
return (
<>
<h1 className="sm:text-2.5xl text-center text-2xl font-medium text-onboarding-text-100">
Welcome back, let{"'"}s get you on board
</h1>
<p className="mt-2.5 text-center text-sm text-onboarding-text-200">
Get back to your issues, projects and workspaces.
</p>
<form onSubmit={handleSubmit(handleFormSubmit)} className="mx-auto mt-8 space-y-4 sm:w-96">
<div className="space-y-1">
<Controller
control={control}
name="email"
rules={{
required: "Email is required",
validate: (value) => checkEmailValidity(value) || "Email is invalid",
}}
render={({ field: { value, onChange } }) => (
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
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"
autoFocus
/>
{value.length > 0 && (
<XCircle
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => onChange("")}
/>
)}
</div>
)}
/>
</div>
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={!isValid} loading={isSubmitting}>
Continue
</Button>
</form>
</>
);
});

View file

@ -1,189 +0,0 @@
import React, { useState } from "react";
import { Controller, useForm } from "react-hook-form";
// services
// hooks
// ui
import { Eye, EyeOff } from "lucide-react";
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
// helpers
import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED } from "@/constants/event-tracker";
import { checkEmailValidity } from "@/helpers/string.helper";
// icons
import { useEventTracker } from "@/hooks/store";
import { AuthService } from "@/services/auth.service";
type Props = {
email: string;
handleSignInRedirection: () => Promise<void>;
};
type TCreatePasswordFormValues = {
email: string;
password: string;
};
const defaultValues: TCreatePasswordFormValues = {
email: "",
password: "",
};
// services
const authService = new AuthService();
export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
const { email, handleSignInRedirection } = props;
// states
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
const [showPassword, setShowPassword] = useState(false);
// store hooks
const { captureEvent } = useEventTracker();
// form info
const {
control,
formState: { errors, isSubmitting, isValid },
handleSubmit,
} = useForm<TCreatePasswordFormValues>({
defaultValues: {
...defaultValues,
email,
},
mode: "onChange",
reValidateMode: "onChange",
});
const handleCreatePassword = async (formData: TCreatePasswordFormValues) => {
const payload = {
password: formData.password,
};
await authService
.setPassword(payload)
.then(async () => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Password created successfully.",
});
captureEvent(PASSWORD_CREATE_SELECTED, {
state: "SUCCESS",
first_time: false,
});
await handleSignInRedirection();
})
.catch((err) => {
captureEvent(PASSWORD_CREATE_SELECTED, {
state: "FAILED",
first_time: false,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
});
});
};
const handleGoToWorkspace = async () => {
setIsGoingToWorkspace(true);
await handleSignInRedirection().finally(() => {
captureEvent(PASSWORD_CREATE_SKIPPED, {
state: "SUCCESS",
first_time: false,
});
setIsGoingToWorkspace(false);
});
};
return (
<>
<h1 className="sm:text-2.5xl text-center text-2xl font-medium text-onboarding-text-100">Set your password</h1>
<p className="mt-2.5 text-center text-sm text-onboarding-text-200">
If you{"'"}d like to do away with codes, set a password here.
</p>
<form onSubmit={handleSubmit(handleCreatePassword)} className="mx-auto mt-5 space-y-4 sm:w-96">
<Controller
control={control}
name="email"
rules={{
required: "Email is required",
validate: (value) => checkEmailValidity(value) || "Email is invalid",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.email)}
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
/>
)}
/>
<div>
<Controller
control={control}
name="password"
rules={{
required: "Password is required",
}}
render={({ field: { value, onChange, ref } }) => (
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
value={value}
onChange={onChange}
ref={ref}
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"
minLength={8}
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>
)}
/>
<p className="mt-2 pb-3 text-xs text-onboarding-text-200">
Whatever you choose now will be your account{"'"}s password until you change it.
</p>
</div>
<div className="space-y-2.5">
<Button
type="submit"
variant="primary"
className="w-full"
size="xl"
disabled={!isValid}
loading={isSubmitting}
>
Set password
</Button>
<Button
type="button"
variant="outline-primary"
className="w-full"
size="xl"
onClick={handleGoToWorkspace}
loading={isGoingToWorkspace}
>
Skip to workspace
</Button>
</div>
</form>
</>
);
};

View file

@ -1,232 +0,0 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { Controller, useForm } from "react-hook-form";
import { Eye, EyeOff, XCircle } from "lucide-react";
import { IPasswordSignInData } from "@plane/types";
// services
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { ESignInSteps, ForgotPasswordPopover } from "@/components/account";
import { FORGOT_PASSWORD, SIGN_IN_WITH_PASSWORD } from "@/constants/event-tracker";
import { checkEmailValidity } from "@/helpers/string.helper";
import { useApplication, useEventTracker } from "@/hooks/store";
import { AuthService } from "@/services/auth.service";
// hooks
// components
// ui
// helpers
// types
// constants
type Props = {
email: string;
handleStepChange: (step: ESignInSteps) => void;
handleEmailClear: () => void;
onSubmit: () => Promise<void>;
};
type TPasswordFormValues = {
email: string;
password: string;
};
const defaultValues: TPasswordFormValues = {
email: "",
password: "",
};
const authService = new AuthService();
export const SignInPasswordForm: React.FC<Props> = observer((props) => {
const { email, handleStepChange, handleEmailClear, onSubmit } = props;
// states
const [isSendingUniqueCode, setIsSendingUniqueCode] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const {
config: { envConfig },
} = useApplication();
const { captureEvent } = useEventTracker();
// derived values
const isSmtpConfigured = envConfig?.is_smtp_configured;
// form info
const {
control,
formState: { errors, isSubmitting, isValid },
getValues,
handleSubmit,
setError,
} = useForm<TPasswordFormValues>({
defaultValues: {
...defaultValues,
email,
},
mode: "onChange",
reValidateMode: "onChange",
});
const handleFormSubmit = async (formData: TPasswordFormValues) => {
const payload: IPasswordSignInData = {
email: formData.email,
password: formData.password,
};
await authService
.passwordSignIn(payload)
.then(async () => {
captureEvent(SIGN_IN_WITH_PASSWORD, {
state: "SUCCESS",
first_time: false,
});
await onSubmit();
})
.catch((err) =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
})
);
};
const handleSendUniqueCode = async () => {
const emailFormValue = getValues("email");
const isEmailValid = checkEmailValidity(emailFormValue);
if (!isEmailValid) {
setError("email", { message: "Email is invalid" });
return;
}
setIsSendingUniqueCode(true);
await authService
.generateUniqueCode({ email: emailFormValue })
.then(() => handleStepChange(ESignInSteps.USE_UNIQUE_CODE_FROM_PASSWORD))
.catch((err) =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
})
)
.finally(() => setIsSendingUniqueCode(false));
};
return (
<>
<h1 className="sm:text-2.5xl text-center text-2xl font-medium text-onboarding-text-100">
Welcome back, let{"'"}s get you on board
</h1>
<p className="mt-2.5 text-center text-sm text-onboarding-text-200">
Get back to your issues, projects and workspaces.
</p>
<form onSubmit={handleSubmit(handleFormSubmit)} className="mx-auto mt-5 space-y-4 sm:w-96">
<div>
<Controller
control={control}
name="email"
rules={{
required: "Email is required",
validate: (value) => checkEmailValidity(value) || "Email is invalid",
}}
render={({ field: { value, onChange } }) => (
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
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"
disabled={isSmtpConfigured}
/>
{value.length > 0 && (
<XCircle
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => {
if (isSmtpConfigured) handleEmailClear();
else onChange("");
}}
/>
)}
</div>
)}
/>
</div>
<div>
<Controller
control={control}
name="password"
rules={{
required: "Password is required",
}}
render={({ field: { value, onChange } }) => (
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
value={value}
onChange={onChange}
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
/>
{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>
)}
/>
<div className="mt-2 w-full pb-3 text-right">
{isSmtpConfigured ? (
<Link
onClick={() => captureEvent(FORGOT_PASSWORD)}
href={`/accounts/forgot-password?email=${email}`}
className="text-xs font-medium text-custom-primary-100"
>
Forgot your password?
</Link>
) : (
<ForgotPasswordPopover />
)}
</div>
</div>
<div className="space-y-2.5">
<Button
type="submit"
variant="primary"
className="w-full"
size="xl"
disabled={!isValid}
loading={isSubmitting}
>
{envConfig?.is_smtp_configured ? "Continue" : "Go to workspace"}
</Button>
{envConfig && envConfig.is_smtp_configured && (
<Button
type="button"
onClick={handleSendUniqueCode}
variant="outline-primary"
className="w-full"
size="xl"
loading={isSendingUniqueCode}
>
{isSendingUniqueCode ? "Sending code" : "Sign in with unique code"}
</Button>
)}
</div>
</form>
</>
);
});

View file

@ -1,131 +0,0 @@
import React, { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
// hooks
import {
SignInEmailForm,
SignInUniqueCodeForm,
SignInPasswordForm,
OAuthOptions,
SignInOptionalSetPasswordForm,
TermsAndConditions,
} from "@/components/account";
import { LatestFeatureBlock } from "@/components/common";
import { NAVIGATE_TO_SIGNUP } from "@/constants/event-tracker";
import { useApplication, useEventTracker } from "@/hooks/store";
import useSignInRedirection from "@/hooks/use-sign-in-redirection";
// components
// constants
export enum ESignInSteps {
EMAIL = "EMAIL",
PASSWORD = "PASSWORD",
UNIQUE_CODE = "UNIQUE_CODE",
OPTIONAL_SET_PASSWORD = "OPTIONAL_SET_PASSWORD",
USE_UNIQUE_CODE_FROM_PASSWORD = "USE_UNIQUE_CODE_FROM_PASSWORD",
}
export const SignInRoot = observer(() => {
// states
const [signInStep, setSignInStep] = useState<ESignInSteps | null>(null);
const [email, setEmail] = useState("");
// sign in redirection hook
const { handleRedirection } = useSignInRedirection();
// mobx store
const {
config: { envConfig },
} = useApplication();
const { captureEvent } = useEventTracker();
// 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 (
<>
<div className="mx-auto flex flex-col">
<>
{signInStep === ESignInSteps.EMAIL && (
<SignInEmailForm onSubmit={handleEmailVerification} updateEmail={(newEmail) => setEmail(newEmail)} />
)}
{signInStep === ESignInSteps.UNIQUE_CODE && (
<SignInUniqueCodeForm
email={email}
handleEmailClear={() => {
setEmail("");
setSignInStep(ESignInSteps.EMAIL);
}}
onSubmit={handleUniqueCodeSignIn}
submitButtonText="Continue"
/>
)}
{signInStep === ESignInSteps.PASSWORD && (
<SignInPasswordForm
email={email}
handleEmailClear={() => {
setEmail("");
setSignInStep(ESignInSteps.EMAIL);
}}
onSubmit={handlePasswordSignIn}
handleStepChange={(step) => setSignInStep(step)}
/>
)}
{signInStep === ESignInSteps.USE_UNIQUE_CODE_FROM_PASSWORD && (
<SignInUniqueCodeForm
email={email}
handleEmailClear={() => {
setEmail("");
setSignInStep(ESignInSteps.EMAIL);
}}
onSubmit={handleUniqueCodeSignIn}
submitButtonText="Go to workspace"
/>
)}
{signInStep === ESignInSteps.OPTIONAL_SET_PASSWORD && (
<SignInOptionalSetPasswordForm email={email} handleSignInRedirection={handleRedirection} />
)}
</>
</div>
{isOAuthEnabled &&
(signInStep === ESignInSteps.EMAIL || (!isSmtpConfigured && signInStep === ESignInSteps.PASSWORD)) && (
<>
<OAuthOptions handleSignInRedirection={handleRedirection} type="sign_in" />
<p className="text-xs text-onboarding-text-300 text-center mt-6">
Don{"'"}t have an account?{" "}
<Link
href="/accounts/sign-up"
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
className="text-custom-primary-100 font-medium underline"
>
Sign up
</Link>
</p>
<TermsAndConditions />
</>
)}
<LatestFeatureBlock />
</>
);
});

View file

@ -1,217 +0,0 @@
import React, { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { XCircle } from "lucide-react";
import { IEmailCheckData, IMagicSignInData } from "@plane/types";
// services
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { CODE_VERIFIED } from "@/constants/event-tracker";
import { checkEmailValidity } from "@/helpers/string.helper";
import { useEventTracker } from "@/hooks/store";
import useTimer from "@/hooks/use-timer";
import { AuthService } from "@/services/auth.service";
import { UserService } from "@/services/user.service";
// hooks
// ui
// helpers
// types
// constants
type Props = {
email: string;
onSubmit: (isPasswordAutoset: boolean) => Promise<void>;
handleEmailClear: () => void;
submitButtonText: string;
};
type TUniqueCodeFormValues = {
email: string;
token: string;
};
const defaultValues: TUniqueCodeFormValues = {
email: "",
token: "",
};
// services
const authService = new AuthService();
const userService = new UserService();
export const SignInUniqueCodeForm: React.FC<Props> = (props) => {
const { email, onSubmit, handleEmailClear, submitButtonText } = props;
// states
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
// store hooks
const { captureEvent } = useEventTracker();
// timer
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30);
// form info
const {
control,
formState: { errors, isSubmitting, isValid },
getValues,
handleSubmit,
reset,
} = useForm<TUniqueCodeFormValues>({
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 () => {
captureEvent(CODE_VERIFIED, {
state: "SUCCESS",
});
const currentUser = await userService.currentUser();
await onSubmit(currentUser.is_password_autoset);
})
.catch((err) => {
captureEvent(CODE_VERIFIED, {
state: "FAILED",
});
setToast({
type: TOAST_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);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "A new unique code has been sent to your email.",
});
reset({
email: formData.email,
token: "",
});
})
.catch((err) =>
setToast({
type: TOAST_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 (
<>
<h1 className="sm:text-2.5xl text-center text-2xl font-medium text-onboarding-text-100">Moving to the runway</h1>
<p className="mt-2.5 text-center text-sm text-onboarding-text-200">
Paste the code you got at
<br />
<span className="font-semibold text-custom-primary-100">{email}</span> below.
</p>
<form onSubmit={handleSubmit(handleUniqueCodeSignIn)} className="mx-auto mt-5 space-y-4 sm:w-96">
<div>
<Controller
control={control}
name="email"
rules={{
required: "Email is required",
validate: (value) => checkEmailValidity(value) || "Email is invalid",
}}
render={({ field: { value, onChange, ref } }) => (
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
ref={ref}
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"
disabled
/>
{value.length > 0 && (
<XCircle
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={handleEmailClear}
/>
)}
</div>
)}
/>
</div>
<div>
<Controller
control={control}
name="token"
rules={{
required: "Code is required",
}}
render={({ field: { value, onChange } }) => (
<Input
value={value}
onChange={onChange}
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
/>
)}
/>
<div className="w-full text-right">
<button
type="button"
onClick={handleRequestNewCode}
className={`text-xs ${
isRequestNewCodeDisabled
? "text-onboarding-text-300"
: "text-onboarding-text-200 hover:text-custom-primary-100"
}`}
disabled={isRequestNewCodeDisabled}
>
{resendTimerCode > 0
? `Request new code in ${resendTimerCode}s`
: isRequestingNewCode
? "Requesting new code"
: "Request new code"}
</button>
</div>
</div>
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={!isValid} loading={isSubmitting}>
{submitButtonText}
</Button>
</form>
</>
);
};

View file

@ -1,107 +0,0 @@
import React from "react";
import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form";
import { XCircle } from "lucide-react";
import { IEmailCheckData } from "@plane/types";
// services
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { checkEmailValidity } from "@/helpers/string.helper";
import { AuthService } from "@/services/auth.service";
// ui
// helpers
// types
type Props = {
onSubmit: () => void;
updateEmail: (email: string) => void;
};
type TEmailFormValues = {
email: string;
};
const authService = new AuthService();
export const SignUpEmailForm: React.FC<Props> = observer((props) => {
const { onSubmit, updateEmail } = props;
// hooks
const {
control,
formState: { errors, isSubmitting, isValid },
handleSubmit,
} = useForm<TEmailFormValues>({
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(() => onSubmit())
.catch((err) =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
})
);
};
return (
<>
<h1 className="sm:text-2.5xl text-center text-2xl font-medium text-onboarding-text-100">
Get on your flight deck
</h1>
<p className="mt-2.5 text-center text-sm text-onboarding-text-200">
Create or join a workspace. Start with your e-mail.
</p>
<form onSubmit={handleSubmit(handleFormSubmit)} className="mx-auto mt-8 space-y-4 sm:w-96">
<div className="space-y-1">
<Controller
control={control}
name="email"
rules={{
required: "Email is required",
validate: (value) => checkEmailValidity(value) || "Email is invalid",
}}
render={({ field: { value, onChange } }) => (
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
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"
autoFocus
/>
{value.length > 0 && (
<XCircle
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => onChange("")}
/>
)}
</div>
)}
/>
</div>
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={!isValid} loading={isSubmitting}>
Verify
</Button>
</form>
</>
);
});

View file

@ -1,5 +0,0 @@
export * from "./email";
export * from "./optional-set-password";
export * from "./password";
export * from "./root";
export * from "./unique-code";

View file

@ -1,194 +0,0 @@
import React, { useState } from "react";
import { Controller, useForm } from "react-hook-form";
// services
import { Eye, EyeOff } from "lucide-react";
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { ESignUpSteps } from "@/components/account";
import { PASSWORD_CREATE_SKIPPED, SETUP_PASSWORD } from "@/constants/event-tracker";
import { checkEmailValidity } from "@/helpers/string.helper";
import { useEventTracker } from "@/hooks/store";
import { AuthService } from "@/services/auth.service";
// hooks
// ui
// helpers
// components
// constants
// icons
type Props = {
email: string;
handleStepChange: (step: ESignUpSteps) => void;
handleSignInRedirection: () => Promise<void>;
};
type TCreatePasswordFormValues = {
email: string;
password: string;
};
const defaultValues: TCreatePasswordFormValues = {
email: "",
password: "",
};
// services
const authService = new AuthService();
export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
const { email, handleSignInRedirection } = props;
// states
const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false);
const [showPassword, setShowPassword] = useState(false);
// store hooks
const { captureEvent } = useEventTracker();
// form info
const {
control,
formState: { errors, isSubmitting, isValid },
handleSubmit,
} = useForm<TCreatePasswordFormValues>({
defaultValues: {
...defaultValues,
email,
},
mode: "onChange",
reValidateMode: "onChange",
});
const handleCreatePassword = async (formData: TCreatePasswordFormValues) => {
const payload = {
password: formData.password,
};
await authService
.setPassword(payload)
.then(async () => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Password created successfully.",
});
captureEvent(SETUP_PASSWORD, {
state: "SUCCESS",
first_time: true,
});
await handleSignInRedirection();
})
.catch((err) => {
captureEvent(SETUP_PASSWORD, {
state: "FAILED",
first_time: true,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
});
});
};
const handleGoToWorkspace = async () => {
setIsGoingToWorkspace(true);
await handleSignInRedirection().finally(() => {
captureEvent(PASSWORD_CREATE_SKIPPED, {
state: "SUCCESS",
first_time: true,
});
setIsGoingToWorkspace(false);
});
};
return (
<>
<h1 className="sm:text-2.5xl text-center text-2xl font-medium text-onboarding-text-100">Moving to the runway</h1>
<p className="mt-2.5 text-center text-sm text-onboarding-text-200">
Let{"'"}s set a password so
<br />
you can do away with codes.
</p>
<form onSubmit={handleSubmit(handleCreatePassword)} className="mx-auto mt-5 space-y-4 sm:w-96">
<Controller
control={control}
name="email"
rules={{
required: "Email is required",
validate: (value) => checkEmailValidity(value) || "Email is invalid",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.email)}
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
/>
)}
/>
<div>
<Controller
control={control}
name="password"
rules={{
required: "Password is required",
}}
render={({ field: { value, onChange } }) => (
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
value={value}
onChange={onChange}
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"
minLength={8}
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>
)}
/>
<p className="mt-2 pb-3 text-xs text-onboarding-text-200">
This password will continue to be your account{"'"}s password.
</p>
</div>
<div className="space-y-2.5">
<Button
type="submit"
variant="primary"
className="w-full"
size="xl"
disabled={!isValid}
loading={isSubmitting}
>
Set password
</Button>
<Button
type="button"
variant="outline-primary"
className="w-full"
size="xl"
onClick={handleGoToWorkspace}
loading={isGoingToWorkspace}
>
Skip to setup
</Button>
</div>
</form>
</>
);
};

View file

@ -1,153 +0,0 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { Controller, useForm } from "react-hook-form";
import { Eye, EyeOff, XCircle } from "lucide-react";
import { IPasswordSignInData } from "@plane/types";
// services
// ui
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
// helpers
import { checkEmailValidity } from "@/helpers/string.helper";
import { AuthService } from "@/services/auth.service";
// types
type Props = {
onSubmit: () => Promise<void>;
};
type TPasswordFormValues = {
email: string;
password: string;
};
const defaultValues: TPasswordFormValues = {
email: "",
password: "",
};
const authService = new AuthService();
export const SignUpPasswordForm: React.FC<Props> = observer((props) => {
const { onSubmit } = props;
// states
const [showPassword, setShowPassword] = useState(false);
// form info
const {
control,
formState: { errors, isSubmitting, isValid },
handleSubmit,
} = useForm<TPasswordFormValues>({
defaultValues: {
...defaultValues,
},
mode: "onChange",
reValidateMode: "onChange",
});
const handleFormSubmit = async (formData: TPasswordFormValues) => {
const payload: IPasswordSignInData = {
email: formData.email,
password: formData.password,
};
await authService
.passwordSignIn(payload)
.then(async () => await onSubmit())
.catch((err) =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
})
);
};
return (
<>
<h1 className="sm:text-2.5xl text-center text-2xl font-medium text-onboarding-text-100">
Get on your flight deck
</h1>
<p className="mt-2.5 text-center text-sm text-onboarding-text-200">
Create or join a workspace. Start with your e-mail.
</p>
<form onSubmit={handleSubmit(handleFormSubmit)} className="mx-auto mt-5 space-y-4 sm:w-96">
<div>
<Controller
control={control}
name="email"
rules={{
required: "Email is required",
validate: (value) => checkEmailValidity(value) || "Email is invalid",
}}
render={({ field: { value, onChange } }) => (
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
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"
/>
{value.length > 0 && (
<XCircle
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => onChange("")}
/>
)}
</div>
)}
/>
</div>
<div>
<Controller
control={control}
name="password"
rules={{
required: "Password is required",
}}
render={({ field: { value, onChange } }) => (
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
value={value}
onChange={onChange}
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
/>
{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>
)}
/>
<p className="mt-2 pb-3 text-xs text-onboarding-text-200">
This password will continue to be your account{"'"}s password.
</p>
</div>
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={!isValid} loading={isSubmitting}>
Create account
</Button>
<p className="text-xs text-onboarding-text-200">
When you click the button above, you agree with our{" "}
<Link href="https://plane.so/terms-and-conditions" target="_blank" rel="noopener noreferrer">
<span className="font-semibold underline">terms and conditions of service.</span>
</Link>
</p>
</form>
</>
);
});

View file

@ -1,106 +0,0 @@
import React, { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
// hooks
import Link from "next/link";
import {
OAuthOptions,
SignUpEmailForm,
SignUpOptionalSetPasswordForm,
SignUpPasswordForm,
SignUpUniqueCodeForm,
TermsAndConditions,
} from "@/components/account";
import { NAVIGATE_TO_SIGNIN } from "@/constants/event-tracker";
import { useApplication, useEventTracker } from "@/hooks/store";
import useSignInRedirection from "@/hooks/use-sign-in-redirection";
// components
// constants
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<ESignUpSteps | null>(null);
const [email, setEmail] = useState("");
// sign in redirection hook
const { handleRedirection } = useSignInRedirection();
// mobx store
const {
config: { envConfig },
} = useApplication();
const { captureEvent } = useEventTracker();
// 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 (
<>
<div className="mx-auto flex flex-col">
<>
{signInStep === ESignUpSteps.EMAIL && (
<SignUpEmailForm onSubmit={handleEmailVerification} updateEmail={(newEmail) => setEmail(newEmail)} />
)}
{signInStep === ESignUpSteps.UNIQUE_CODE && (
<SignUpUniqueCodeForm
email={email}
handleEmailClear={() => {
setEmail("");
setSignInStep(ESignUpSteps.EMAIL);
}}
onSubmit={handleUniqueCodeSignIn}
/>
)}
{signInStep === ESignUpSteps.PASSWORD && <SignUpPasswordForm onSubmit={handlePasswordSignIn} />}
{signInStep === ESignUpSteps.OPTIONAL_SET_PASSWORD && (
<SignUpOptionalSetPasswordForm
email={email}
handleSignInRedirection={handleRedirection}
handleStepChange={(step) => setSignInStep(step)}
/>
)}
</>
</div>
{isOAuthEnabled && signInStep && OAUTH_ENABLED_STEPS.includes(signInStep) && (
<>
<OAuthOptions handleSignInRedirection={handleRedirection} type="sign_up" />
<p className="text-xs text-onboarding-text-300 text-center mt-6">
Already using Plane?{" "}
<Link
href="/"
onClick={() => captureEvent(NAVIGATE_TO_SIGNIN, {})}
className="text-custom-primary-100 font-medium underline"
>
Sign in
</Link>
</p>
<TermsAndConditions isSignUp />
</>
)}
</>
);
});

View file

@ -1,222 +0,0 @@
import React, { useState } from "react";
import Link from "next/link";
import { Controller, useForm } from "react-hook-form";
import { XCircle } from "lucide-react";
import { IEmailCheckData, IMagicSignInData } from "@plane/types";
// services
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { CODE_VERIFIED } from "@/constants/event-tracker";
import { checkEmailValidity } from "@/helpers/string.helper";
import { useEventTracker } from "@/hooks/store";
import useTimer from "@/hooks/use-timer";
import { AuthService } from "@/services/auth.service";
import { UserService } from "@/services/user.service";
// hooks
// ui
// helpers
// types
// constants
type Props = {
email: string;
handleEmailClear: () => void;
onSubmit: (isPasswordAutoset: boolean) => Promise<void>;
};
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> = (props) => {
const { email, handleEmailClear, onSubmit } = props;
// states
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
// store hooks
const { captureEvent } = useEventTracker();
// timer
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30);
// form info
const {
control,
formState: { errors, isSubmitting, isValid },
getValues,
handleSubmit,
reset,
} = useForm<TUniqueCodeFormValues>({
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 () => {
captureEvent(CODE_VERIFIED, {
state: "SUCCESS",
});
const currentUser = await userService.currentUser();
await onSubmit(currentUser.is_password_autoset);
})
.catch((err) => {
captureEvent(CODE_VERIFIED, {
state: "FAILED",
});
setToast({
type: TOAST_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);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "A new unique code has been sent to your email.",
});
reset({
email: formData.email,
token: "",
});
})
.catch((err) =>
setToast({
type: TOAST_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 (
<>
<h1 className="sm:text-2.5xl text-center text-2xl font-medium text-onboarding-text-100">Moving to the runway</h1>
<p className="mt-2.5 text-center text-sm text-onboarding-text-200">
Paste the code you got at
<br />
<span className="font-semibold text-custom-primary-100">{email}</span> below.
</p>
<form onSubmit={handleSubmit(handleUniqueCodeSignIn)} className="mx-auto mt-5 space-y-4 sm:w-96">
<div>
<Controller
control={control}
name="email"
rules={{
required: "Email is required",
validate: (value) => checkEmailValidity(value) || "Email is invalid",
}}
render={({ field: { value, onChange, ref } }) => (
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
ref={ref}
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"
disabled
/>
{value.length > 0 && (
<XCircle
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={handleEmailClear}
/>
)}
</div>
)}
/>
</div>
<div>
<Controller
control={control}
name="token"
rules={{
required: "Code is required",
}}
render={({ field: { value, onChange } }) => (
<Input
value={value}
onChange={onChange}
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
/>
)}
/>
<div className="w-full text-right">
<button
type="button"
onClick={handleRequestNewCode}
className={`text-xs ${
isRequestNewCodeDisabled
? "text-onboarding-text-300"
: "text-onboarding-text-200 hover:text-custom-primary-100"
}`}
disabled={isRequestNewCodeDisabled}
>
{resendTimerCode > 0
? `Request new code in ${resendTimerCode}s`
: isRequestingNewCode
? "Requesting new code"
: "Request new code"}
</button>
</div>
</div>
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={!isValid} loading={isSubmitting}>
Create account
</Button>
<p className="text-xs text-onboarding-text-200">
When you click the button above, you agree with our{" "}
<Link href="https://plane.so/terms-and-conditions" target="_blank" rel="noopener noreferrer">
<span className="font-semibold underline">terms and conditions of service.</span>
</Link>
</p>
</form>
</>
);
};