feat(auth): Show Last used label on SSO sign-in method (#17093)

Fixes #17006 

## Changes
Added `lastAuthenticateWorkspaceSsoMethodState` Recoil atom to track the
last used SSO method, persisted in localStorage
Updated `SignInUpWithGoogle` component to display a "Last" pill badge
when Google was the last auth method
Updated `SignInUpWithMicrosoft` component to display a "Last" pill badge
when Microsoft was the last auth method
Updated `SignInUpWithSSO` component to display a "Last" pill badge when
SSO was the last auth method.


The state is saved after successful authentication redirect, ensuring it
persists across sessions

## Implementation Details
Uses existing `localStorageEffect` for persistence across browser
sessions
Leverages the existing Pill component from twenty-ui with blue accent
color
The label is positioned on the right border of the button using absolute
positioning
State is saved in the `useAuth` hook during Google/Microsoft sign-in and
in the SSO login component

<img width="684" height="608" alt="image"
src="https://github.com/user-attachments/assets/629a1599-aab0-44e4-b684-ae91a8e725fd"
/>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Highlights the most recently used authentication method and preserves
it across sessions.
> 
> - Introduces `AuthenticatedMethod` enum and
`lastAuthenticatedMethodState` (persisted via `localStorageEffect`) and
preserves it through `useAuth.clearSession`
> - Displays a "Last" pill via `LastUsedPill` on `SignInUpWithGoogle`,
`SignInUpWithMicrosoft`, `SignInUpWithSSO`, and
`SignInUpWithCredentials` when appropriate; adds
`StyledSSOButtonContainer` for badge positioning
> - Adds `useHasMultipleAuthMethods` to detect when to show the badge;
threads `isGlobalScope` to relevant components
> - Sets last-used method on click/submit in SSO and credentials flows
(`useSignInUp`, SSO button handlers)
> - Refactors `SignInUpGlobalScopeForm` to use `SignInUpWithCredentials`
and updates `SignInUp` title logic for global scope (e.g., "Welcome to
Twenty")
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
70d552760916e7d5a8c83a33437585ca376253fc. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Manikanth Martha
2026-01-14 04:20:38 +05:30
committed by GitHub
parent 25a9d49ff1
commit 55cc7f809e
13 changed files with 234 additions and 136 deletions
@@ -26,12 +26,14 @@ import {
type AuthTokenPair,
} from '~/generated-metadata/graphql';
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
import { lastAuthenticatedMethodState } from '@/auth/states/lastAuthenticatedMethodState';
import { loginTokenState } from '@/auth/states/loginTokenState';
import {
SignInUpStep,
signInUpStepState,
@@ -66,7 +68,6 @@ import { iconsState } from 'twenty-ui/display';
import { type AuthToken } from '~/generated/graphql';
import { cookieStorage } from '~/utils/cookie-storage';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { loginTokenState } from '@/auth/states/loginTokenState';
export const useAuth = () => {
const setTokenPair = useSetRecoilState(tokenPairState);
@@ -121,7 +122,7 @@ export const useAuth = () => {
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
const clearSession = useRecoilCallback(
({ snapshot }) =>
({ snapshot, set }) =>
async () => {
const emptySnapshot = snapshot_UNSTABLE();
@@ -152,6 +153,9 @@ export const useAuth = () => {
const workspacePublicData = snapshot
.getLoadable(workspacePublicDataState)
.getValue();
const lastAuthenticatedMethod = snapshot
.getLoadable(lastAuthenticatedMethodState)
.getValue();
const initialSnapshot = emptySnapshot.map(({ set }) => {
set(iconsState, iconsValue);
@@ -174,12 +178,14 @@ export const useAuth = () => {
return undefined;
});
goToRecoilSnapshot(initialSnapshot);
sessionStorage.clear();
localStorage.clear();
goToRecoilSnapshot(initialSnapshot);
set(lastAuthenticatedMethodState, lastAuthenticatedMethod);
await client.clearStore();
// We need to explicitly clear the state to trigger the cookie deletion which include the parent domain
setLastAuthenticateWorkspaceDomain(null);
await loadMockedObjectMetadataItems();
navigate(AppPath.SignInUp);
@@ -4,39 +4,29 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro';
import { motion } from 'framer-motion';
import { useState } from 'react';
import { FormProvider } from 'react-hook-form';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
import { ClickToActionLink, UndecoratedLink } from 'twenty-ui/navigation';
import { useAuth } from '@/auth/hooks/useAuth';
import { SignInUpEmailField } from '@/auth/sign-in-up/components/internal/SignInUpEmailField';
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/internal/SignInUpPasswordField';
import { SignInUpWithCredentials } from '@/auth/sign-in-up/components/internal/SignInUpWithCredentials';
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/internal/SignInUpWithGoogle';
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/internal/SignInUpWithMicrosoft';
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
import { signInUpModeState } from '@/auth/states/signInUpModeState';
import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { SignInUpMode } from '@/auth/types/signInUpMode';
import { getAvailableWorkspacePathAndSearchParams } from '@/auth/utils/availableWorkspacesUtils';
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { useCaptcha } from '@/client-config/hooks/useCaptcha';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { isDefined } from 'twenty-shared/utils';
import {
Avatar,
HorizontalSeparator,
IconChevronRight,
IconPlus,
} from 'twenty-ui/display';
import { Loader } from 'twenty-ui/feedback';
import { MainButton } from 'twenty-ui/input';
import { type AvailableWorkspace } from '~/generated/graphql';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
@@ -46,13 +36,6 @@ const StyledContentContainer = styled(motion.div)`
min-width: 200px;
`;
const StyledForm = styled.form`
align-items: center;
display: flex;
flex-direction: column;
width: 100%;
`;
const StyledWorkspaceContainer = styled.div`
background-color: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.light};
@@ -147,43 +130,12 @@ export const SignInUpGlobalScopeForm = () => {
const { signOut } = useAuth();
const { createWorkspace } = useSignUpInNewWorkspace();
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const [signInUpMode] = useRecoilState(signInUpModeState);
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
const theme = useTheme();
const { t } = useLingui();
const isRequestingCaptchaToken = useRecoilValue(
isRequestingCaptchaTokenState,
);
const { isCaptchaReady } = useCaptcha();
const [showErrors, setShowErrors] = useState(false);
const { form } = useSignInUpForm();
const { submitCredentials, continueWithCredentials } = useSignInUp(form);
const handleSubmit = async () => {
if (isDefined(form?.formState?.errors?.email)) {
setShowErrors(true);
return;
}
if (signInUpStep === SignInUpStep.Password) {
await submitCredentials(form.getValues());
return;
}
await continueWithCredentials();
};
const onEmailChange = (email: string) => {
if (email !== form.getValues('email')) {
setSignInUpStep(SignInUpStep.Email);
}
};
const getAvailableWorkspaceUrl = (availableWorkspace: AvailableWorkspace) => {
const { pathname, searchParams } = getAvailableWorkspacePathAndSearchParams(
availableWorkspace,
@@ -263,48 +215,23 @@ export const SignInUpGlobalScopeForm = () => {
{signInUpStep !== SignInUpStep.WorkspaceSelection && (
<StyledContentContainer>
{authProviders.google && (
<SignInUpWithGoogle action="list-available-workspaces" />
<SignInUpWithGoogle
action="list-available-workspaces"
isGlobalScope
/>
)}
{authProviders.microsoft && (
<SignInUpWithMicrosoft action="list-available-workspaces" />
<SignInUpWithMicrosoft
action="list-available-workspaces"
isGlobalScope
/>
)}
{(authProviders.google || authProviders.microsoft) && (
<HorizontalSeparator />
)}
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<FormProvider {...form}>
<StyledForm onSubmit={form.handleSubmit(handleSubmit)}>
<SignInUpEmailField
showErrors={showErrors}
onInputChange={onEmailChange}
/>
{signInUpStep === SignInUpStep.Password && (
<SignInUpPasswordField
showErrors={showErrors}
signInUpMode={signInUpMode}
/>
)}
<MainButton
disabled={
isRequestingCaptchaToken ||
form.formState.isSubmitting ||
(signInUpStep !== SignInUpStep.Password && !isCaptchaReady)
}
title={
signInUpStep === SignInUpStep.Password
? signInUpMode === SignInUpMode.SignIn
? t`Sign In`
: t`Sign Up`
: t`Continue`
}
type="submit"
variant={
signInUpStep === SignInUpStep.Init ? 'secondary' : 'primary'
}
Icon={() => (form.formState.isSubmitting ? <Loader /> : null)}
fullWidth
/>
</StyledForm>
<SignInUpWithCredentials isGlobalScope />
</FormProvider>
</StyledContentContainer>
)}
@@ -0,0 +1,28 @@
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { Pill } from 'twenty-ui/components';
const StyledPill = styled(Pill)`
background: ${({ theme }) => theme.color.blue3};
border: 1px solid ${({ theme }) => theme.color.blue5};
border-radius: ${({ theme }) => theme.border.radius.pill};
color: ${({ theme }) => theme.color.blue};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
position: absolute;
right: -${({ theme }) => theme.spacing(5)};
top: -${({ theme }) => theme.spacing(2)};
`;
export const LastUsedPill = () => {
const { t } = useLingui();
return (
<StyledPill
label={t({
message: 'Last',
comment:
'Short label (keep brief) indicating the most recently used login method',
})}
/>
);
};
@@ -0,0 +1,6 @@
import styled from '@emotion/styled';
export const StyledSSOButtonContainer = styled.div`
position: relative;
width: 100%;
`;
@@ -1,12 +1,17 @@
import { useHasMultipleAuthMethods } from '@/auth/sign-in-up/hooks/useHasMultipleAuthMethods';
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
import { type Form } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { lastAuthenticatedMethodState } from '@/auth/states/lastAuthenticatedMethodState';
import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { LastUsedPill } from '@/auth/sign-in-up/components/internal/LastUsedPill';
import { SignInUpEmailField } from '@/auth/sign-in-up/components/internal/SignInUpEmailField';
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/internal/SignInUpPasswordField';
import { StyledSSOButtonContainer } from '@/auth/sign-in-up/components/internal/SignInUpSSOButtonStyles';
import { AuthenticatedMethod } from '@/auth/types/AuthenticatedMethod.enum';
import { SignInUpMode } from '@/auth/types/signInUpMode';
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { captchaState } from '@/client-config/states/captchaState';
@@ -26,7 +31,11 @@ const StyledForm = styled.form`
width: 100%;
`;
export const SignInUpWithCredentials = () => {
export const SignInUpWithCredentials = ({
isGlobalScope,
}: {
isGlobalScope?: boolean;
}) => {
const { t } = useLingui();
const form = useFormContext<Form>();
@@ -36,6 +45,8 @@ export const SignInUpWithCredentials = () => {
const isRequestingCaptchaToken = useRecoilValue(
isRequestingCaptchaTokenState,
);
const lastAuthenticatedMethod = useRecoilValue(lastAuthenticatedMethodState);
const hasMultipleAuthMethods = useHasMultipleAuthMethods();
const {
signInUpMode,
@@ -44,6 +55,11 @@ export const SignInUpWithCredentials = () => {
submitCredentials,
} = useSignInUp(form);
const isLastUsed =
signInUpStep === SignInUpStep.Init &&
lastAuthenticatedMethod === AuthenticatedMethod.EMAIL &&
(isGlobalScope || hasMultipleAuthMethods);
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
@@ -132,16 +148,19 @@ export const SignInUpWithCredentials = () => {
signInUpMode={signInUpMode}
/>
)}
<MainButton
title={buttonTitle}
type="submit"
variant={
signInUpStep === SignInUpStep.Init ? 'secondary' : 'primary'
}
Icon={() => (form.formState.isSubmitting ? <Loader /> : null)}
disabled={isSubmitButtonDisabled}
fullWidth
/>
<StyledSSOButtonContainer>
<MainButton
title={buttonTitle}
type="submit"
variant={
signInUpStep === SignInUpStep.Init ? 'secondary' : 'primary'
}
Icon={() => (form.formState.isSubmitting ? <Loader /> : null)}
disabled={isSubmitButtonDisabled}
fullWidth
/>
{isLastUsed && <LastUsedPill />}
</StyledSSOButtonContainer>
</StyledForm>
)}
</>
@@ -1,15 +1,20 @@
import { useHasMultipleAuthMethods } from '@/auth/sign-in-up/hooks/useHasMultipleAuthMethods';
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle';
import { lastAuthenticatedMethodState } from '@/auth/states/lastAuthenticatedMethodState';
import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { AuthenticatedMethod } from '@/auth/types/AuthenticatedMethod.enum';
import { type SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { memo } from 'react';
import { useRecoilValue } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { HorizontalSeparator, IconGoogle } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
import { type SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
import { LastUsedPill } from './LastUsedPill';
import { StyledSSOButtonContainer } from './SignInUpSSOButtonStyles';
const GoogleIcon = memo(() => {
const theme = useTheme();
@@ -18,21 +23,40 @@ const GoogleIcon = memo(() => {
export const SignInUpWithGoogle = ({
action,
isGlobalScope,
}: {
action: SocialSSOSignInUpActionType;
isGlobalScope?: boolean;
}) => {
const { t } = useLingui();
const signInUpStep = useRecoilValue(signInUpStepState);
const [lastAuthenticatedMethod, setLastAuthenticatedMethod] = useRecoilState(
lastAuthenticatedMethodState,
);
const { signInWithGoogle } = useSignInWithGoogle();
const hasMultipleAuthMethods = useHasMultipleAuthMethods();
const handleClick = () => {
setLastAuthenticatedMethod(AuthenticatedMethod.GOOGLE);
signInWithGoogle({ action });
};
const isLastUsed = lastAuthenticatedMethod === AuthenticatedMethod.GOOGLE;
return (
<>
<MainButton
Icon={GoogleIcon}
title={t`Continue with Google`}
onClick={() => signInWithGoogle({ action })}
variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'}
fullWidth
/>
<StyledSSOButtonContainer>
<MainButton
Icon={GoogleIcon}
title={t`Continue with Google`}
onClick={handleClick}
variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'}
fullWidth
/>
{isLastUsed && (isGlobalScope || hasMultipleAuthMethods) && (
<LastUsedPill />
)}
</StyledSSOButtonContainer>
<HorizontalSeparator visible={false} />
</>
);
@@ -1,35 +1,58 @@
import { useHasMultipleAuthMethods } from '@/auth/sign-in-up/hooks/useHasMultipleAuthMethods';
import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft';
import { lastAuthenticatedMethodState } from '@/auth/states/lastAuthenticatedMethodState';
import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { AuthenticatedMethod } from '@/auth/types/AuthenticatedMethod.enum';
import { type SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { HorizontalSeparator, IconMicrosoft } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
import { type SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
import { LastUsedPill } from './LastUsedPill';
import { StyledSSOButtonContainer } from './SignInUpSSOButtonStyles';
export const SignInUpWithMicrosoft = ({
action,
isGlobalScope,
}: {
action: SocialSSOSignInUpActionType;
isGlobalScope?: boolean;
}) => {
const theme = useTheme();
const { t } = useLingui();
const signInUpStep = useRecoilValue(signInUpStepState);
const [lastAuthenticatedMethod, setLastAuthenticatedMethod] = useRecoilState(
lastAuthenticatedMethodState,
);
const { signInWithMicrosoft } = useSignInWithMicrosoft();
const hasMultipleAuthMethods = useHasMultipleAuthMethods();
const handleClick = () => {
setLastAuthenticatedMethod(AuthenticatedMethod.MICROSOFT);
signInWithMicrosoft({ action });
};
const isLastUsed = lastAuthenticatedMethod === AuthenticatedMethod.MICROSOFT;
return (
<>
<MainButton
Icon={() => <IconMicrosoft size={theme.icon.size.md} />}
title={t`Continue with Microsoft`}
onClick={() => signInWithMicrosoft({ action })}
variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'}
fullWidth
/>
<StyledSSOButtonContainer>
<MainButton
Icon={() => <IconMicrosoft size={theme.icon.size.md} />}
title={t`Continue with Microsoft`}
onClick={handleClick}
variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'}
fullWidth
/>
{isLastUsed && (isGlobalScope || hasMultipleAuthMethods) && (
<LastUsedPill />
)}
</StyledSSOButtonContainer>
<HorizontalSeparator visible={false} />
</>
);
@@ -1,27 +1,36 @@
import { useHasMultipleAuthMethods } from '@/auth/sign-in-up/hooks/useHasMultipleAuthMethods';
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
import { lastAuthenticatedMethodState } from '@/auth/states/lastAuthenticatedMethodState';
import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { AuthenticatedMethod } from '@/auth/types/AuthenticatedMethod.enum';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { HorizontalSeparator, IconLock } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
import { LastUsedPill } from './LastUsedPill';
import { StyledSSOButtonContainer } from './SignInUpSSOButtonStyles';
export const SignInUpWithSSO = () => {
const theme = useTheme();
const { t } = useLingui();
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
const signInUpStep = useRecoilValue(signInUpStepState);
const [lastAuthenticatedMethod, setLastAuthenticatedMethod] = useRecoilState(
lastAuthenticatedMethodState,
);
const hasMultipleAuthMethods = useHasMultipleAuthMethods();
const { redirectToSSOLoginPage } = useSSO();
const signInWithSSO = () => {
setLastAuthenticatedMethod(AuthenticatedMethod.SSO);
if (
isDefined(workspaceAuthProviders) &&
workspaceAuthProviders.sso.length === 1
@@ -32,15 +41,20 @@ export const SignInUpWithSSO = () => {
setSignInUpStep(SignInUpStep.SSOIdentityProviderSelection);
};
const isLastUsed = lastAuthenticatedMethod === AuthenticatedMethod.SSO;
return (
<>
<MainButton
Icon={() => <IconLock size={theme.icon.size.md} />}
title={t`Single sign-on (SSO)`}
onClick={signInWithSSO}
variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'}
fullWidth
/>
<StyledSSOButtonContainer>
<MainButton
Icon={() => <IconLock size={theme.icon.size.md} />}
title={t`Single sign-on (SSO)`}
onClick={signInWithSSO}
variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'}
fullWidth
/>
{isLastUsed && hasMultipleAuthMethods && <LastUsedPill />}
</StyledSSOButtonContainer>
<HorizontalSeparator visible={false} />
</>
);
@@ -0,0 +1,19 @@
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useRecoilValue } from 'recoil';
export const useHasMultipleAuthMethods = () => {
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
if (!workspaceAuthProviders) {
return false;
}
let enabledMethodsCount = 0;
if (workspaceAuthProviders.google) enabledMethodsCount++;
if (workspaceAuthProviders.microsoft) enabledMethodsCount++;
if (workspaceAuthProviders.password) enabledMethodsCount++;
if (workspaceAuthProviders.sso.length > 0) enabledMethodsCount++;
return enabledMethodsCount > 1;
};
@@ -3,11 +3,13 @@ import { type SubmitHandler, type UseFormReturn } from 'react-hook-form';
import { useLocation, useParams, useSearchParams } from 'react-router-dom';
import { type Form } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { lastAuthenticatedMethodState } from '@/auth/states/lastAuthenticatedMethodState';
import { signInUpModeState } from '@/auth/states/signInUpModeState';
import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { AuthenticatedMethod } from '@/auth/types/AuthenticatedMethod.enum';
import { SignInUpMode } from '@/auth/types/signInUpMode';
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
import { useCaptcha } from '@/client-config/hooks/useCaptcha';
@@ -16,7 +18,7 @@ import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCu
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { useLingui } from '@lingui/react/macro';
import { useRecoilState } from 'recoil';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { buildAppPathWithQueryParams } from '~/utils/buildAppPathWithQueryParams';
@@ -31,6 +33,9 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
const { isCaptchaReady } = useCaptcha();
const setLastAuthenticatedMethod = useSetRecoilState(
lastAuthenticatedMethodState,
);
const location = useLocation();
@@ -121,6 +126,8 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
const token = readCaptchaToken();
try {
setLastAuthenticatedMethod(AuthenticatedMethod.EMAIL);
if (
!isInviteMode &&
signInUpMode === SignInUpMode.SignIn &&
@@ -190,6 +197,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
enqueueErrorSnackBar,
buildSearchParamsFromUrlSyncedStates,
isOnAWorkspace,
setLastAuthenticatedMethod,
t,
],
);
@@ -0,0 +1,12 @@
import { atom } from 'recoil';
import { type AuthenticatedMethod } from '@/auth/types/AuthenticatedMethod.enum';
import { localStorageEffect } from '~/utils/recoil/localStorageEffect';
const LAST_AUTHENTICATED_METHOD_STORAGE_KEY = 'lastAuthenticatedMethodState';
export const lastAuthenticatedMethodState = atom<AuthenticatedMethod | null>({
key: LAST_AUTHENTICATED_METHOD_STORAGE_KEY,
default: null,
effects: [localStorageEffect()],
});
@@ -0,0 +1,6 @@
export enum AuthenticatedMethod {
EMAIL = 'EMAIL',
GOOGLE = 'GOOGLE',
MICROSOFT = 'MICROSOFT',
SSO = 'SSO',
}
@@ -20,7 +20,6 @@ import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWork
import { useGetPublicWorkspaceDataByDomain } from '@/domain-manager/hooks/useGetPublicWorkspaceDataByDomain';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
import { useIsCurrentLocationOnDefaultDomain } from '@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain';
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
import { useMemo } from 'react';
import { SignInUpGlobalScopeFormEffect } from '@/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect';
@@ -101,6 +100,8 @@ export const SignInUp = () => {
setSignInUpStep(SignInUpStep.Init);
};
const isGlobalScope = isDefaultDomain && isMultiWorkspaceEnabled;
const title = useMemo(() => {
if (isDefined(workspaceInviteHash)) {
const workspaceName = workspaceFromInviteHash?.displayName ?? '';
@@ -119,17 +120,22 @@ export const SignInUp = () => {
return t`Verify code from the app`;
}
const workspaceName = !isDefined(workspacePublicData?.displayName)
? DEFAULT_WORKSPACE_NAME
: workspacePublicData?.displayName === ''
? t`Your Workspace`
: workspacePublicData?.displayName;
if (isGlobalScope) {
return t`Welcome to Twenty`;
}
return t`Welcome to ${workspaceName}`;
const workspaceName = workspacePublicData?.displayName;
if (!workspaceName) {
return t`Welcome to your workspace`;
}
return t`Welcome, ${workspaceName}.`;
}, [
workspaceInviteHash,
signInUpStep,
workspacePublicData?.displayName,
isGlobalScope,
t,
workspaceFromInviteHash?.displayName,
]);