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:
@@ -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);
|
||||
|
||||
+11
-84
@@ -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>
|
||||
)}
|
||||
|
||||
+28
@@ -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',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const StyledSSOButtonContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
+30
-11
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
+33
-9
@@ -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} />
|
||||
</>
|
||||
);
|
||||
|
||||
+32
-9
@@ -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} />
|
||||
</>
|
||||
);
|
||||
|
||||
+23
-9
@@ -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,
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user