import { FooterNote } from '@/auth/sign-in-up/components/FooterNote'; import { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSeparator'; import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword'; import { SignInUpMode, useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp'; import { useSignInUpForm, validationSchema, } from '@/auth/sign-in-up/hooks/useSignInUpForm'; import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle'; import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft'; import { SignInUpStep } from '@/auth/states/signInUpStepState'; import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState'; import { authProvidersState } from '@/client-config/states/authProvidersState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState'; import { TextInput } from '@/ui/input/components/TextInput'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { motion } from 'framer-motion'; import { useMemo, useState } from 'react'; import { Controller } from 'react-hook-form'; import { useRecoilState, useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; import { ActionLink, IconGoogle, IconKey, IconMicrosoft, Loader, MainButton, } from 'twenty-ui'; import { isDefined } from '~/utils/isDefined'; const StyledContentContainer = styled.div` margin-bottom: ${({ theme }) => theme.spacing(8)}; margin-top: ${({ theme }) => theme.spacing(4)}; `; const StyledForm = styled.form` align-items: center; display: flex; flex-direction: column; width: 100%; `; const StyledFullWidthMotionDiv = styled(motion.div)` width: 100%; `; const StyledInputContainer = styled.div` margin-bottom: ${({ theme }) => theme.spacing(3)}; `; export const SignInUpForm = () => { const captchaProvider = useRecoilValue(captchaProviderState); const isRequestingCaptchaToken = useRecoilValue( isRequestingCaptchaTokenState, ); const [authProviders] = useRecoilState(authProvidersState); const [showErrors, setShowErrors] = useState(false); const { signInWithGoogle } = useSignInWithGoogle(); const { signInWithMicrosoft } = useSignInWithMicrosoft(); const { form } = useSignInUpForm(); const { handleResetPassword } = useHandleResetPassword(); const { signInUpStep, signInUpMode, continueWithCredentials, continueWithEmail, continueWithSSO, submitCredentials, submitSSOEmail, } = useSignInUp(form); const toggleSSOMode = () => { if (signInUpStep === SignInUpStep.SSOEmail) { continueWithEmail(); } else { continueWithSSO(); } }; const handleKeyDown = async ( event: React.KeyboardEvent, ) => { if (event.key === Key.Enter) { event.preventDefault(); if (signInUpStep === SignInUpStep.Init) { continueWithEmail(); } else if (signInUpStep === SignInUpStep.Email) { if (isDefined(form?.formState?.errors?.email)) { setShowErrors(true); return; } continueWithCredentials(); } else if (signInUpStep === SignInUpStep.Password) { if (!form.formState.isSubmitting) { setShowErrors(true); form.handleSubmit(submitCredentials)(); } } else if (signInUpStep === SignInUpStep.SSOEmail) { submitSSOEmail(form.getValues('email')); } } }; const buttonTitle = useMemo(() => { if (signInUpStep === SignInUpStep.Init) { return 'Continue With Email'; } if (signInUpStep === SignInUpStep.Email) { return 'Continue'; } if (signInUpStep === SignInUpStep.SSOEmail) { return 'Continue with SSO'; } return signInUpMode === SignInUpMode.SignIn ? 'Sign in' : 'Sign up'; }, [signInUpMode, signInUpStep]); const theme = useTheme(); const shouldWaitForCaptchaToken = signInUpStep !== SignInUpStep.Init && isDefined(captchaProvider?.provider) && isRequestingCaptchaToken; const isEmailStepSubmitButtonDisabledCondition = signInUpStep === SignInUpStep.Email && (!validationSchema.shape.email.safeParse(form.watch('email')).success || shouldWaitForCaptchaToken); // TODO: isValid is actually a proxy function. If it is not rendered the first time, react might not trigger re-renders // We make the isValid check synchronous and update a reactState to make sure this does not happen const isPasswordStepSubmitButtonDisabledCondition = signInUpStep === SignInUpStep.Password && (!form.formState.isValid || form.formState.isSubmitting || shouldWaitForCaptchaToken); const isSubmitButtonDisabled = isEmailStepSubmitButtonDisabledCondition || isPasswordStepSubmitButtonDisabledCondition; return ( <> {authProviders.google && ( <> } title="Continue with Google" onClick={signInWithGoogle} fullWidth /> )} {authProviders.microsoft && ( <> } title="Continue with Microsoft" onClick={signInWithMicrosoft} fullWidth /> )} {authProviders.sso && ( <> } title={ signInUpStep === SignInUpStep.SSOEmail ? 'Continue with email' : 'Single sign-on (SSO)' } onClick={toggleSSOMode} fullWidth /> )} {authProviders.password && (signInUpStep === SignInUpStep.Password || signInUpStep === SignInUpStep.Email || signInUpStep === SignInUpStep.Init) && ( { event.preventDefault(); }} > {signInUpStep !== SignInUpStep.Init && ( ( { onChange(value); if (signInUpStep === SignInUpStep.Password) { continueWithEmail(); } }} error={showErrors ? error?.message : undefined} fullWidth disableHotkeys onKeyDown={handleKeyDown} /> )} /> )} {signInUpStep === SignInUpStep.Password && ( ( )} /> )} { if (signInUpStep === SignInUpStep.Init) { continueWithEmail(); return; } if (signInUpStep === SignInUpStep.Email) { if (isDefined(form?.formState?.errors?.email)) { setShowErrors(true); return; } continueWithCredentials(); return; } setShowErrors(true); form.handleSubmit(submitCredentials)(); }} Icon={() => form.formState.isSubmitting && } disabled={isSubmitButtonDisabled} fullWidth /> )} { event.preventDefault(); }} > {signInUpStep === SignInUpStep.SSOEmail && ( <> ( )} /> { setShowErrors(true); submitSSOEmail(form.getValues('email')); }} Icon={() => form.formState.isSubmitting && } disabled={isSubmitButtonDisabled} fullWidth /> )} {signInUpStep === SignInUpStep.Password && ( Forgot your password? )} {signInUpStep === SignInUpStep.Init && } ); };