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 

<img width="719" height="553" alt="image"
src="https://github.com/user-attachments/assets/1aae680f-de4e-424b-baf7-c0d402581d28"
/>

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Kailash Rajput
2025-08-21 14:50:32 +05:30
committed by GitHub
parent 809fe5699f
commit f89a01342b
7 changed files with 274 additions and 269 deletions
@@ -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 (
<StyledDashContainer>
<StyledDash />
</StyledDashContainer>
);
};
@@ -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 (
<StyledSlot isActive={isActive}>
{char ? (
<StyledInputChar>{char}</StyledInputChar>
) : (
<StyledPlaceholderChar>{placeholderChar ?? 'X'}</StyledPlaceholderChar>
)}
{hasFakeCaret && (
<StyledCaretContainer>
<StyledCaret />
</StyledCaretContainer>
)}
</StyledSlot>
);
};
@@ -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 (
<StyledCaretContainer>
<StyledCaret />
</StyledCaretContainer>
);
};
const FakeDash = () => {
return (
<StyledDashContainer>
<StyledDash />
</StyledDashContainer>
);
};
export const Slot = (props: SlotProps) => {
return (
<StyledSlot isActive={props.isActive}>
<StyledPlaceholderChar>
{props.char ?? props.placeholderChar}
</StyledPlaceholderChar>
{props.hasFakeCaret && <FakeCaret />}
</StyledSlot>
);
};
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<OTPFormValues>({
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<OTPFormValues>();
return (
@@ -231,29 +35,25 @@ export const TwoFactorAuthenticationVerificationForSettings = () => {
value={value}
render={({ slots }) => (
<StyledOTPContainer>
<StyledSlotGroup>
{slots.slice(0, 3).map((slot, idx) => (
<Slot
key={idx}
char={slot.char}
placeholderChar={slot.placeholderChar}
isActive={slot.isActive}
hasFakeCaret={slot.hasFakeCaret}
/>
))}
</StyledSlotGroup>
<FakeDash />
<StyledSlotGroup>
{slots.slice(3).map((slot, idx) => (
<Slot
key={idx}
char={slot.char}
placeholderChar={slot.placeholderChar}
isActive={slot.isActive}
hasFakeCaret={slot.hasFakeCaret}
/>
))}
</StyledSlotGroup>
{slots.slice(0, 3).map((slot, idx) => (
<TwoFactorAuthenticationVerificationCodeSlot
key={idx}
char={slot.char}
placeholderChar={slot.placeholderChar}
isActive={slot.isActive}
hasFakeCaret={slot.hasFakeCaret}
/>
))}
<TwoFactorAuthenticationVerificationCodeDash />
{slots.slice(3).map((slot, idx) => (
<TwoFactorAuthenticationVerificationCodeSlot
key={idx + 3}
char={slot.char}
placeholderChar={slot.placeholderChar}
isActive={slot.isActive}
hasFakeCaret={slot.hasFakeCaret}
/>
))}
</StyledOTPContainer>
)}
/>
@@ -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<OTPFormValues>({
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,
};
};
@@ -80,7 +80,7 @@ export const SettingsProfile = () => {
has2FAMethod ? (
<Status text={'Active'} color={'turquoise'} />
) : (
<Status text={'Setup'} color={'blue'} />
<Status text={'Deactivated'} color={'gray'} />
)
}
/>
@@ -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 = () => {
) : (
<Section>
<TwoFactorAuthenticationSetupForSettingsEffect />
<H2Title title={t`1. Scan the QR code`} />
<StyledInstructions>
<Trans>
Use an authenticator app like Google Authenticator, Authy, or
Microsoft Authenticator to scan this QR code.
</Trans>
</StyledInstructions>
<H2Title
title={t`Authenticator app`}
description={t`Authenticator apps and browser extensions like 1Password, Authy, Microsoft Authenticator, etc. generate one-time passwords that are used as a second factor to verify your identity when prompted during sign-in.`}
/>
<StyledQRCodeContainer>
{!qrCode ? <Loader /> : <QRCode value={qrCode} />}
{qrCode && (
<StyledCopySetupKeyLink onClick={handleCopySetupKey}>
<IconCopy size={theme.icon.size.sm} />
<Trans>Copy Setup Key</Trans>
</StyledCopySetupKeyLink>
{!qrCode ? (
<Loader />
) : (
<>
<StyledQRCodeWrapper>
<StyledQRCode value={qrCode} />
</StyledQRCodeWrapper>
<StyledCopySetupKeyText>
<Trans>Can't scan? Copy the</Trans>{' '}
<StyledCopySetupKeyLink onClick={handleCopySetupKey}>
<Trans>setup key</Trans>
</StyledCopySetupKeyLink>
</StyledCopySetupKeyText>
</>
)}
</StyledQRCodeContainer>
<StyledDivider />
<H2Title title={t`2. Enter the code`} />
<StyledInstructions>
<Trans>
Enter the 6-digit verification code from your authenticator
app to complete the setup.
</Trans>
</StyledInstructions>
<H2Title
title={t`Verify the code from the app`}
description={t`Copy paste the code below`}
/>
<TwoFactorAuthenticationVerificationForSettings />
</Section>
)}
@@ -50,7 +50,7 @@ export const H2Title = ({
<StyledDescription>
<OverflowingTextWithTooltip
text={description}
displayedMaxRows={2}
displayedMaxRows={5}
isTooltipMultiline={true}
/>
</StyledDescription>