From 55cc7f809e2008a9ac31e3830da920e226af4a79 Mon Sep 17 00:00:00 2001 From: Manikanth Martha Date: Wed, 14 Jan 2026 04:20:38 +0530 Subject: [PATCH] feat(auth): Show Last used label on SSO sign-in method (#17093) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 image --- > [!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") > > 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). --------- Co-authored-by: Félix Malfait --- .../src/modules/auth/hooks/useAuth.ts | 18 ++-- .../components/SignInUpGlobalScopeForm.tsx | 95 +++---------------- .../components/internal/LastUsedPill.tsx | 28 ++++++ .../internal/SignInUpSSOButtonStyles.ts | 6 ++ .../internal/SignInUpWithCredentials.tsx | 41 +++++--- .../internal/SignInUpWithGoogle.tsx | 42 ++++++-- .../internal/SignInUpWithMicrosoft.tsx | 41 ++++++-- .../components/internal/SignInUpWithSSO.tsx | 32 +++++-- .../hooks/useHasMultipleAuthMethods.ts | 19 ++++ .../auth/sign-in-up/hooks/useSignInUp.ts | 10 +- .../states/lastAuthenticatedMethodState.ts | 12 +++ .../auth/types/AuthenticatedMethod.enum.ts | 6 ++ .../twenty-front/src/pages/auth/SignInUp.tsx | 20 ++-- 13 files changed, 234 insertions(+), 136 deletions(-) create mode 100644 packages/twenty-front/src/modules/auth/sign-in-up/components/internal/LastUsedPill.tsx create mode 100644 packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpSSOButtonStyles.ts create mode 100644 packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHasMultipleAuthMethods.ts create mode 100644 packages/twenty-front/src/modules/auth/states/lastAuthenticatedMethodState.ts create mode 100644 packages/twenty-front/src/modules/auth/types/AuthenticatedMethod.enum.ts diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index 596d7a2566..9f557a8ca9 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -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); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx index d1c36f3b95..bc6981ce0c 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx @@ -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 && ( {authProviders.google && ( - + )} {authProviders.microsoft && ( - + )} {(authProviders.google || authProviders.microsoft) && ( )} {/* eslint-disable-next-line react/jsx-props-no-spreading */} - - - {signInUpStep === SignInUpStep.Password && ( - - )} - (form.formState.isSubmitting ? : null)} - fullWidth - /> - + )} diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/LastUsedPill.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/LastUsedPill.tsx new file mode 100644 index 0000000000..33faf652f7 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/LastUsedPill.tsx @@ -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 ( + + ); +}; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpSSOButtonStyles.ts b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpSSOButtonStyles.ts new file mode 100644 index 0000000000..874e2fdd3c --- /dev/null +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpSSOButtonStyles.ts @@ -0,0 +1,6 @@ +import styled from '@emotion/styled'; + +export const StyledSSOButtonContainer = styled.div` + position: relative; + width: 100%; +`; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWithCredentials.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWithCredentials.tsx index 0af65a50fd..96ddc5c632 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWithCredentials.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWithCredentials.tsx @@ -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
(); @@ -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) => { event.preventDefault(); @@ -132,16 +148,19 @@ export const SignInUpWithCredentials = () => { signInUpMode={signInUpMode} /> )} - (form.formState.isSubmitting ? : null)} - disabled={isSubmitButtonDisabled} - fullWidth - /> + + (form.formState.isSubmitting ? : null)} + disabled={isSubmitButtonDisabled} + fullWidth + /> + {isLastUsed && } + )} diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWithGoogle.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWithGoogle.tsx index f3c95346e0..fd0c789d07 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWithGoogle.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWithGoogle.tsx @@ -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 ( <> - signInWithGoogle({ action })} - variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'} - fullWidth - /> + + + {isLastUsed && (isGlobalScope || hasMultipleAuthMethods) && ( + + )} + ); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWithMicrosoft.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWithMicrosoft.tsx index 7402935b72..c8faae462c 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWithMicrosoft.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWithMicrosoft.tsx @@ -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 ( <> - } - title={t`Continue with Microsoft`} - onClick={() => signInWithMicrosoft({ action })} - variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'} - fullWidth - /> + + } + title={t`Continue with Microsoft`} + onClick={handleClick} + variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'} + fullWidth + /> + {isLastUsed && (isGlobalScope || hasMultipleAuthMethods) && ( + + )} + ); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWithSSO.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWithSSO.tsx index 9133728ed9..a3af13ba05 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWithSSO.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWithSSO.tsx @@ -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 ( <> - } - title={t`Single sign-on (SSO)`} - onClick={signInWithSSO} - variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'} - fullWidth - /> + + } + title={t`Single sign-on (SSO)`} + onClick={signInWithSSO} + variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'} + fullWidth + /> + {isLastUsed && hasMultipleAuthMethods && } + ); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHasMultipleAuthMethods.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHasMultipleAuthMethods.ts new file mode 100644 index 0000000000..ca19f894b6 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHasMultipleAuthMethods.ts @@ -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; +}; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts index 23a451e887..d9e4b853c5 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts @@ -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) => { 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) => { const token = readCaptchaToken(); try { + setLastAuthenticatedMethod(AuthenticatedMethod.EMAIL); + if ( !isInviteMode && signInUpMode === SignInUpMode.SignIn && @@ -190,6 +197,7 @@ export const useSignInUp = (form: UseFormReturn) => { enqueueErrorSnackBar, buildSearchParamsFromUrlSyncedStates, isOnAWorkspace, + setLastAuthenticatedMethod, t, ], ); diff --git a/packages/twenty-front/src/modules/auth/states/lastAuthenticatedMethodState.ts b/packages/twenty-front/src/modules/auth/states/lastAuthenticatedMethodState.ts new file mode 100644 index 0000000000..4c12ca408d --- /dev/null +++ b/packages/twenty-front/src/modules/auth/states/lastAuthenticatedMethodState.ts @@ -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({ + key: LAST_AUTHENTICATED_METHOD_STORAGE_KEY, + default: null, + effects: [localStorageEffect()], +}); diff --git a/packages/twenty-front/src/modules/auth/types/AuthenticatedMethod.enum.ts b/packages/twenty-front/src/modules/auth/types/AuthenticatedMethod.enum.ts new file mode 100644 index 0000000000..17b8a342bc --- /dev/null +++ b/packages/twenty-front/src/modules/auth/types/AuthenticatedMethod.enum.ts @@ -0,0 +1,6 @@ +export enum AuthenticatedMethod { + EMAIL = 'EMAIL', + GOOGLE = 'GOOGLE', + MICROSOFT = 'MICROSOFT', + SSO = 'SSO', +} diff --git a/packages/twenty-front/src/pages/auth/SignInUp.tsx b/packages/twenty-front/src/pages/auth/SignInUp.tsx index 0e3a5115e0..5d2ee89e44 100644 --- a/packages/twenty-front/src/pages/auth/SignInUp.tsx +++ b/packages/twenty-front/src/pages/auth/SignInUp.tsx @@ -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, ]);