Improve email validation modal design (#12490)

closes https://github.com/twentyhq/core-team-issues/issues/1020
This commit is contained in:
nitin
2025-06-12 22:35:36 +05:30
committed by GitHub
parent 4f307a24b0
commit a5c0922399
7 changed files with 255 additions and 112 deletions

View File

@ -3,32 +3,83 @@ import styled from '@emotion/styled';
import { SubTitle } from '@/auth/components/SubTitle'; import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title'; import { Title } from '@/auth/components/Title';
import { useHandleResendEmailVerificationToken } from '@/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken'; import { useHandleResendEmailVerificationToken } from '@/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken';
import { useTheme } from '@emotion/react'; import {
import { IconMail } from 'twenty-ui/display'; SignInUpStep,
import { Loader } from 'twenty-ui/feedback'; signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { OnboardingModalCircularIcon } from '@/onboarding/components/OnboardingModalCircularIcon';
import { t } from '@lingui/core/macro';
import { useSetRecoilState } from 'recoil';
import {
IconGmail,
IconMail,
IconMailX,
IconMicrosoft,
} from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input'; import { MainButton } from 'twenty-ui/input';
import { RGBA } from 'twenty-ui/theme';
import { AnimatedEaseIn } from 'twenty-ui/utilities'; import { AnimatedEaseIn } from 'twenty-ui/utilities';
const StyledMailContainer = styled.div` const StyledContainer = styled.div`
align-items: center;
display: flex; display: flex;
justify-content: center; flex-direction: column;
border: 2px solid ${(props) => props.color}; align-items: center;
border-radius: ${({ theme }) => theme.border.radius.rounded}; gap: ${({ theme }) => theme.spacing(8)};
box-shadow: ${(props) => width: 100%;
props.color && `-4px 4px 0 -2px ${RGBA(props.color, 1)}`}; `;
height: 36px;
width: 36px; const StyledTextContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(4)}; display: flex;
flex-direction: column;
align-items: center;
text-align: center;
`; `;
const StyledEmail = styled.span` const StyledEmail = styled.span`
font-weight: ${({ theme }) => theme.font.weight.medium}; font-weight: ${({ theme }) => theme.font.weight.medium};
`; `;
const StyledButtonContainer = styled.div` const StyledButtonsContainer = styled.div`
margin-top: ${({ theme }) => theme.spacing(8)}; display: flex;
flex-direction: column;
align-items: center;
gap: ${({ theme }) => theme.spacing(3)};
width: 100%;
max-width: 240px;
`;
const StyledBottomLinks = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
justify-content: center;
`;
const StyledLinkButton = styled.button`
background: none;
border: none;
font-family: ${({ theme }) => theme.font.family};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.regular};
line-height: 140%;
color: ${({ theme }) => theme.font.color.tertiary};
cursor: pointer;
&:hover {
color: ${({ theme }) => theme.font.color.secondary};
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
const StyledDot = styled.div`
background: ${({ theme }) => theme.font.color.light};
border-radius: 50%;
height: 2px;
width: 2px;
`; `;
export const EmailVerificationSent = ({ export const EmailVerificationSent = ({
@ -38,46 +89,101 @@ export const EmailVerificationSent = ({
email: string | null; email: string | null;
isError?: boolean; isError?: boolean;
}) => { }) => {
const theme = useTheme(); const setSignInUpStep = useSetRecoilState(signInUpStepState);
const color =
theme.name === 'light' ? theme.grayScale.gray90 : theme.grayScale.gray10;
const { handleResendEmailVerificationToken, loading: isLoading } = const { handleResendEmailVerificationToken, loading: isLoading } =
useHandleResendEmailVerificationToken(); useHandleResendEmailVerificationToken();
return ( const handleOpenGmail = () => {
const gmailUrl = email
? `https://mail.google.com/mail/u/${email}/`
: 'https://mail.google.com/';
window.open(gmailUrl, '_blank');
};
const handleOpenOutlook = () => {
const outlookUrl = email
? `https://outlook.live.com/mail/${email}/`
: 'https://outlook.live.com/';
window.open(outlookUrl, '_blank');
};
const handleChangeEmail = () => {
setSignInUpStep(SignInUpStep.Email);
};
const title = isError ? t`Email Verification Failed` : t`Check your Emails`;
const subtitle = isError
? t`We encountered an issue verifying`
: t`A verification email has been sent to`;
const Icon = isError ? IconMailX : IconMail;
const mainButtons = isError ? (
<> <>
<AnimatedEaseIn> <MainButton
<StyledMailContainer color={color}> title={t`Try with another email`}
<IconMail color={color} size={24} stroke={3} /> onClick={handleChangeEmail}
</StyledMailContainer> variant="secondary"
</AnimatedEaseIn> fullWidth
<Title animate> />
{isError ? 'Email Verification Failed' : 'Confirm Your Email Address'} <MainButton
</Title> title={isLoading ? t`Sending...` : t`Resend email`}
<SubTitle> onClick={handleResendEmailVerificationToken(email)}
{isError ? ( disabled={isLoading}
<> fullWidth
Oops! We encountered an issue verifying{' '} />
<StyledEmail>{email}</StyledEmail>. Please request a new </>
verification email and try again. ) : (
</> <>
) : ( <MainButton
<> title={t`Open Gmail`}
A verification email has been sent to{' '} onClick={handleOpenGmail}
<StyledEmail>{email}</StyledEmail>. Please check your inbox and Icon={IconGmail}
click the link in the email to activate your account. variant="secondary"
</> fullWidth
)} />
</SubTitle> <MainButton
<StyledButtonContainer> title={t`Open Outlook`}
<MainButton onClick={handleOpenOutlook}
title="Click to resend" Icon={IconMicrosoft}
onClick={handleResendEmailVerificationToken(email)} variant="secondary"
Icon={() => (isLoading ? <Loader /> : undefined)} fullWidth
width={200} />
/>
</StyledButtonContainer>
</> </>
); );
return (
<StyledContainer>
<AnimatedEaseIn>
<OnboardingModalCircularIcon Icon={Icon} />
</AnimatedEaseIn>
<StyledTextContainer>
<Title animate noMarginTop>
{title}
</Title>
<SubTitle>
{subtitle} <StyledEmail>{email}</StyledEmail>
</SubTitle>
</StyledTextContainer>
<StyledButtonsContainer>{mainButtons}</StyledButtonsContainer>
{!isError && (
<StyledBottomLinks>
<StyledLinkButton
onClick={handleResendEmailVerificationToken(email)}
disabled={isLoading}
>
{isLoading ? t`Sending...` : t`Resend email`}
</StyledLinkButton>
<StyledDot />
<StyledLinkButton onClick={handleChangeEmail}>
{t`Change email`}
</StyledLinkButton>
</StyledBottomLinks>
)}
</StyledContainer>
);
}; };

