Fixing Singup sequence FLASHING💥 (#11371)

After investiagting the different options ([see related
issue](https://github.com/twentyhq/core-team-issues/issues/660#issuecomment-2766030972))
I decided to add a "Verify Component" and a to build a custom Layout for
this route.

Reason I cannot use the default one is to have all preloaded once the
user changes website and lands on the verify route.

Reason I did not modify the DefaultLayout to match our need is that is
would require many changes in order to avoid preloading states for our
specific usecase.

Fixes https://github.com/twentyhq/core-team-issues/issues/660

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Guillim
2025-04-04 17:25:15 +02:00
committed by GitHub
parent 609e06fd14
commit 10e140495c
22 changed files with 302 additions and 143 deletions

View File

@ -16,8 +16,8 @@ import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider
import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager';
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider';
import { UserThemeProviderEffect } from '@/ui/theme/components/AppThemeProvider';
import { BaseThemeProvider } from '@/ui/theme/components/BaseThemeProvider';
import { UserThemeProviderEffect } from '@/ui/theme/components/UserThemeProviderEffect';
import { PageFavicon } from '@/ui/utilities/page-favicon/components/PageFavicon';
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
import { ServerPreconnect } from '@/ui/utilities/server-preconnect/components/ServerPreconnect';

View File

@ -4,6 +4,7 @@ import {
useLocation,
useNavigate,
useParams,
useSearchParams,
} from 'react-router-dom';
import { useRecoilValue } from 'recoil';
@ -13,6 +14,7 @@ import {
} from '@/analytics/hooks/useEventTracker';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
@ -21,10 +23,9 @@ import { AppPath } from '@/types/AppPath';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { SettingsPath } from '@/types/SettingsPath';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { isDefined } from 'twenty-shared/utils';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
import { isDefined } from 'twenty-shared/utils';
// TODO: break down into smaller functions and / or hooks
// - moved usePageChangeEffectNavigateLocation into dedicated hook
@ -58,11 +59,16 @@ export const PageChangeEffect = () => {
}
}, [location, previousLocation]);
const [searchParams] = useSearchParams();
const navigationParams = searchParams.get('animateModal')
? `?animateModal=${searchParams.get('animateModal')}`
: '';
useEffect(() => {
if (isDefined(pageChangeEffectNavigateLocation)) {
navigate(pageChangeEffectNavigateLocation);
navigate(pageChangeEffectNavigateLocation + navigationParams);
}
}, [navigate, pageChangeEffectNavigateLocation]);
}, [navigate, pageChangeEffectNavigateLocation, navigationParams]);
useEffect(() => {
const isLeavingRecordIndexPage = !!matchPath(

View File

@ -1,12 +1,13 @@
import { AppRouterProviders } from '@/app/components/AppRouterProviders';
import { SettingsRoutes } from '@/app/components/SettingsRoutes';
import { Verify } from '@/auth/components/Verify';
import { VerifyEffect } from '@/auth/components/VerifyEffect';
import { VerifyEmailEffect } from '@/auth/components/VerifyEmailEffect';
import indexAppPath from '@/navigation/utils/indexAppPath';
import { AppPath } from '@/types/AppPath';
import { BlankLayout } from '@/ui/layout/page/components/BlankLayout';
import { DefaultLayout } from '@/ui/layout/page/components/DefaultLayout';
import {
createBrowserRouter,
createRoutesFromElements,
@ -38,7 +39,7 @@ export const useCreateAppRouter = (
loader={async () => Promise.resolve(null)}
>
<Route element={<DefaultLayout />}>
<Route path={AppPath.Verify} element={<VerifyEffect />} />
<Route path={AppPath.Verify} element={<Verify />} />
<Route path={AppPath.VerifyEmail} element={<VerifyEmailEffect />} />
<Route path={AppPath.SignInUp} element={<SignInUp />} />
<Route path={AppPath.Invite} element={<SignInUp />} />

View File

@ -8,10 +8,20 @@ const StyledContent = styled(Modal.Content)`
justify-content: center;
`;
type AuthModalProps = { children: React.ReactNode };
type AuthModalProps = {
children: React.ReactNode;
isOpenAnimated?: boolean;
};
export const AuthModal = ({ children }: AuthModalProps) => (
<Modal padding={'none'} modalVariant="primary">
export const AuthModal = ({
children,
isOpenAnimated = true,
}: AuthModalProps) => (
<Modal
padding={'none'}
modalVariant="primary"
isOpenAnimated={isOpenAnimated}
>
<ScrollWrapper componentInstanceId="scroll-wrapper-modal-content">
<StyledContent>{children}</StyledContent>
</ScrollWrapper>

View File

@ -6,10 +6,11 @@ import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin';
import { AppPath } from '@/types/AppPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { isDefined } from 'twenty-shared/utils';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { SignInUpLoading } from '~/pages/auth/SignInUpLoading';
export const VerifyEffect = () => {
export const Verify = () => {
const [searchParams] = useSearchParams();
const loginToken = searchParams.get('loginToken');
const errorMessage = searchParams.get('errorMessage');
@ -36,5 +37,5 @@ export const VerifyEffect = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <></>;
return <SignInUpLoading />;
};

View File

@ -3,14 +3,14 @@ import { AppPath } from '@/types/AppPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { useLingui } from '@lingui/react/macro';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { EmailVerificationSent } from '../sign-in-up/components/EmailVerificationSent';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { EmailVerificationSent } from '../sign-in-up/components/EmailVerificationSent';
export const VerifyEmailEffect = () => {
const { getLoginTokenFromEmailVerificationToken } = useAuth();
@ -50,6 +50,7 @@ export const VerifyEmailEffect = () => {
if (workspaceUrl.slice(0, -1) !== window.location.origin) {
return redirectToWorkspaceDomain(workspaceUrl, AppPath.Verify, {
loginToken: loginToken.token,
animateModal: false,
});
}
verifyLoginToken(loginToken.token);

View File

@ -416,6 +416,7 @@ export const useAuth = () => {
{
...(!isEmailVerificationRequired && {
loginToken: signUpResult.data.signUp.loginToken.token,
animateModal: false,
}),
email,
},

View File

@ -2,6 +2,8 @@ import { useRecoilValue } from 'recoil';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
import { AppFullScreenErrorFallback } from '@/error-handler/components/AppFullScreenErrorFallback';
import { AppPath } from '@/types/AppPath';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
children,
@ -10,8 +12,15 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
clientConfigApiStatusState,
);
const { isMatchingLocation } = useIsMatchingLocation();
// TODO: Implement a better loading strategy
if (!isLoaded) return null;
if (
!isLoaded &&
!isMatchingLocation(AppPath.Verify) &&
!isMatchingLocation(AppPath.VerifyEmail)
)
return null;
return isErrored && error instanceof Error ? (
<AppFullScreenErrorFallback

View File

@ -10,7 +10,6 @@ export const ObjectMetadataItemsProvider = ({
children,
}: React.PropsWithChildren) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const shouldDisplayChildren = objectMetadataItems.length > 0;
return (

View File

@ -13,7 +13,7 @@ export const useImpersonationRedirect = () => {
return redirectToWorkspaceDomain(
getWorkspaceUrl(workspaceUrls),
AppPath.Verify,
{ loginToken },
{ loginToken, animateModal: false },
);
};

View File

@ -56,16 +56,16 @@ const getResult = (isDefaultLayoutAuthModalVisible = true) =>
// prettier-ignore
const testCases = [
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: false },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.COMPLETED, res: false },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.COMPLETED, res: false },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.COMPLETED, res: false },
{ loc: AppPath.Verify, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: false },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: false },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: false },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: false },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: false },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.COMPLETED, res: false },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: true },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.COMPLETED, res: true },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.COMPLETED, res: true },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.COMPLETED, res: true },
{ loc: AppPath.Verify, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: true },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: true },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: true },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: true },
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.COMPLETED, res: true },
{ loc: AppPath.VerifyEmail, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: true },
{ loc: AppPath.VerifyEmail, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.COMPLETED, res: true },

