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:
+22
@@ -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>
|
||||
);
|
||||
};
|
||||
+91
@@ -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>
|
||||
);
|
||||
};
|
||||
+24
-224
@@ -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>
|
||||
)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user