Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions packages/twenty-front/src/modules/auth/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { LAST_AUTHENTICATED_METHOD_STORAGE_KEY } from '@/auth/states/lastAuthenticatedMethodState';
import { loginTokenState } from '@/auth/states/loginTokenState';
import {
SignInUpStep,
signInUpStepState,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -176,10 +177,34 @@ export const useAuth = () => {

goToRecoilSnapshot(initialSnapshot);

This comment was marked as outdated.


sessionStorage.clear();
localStorage.clear();
let lastAuthenticatedMethod: string | null = null;
try {
lastAuthenticatedMethod = localStorage.getItem(
LAST_AUTHENTICATED_METHOD_STORAGE_KEY,
);
} catch {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for try/catch like this

// Ignore storage errors - last auth method is non-critical
}

try {
sessionStorage.clear();
localStorage.clear();
} catch {
// Ignore storage errors during sign-out
}

if (lastAuthenticatedMethod !== null) {
try {
localStorage.setItem(
LAST_AUTHENTICATED_METHOD_STORAGE_KEY,
lastAuthenticatedMethod,
);
} catch {
// Ignore failures preserving last auth method - it's non-critical
}
}

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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import styled from '@emotion/styled';
import { Pill } from 'twenty-ui/components';

export const StyledSSOButtonContainer = styled.div`
position: relative;
width: 100%;
`;

export const StyledLastUsedPill = 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.blue9};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
position: absolute;
right: -${({ theme }) => theme.spacing(5)};
top: -${({ theme }) => theme.spacing(2)};
`;
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { useLastAuthenticatedMethod } from '@/auth/sign-in-up/hooks/useLastAuthenticatedMethod';
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle';
import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
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 { HorizontalSeparator, IconGoogle } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
import { type SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
import {
StyledLastUsedPill,
StyledSSOButtonContainer,
} from './SignInUpSSOButtonStyles';

const GoogleIcon = memo(() => {
const theme = useTheme();
Expand All @@ -23,16 +28,29 @@ export const SignInUpWithGoogle = ({
}) => {
const { t } = useLingui();
const signInUpStep = useRecoilValue(signInUpStepState);
const { lastAuthenticatedMethod, setLastAuthenticatedMethod } =
useLastAuthenticatedMethod();
const { signInWithGoogle } = useSignInWithGoogle();

const handleClick = () => {
setLastAuthenticatedMethod('google');
signInWithGoogle({ action });
Comment on lines +39 to +41

This comment was marked as outdated.

};
Comment on lines +39 to +42
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The localStorage manipulation logic is duplicated. Instead of directly using localStorage.setItem with JSON.stringify, consider using useSetRecoilState to update the lastAuthenticatedMethodState, which would handle persistence automatically through the existing localStorageEffect.

Copilot uses AI. Check for mistakes.

const isLastUsed = lastAuthenticatedMethod === '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 && <StyledLastUsedPill label={t`Last`} />}
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pill label displays "Last" but should display "Last used" to match the PR description and be more descriptive.

Suggested change
{isLastUsed && <StyledLastUsedPill label={t`Last`} />}
{isLastUsed && <StyledLastUsedPill label={t`Last used`} />}

Copilot uses AI. Check for mistakes.
</StyledSSOButtonContainer>
<HorizontalSeparator visible={false} />
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { useLastAuthenticatedMethod } from '@/auth/sign-in-up/hooks/useLastAuthenticatedMethod';
import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft';
import {
SignInUpStep,
signInUpStepState,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix lint

} from '@/auth/states/signInUpStepState';
import { type SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { 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 {
StyledLastUsedPill,
StyledSSOButtonContainer,
} from './SignInUpSSOButtonStyles';

export const SignInUpWithMicrosoft = ({
action,
Expand All @@ -19,17 +24,29 @@ export const SignInUpWithMicrosoft = ({
const { t } = useLingui();

const signInUpStep = useRecoilValue(signInUpStepState);
const { lastAuthenticatedMethod, setLastAuthenticatedMethod } =
useLastAuthenticatedMethod();
const { signInWithMicrosoft } = useSignInWithMicrosoft();

const handleClick = () => {
setLastAuthenticatedMethod('microsoft');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use ennum don't hardcode the string (same for other places)

signInWithMicrosoft({ action });
};
Comment on lines +35 to +38
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The localStorage manipulation logic is duplicated. Instead of directly using localStorage.setItem with JSON.stringify, consider using useSetRecoilState to update the lastAuthenticatedMethodState, which would handle persistence automatically through the existing localStorageEffect.

Copilot uses AI. Check for mistakes.

const isLastUsed = lastAuthenticatedMethod === '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 && <StyledLastUsedPill label={t`Last`} />}
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pill label displays "Last" but should display "Last used" to match the PR description and be more descriptive.

Suggested change
{isLastUsed && <StyledLastUsedPill label={t`Last`} />}
{isLastUsed && <StyledLastUsedPill label={t`Last used`} />}

Copilot uses AI. Check for mistakes.
</StyledSSOButtonContainer>
<HorizontalSeparator visible={false} />
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useLastAuthenticatedMethod } from '@/auth/sign-in-up/hooks/useLastAuthenticatedMethod';
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
import {
SignInUpStep,
Expand All @@ -10,18 +11,24 @@ import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { HorizontalSeparator, IconLock } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
import {
StyledLastUsedPill,
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 } =
useLastAuthenticatedMethod();

const { redirectToSSOLoginPage } = useSSO();

const signInWithSSO = () => {
setLastAuthenticatedMethod('sso');
if (
isDefined(workspaceAuthProviders) &&
workspaceAuthProviders.sso.length === 1
Expand All @@ -32,15 +39,20 @@ export const SignInUpWithSSO = () => {
setSignInUpStep(SignInUpStep.SSOIdentityProviderSelection);
};

const isLastUsed = lastAuthenticatedMethod === 'sso';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never hardcode strings like that, instead reference the enum (e.g. AuthenticationMethod.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 && <StyledLastUsedPill label={t`Last`} />}
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pill label displays "Last" but according to the PR title and description, it should display "Last used". Consider updating the label to be more descriptive and match the intended design.

Suggested change
{isLastUsed && <StyledLastUsedPill label={t`Last`} />}
{isLastUsed && <StyledLastUsedPill label={t`Last used`} />}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If only a single method is enabled on that workspace then we probably shouldn't show last used

</StyledSSOButtonContainer>
<HorizontalSeparator visible={false} />
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useRecoilValue } from 'recoil';

import {
LAST_AUTHENTICATED_METHOD_STORAGE_KEY,
lastAuthenticatedMethodState,
type LastAuthenticatedMethod,
} from '@/auth/states/lastAuthenticatedMethodState';

export const useLastAuthenticatedMethod = () => {
const lastAuthenticatedMethod = useRecoilValue(lastAuthenticatedMethodState);
const setLastAuthenticatedMethod = (method: LastAuthenticatedMethod) => {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Setter bypasses Recoil state, writing only to localStorage so lastAuthenticatedMethodState stays stale until reload

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/modules/auth/sign-in-up/hooks/useLastAuthenticatedMethod.ts, line 11:

<comment>Setter bypasses Recoil state, writing only to localStorage so `lastAuthenticatedMethodState` stays stale until reload</comment>

<file context>
@@ -0,0 +1,22 @@
+
+export const useLastAuthenticatedMethod = () => {
+  const lastAuthenticatedMethod = useRecoilValue(lastAuthenticatedMethodState);
+  const setLastAuthenticatedMethod = (method: LastAuthenticatedMethod) => {
+    localStorage.setItem(
+      LAST_AUTHENTICATED_METHOD_STORAGE_KEY,
</file context>

✅ Addressed in 7d62fbd

localStorage.setItem(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The effect takes care of writing to local storage directly via the atom, you shouldn't access local storage directly

LAST_AUTHENTICATED_METHOD_STORAGE_KEY,
JSON.stringify(method),
);
};

This comment was marked as outdated.


return {
lastAuthenticatedMethod,
setLastAuthenticatedMethod,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { atom } from 'recoil';
import { localStorageEffect } from '~/utils/recoil/localStorageEffect';

export type LastAuthenticatedMethod = 'google' | 'microsoft' | 'sso' | null;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One export max per file.

All caps enum AuthenticatedMethod = {
GOOGLE = 'GOOGLE'
...
}


export const LAST_AUTHENTICATED_METHOD_STORAGE_KEY =
'lastAuthenticatedMethodState';

export const lastAuthenticatedMethodState = atom<LastAuthenticatedMethod>({
key: LAST_AUTHENTICATED_METHOD_STORAGE_KEY,
default: null,
effects: [localStorageEffect()],
});
1 change: 1 addition & 0 deletions packages/twenty-ui/src/theme/constants/FontDark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const FONT_DARK = {
extraLight: GRAY_SCALE_DARK.gray7,
inverted: GRAY_SCALE_DARK.gray1,
danger: COLOR_DARK.red,
indigo: COLOR_DARK.blue9,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already use these colors, try maybe theme.color.blue? etc ; I don't think we need to introduce these

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to introduce indigo as blue was not working for fonts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, we can directly give the font color as color: ${({ theme }) => theme.color.blue}; , no need to introduce indigo

},
...FONT_COMMON,
};
1 change: 1 addition & 0 deletions packages/twenty-ui/src/theme/constants/FontLight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const FONT_LIGHT = {
extraLight: GRAY_SCALE_LIGHT.gray7,
inverted: GRAY_SCALE_LIGHT.gray1,
danger: COLOR_LIGHT.red,
indigo: COLOR_LIGHT.blue9,
},
...FONT_COMMON,
};