From f89a01342bc6dbc3bd0966504570af218d3b9e86 Mon Sep 17 00:00:00 2001
From: Kailash Rajput <98249911+Kailash8799@users.noreply.github.com>
Date: Thu, 21 Aug 2025 14:50:32 +0530
Subject: [PATCH] fix ui as per figma file (#14008)
Fix issue: #13742
Step number look better then without number so did't removed that
Added description in H2Title component
Not added new description shown in figma because it is to large and
H2Title truncate after 2 lines
We can add this figma content only if we do not use H2Title component
---------
Co-authored-by: Lucas Bordeau
---
...ctorAuthenticationVerificationCodeDash.tsx | 22 ++
...ctorAuthenticationVerificationCodeSlot.tsx | 91 +++++++
...rAuthenticationVerificationForSettings.tsx | 248 ++----------------
.../useTwoFactorVerificationForSettings.ts | 80 ++++++
.../src/pages/settings/SettingsProfile.tsx | 2 +-
.../SettingsTwoFactorAuthenticationMethod.tsx | 98 ++++---
.../display/typography/components/H2Title.tsx | 2 +-
7 files changed, 274 insertions(+), 269 deletions(-)
create mode 100644 packages/twenty-front/src/modules/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationCodeDash.tsx
create mode 100644 packages/twenty-front/src/modules/settings/two-factor-authentication/components/TwoFactorAuthenticationVerificationCodeSlot.tsx
create mode 100644 packages/twenty-front/src/modules/settings/two-factor-authentication/hooks/useTwoFactorVerificationForSettings.ts
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 = ({