fix(front): constrain 2FA sign-in screens and dedupe their shared shell (#22886)

## Problem

On the card-less onboarding sign-in (`/welcome`), the **2FA
verification** step renders with a full-viewport-wide submit button.

The 2FA **verify** and **provision** forms hard-code `width: 100%` on
their root `StyledForm`. That was harmless while `/welcome` rendered
inside the `AuthModal` `medium` card, which bounded the width. Since
#22398 removed v1 onboarding, `/welcome` renders card-less under
`BlankLayout`, so nothing bounds those forms and they stretch to the
full viewport.

Other steps are **not** affected, which is why only 2FA looks wrong:
- Sign-in form: root sets `width: ONBOARDING_CONTENT_BLOCK_WIDTH;
max-width: 100%` -> capped at 440.
- SSO selection / workspace-scope: base container (`min-width: 240`, no
width) -> shrink-to-fit.
- **2FA verify / provision: `width: 100%` -> full viewport.**

## Fix

Cap the two 2FA forms at `ONBOARDING_CONTENT_BLOCK_WIDTH` (with
`max-width: 100%`), so they sit in the same block as the sign-in page
instead of forcing full-width.

## Refactor (same PR)

The verify and provision components (both introduced together in #13141)
duplicated their layout shell. Extracted the shared instruction-text and
main-content blocks into `SignInUpTwoFactorAuthenticationStyles.ts`. The
form container stays local to each component since the element differs
(a `div` in provision, a `form` in verification).

## Verification

- Reproduced the flex box-model at 1280px: `width:100%` root -> 1196px
full-width button; `width: 440px` -> 440px centered button.
- lint + format + typecheck pass on the changed files.
This commit is contained in:
Charles Bochet
2026-07-14 17:16:01 +02:00
committed by GitHub
parent f13bde1e03
commit 75d9e0b93a
3 changed files with 48 additions and 50 deletions
@@ -4,6 +4,11 @@ import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import {
StyledTwoFactorInstructions,
StyledTwoFactorMainContent,
} from '@/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationStyles';
import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/OnboardingContentBlockWidth';
import { extractSecretFromOtpUri } from '@/settings/two-factor-authentication/utils/extractSecretFromOtpUri';
import { styled } from '@linaria/react';
import { Trans, useLingui } from '@lingui/react/macro';
@@ -20,36 +25,12 @@ import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
const QRCode = resolveCjsModuleDefaultExport(QRCodeModule);
const StyledMainContentContainer = styled.div`
margin-bottom: ${themeCssVariables.spacing[8]};
margin-top: ${themeCssVariables.spacing[4]};
text-align: center;
`;
const StyledTextContainer = styled.div`
align-items: center;
color: ${themeCssVariables.font.color.tertiary};
font-size: ${themeCssVariables.font.size.sm};
margin-bottom: ${themeCssVariables.spacing[4]};
max-width: 280px;
text-align: center;
& > a {
color: ${themeCssVariables.font.color.tertiary};
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
`;
const StyledForm = styled.div`
align-items: center;
display: flex;
flex-direction: column;
width: 100%;
max-width: 100%;
width: ${ONBOARDING_CONTENT_BLOCK_WIDTH}px;
`;
const StyledCopySetupKeyLink = styled.button`
@@ -94,13 +75,13 @@ export const SignInUpTwoFactorAuthenticationProvision = () => {
<>
<TwoFactorAuthenticationSetupEffect />
<StyledForm>
<StyledTextContainer>
<StyledTwoFactorInstructions>
<Trans>
Use authenticator apps and browser extensions like 1Password, Authy,
Microsoft Authenticator to generate one-time passwords
</Trans>
</StyledTextContainer>
<StyledMainContentContainer>
</StyledTwoFactorInstructions>
<StyledTwoFactorMainContent>
{!qrCode ? <Loader /> : <QRCode value={qrCode} />}
{qrCode && (
<StyledCopySetupKeyLink onClick={handleCopySetupKey}>
@@ -108,7 +89,7 @@ export const SignInUpTwoFactorAuthenticationProvision = () => {
<Trans>Copy Setup Key</Trans>
</StyledCopySetupKeyLink>
)}
</StyledMainContentContainer>
</StyledTwoFactorMainContent>
<MainButton
title={t`Next`}
onClick={handleClick}
@@ -0,0 +1,26 @@
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
export const StyledTwoFactorMainContent = styled.div`
margin-bottom: ${themeCssVariables.spacing[8]};
margin-top: ${themeCssVariables.spacing[4]};
text-align: center;
`;
export const StyledTwoFactorInstructions = styled.div`
align-items: center;
color: ${themeCssVariables.font.color.tertiary};
font-size: ${themeCssVariables.font.size.sm};
margin-bottom: ${themeCssVariables.spacing[4]};
max-width: 280px;
text-align: center;
& > a {
color: ${themeCssVariables.font.color.tertiary};
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
`;
@@ -10,7 +10,12 @@ import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import {
StyledTwoFactorInstructions,
StyledTwoFactorMainContent,
} from '@/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationStyles';
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/OnboardingContentBlockWidth';
import { useCaptcha } from '@/client-config/hooks/useCaptcha';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Trans, useLingui } from '@lingui/react/macro';
@@ -25,17 +30,12 @@ import { useNavigateApp } from '~/hooks/useNavigateApp';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
const StyledMainContentContainer = styled.div`
margin-bottom: ${themeCssVariables.spacing[8]};
margin-top: ${themeCssVariables.spacing[4]};
text-align: center;
`;
const StyledForm = styled.form`
align-items: center;
display: flex;
flex-direction: column;
width: 100%;
max-width: 100%;
width: ${ONBOARDING_CONTENT_BLOCK_WIDTH}px;
`;
const StyledSlot = styled.div<{ isActive: boolean }>`
@@ -161,15 +161,6 @@ const StyledOTPContainer = styled.div`
const StyledSlotGroup = styled.div`
display: flex;
`;
const StyledTextContainer = styled.div`
align-items: center;
color: ${themeCssVariables.font.color.tertiary};
font-size: ${themeCssVariables.font.size.sm};
margin-bottom: ${themeCssVariables.spacing[4]};
max-width: 280px;
text-align: center;
`;
const StyledActionBackLinkContainer = styled.div`
margin: ${themeCssVariables.spacing[3]} 0 0;
@@ -228,10 +219,10 @@ export const SignInUpTOTPVerification = () => {
return (
<StyledForm onSubmit={form.handleSubmit(submitOTP)}>
<StyledTextContainer>
<StyledTwoFactorInstructions>
<Trans>Paste the code below</Trans>
</StyledTextContainer>
<StyledMainContentContainer>
</StyledTwoFactorInstructions>
<StyledTwoFactorMainContent>
{/* // oxlint-disable-next-line react/jsx-props-no-spreading */}
<Controller
name="otp"
@@ -270,7 +261,7 @@ export const SignInUpTOTPVerification = () => {
/>
)}
/>
</StyledMainContentContainer>
</StyledTwoFactorMainContent>
<MainButton
title={t`Submit`}
type="submit"