View File

@ -0,0 +1,33 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent } from 'twenty-ui/display';
import { RGBA } from 'twenty-ui/theme';
const StyledCheckContainer = styled.div<{ color: string }>`
align-items: center;
display: flex;
justify-content: center;
border: 2px solid ${({ color }) => color};
border-radius: ${({ theme }) => theme.border.radius.rounded};
box-shadow: ${({ color }) => color && `-4px 4px 0 -2px ${RGBA(color, 1)}`};
height: 36px;
width: 36px;
`;
type OnboardingModalCircularIconProps = {
Icon: IconComponent;
};
export const OnboardingModalCircularIcon = ({
Icon,
}: OnboardingModalCircularIconProps) => {
const theme = useTheme();
const color =
theme.name === 'light' ? theme.grayScale.gray90 : theme.grayScale.gray10;
return (
<StyledCheckContainer color={color}>
<Icon size={24} color={color} stroke={3} />
</StyledCheckContainer>
);
};

View File

@ -37,7 +37,7 @@ const StyledModalDiv = styled(motion.div)<{
z-index: ${RootStackingContextZIndices.RootModal}; // should be higher than Backdrop's z-index z-index: ${RootStackingContextZIndices.RootModal}; // should be higher than Backdrop's z-index
width: ${({ isMobile, size, theme }) => { width: ${({ isMobile, size, theme }) => {
if (isMobile) return theme.modal.size.fullscreen; if (isMobile) return theme.modal.size.fullscreen.width;
switch (size) { switch (size) {
case 'small': case 'small':
return theme.modal.size.sm.width; return theme.modal.size.sm.width;

View File

@ -1,85 +1,86 @@
import { SubTitle } from '@/auth/components/SubTitle'; import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title'; import { Title } from '@/auth/components/Title';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
import { OnboardingModalCircularIcon } from '@/onboarding/components/OnboardingModalCircularIcon';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { Modal } from '@/ui/layout/modal/components/Modal'; import { Modal } from '@/ui/layout/modal/components/Modal';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useState } from 'react';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { IconCheck } from 'twenty-ui/display'; import { IconCheck } from 'twenty-ui/display';
import { Loader } from 'twenty-ui/feedback';
import { MainButton } from 'twenty-ui/input'; import { MainButton } from 'twenty-ui/input';
import { RGBA } from 'twenty-ui/theme';
import { AnimatedEaseIn } from 'twenty-ui/utilities'; import { AnimatedEaseIn } from 'twenty-ui/utilities';
import { useGetCurrentUserLazyQuery } from '~/generated/graphql'; import { useGetCurrentUserLazyQuery } from '~/generated/graphql';
import { useNavigateApp } from '~/hooks/useNavigateApp'; import { useNavigateApp } from '~/hooks/useNavigateApp';
const StyledCheckContainer = styled.div` const StyledModalContent = styled(Modal.Content)`
align-items: center; gap: ${({ theme }) => theme.spacing(8)};
display: flex;
justify-content: center;
border: 2px solid ${(props) => props.color};
border-radius: ${({ theme }) => theme.border.radius.rounded};
box-shadow: ${(props) =>
props.color && `-4px 4px 0 -2px ${RGBA(props.color, 1)}`};
height: 36px;
width: 36px;
margin-bottom: ${({ theme }) => theme.spacing(4)};
`; `;
const StyledButtonContainer = styled.div` const StyledTitleContainer = styled.div`
margin-top: ${({ theme }) => theme.spacing(8)}; display: flex;
flex-direction: column;
align-items: center;
text-align: center;
`; `;
export const PaymentSuccess = () => { export const PaymentSuccess = () => {
const theme = useTheme();
const navigate = useNavigateApp(); const navigate = useNavigateApp();
const subscriptionStatus = useSubscriptionStatus(); const subscriptionStatus = useSubscriptionStatus();
const [getCurrentUser] = useGetCurrentUserLazyQuery(); const [getCurrentUser] = useGetCurrentUserLazyQuery();
const setCurrentUser = useSetRecoilState(currentUserState); const setCurrentUser = useSetRecoilState(currentUserState);
const color = const [isLoading, setIsLoading] = useState(false);
theme.name === 'light' ? theme.grayScale.gray90 : theme.grayScale.gray10;
const navigateWithSubscriptionCheck = async () => { const navigateWithSubscriptionCheck = async () => {
if (isDefined(subscriptionStatus)) { if (isLoading) return;
navigate(AppPath.CreateWorkspace);
return; setIsLoading(true);
try {
if (isDefined(subscriptionStatus)) {
navigate(AppPath.CreateWorkspace);
return;
}
const result = await getCurrentUser({ fetchPolicy: 'network-only' });
const currentUser = result.data?.currentUser;
const refreshedSubscriptionStatus =
currentUser?.currentWorkspace?.currentBillingSubscription?.status;
if (isDefined(currentUser) && isDefined(refreshedSubscriptionStatus)) {
setCurrentUser(currentUser);
navigate(AppPath.CreateWorkspace);
return;
}
throw new Error(
"We're waiting for a confirmation from our payment provider (Stripe).\n" +
'Please try again in a few seconds, sorry.',
);
} catch (error) {
setIsLoading(false);
throw error;
} }
const result = await getCurrentUser({ fetchPolicy: 'network-only' });
const currentUser = result.data?.currentUser;
const refreshedSubscriptionStatus =
currentUser?.currentWorkspace?.currentBillingSubscription?.status;
if (isDefined(currentUser) && isDefined(refreshedSubscriptionStatus)) {
setCurrentUser(currentUser);
navigate(AppPath.CreateWorkspace);
return;
}
throw new Error(
"We're waiting for a confirmation from our payment provider (Stripe).\n" +
'Please try again in a few seconds, sorry.',
);
}; };
return ( return (
<Modal.Content isVerticalCentered isHorizontalCentered> <StyledModalContent isVerticalCentered isHorizontalCentered>
<AnimatedEaseIn> <AnimatedEaseIn>
<StyledCheckContainer color={color}> <OnboardingModalCircularIcon Icon={IconCheck} />
<IconCheck color={color} size={24} stroke={3} />
</StyledCheckContainer>
</AnimatedEaseIn> </AnimatedEaseIn>
<Title>All set!</Title> <StyledTitleContainer>
<SubTitle>Your account has been activated.</SubTitle> <Title noMarginTop>All set!</Title>
<StyledButtonContainer> <SubTitle>Your account has been activated.</SubTitle>
<MainButton </StyledTitleContainer>
title="Start" <MainButton
width={200} title="Start"
onClick={navigateWithSubscriptionCheck} width={200}
/> onClick={navigateWithSubscriptionCheck}
</StyledButtonContainer> Icon={() => (isLoading ? <Loader /> : null)}
</Modal.Content> disabled={isLoading}
/>
</StyledModalContent>
); );
}; };

View File

@ -4,18 +4,18 @@ export {
IconAlertCircle, IconAlertCircle,
IconAlertTriangle, IconAlertTriangle,
IconApi, IconApi,
IconAppWindow,
IconApps, IconApps,
IconAppWindow,
IconArchive, IconArchive,
IconArchiveOff, IconArchiveOff,
IconArrowBackUp, IconArrowBackUp,
IconArrowDown, IconArrowDown,
IconArrowLeft, IconArrowLeft,
IconArrowRight, IconArrowRight,
IconArrowUp,
IconArrowUpRight,
IconArrowsDiagonal, IconArrowsDiagonal,
IconArrowsVertical, IconArrowsVertical,
IconArrowUp,
IconArrowUpRight,
IconAt, IconAt,
IconBaselineDensitySmall, IconBaselineDensitySmall,
IconBell, IconBell,
@ -47,8 +47,8 @@ export {
IconChevronDown, IconChevronDown,
IconChevronLeft, IconChevronLeft,
IconChevronRight, IconChevronRight,
IconChevronUp,
IconChevronsRight, IconChevronsRight,
IconChevronUp,
IconCircleDot, IconCircleDot,
IconCircleOff, IconCircleOff,
IconCirclePlus, IconCirclePlus,
@ -202,6 +202,7 @@ export {
IconLogout, IconLogout,
IconMail, IconMail,
IconMailCog, IconMailCog,
IconMailX,
IconMap, IconMap,
IconMaximize, IconMaximize,
IconMessage, IconMessage,

View File

@ -65,18 +65,18 @@ export {
IconAlertCircle, IconAlertCircle,
IconAlertTriangle, IconAlertTriangle,
IconApi, IconApi,
IconAppWindow,
IconApps, IconApps,
IconAppWindow,
IconArchive, IconArchive,
IconArchiveOff, IconArchiveOff,
IconArrowBackUp, IconArrowBackUp,
IconArrowDown, IconArrowDown,
IconArrowLeft, IconArrowLeft,
IconArrowRight, IconArrowRight,
IconArrowUp,
IconArrowUpRight,
IconArrowsDiagonal, IconArrowsDiagonal,
IconArrowsVertical, IconArrowsVertical,
IconArrowUp,
IconArrowUpRight,
IconAt, IconAt,
IconBaselineDensitySmall, IconBaselineDensitySmall,
IconBell, IconBell,
@ -108,8 +108,8 @@ export {
IconChevronDown, IconChevronDown,
IconChevronLeft, IconChevronLeft,
IconChevronRight, IconChevronRight,
IconChevronUp,
IconChevronsRight, IconChevronsRight,
IconChevronUp,
IconCircleDot, IconCircleDot,
IconCircleOff, IconCircleOff,
IconCirclePlus, IconCirclePlus,
@ -263,6 +263,7 @@ export {
IconLogout, IconLogout,
IconMail, IconMail,
IconMailCog, IconMailCog,
IconMailX,
IconMap, IconMap,
IconMaximize, IconMaximize,
IconMessage, IconMessage,

View File

@ -16,6 +16,7 @@ export const MODAL: {
height: '800px', height: '800px',
}, },
fullscreen: { fullscreen: {
width: '100dvw',
height: '100dvh', height: '100dvh',
}, },
}, },