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:
parent
ae43d05714
commit
59335618b4
903 changed files with 25736 additions and 16041 deletions
29
web/components/account/auth-forms/auth-banner.tsx
Normal file
29
web/components/account/auth-forms/auth-banner.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
108
web/components/account/auth-forms/auth-header.tsx
Normal file
108
web/components/account/auth-forms/auth-header.tsx
Normal 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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
150
web/components/account/auth-forms/auth-root.tsx
Normal file
150
web/components/account/auth-forms/auth-root.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
77
web/components/account/auth-forms/email.tsx
Normal file
77
web/components/account/auth-forms/email.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
|
|
@ -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";
|
||||
224
web/components/account/auth-forms/password.tsx
Normal file
224
web/components/account/auth-forms/password.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
177
web/components/account/auth-forms/unique-code.tsx
Normal file
177
web/components/account/auth-forms/unique-code.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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!",
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export * from "./github-sign-in";
|
||||
export * from "./google-sign-in";
|
||||
export * from "./o-auth-options";
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
39
web/components/account/oauth/github-button.tsx
Normal file
39
web/components/account/oauth/github-button.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
33
web/components/account/oauth/google-button.tsx
Normal file
33
web/components/account/oauth/google-button.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
3
web/components/account/oauth/index.ts
Normal file
3
web/components/account/oauth/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./oauth-options";
|
||||
export * from "./google-button";
|
||||
export * from "./github-button";
|
||||
28
web/components/account/oauth/oauth-options.tsx
Normal file
28
web/components/account/oauth/oauth-options.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
67
web/components/account/password-strength-meter.tsx
Normal file
67
web/components/account/password-strength-meter.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
export * from "./email";
|
||||
export * from "./optional-set-password";
|
||||
export * from "./password";
|
||||
export * from "./root";
|
||||
export * from "./unique-code";
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -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 />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue