diff --git a/packages/twenty-front/src/modules/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationCodeDash.tsx b/packages/twenty-front/src/modules/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationCodeDash.tsx
new file mode 100644
index 0000000000..1771c0513a
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationCodeDash.tsx
@@ -0,0 +1,22 @@
+import styled from '@emotion/styled';
+
+const StyledDashContainer = styled.div`
+ display: flex;
+ width: ${({ theme }) => theme.spacing(2)};
+ justify-content: center;
+ align-items: center;
+`;
+
+const StyledDash = styled.div`
+ background-color: ${({ theme }) => theme.font.color.light};
+ height: ${({ theme }) => theme.spacing(0.4)};
+ width: ${({ theme }) => theme.spacing(1.5)};
+`;
+
+export const TwoFactorAuthenticationVerificationCodeDash = () => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationCodeSlot.tsx b/packages/twenty-front/src/modules/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationCodeSlot.tsx
new file mode 100644
index 0000000000..9579037924
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationCodeSlot.tsx
@@ -0,0 +1,91 @@
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+import { type SlotProps } from 'input-otp';
+
+const StyledSlot = styled.div<{ isActive: boolean }>`
+ position: relative;
+ width: 24px;
+ height: 32px;
+ font-size: ${({ theme }) => theme.font.size.md};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s;
+ background-color: ${({ theme }) => theme.background.transparent.lighter};
+ border: 1px solid ${({ theme }) => theme.border.color.medium};
+ border-radius: ${({ theme }) => theme.border.radius.sm};
+
+ .group:hover &,
+ .group:focus-within & {
+ border-color: ${({ theme }) => theme.border.color.medium};
+ }
+
+ outline: 0;
+ outline-color: ${({ theme }) => theme.border.color.medium};
+
+ ${({ isActive, theme }) =>
+ isActive &&
+ css`
+ outline-width: 1px;
+ outline-style: solid;
+ outline-color: ${theme.border.color.strong};
+ `}
+`;
+
+const StyledCaretContainer = styled.div`
+ align-items: center;
+ animation: caret-blink 1s steps(2, start) infinite;
+ display: flex;
+ inset: 0;
+ justify-content: center;
+ pointer-events: none;
+ position: absolute;
+
+ @keyframes caret-blink {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0;
+ }
+ }
+`;
+
+const StyledPlaceholderChar = styled.div`
+ opacity: 0.2;
+`;
+
+const StyledInputChar = styled.div`
+ opacity: 1;
+`;
+
+const StyledCaret = styled.div`
+ width: 1px;
+ height: 20px;
+ background-color: ${({ theme }) => theme.font.color.primary};
+`;
+
+type TwoFactorAuthenticationVerificationCodeSlotProps = SlotProps;
+
+export const TwoFactorAuthenticationVerificationCodeSlot = ({
+ char,
+ hasFakeCaret,
+ isActive,
+ placeholderChar,
+}: TwoFactorAuthenticationVerificationCodeSlotProps) => {
+ return (
+
+ {char ? (
+ {char}
+ ) : (
+ {placeholderChar ?? 'X'}
+ )}
+ {hasFakeCaret && (
+
+
+
+ )}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationForSettings.tsx b/packages/twenty-front/src/modules/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationForSettings.tsx
index ea38c82994..6ab4716848 100644
--- a/packages/twenty-front/src/modules/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationForSettings.tsx
+++ b/packages/twenty-front/src/modules/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationForSettings.tsx
@@ -1,16 +1,9 @@
-import { useMutation } from '@apollo/client';
-import { css } from '@emotion/react';
import styled from '@emotion/styled';
-import { useLingui } from '@lingui/react/macro';
-import { OTPInput, type SlotProps } from 'input-otp';
-import { useState } from 'react';
-import { Controller, useForm, useFormContext } from 'react-hook-form';
+import { OTPInput } from 'input-otp';
+import { Controller, useFormContext } from 'react-hook-form';
-import { VERIFY_TWO_FACTOR_AUTHENTICATION_METHOD_FOR_AUTHENTICATED_USER } from '@/settings/two-factor-authentication/graphql/mutations/verifyTwoFactorAuthenticationMethod';
-import { SettingsPath } from '@/types/SettingsPath';
-import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
-import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
-import { useNavigateSettings } from '~/hooks/useNavigateSettings';
+import { TwoFactorAuthenticationVerificationCodeDash } from '@/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationCodeDash';
+import { TwoFactorAuthenticationVerificationCodeSlot } from '@/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationCodeSlot';
// OTP Form Types
type OTPFormValues = {
@@ -19,7 +12,7 @@ type OTPFormValues = {
const StyledOTPContainer = styled.div`
display: flex;
-
+ gap: ${({ theme }) => theme.spacing(1)};
margin-bottom: ${({ theme }) => theme.spacing(8)};
&:has(:disabled) {
@@ -27,196 +20,7 @@ const StyledOTPContainer = styled.div`
}
`;
-const StyledSlotGroup = styled.div`
- display: flex;
-`;
-
-const StyledSlot = styled.div<{ isActive: boolean }>`
- position: relative;
- width: 2.5rem;
- height: 3.5rem;
- font-size: 2rem;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.3s;
- border-top: 1px solid ${({ theme }) => theme.border.color.medium};
- border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
- border-right: 1px solid ${({ theme }) => theme.border.color.medium};
-
- &:first-of-type {
- border-left: 1px solid ${({ theme }) => theme.border.color.medium};
- border-top-left-radius: 0.375rem;
- border-bottom-left-radius: 0.375rem;
- }
-
- &:last-of-type {
- border-top-right-radius: 0.375rem;
- border-bottom-right-radius: 0.375rem;
- }
-
- .group:hover &,
- .group:focus-within & {
- border-color: ${({ theme }) => theme.border.color.medium};
- }
-
- outline: 0;
- outline-color: ${({ theme }) => theme.border.color.medium};
-
- ${({ isActive, theme }) =>
- isActive &&
- css`
- outline-width: 1px;
- outline-style: solid;
- outline-color: ${theme.border.color.strong};
- `}
-`;
-
-const StyledPlaceholderChar = styled.div`
- .group:has(input[data-input-otp-placeholder-shown]) & {
- opacity: 0.2;
- }
-`;
-
-const StyledCaretContainer = styled.div`
- align-items: center;
- animation: caret-blink 1s steps(2, start) infinite;
- display: flex;
- inset: 0;
- justify-content: center;
- pointer-events: none;
- position: absolute;
-
- @keyframes caret-blink {
- 0%,
- 100% {
- opacity: 1;
- }
- 50% {
- opacity: 0;
- }
- }
-`;
-
-const StyledCaret = styled.div`
- width: 1px;
- height: 2rem;
- background-color: ${({ theme }) => theme.font.color.primary};
-`;
-
-const StyledDashContainer = styled.div`
- display: flex;
- width: 2.5rem;
- justify-content: center;
- align-items: center;
-`;
-
-const StyledDash = styled.div`
- background-color: ${({ theme }) => theme.font.color.tertiary};
- border-radius: 9999px;
- height: 0.25rem;
- width: 0.75rem;
-`;
-
-const FakeCaret = () => {
- return (
-
-
-
- );
-};
-
-const FakeDash = () => {
- return (
-
-
-
- );
-};
-
-export const Slot = (props: SlotProps) => {
- return (
-
-
- {props.char ?? props.placeholderChar}
-
- {props.hasFakeCaret && }
-
- );
-};
-
-export const useTwoFactorVerificationForSettings = () => {
- const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
- const navigate = useNavigateSettings();
- const { t } = useLingui();
- const [isLoading, setIsLoading] = useState(false);
- const { loadCurrentUser } = useLoadCurrentUser();
-
- const [verifyTwoFactorAuthenticationMethod] = useMutation(
- VERIFY_TWO_FACTOR_AUTHENTICATION_METHOD_FOR_AUTHENTICATED_USER,
- );
-
- const formConfig = useForm({
- mode: 'onChange',
- defaultValues: {
- otp: '',
- },
- });
-
- const { isSubmitting } = formConfig.formState;
- const otpValue = formConfig.watch('otp');
- const canSave = !isSubmitting && otpValue?.length === 6;
-
- const handleVerificationSuccess = async () => {
- enqueueSuccessSnackBar({
- message: t`Two-factor authentication setup completed successfully!`,
- });
-
- // Reload current user to refresh 2FA status
- await loadCurrentUser();
-
- // Navigate back to profile page
- navigate(SettingsPath.ProfilePage);
- };
-
- const handleSave = async (values: OTPFormValues) => {
- try {
- setIsLoading(true);
-
- await verifyTwoFactorAuthenticationMethod({
- variables: {
- otp: values.otp,
- },
- });
-
- await handleVerificationSuccess();
- } catch {
- enqueueErrorSnackBar({
- message: t`Invalid verification code. Please try again.`,
- });
- } finally {
- setIsLoading(false);
- }
- };
-
- const handleCancel = () => {
- // Reset form and navigate back to profile page
- formConfig.reset();
- navigate(SettingsPath.ProfilePage);
- };
-
- return {
- formConfig,
- isLoading,
- canSave,
- isSubmitting,
- handleSave,
- handleCancel,
- };
-};
-
export const TwoFactorAuthenticationVerificationForSettings = () => {
- // Use the form context from the parent instead of creating a new form instance
const formContext = useFormContext();
return (
@@ -231,29 +35,25 @@ export const TwoFactorAuthenticationVerificationForSettings = () => {
value={value}
render={({ slots }) => (
-
- {slots.slice(0, 3).map((slot, idx) => (
-
- ))}
-
-
-
- {slots.slice(3).map((slot, idx) => (
-
- ))}
-
+ {slots.slice(0, 3).map((slot, idx) => (
+
+ ))}
+
+ {slots.slice(3).map((slot, idx) => (
+
+ ))}
)}
/>
diff --git a/packages/twenty-front/src/modules/settings/two-factor-authentication/hooks/useTwoFactorVerificationForSettings.ts b/packages/twenty-front/src/modules/settings/two-factor-authentication/hooks/useTwoFactorVerificationForSettings.ts
new file mode 100644
index 0000000000..b6e31af495
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/two-factor-authentication/hooks/useTwoFactorVerificationForSettings.ts
@@ -0,0 +1,80 @@
+import { type OTPFormValues } from '@/auth/sign-in-up/hooks/useTwoFactorAuthenticationForm';
+import { VERIFY_TWO_FACTOR_AUTHENTICATION_METHOD_FOR_AUTHENTICATED_USER } from '@/settings/two-factor-authentication/graphql/mutations/verifyTwoFactorAuthenticationMethod';
+import { SettingsPath } from '@/types/SettingsPath';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
+import { useMutation } from '@apollo/client';
+import { useLingui } from '@lingui/react/macro';
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { useNavigateSettings } from '~/hooks/useNavigateSettings';
+
+export const useTwoFactorVerificationForSettings = () => {
+ const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
+ const navigate = useNavigateSettings();
+ const { t } = useLingui();
+ const [isLoading, setIsLoading] = useState(false);
+ const { loadCurrentUser } = useLoadCurrentUser();
+
+ const [verifyTwoFactorAuthenticationMethod] = useMutation(
+ VERIFY_TWO_FACTOR_AUTHENTICATION_METHOD_FOR_AUTHENTICATED_USER,
+ );
+
+ const formConfig = useForm({
+ mode: 'onChange',
+ defaultValues: {
+ otp: '',
+ },
+ });
+
+ const { isSubmitting } = formConfig.formState;
+ const otpValue = formConfig.watch('otp');
+ const canSave = !isSubmitting && otpValue?.length === 6;
+
+ const handleVerificationSuccess = async () => {
+ enqueueSuccessSnackBar({
+ message: t`Two-factor authentication setup completed successfully!`,
+ });
+
+ // Reload current user to refresh 2FA status
+ await loadCurrentUser();
+
+ // Navigate back to profile page
+ navigate(SettingsPath.ProfilePage);
+ };
+
+ const handleSave = async (values: OTPFormValues) => {
+ try {
+ setIsLoading(true);
+
+ await verifyTwoFactorAuthenticationMethod({
+ variables: {
+ otp: values.otp,
+ },
+ });
+
+ await handleVerificationSuccess();
+ } catch {
+ enqueueErrorSnackBar({
+ message: t`Invalid verification code. Please try again.`,
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleCancel = () => {
+ // Reset form and navigate back to profile page
+ formConfig.reset();
+ navigate(SettingsPath.ProfilePage);
+ };
+
+ return {
+ formConfig,
+ isLoading,
+ canSave,
+ isSubmitting,
+ handleSave,
+ handleCancel,
+ };
+};
diff --git a/packages/twenty-front/src/pages/settings/SettingsProfile.tsx b/packages/twenty-front/src/pages/settings/SettingsProfile.tsx
index c6bd180c3e..8b4f00b26f 100644
--- a/packages/twenty-front/src/pages/settings/SettingsProfile.tsx
+++ b/packages/twenty-front/src/pages/settings/SettingsProfile.tsx
@@ -80,7 +80,7 @@ export const SettingsProfile = () => {
has2FAMethod ? (
) : (
-
+
)
}
/>
diff --git a/packages/twenty-front/src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx b/packages/twenty-front/src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
index 21eab7038d..9f7a35b39e 100644
--- a/packages/twenty-front/src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
+++ b/packages/twenty-front/src/pages/settings/SettingsTwoFactorAuthenticationMethod.tsx
@@ -9,11 +9,9 @@ import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { DeleteTwoFactorAuthentication } from '@/settings/two-factor-authentication/components/DeleteTwoFactorAuthenticationMethod';
import { TwoFactorAuthenticationSetupForSettingsEffect } from '@/settings/two-factor-authentication/components/TwoFactorAuthenticationSetupForSettingsEffect';
-import {
- TwoFactorAuthenticationVerificationForSettings,
- useTwoFactorVerificationForSettings,
-} from '@/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationForSettings';
+import { TwoFactorAuthenticationVerificationForSettings } from '@/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationForSettings';
import { useCurrentUserWorkspaceTwoFactorAuthentication } from '@/settings/two-factor-authentication/hooks/useCurrentUserWorkspaceTwoFactorAuthentication';
+import { useTwoFactorVerificationForSettings } from '@/settings/two-factor-authentication/hooks/useTwoFactorVerificationForSettings';
import { extractSecretFromOtpUri } from '@/settings/two-factor-authentication/utils/extractSecretFromOtpUri';
import { SettingsPath } from '@/types/SettingsPath';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@@ -26,40 +24,55 @@ import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledQRCodeContainer = styled.div`
margin: ${({ theme }) => theme.spacing(4)} 0;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: ${({ theme }) => theme.spacing(3)};
`;
-const StyledInstructions = styled.div`
+const StyledQRCodeWrapper = styled.div`
+ align-items: center;
+ background-color: ${({ theme }) => theme.background.secondary};
+ border: 1px solid ${({ theme }) => theme.border.color.light};
+ border-radius: ${({ theme }) => theme.border.radius.md};
+ display: flex;
+ justify-content: center;
+ padding: ${({ theme }) => theme.spacing(4)};
+`;
+
+const StyledQRCode = styled(QRCode)`
+ height: 137px;
+ width: 137px;
+`;
+
+const StyledCopySetupKeyText = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.sm};
- margin-bottom: ${({ theme }) => theme.spacing(4)};
- max-width: 400px;
-`;
-
-const StyledDivider = styled.div`
- width: 100%;
- height: 1px;
- background-color: ${({ theme }) => theme.border.color.light};
- margin: ${({ theme }) => theme.spacing(6)} 0;
+ text-align: left;
+ line-height: 1.5;
`;
const StyledCopySetupKeyLink = styled.button`
background: none;
border: none;
- color: ${({ theme }) => theme.font.color.secondary};
+ color: ${({ theme }) => theme.font.color.tertiary};
cursor: pointer;
- display: flex;
- align-items: center;
- gap: ${({ theme }) => theme.spacing(1)};
+ display: inline;
font-size: ${({ theme }) => theme.font.size.sm};
- margin-top: ${({ theme }) => theme.spacing(2)};
padding: 0;
text-decoration: underline;
+ margin-left: 0;
&:hover {
- color: ${({ theme }) => theme.font.color.primary};
+ color: ${({ theme }) => theme.font.color.secondary};
}
`;
+const StyledDivider = styled.div`
+ margin: ${({ theme }) => theme.spacing(6)} 0;
+ width: 100%;
+`;
+
export const SettingsTwoFactorAuthenticationMethod = () => {
const { t } = useLingui();
const theme = useTheme();
@@ -133,35 +146,34 @@ export const SettingsTwoFactorAuthenticationMethod = () => {
) : (
-
-
-
-
- Use an authenticator app like Google Authenticator, Authy, or
- Microsoft Authenticator to scan this QR code.
-
-
+
- {!qrCode ? : }
- {qrCode && (
-
-
- Copy Setup Key
-
+ {!qrCode ? (
+
+ ) : (
+ <>
+
+
+
+
+ Can't scan? Copy the{' '}
+
+ setup key
+
+
+ >
)}
-
-
-
-
- Enter the 6-digit verification code from your authenticator
- app to complete the setup.
-
-
-
+
)}
diff --git a/packages/twenty-ui/src/display/typography/components/H2Title.tsx b/packages/twenty-ui/src/display/typography/components/H2Title.tsx
index f02053f4fe..022a967e59 100644
--- a/packages/twenty-ui/src/display/typography/components/H2Title.tsx
+++ b/packages/twenty-ui/src/display/typography/components/H2Title.tsx
@@ -50,7 +50,7 @@ export const H2Title = ({