View File

@ -6,9 +6,9 @@ import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { AppPath } from '@/types/AppPath';
import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { isDefined } from 'twenty-shared/utils';
import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { isDefined } from 'twenty-shared/utils';
export const useShowAuthModal = () => {
const { isMatchingLocation } = useIsMatchingLocation();
@ -21,8 +21,11 @@ export const useShowAuthModal = () => {
);
return useMemo(() => {
if (isMatchingLocation(AppPath.Verify)) {
return false;
if (
isMatchingLocation(AppPath.Verify) ||
isMatchingLocation(AppPath.VerifyEmail)
) {
return true;
}
if (

View File

@ -7,6 +7,7 @@ import {
useListenClickOutside,
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import React, { useEffect, useRef } from 'react';
@ -148,6 +149,7 @@ export type ModalProps = React.PropsWithChildren & {
className?: string;
hotkeyScope?: ModalHotkeyScope;
onEnter?: () => void;
isOpenAnimated?: boolean;
modalVariant?: ModalVariants;
} & (
| { isClosable: true; onClose: () => void }
@ -170,6 +172,7 @@ export const Modal = ({
isClosable = false,
onClose,
modalVariant = 'primary',
isOpenAnimated = true,
}: ModalProps) => {
const isMobile = useIsMobile();
const modalRef = useRef<HTMLDivElement>(null);
@ -223,6 +226,8 @@ export const Modal = ({
e.stopPropagation();
};
const theme = useTheme();
return (
<StyledBackDrop
className="modal-backdrop"
@ -233,12 +238,13 @@ export const Modal = ({
ref={modalRef}
size={size}
padding={padding}
initial="hidden"
initial={isOpenAnimated ? 'hidden' : 'visible'}
animate="visible"
exit="exit"
layout
modalVariant={modalVariant}
variants={modalAnimation}
transition={{ duration: theme.animation.duration.normal }}
className={className}
isMobile={isMobile}
>

View File

@ -17,7 +17,7 @@ import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { Global, css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
import { Outlet } from 'react-router-dom';
import { Outlet, useSearchParams } from 'react-router-dom';
import { useScreenSize } from 'twenty-ui/utilities';
const StyledLayout = styled.div`
@ -63,6 +63,8 @@ export const DefaultLayout = () => {
const windowsWidth = useScreenSize().width;
const showAuthModal = useShowAuthModal();
const useShowFullScreen = useShowFullscreen();
const [searchParams] = useSearchParams();
const animateModal = searchParams.get('animateModal') !== 'false';
return (
<>
@ -86,7 +88,9 @@ export const DefaultLayout = () => {
2
: 0,
}}
transition={{ duration: theme.animation.duration.normal }}
transition={{
duration: theme.animation.duration.normal,
}}
>
{!showAuthModal && (
<>
@ -104,7 +108,7 @@ export const DefaultLayout = () => {
<SignInBackgroundMockPage />
<AnimatePresence mode="wait">
<LayoutGroup>
<AuthModal>
<AuthModal isOpenAnimated={animateModal}>
<Outlet />
</AuthModal>
</LayoutGroup>

View File

@ -1,30 +1,27 @@
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Workspaces, workspacesState } from '@/auth/states/workspaces';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { useLingui } from '@lingui/react/macro';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
import { multiWorkspaceDropdownState } from '@/ui/navigation/navigation-drawer/states/multiWorkspaceDropdownState';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { SettingsPath } from '@/types/SettingsPath';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MultiWorkspaceDropdownId';
import { useAuth } from '@/auth/hooks/useAuth';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { Workspaces, workspacesState } from '@/auth/states/workspaces';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { AppPath } from '@/types/AppPath';
import { useSignUpInNewWorkspaceMutation } from '~/generated/graphql';
import { SettingsPath } from '@/types/SettingsPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { SelectHotkeyScope } from '@/ui/input/types/SelectHotkeyScope';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MultiWorkspaceDropdownId';
import { multiWorkspaceDropdownState } from '@/ui/navigation/navigation-drawer/states/multiWorkspaceDropdownState';
import { useColorScheme } from '@/ui/theme/hooks/useColorScheme';
import styled from '@emotion/styled';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import {
Avatar,
IconDotsVertical,
@ -39,6 +36,9 @@ import {
MenuItemSelectAvatar,
UndecoratedLink,
} from 'twenty-ui/navigation';
import { useSignUpInNewWorkspaceMutation } from '~/generated/graphql';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledDescription = styled.div`
color: ${({ theme }) => theme.font.color.light};
@ -79,6 +79,7 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
AppPath.Verify,
{
loginToken: data.signUpInNewWorkspace.loginToken.token,
animateModal: false,
},
'_blank',
);

View File

@ -15,6 +15,8 @@ export const UserProvider = ({ children }: React.PropsWithChildren) => {
const dateTimeFormat = useRecoilValue(dateTimeFormatState);
return !isCurrentUserLoaded &&
!isMatchingLocation(AppPath.Verify) &&
!isMatchingLocation(AppPath.VerifyEmail) &&
!isMatchingLocation(AppPath.CreateWorkspace) ? (
<UserOrMetadataLoader />
) : (

View File

@ -3,8 +3,8 @@ import { useRecoilState, useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
import { workspacesState } from '@/auth/states/workspaces';
@ -16,15 +16,18 @@ import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDateFormatFromWorkspaceDateFormat';
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
import { AppPath } from '@/types/AppPath';
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
import { WorkspaceMember } from '~/generated-metadata/graphql';
import { useGetCurrentUserQuery } from '~/generated/graphql';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceMember } from '~/generated-metadata/graphql';
import { useGetCurrentUserQuery } from '~/generated/graphql';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
export const UserProviderEffect = () => {
const [isLoading, setIsLoading] = useState(true);
const { isMatchingLocation } = useIsMatchingLocation();
const [isCurrentUserLoaded, setIsCurrentUserLoaded] = useRecoilState(
isCurrentUserLoadedState,
@ -44,7 +47,10 @@ export const UserProviderEffect = () => {
);
const { loading: queryLoading, data: queryData } = useGetCurrentUserQuery({
skip: isCurrentUserLoaded,
skip:
isCurrentUserLoaded ||
isMatchingLocation(AppPath.Verify) ||
isMatchingLocation(AppPath.VerifyEmail),
});
useEffect(() => {

View File

@ -0,0 +1,100 @@
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { useRecoilValue } from 'recoil';
import { Logo } from '@/auth/components/Logo';
import { Title } from '@/auth/components/Title';
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
import { useMemo } from 'react';
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { motion } from 'framer-motion';
import { isDefined } from 'twenty-shared/utils';
import { Loader } from 'twenty-ui/feedback';
import { MainButton } from 'twenty-ui/input';
import { PublicWorkspaceDataOutput } from '~/generated/graphql';
const StyledContentContainer = styled(motion.div)`
margin-bottom: ${({ theme }) => theme.spacing(8)};
margin-top: ${({ theme }) => theme.spacing(4)};
`;
const StyledForm = styled.form`
align-items: center;
display: flex;
flex-direction: column;
width: 100%;
margin-top: ${({ theme }) => theme.spacing(10)};
`;
const StandardContent = ({
workspacePublicData,
signInUpForm,
title,
}: {
workspacePublicData: PublicWorkspaceDataOutput | null;
signInUpForm: JSX.Element | null;
title: string;
}) => {
return (
<>
<Logo secondaryLogo={workspacePublicData?.logo} />
<Title animate={false}>{title}</Title>
{signInUpForm}
</>
);
};
export const SignInUpLoading = () => {
const { t } = useLingui();
const workspacePublicData = useRecoilValue(workspacePublicDataState);
const { workspaceInviteHash, workspace: workspaceFromInviteHash } =
useWorkspaceFromInviteHash();
const title = useMemo(() => {
if (isDefined(workspaceInviteHash)) {
return `Join ${workspaceFromInviteHash?.displayName ?? ''} team`;
}
const workspaceName = !isDefined(workspacePublicData?.displayName)
? DEFAULT_WORKSPACE_NAME
: !isNonEmptyString(workspacePublicData?.displayName)
? t`Your Workspace`
: workspacePublicData?.displayName;
return t`Welcome to ${workspaceName}`;
}, [
workspaceFromInviteHash?.displayName,
workspaceInviteHash,
workspacePublicData?.displayName,
t,
]);
return (
<StandardContent
workspacePublicData={workspacePublicData}
signInUpForm={
<>
<p style={{ color: 'red', backgroundColor: 'blue' }}>
SignInUpLoading
</p>
<StyledContentContainer>
<StyledForm>
<MainButton
disabled={true}
title={t`Continue`}
type="submit"
variant={'primary'}
Icon={() => <Loader />}
fullWidth
/>
</StyledForm>
</StyledContentContainer>
</>
}
title={title}
/>
);
};

View File

@ -11,16 +11,16 @@ import { billingState } from '@/client-config/states/billingState';
import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro';
import { useRecoilState, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { Loader } from 'twenty-ui/feedback';
import { CardPicker, MainButton } from 'twenty-ui/input';
import { ActionLink, CAL_LINK } from 'twenty-ui/navigation';
import {
BillingPlanKey,
BillingPriceLicensedDto,
SubscriptionInterval,
useBillingBaseProductPricesQuery,
} from '~/generated/graphql';
import { isDefined } from 'twenty-shared/utils';
import { ActionLink, CAL_LINK } from 'twenty-ui/navigation';
import { CardPicker, MainButton } from 'twenty-ui/input';
import { Loader } from 'twenty-ui/feedback';
const StyledSubscriptionContainer = styled.div<{
withLongerMarginBottom: boolean;
@ -80,6 +80,10 @@ const StyledLinkGroup = styled.div`
}
`;
const StyledChooseYourPlanPlaceholder = styled.div`
height: 566px;
`;
export const ChooseYourPlan = () => {
const billing = useRecoilValue(billingState);
const { t } = useLingui();
@ -182,82 +186,87 @@ export const ChooseYourPlan = () => {
)?.baseProduct.name;
return (
isDefined(baseProductPrice) &&
isDefined(billing) && (
<>
<Title noMarginTop>
{hasWithoutCreditCardTrialPeriod
? t`Choose your Trial`
: t`Get your subscription`}
</Title>
{hasWithoutCreditCardTrialPeriod ? (
<SubTitle>
<Trans>Cancel anytime</Trans>
</SubTitle>
) : (
withCreditCardTrialPeriod && (
<>
{isDefined(baseProductPrice) && isDefined(billing) ? (
<>
<Title noMarginTop>
{hasWithoutCreditCardTrialPeriod
? t`Choose your Trial`
: t`Get your subscription`}
</Title>
{hasWithoutCreditCardTrialPeriod ? (
<SubTitle>
{t`Enjoy a ${withCreditCardTrialPeriodDuration}-days free trial`}
<Trans>Cancel anytime</Trans>
</SubTitle>
)
)}
<StyledSubscriptionContainer
withLongerMarginBottom={!hasWithoutCreditCardTrialPeriod}
>
<StyledSubscriptionPriceContainer>
<SubscriptionPrice
type={baseProductPrice.recurringInterval}
price={baseProductPrice.unitAmount / 100}
/>
</StyledSubscriptionPriceContainer>
<StyledBenefitsContainer>
{benefits.map((benefit) => (
<SubscriptionBenefit key={benefit}>{benefit}</SubscriptionBenefit>
))}
</StyledBenefitsContainer>
</StyledSubscriptionContainer>
{hasWithoutCreditCardTrialPeriod && (
<StyledChooseTrialContainer>
{billing.trialPeriods.map((trialPeriod) => (
<CardPicker
checked={
billingCheckoutSession.requirePaymentMethod ===
trialPeriod.isCreditCardRequired
}
handleChange={handleTrialPeriodChange(
trialPeriod.isCreditCardRequired,
)}
key={trialPeriod.duration}
>
<TrialCard
duration={trialPeriod.duration}
withCreditCard={trialPeriod.isCreditCardRequired}
/>
</CardPicker>
))}
</StyledChooseTrialContainer>
)}
<MainButton
title={t`Continue`}
onClick={handleCheckoutSession}
width={200}
Icon={() => isSubmitting && <Loader />}
disabled={isSubmitting}
/>
<StyledLinkGroup>
<ActionLink onClick={signOut}>
<Trans>Log out</Trans>
</ActionLink>
<span />
<ActionLink onClick={handleSwitchPlan(alternatePlan)}>
<Trans>Switch to {alternatePlanName}</Trans>
</ActionLink>
<span />
<ActionLink href={CAL_LINK} target="_blank" rel="noreferrer">
<Trans>Book a Call</Trans>
</ActionLink>
</StyledLinkGroup>
</>
)
) : (
withCreditCardTrialPeriod && (
<SubTitle>
{t`Enjoy a ${withCreditCardTrialPeriodDuration}-days free trial`}
</SubTitle>
)
)}
<StyledSubscriptionContainer
withLongerMarginBottom={!hasWithoutCreditCardTrialPeriod}
>
<StyledSubscriptionPriceContainer>
<SubscriptionPrice
type={baseProductPrice.recurringInterval}
price={baseProductPrice.unitAmount / 100}
/>
</StyledSubscriptionPriceContainer>
<StyledBenefitsContainer>
{benefits.map((benefit) => (
<SubscriptionBenefit key={benefit}>
{benefit}
</SubscriptionBenefit>
))}
</StyledBenefitsContainer>
</StyledSubscriptionContainer>
{hasWithoutCreditCardTrialPeriod && (
<StyledChooseTrialContainer>
{billing.trialPeriods.map((trialPeriod) => (
<CardPicker
checked={
billingCheckoutSession.requirePaymentMethod ===
trialPeriod.isCreditCardRequired
}
handleChange={handleTrialPeriodChange(
trialPeriod.isCreditCardRequired,
)}
key={trialPeriod.duration}
>
<TrialCard
duration={trialPeriod.duration}
withCreditCard={trialPeriod.isCreditCardRequired}
/>
</CardPicker>
))}
</StyledChooseTrialContainer>
)}
<MainButton
title={t`Continue`}
onClick={handleCheckoutSession}
width={200}
Icon={() => isSubmitting && <Loader />}
disabled={isSubmitting}
/>
<StyledLinkGroup>
<ActionLink onClick={signOut}>
<Trans>Log out</Trans>
</ActionLink>
<span />
<ActionLink onClick={handleSwitchPlan(alternatePlan)}>
<Trans>Switch to {alternatePlanName}</Trans>
</ActionLink>
<span />
<ActionLink href={CAL_LINK} target="_blank" rel="noreferrer">
<Trans>Book a Call</Trans>
</ActionLink>
</StyledLinkGroup>
</>
) : (
<StyledChooseYourPlanPlaceholder />
)}
</>
);
};

View File

@ -1,8 +1,8 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces';
import { WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type';
@ -44,7 +44,7 @@ export class DomainManagerService {
private appendSearchParams(
url: URL,
searchParams: Record<string, string | number>,
searchParams: Record<string, string | number | boolean>,
) {
Object.entries(searchParams).forEach(([key, value]) => {
url.searchParams.set(key, value.toString());
@ -78,7 +78,7 @@ export class DomainManagerService {
}: {
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType;
pathname?: string;
searchParams?: Record<string, string | number>;
searchParams?: Record<string, string | number | boolean>;
}) {
const workspaceUrls = this.getWorkspaceUrls(workspace);

View File

@ -2,11 +2,11 @@ import { DynamicModule, Global } from '@nestjs/common';
import { EmailModuleAsyncOptions } from 'src/engine/core-modules/email/interfaces/email.interface';
import { EMAIL_DRIVER } from 'src/engine/core-modules/email/email.constants';
import { LoggerDriver } from 'src/engine/core-modules/email/drivers/logger.driver';
import { SmtpDriver } from 'src/engine/core-modules/email/drivers/smtp.driver';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EmailSenderService } from 'src/engine/core-modules/email/email-sender.service';
import { EMAIL_DRIVER } from 'src/engine/core-modules/email/email.constants';
import { EmailService } from 'src/engine/core-modules/email/email.service';
@Global()
export class EmailModule {