Refactor login (#748)
* wip refactor login * wip refactor login * Fix lint conflicts * Complete Sign In only * Feature complete * Fix test * Fix test
This commit is contained in:
@ -4,6 +4,8 @@ type Props = React.ComponentProps<'div'>;
|
||||
|
||||
const StyledLogo = styled.div`
|
||||
height: 48px;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
@ -11,9 +11,6 @@ const StyledContainer = styled.div`
|
||||
flex-direction: column;
|
||||
padding: ${({ theme }) => theme.spacing(10)};
|
||||
width: calc(400px - ${({ theme }) => theme.spacing(10 * 2)});
|
||||
> * + * {
|
||||
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||
}
|
||||
`;
|
||||
|
||||
export function AuthModal({ children, ...restProps }: Props) {
|
||||
@ -1,71 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { keyframes } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useOnboardingStatus } from '../hooks/useOnboardingStatus';
|
||||
import { OnboardingStatus } from '../utils/getOnboardingStatus';
|
||||
|
||||
const EmptyContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const FadeInStyle = styled.div`
|
||||
animation: ${fadeIn} 1s forwards;
|
||||
opacity: 0;
|
||||
`;
|
||||
|
||||
export function RequireOnboarded({
|
||||
children,
|
||||
}: {
|
||||
children: JSX.Element;
|
||||
}): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onboardingStatus = useOnboardingStatus();
|
||||
|
||||
useEffect(() => {
|
||||
if (onboardingStatus === OnboardingStatus.OngoingUserCreation) {
|
||||
navigate('/auth');
|
||||
} else if (onboardingStatus === OnboardingStatus.OngoingWorkspaceCreation) {
|
||||
navigate('/auth/create/workspace');
|
||||
} else if (onboardingStatus === OnboardingStatus.OngoingProfileCreation) {
|
||||
navigate('/auth/create/profile');
|
||||
}
|
||||
}, [onboardingStatus, navigate]);
|
||||
|
||||
if (onboardingStatus && onboardingStatus !== OnboardingStatus.Completed) {
|
||||
return (
|
||||
<EmptyContainer>
|
||||
<FadeInStyle>
|
||||
{onboardingStatus === OnboardingStatus.OngoingUserCreation && (
|
||||
<div>
|
||||
Please hold on a moment, we're directing you to our login page...
|
||||
</div>
|
||||
)}
|
||||
{onboardingStatus !== OnboardingStatus.OngoingUserCreation && (
|
||||
<div>
|
||||
Please hold on a moment, we're directing you to our onboarding
|
||||
flow...
|
||||
</div>
|
||||
)}
|
||||
</FadeInStyle>
|
||||
</EmptyContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { keyframes } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useOnboardingStatus } from '../hooks/useOnboardingStatus';
|
||||
import { OnboardingStatus } from '../utils/getOnboardingStatus';
|
||||
|
||||
const EmptyContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const FadeInStyle = styled.div`
|
||||
animation: ${fadeIn} 1s forwards;
|
||||
opacity: 0;
|
||||
`;
|
||||
|
||||
export function RequireOnboarding({
|
||||
children,
|
||||
}: {
|
||||
children: JSX.Element;
|
||||
}): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onboardingStatus = useOnboardingStatus();
|
||||
|
||||
useEffect(() => {
|
||||
if (onboardingStatus === OnboardingStatus.Completed) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [navigate, onboardingStatus]);
|
||||
|
||||
if (onboardingStatus === OnboardingStatus.Completed) {
|
||||
return (
|
||||
<EmptyContainer>
|
||||
<FadeInStyle>
|
||||
Please hold on a moment, we're directing you to the app...
|
||||
</FadeInStyle>
|
||||
</EmptyContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
@ -7,7 +7,6 @@ type OwnProps = {
|
||||
|
||||
const StyledSubTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export function SubTitle({ children }: OwnProps): JSX.Element {
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { AnimatedTextWord } from '@/ui/animation/components/AnimatedTextWord';
|
||||
import { AnimatedEaseIn } from '../../ui/animation/components/AnimatedEaseIn';
|
||||
|
||||
type Props = React.PropsWithChildren & {
|
||||
animate?: boolean;
|
||||
@ -11,17 +11,17 @@ const StyledTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.xl};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
`;
|
||||
|
||||
const StyledAnimatedTextWord = styled(AnimatedTextWord)`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.xl};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export function Title({ children, animate = false }: Props) {
|
||||
if (animate && typeof children === 'string') {
|
||||
return <StyledAnimatedTextWord text={children} />;
|
||||
if (animate) {
|
||||
return (
|
||||
<StyledTitle>
|
||||
<AnimatedEaseIn>{children}</AnimatedEaseIn>
|
||||
</StyledTitle>
|
||||
);
|
||||
}
|
||||
|
||||
return <StyledTitle>{children}</StyledTitle>;
|
||||
@ -1,8 +1,10 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import {
|
||||
useChallengeMutation,
|
||||
useCheckUserExistsLazyQuery,
|
||||
useSignUpMutation,
|
||||
useVerifyMutation,
|
||||
} from '~/generated/graphql';
|
||||
@ -13,12 +15,16 @@ import { tokenPairState } from '../states/tokenPairState';
|
||||
|
||||
export function useAuth() {
|
||||
const [, setTokenPair] = useRecoilState(tokenPairState);
|
||||
const [, setCurrentUser] = useRecoilState(currentUserState);
|
||||
const [, setIsAuthenticating] = useRecoilState(isAuthenticatingState);
|
||||
const [, setCurrentUser] = useRecoilState(currentUserState);
|
||||
|
||||
const [challenge] = useChallengeMutation();
|
||||
const [signUp] = useSignUpMutation();
|
||||
const [verify] = useVerifyMutation();
|
||||
const [checkUserExistsQuery, { data: checkUserExistsData }] =
|
||||
useCheckUserExistsLazyQuery();
|
||||
|
||||
const client = useApolloClient();
|
||||
|
||||
const handleChallenge = useCallback(
|
||||
async (email: string, password: string) => {
|
||||
@ -65,21 +71,25 @@ export function useAuth() {
|
||||
[setIsAuthenticating, setTokenPair, verify],
|
||||
);
|
||||
|
||||
const handleLogin = useCallback(
|
||||
const handleCrendentialsSignIn = useCallback(
|
||||
async (email: string, password: string) => {
|
||||
const { loginToken } = await handleChallenge(email, password);
|
||||
|
||||
await handleVerify(loginToken.token);
|
||||
const { user } = await handleVerify(loginToken.token);
|
||||
return { user };
|
||||
},
|
||||
[handleChallenge, handleVerify],
|
||||
);
|
||||
|
||||
const handleLogout = useCallback(() => {
|
||||
const handleSignOut = useCallback(() => {
|
||||
setTokenPair(null);
|
||||
setCurrentUser(null);
|
||||
}, [setTokenPair, setCurrentUser]);
|
||||
client.clearStore().then(() => {
|
||||
sessionStorage.clear();
|
||||
});
|
||||
}, [setTokenPair, client, setCurrentUser]);
|
||||
|
||||
const handleSignUp = useCallback(
|
||||
const handleCredentialsSignUp = useCallback(
|
||||
async (email: string, password: string, workspaceInviteHash?: string) => {
|
||||
const signUpResult = await signUp({
|
||||
variables: {
|
||||
@ -97,16 +107,33 @@ export function useAuth() {
|
||||
throw new Error('No login token');
|
||||
}
|
||||
|
||||
await handleVerify(signUpResult.data?.signUp.loginToken.token);
|
||||
const { user } = await handleVerify(
|
||||
signUpResult.data?.signUp.loginToken.token,
|
||||
);
|
||||
|
||||
setCurrentUser(user);
|
||||
|
||||
return { user };
|
||||
},
|
||||
[signUp, handleVerify],
|
||||
[signUp, handleVerify, setCurrentUser],
|
||||
);
|
||||
|
||||
const handleGoogleLogin = useCallback((workspaceInviteHash?: string) => {
|
||||
window.location.href =
|
||||
`${process.env.REACT_APP_AUTH_URL}/google/${
|
||||
workspaceInviteHash ? '?inviteHash=' + workspaceInviteHash : ''
|
||||
}` || '';
|
||||
}, []);
|
||||
|
||||
return {
|
||||
challenge: handleChallenge,
|
||||
verify: handleVerify,
|
||||
login: handleLogin,
|
||||
signUp: handleSignUp,
|
||||
logout: handleLogout,
|
||||
|
||||
checkUserExists: { checkUserExistsData, checkUserExistsQuery },
|
||||
|
||||
signOut: handleSignOut,
|
||||
signUpWithCredentials: handleCredentialsSignUp,
|
||||
signInWithCredentials: handleCrendentialsSignIn,
|
||||
signInWithGoogle: handleGoogleLogin,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useIsLogged } from '../hooks/useIsLogged';
|
||||
@ -12,10 +11,5 @@ export function useOnboardingStatus(): OnboardingStatus | undefined {
|
||||
const [currentUser] = useRecoilState(currentUserState);
|
||||
const isLoggedIn = useIsLogged();
|
||||
|
||||
const onboardingStatus = useMemo(
|
||||
() => getOnboardingStatus(isLoggedIn, currentUser),
|
||||
[currentUser, isLoggedIn],
|
||||
);
|
||||
|
||||
return onboardingStatus;
|
||||
return getOnboardingStatus(isLoggedIn, currentUser);
|
||||
}
|
||||
|
||||
@ -48,6 +48,11 @@ export const VERIFY = gql`
|
||||
logo
|
||||
}
|
||||
}
|
||||
settings {
|
||||
id
|
||||
colorScheme
|
||||
locale
|
||||
}
|
||||
}
|
||||
tokens {
|
||||
accessToken {
|
||||
|
||||
210
front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx
Normal file
210
front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx
Normal file
@ -0,0 +1,210 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { AnimatedEaseIn } from '@/ui/animation/components/AnimatedEaseIn';
|
||||
import { MainButton } from '@/ui/button/components/MainButton';
|
||||
import { IconBrandGoogle } from '@/ui/icon';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
import { Logo } from '../../components/Logo';
|
||||
import { Title } from '../../components/Title';
|
||||
import { SignInUpMode, SignInUpStep, useSignInUp } from '../hooks/useSignInUp';
|
||||
|
||||
import { FooterNote } from './FooterNote';
|
||||
import { HorizontalSeparator } from './HorizontalSeparator';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
width: 200px;
|
||||
`;
|
||||
|
||||
const StyledFooterNote = styled(FooterNote)`
|
||||
max-width: 280px;
|
||||
`;
|
||||
|
||||
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 function SignInUpForm() {
|
||||
const {
|
||||
authProviders,
|
||||
signInWithGoogle,
|
||||
signInUpStep,
|
||||
signInUpMode,
|
||||
showErrors,
|
||||
setShowErrors,
|
||||
continueWithCredentials,
|
||||
continueWithEmail,
|
||||
submitCredentials,
|
||||
form: {
|
||||
control,
|
||||
watch,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
},
|
||||
} = useSignInUp();
|
||||
const theme = useTheme();
|
||||
|
||||
const buttonTitle = useMemo(() => {
|
||||
if (signInUpStep === SignInUpStep.Init) {
|
||||
return 'Continue With Email';
|
||||
}
|
||||
|
||||
if (signInUpStep === SignInUpStep.Email) {
|
||||
return 'Continue';
|
||||
}
|
||||
|
||||
return signInUpMode === SignInUpMode.SignIn ? 'Sign in' : 'Sign up';
|
||||
}, [signInUpMode, signInUpStep]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatedEaseIn>
|
||||
<Logo />
|
||||
</AnimatedEaseIn>
|
||||
<Title animate>
|
||||
{signInUpMode === SignInUpMode.SignIn
|
||||
? 'Sign in to Twenty'
|
||||
: 'Sign up to Twenty'}
|
||||
</Title>
|
||||
<StyledContentContainer>
|
||||
{authProviders.google && (
|
||||
<>
|
||||
<MainButton
|
||||
icon={<IconBrandGoogle size={theme.icon.size.sm} stroke={4} />}
|
||||
title="Continue with Google"
|
||||
onClick={signInWithGoogle}
|
||||
fullWidth
|
||||
/>
|
||||
<HorizontalSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
<StyledForm
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
{signInUpStep !== SignInUpStep.Init && (
|
||||
<StyledFullWidthMotionDiv
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 800,
|
||||
damping: 35,
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
render={({
|
||||
field: { onChange, onBlur, value },
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<StyledInputContainer>
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={value}
|
||||
placeholder="Email"
|
||||
onBlur={onBlur}
|
||||
onChange={(value: string) => {
|
||||
onChange(value);
|
||||
if (signInUpStep === SignInUpStep.Password) {
|
||||
continueWithEmail();
|
||||
}
|
||||
}}
|
||||
error={showErrors ? error?.message : undefined}
|
||||
fullWidth
|
||||
disableHotkeys
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
/>
|
||||
</StyledFullWidthMotionDiv>
|
||||
)}
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<StyledFullWidthMotionDiv
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 800,
|
||||
damping: 35,
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
name="password"
|
||||
control={control}
|
||||
render={({
|
||||
field: { onChange, onBlur, value },
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<StyledInputContainer>
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={value}
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
error={showErrors ? error?.message : undefined}
|
||||
fullWidth
|
||||
disableHotkeys
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
/>
|
||||
</StyledFullWidthMotionDiv>
|
||||
)}
|
||||
|
||||
<MainButton
|
||||
variant="secondary"
|
||||
title={buttonTitle}
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
if (signInUpStep === SignInUpStep.Init) {
|
||||
continueWithEmail();
|
||||
return;
|
||||
}
|
||||
if (signInUpStep === SignInUpStep.Email) {
|
||||
continueWithCredentials();
|
||||
return;
|
||||
}
|
||||
setShowErrors(true);
|
||||
handleSubmit(submitCredentials)();
|
||||
}}
|
||||
disabled={
|
||||
SignInUpStep.Init
|
||||
? false
|
||||
: signInUpStep === SignInUpStep.Email
|
||||
? !watch('email')
|
||||
: !watch('email') || !watch('password') || isSubmitting
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledForm>
|
||||
</StyledContentContainer>
|
||||
<StyledFooterNote>
|
||||
By using Twenty, you agree to the Terms of Service and Data Processing
|
||||
Agreement.
|
||||
</StyledFooterNote>
|
||||
</>
|
||||
);
|
||||
}
|
||||
178
front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx
Normal file
178
front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
import { isDemoModeState } from '@/client-config/states/isDemoModeState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
|
||||
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import { currentUserState } from '../../states/currentUserState';
|
||||
import { PASSWORD_REGEX } from '../../utils/passwordRegex';
|
||||
|
||||
export enum SignInUpMode {
|
||||
SignIn = 'sign-in',
|
||||
SignUp = 'sign-up',
|
||||
}
|
||||
|
||||
export enum SignInUpStep {
|
||||
Init = 'init',
|
||||
Email = 'email',
|
||||
Password = 'password',
|
||||
}
|
||||
const validationSchema = Yup.object()
|
||||
.shape({
|
||||
exist: Yup.boolean().required(),
|
||||
email: Yup.string()
|
||||
.email('Email must be a valid email')
|
||||
.required('Email must be a valid email'),
|
||||
password: Yup.string()
|
||||
.matches(PASSWORD_REGEX, 'Password must contain at least 8 characters')
|
||||
.required(),
|
||||
})
|
||||
.required();
|
||||
|
||||
type Form = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export function useSignInUp() {
|
||||
const navigate = useNavigate();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const isMatchingLocation = useIsMatchingLocation();
|
||||
const [authProviders] = useRecoilState(authProvidersState);
|
||||
const isDemoMode = useRecoilValue(isDemoModeState);
|
||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||
const [signInUpStep, setSignInUpStep] = useState<SignInUpStep>(
|
||||
SignInUpStep.Init,
|
||||
);
|
||||
const [signInUpMode, setSignInUpMode] = useState<SignInUpMode>(
|
||||
isMatchingLocation(AppPath.SignIn)
|
||||
? SignInUpMode.SignIn
|
||||
: SignInUpMode.SignUp,
|
||||
);
|
||||
const [showErrors, setShowErrors] = useState(false);
|
||||
const [, setCurrentUser] = useRecoilState(currentUserState);
|
||||
|
||||
const form = useForm<Form>({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
exist: false,
|
||||
email: isDemoMode ? 'tim@apple.dev' : '',
|
||||
password: isDemoMode ? 'Applecar2025' : '',
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const {
|
||||
signInWithCredentials,
|
||||
signUpWithCredentials,
|
||||
signInWithGoogle,
|
||||
checkUserExists: { checkUserExistsQuery },
|
||||
} = useAuth();
|
||||
|
||||
const continueWithEmail = useCallback(() => {
|
||||
setSignInUpStep(SignInUpStep.Email);
|
||||
setSignInUpMode(
|
||||
isMatchingLocation(AppPath.SignIn)
|
||||
? SignInUpMode.SignIn
|
||||
: SignInUpMode.SignUp,
|
||||
);
|
||||
}, [setSignInUpStep, setSignInUpMode, isMatchingLocation]);
|
||||
|
||||
const continueWithCredentials = useCallback(() => {
|
||||
checkUserExistsQuery({
|
||||
variables: {
|
||||
email: form.getValues('email'),
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (data?.checkUserExists.exists) {
|
||||
setSignInUpMode(SignInUpMode.SignIn);
|
||||
} else {
|
||||
setSignInUpMode(SignInUpMode.SignUp);
|
||||
}
|
||||
setSignInUpStep(SignInUpStep.Password);
|
||||
},
|
||||
});
|
||||
}, [setSignInUpStep, checkUserExistsQuery, form, setSignInUpMode]);
|
||||
|
||||
const submitCredentials: SubmitHandler<Form> = useCallback(
|
||||
async (data) => {
|
||||
try {
|
||||
if (!data.email || !data.password) {
|
||||
throw new Error('Email and password are required');
|
||||
}
|
||||
if (signInUpMode === SignInUpMode.SignIn) {
|
||||
const { user } = await signInWithCredentials(
|
||||
data.email,
|
||||
data.password,
|
||||
);
|
||||
setCurrentUser(user);
|
||||
} else {
|
||||
const { user } = await signUpWithCredentials(
|
||||
data.email,
|
||||
data.password,
|
||||
workspaceInviteHash,
|
||||
);
|
||||
setCurrentUser(user);
|
||||
}
|
||||
navigate('/create/workspace');
|
||||
} catch (err: any) {
|
||||
enqueueSnackBar(err?.message, {
|
||||
variant: 'error',
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
navigate,
|
||||
signInWithCredentials,
|
||||
signUpWithCredentials,
|
||||
workspaceInviteHash,
|
||||
enqueueSnackBar,
|
||||
signInUpMode,
|
||||
setCurrentUser,
|
||||
],
|
||||
);
|
||||
|
||||
const goBackToEmailStep = useCallback(() => {
|
||||
setSignInUpStep(SignInUpStep.Email);
|
||||
}, [setSignInUpStep]);
|
||||
|
||||
useScopedHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
if (signInUpStep === SignInUpStep.Init) {
|
||||
continueWithEmail();
|
||||
}
|
||||
|
||||
if (signInUpStep === SignInUpStep.Email) {
|
||||
continueWithCredentials();
|
||||
}
|
||||
|
||||
if (signInUpStep === SignInUpStep.Password) {
|
||||
form.handleSubmit(submitCredentials)();
|
||||
}
|
||||
},
|
||||
PageHotkeyScope.SignInUp,
|
||||
[continueWithEmail],
|
||||
);
|
||||
|
||||
return {
|
||||
authProviders,
|
||||
signInWithGoogle: () => signInWithGoogle(workspaceInviteHash),
|
||||
signInUpStep,
|
||||
signInUpMode,
|
||||
showErrors,
|
||||
setShowErrors,
|
||||
continueWithCredentials,
|
||||
continueWithEmail,
|
||||
goBackToEmailStep,
|
||||
submitCredentials,
|
||||
form,
|
||||
};
|
||||
}
|
||||
@ -17,11 +17,11 @@ import SubNavbar from '@/ui/navbar/components/SubNavbar';
|
||||
export function SettingsNavbar() {
|
||||
const theme = useTheme();
|
||||
|
||||
const { logout } = useAuth();
|
||||
const { signOut } = useAuth();
|
||||
|
||||
const handleLogout = useCallback(() => {
|
||||
logout();
|
||||
}, [logout]);
|
||||
signOut();
|
||||
}, [signOut]);
|
||||
|
||||
return (
|
||||
<SubNavbar backButtonTitle="Settings">
|
||||
|
||||
@ -1,5 +1,16 @@
|
||||
export enum AppPath {
|
||||
AuthCatchAll = `/auth/*`,
|
||||
// Not logged-in
|
||||
Verify = 'verify',
|
||||
SignIn = 'sign-in',
|
||||
SignUp = 'sign-up',
|
||||
Invite = 'invite/:workspaceInviteHash',
|
||||
|
||||
// Onboarding
|
||||
CreateWorkspace = 'create/workspace',
|
||||
CreateProfile = 'create/profile',
|
||||
|
||||
// Onboarded
|
||||
Index = '',
|
||||
PeoplePage = '/people',
|
||||
CompaniesPage = '/companies',
|
||||
CompanyShowPage = '/companies/:companyId',
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
export enum AuthPath {
|
||||
Index = '',
|
||||
Callback = 'callback',
|
||||
PasswordLogin = 'password-login',
|
||||
CreateWorkspace = 'create/workspace',
|
||||
CreateProfile = 'create/profile',
|
||||
InviteLink = 'invite/:workspaceInviteHash',
|
||||
}
|
||||
@ -1,8 +1,7 @@
|
||||
export enum PageHotkeyScope {
|
||||
Settings = 'settings',
|
||||
CreateWokspace = 'create-workspace',
|
||||
PasswordLogin = 'password-login',
|
||||
AuthIndex = 'auth-index',
|
||||
SignInUp = 'sign-in-up',
|
||||
CreateProfile = 'create-profile',
|
||||
ShowPage = 'show-page',
|
||||
PersonShowPage = 'person-show-page',
|
||||
|
||||
@ -9,7 +9,7 @@ type Props = Omit<
|
||||
|
||||
export function AnimatedEaseIn({
|
||||
children,
|
||||
duration = 0.8,
|
||||
duration = 0.3,
|
||||
...restProps
|
||||
}: Props) {
|
||||
const initial = { opacity: 0 };
|
||||
|
||||
@ -55,9 +55,9 @@ const StyledButton = styled.button<Pick<Props, 'fullWidth' | 'variant'>>`
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
justify-content: center;
|
||||
outline: none;
|
||||
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
|
||||
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
|
||||
|
||||
${({ theme, variant }) => {
|
||||
switch (variant) {
|
||||
case 'secondary':
|
||||
|
||||
@ -20,6 +20,7 @@ type OwnProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
|
||||
label?: string;
|
||||
onChange?: (text: string) => void;
|
||||
fullWidth?: boolean;
|
||||
disableHotkeys?: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
@ -104,6 +105,7 @@ export function TextInput({
|
||||
error,
|
||||
required,
|
||||
type,
|
||||
disableHotkeys = false,
|
||||
...props
|
||||
}: OwnProps): JSX.Element {
|
||||
const theme = useTheme();
|
||||
@ -117,16 +119,20 @@ export function TextInput({
|
||||
|
||||
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
|
||||
onFocus?.(e);
|
||||
setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput);
|
||||
if (!disableHotkeys) {
|
||||
setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
|
||||
onBlur?.(e);
|
||||
goBackToPreviousHotkeyScope();
|
||||
if (!disableHotkeys) {
|
||||
goBackToPreviousHotkeyScope();
|
||||
}
|
||||
};
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Enter, Key.Escape],
|
||||
[Key.Escape, Key.Enter],
|
||||
() => {
|
||||
inputRef.current?.blur();
|
||||
},
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { AnimatePresence, LayoutGroup } from 'framer-motion';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { AuthModal } from '@/auth/components/Modal';
|
||||
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
||||
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
||||
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
||||
import { NavbarAnimatedContainer } from '@/ui/navbar/components/NavbarAnimatedContainer';
|
||||
import { MOBILE_VIEWPORT } from '@/ui/themes/themes';
|
||||
import { AppNavbar } from '~/AppNavbar';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { CompaniesMockMode } from '~/pages/companies/CompaniesMockMode';
|
||||
|
||||
import { AppPath } from '../../../types/AppPath';
|
||||
import { isNavbarOpenedState } from '../states/isNavbarOpenedState';
|
||||
|
||||
const StyledLayout = styled.div`
|
||||
@ -38,22 +46,71 @@ type OwnProps = {
|
||||
};
|
||||
|
||||
export function DefaultLayout({ children }: OwnProps) {
|
||||
const currentUser = useRecoilState(currentUserState);
|
||||
const userIsAuthenticated = !!currentUser;
|
||||
const navigate = useNavigate();
|
||||
const isMatchingLocation = useIsMatchingLocation();
|
||||
|
||||
const onboardingStatus = useOnboardingStatus();
|
||||
useEffect(() => {
|
||||
const isMachinOngoingUserCreationRoute =
|
||||
isMatchingLocation(AppPath.SignUp) ||
|
||||
isMatchingLocation(AppPath.SignIn) ||
|
||||
isMatchingLocation(AppPath.Invite) ||
|
||||
isMatchingLocation(AppPath.Verify);
|
||||
|
||||
const isMatchingOnboardingRoute =
|
||||
isMatchingLocation(AppPath.SignUp) ||
|
||||
isMatchingLocation(AppPath.SignIn) ||
|
||||
isMatchingLocation(AppPath.Invite) ||
|
||||
isMatchingLocation(AppPath.Verify) ||
|
||||
isMatchingLocation(AppPath.CreateWorkspace) ||
|
||||
isMatchingLocation(AppPath.CreateProfile);
|
||||
|
||||
if (
|
||||
onboardingStatus === OnboardingStatus.OngoingUserCreation &&
|
||||
!isMachinOngoingUserCreationRoute
|
||||
) {
|
||||
navigate(AppPath.SignIn);
|
||||
} else if (
|
||||
onboardingStatus === OnboardingStatus.OngoingWorkspaceCreation &&
|
||||
!isMatchingLocation(AppPath.CreateWorkspace)
|
||||
) {
|
||||
navigate(AppPath.CreateWorkspace);
|
||||
} else if (
|
||||
onboardingStatus === OnboardingStatus.OngoingProfileCreation &&
|
||||
!isMatchingLocation(AppPath.CreateProfile)
|
||||
) {
|
||||
navigate(AppPath.CreateProfile);
|
||||
} else if (
|
||||
onboardingStatus === OnboardingStatus.Completed &&
|
||||
isMatchingOnboardingRoute
|
||||
) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [onboardingStatus, navigate, isMatchingLocation]);
|
||||
|
||||
return (
|
||||
<StyledLayout>
|
||||
{userIsAuthenticated ? (
|
||||
<>
|
||||
<CommandMenu />
|
||||
<NavbarAnimatedContainer>
|
||||
<AppNavbar />
|
||||
</NavbarAnimatedContainer>
|
||||
<MainContainer>{children}</MainContainer>
|
||||
</>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
<>
|
||||
<CommandMenu />
|
||||
<NavbarAnimatedContainer>
|
||||
<AppNavbar />
|
||||
</NavbarAnimatedContainer>
|
||||
<MainContainer>
|
||||
{onboardingStatus &&
|
||||
onboardingStatus !== OnboardingStatus.Completed ? (
|
||||
<>
|
||||
<CompaniesMockMode />
|
||||
<AnimatePresence mode="wait">
|
||||
<LayoutGroup>
|
||||
<AuthModal>{children}</AuthModal>
|
||||
</LayoutGroup>
|
||||
</AnimatePresence>
|
||||
</>
|
||||
) : (
|
||||
<>{children}</>
|
||||
)}
|
||||
</MainContainer>
|
||||
</>
|
||||
</StyledLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ type Props = {
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.h2`
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { useGetCurrentUserQuery } from '~/generated/graphql';
|
||||
|
||||
@ -9,11 +8,7 @@ export function UserProvider({ children }: React.PropsWithChildren) {
|
||||
const [, setCurrentUser] = useRecoilState(currentUserState);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const isLogged = useIsLogged();
|
||||
|
||||
const { data, loading } = useGetCurrentUserQuery({
|
||||
skip: !isLogged,
|
||||
});
|
||||
const { data, loading } = useGetCurrentUserQuery();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
|
||||
Reference in New Issue
Block a user