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:
@ -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';
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 />} />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 />;
|
||||
};
|
||||
@ -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);
|
||||
|
||||
@ -416,6 +416,7 @@ export const useAuth = () => {
|
||||
{
|
||||
...(!isEmailVerificationRequired && {
|
||||
loginToken: signUpResult.data.signUp.loginToken.token,
|
||||
animateModal: false,
|
||||
}),
|
||||
email,
|
||||
},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -10,7 +10,6 @@ export const ObjectMetadataItemsProvider = ({
|
||||
children,
|
||||
}: React.PropsWithChildren) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const shouldDisplayChildren = objectMetadataItems.length > 0;
|
||||
|
||||
return (
|
||||
|
||||
@ -13,7 +13,7 @@ export const useImpersonationRedirect = () => {
|
||||
return redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(workspaceUrls),
|
||||
AppPath.Verify,
|
||||
{ loginToken },
|
||||
{ loginToken, animateModal: false },
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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}
|
||||
>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
);
|
||||
|
||||
@ -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 />
|
||||
) : (
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
100
packages/twenty-front/src/pages/auth/SignInUpLoading.tsx
Normal file
100
packages/twenty-front/src/pages/auth/SignInUpLoading.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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 />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user