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 { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager';
|
||||||
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
|
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
|
||||||
import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider';
|
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 { BaseThemeProvider } from '@/ui/theme/components/BaseThemeProvider';
|
||||||
|
import { UserThemeProviderEffect } from '@/ui/theme/components/UserThemeProviderEffect';
|
||||||
import { PageFavicon } from '@/ui/utilities/page-favicon/components/PageFavicon';
|
import { PageFavicon } from '@/ui/utilities/page-favicon/components/PageFavicon';
|
||||||
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
|
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
|
||||||
import { ServerPreconnect } from '@/ui/utilities/server-preconnect/components/ServerPreconnect';
|
import { ServerPreconnect } from '@/ui/utilities/server-preconnect/components/ServerPreconnect';
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
useLocation,
|
useLocation,
|
||||||
useNavigate,
|
useNavigate,
|
||||||
useParams,
|
useParams,
|
||||||
|
useSearchParams,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ import {
|
|||||||
} from '@/analytics/hooks/useEventTracker';
|
} from '@/analytics/hooks/useEventTracker';
|
||||||
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
||||||
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
|
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
|
||||||
|
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
|
||||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||||
@ -21,10 +23,9 @@ import { AppPath } from '@/types/AppPath';
|
|||||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||||
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
|
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
|
// TODO: break down into smaller functions and / or hooks
|
||||||
// - moved usePageChangeEffectNavigateLocation into dedicated hook
|
// - moved usePageChangeEffectNavigateLocation into dedicated hook
|
||||||
@ -58,11 +59,16 @@ export const PageChangeEffect = () => {
|
|||||||
}
|
}
|
||||||
}, [location, previousLocation]);
|
}, [location, previousLocation]);
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const navigationParams = searchParams.get('animateModal')
|
||||||
|
? `?animateModal=${searchParams.get('animateModal')}`
|
||||||
|
: '';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDefined(pageChangeEffectNavigateLocation)) {
|
if (isDefined(pageChangeEffectNavigateLocation)) {
|
||||||
navigate(pageChangeEffectNavigateLocation);
|
navigate(pageChangeEffectNavigateLocation + navigationParams);
|
||||||
}
|
}
|
||||||
}, [navigate, pageChangeEffectNavigateLocation]);
|
}, [navigate, pageChangeEffectNavigateLocation, navigationParams]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isLeavingRecordIndexPage = !!matchPath(
|
const isLeavingRecordIndexPage = !!matchPath(
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { AppRouterProviders } from '@/app/components/AppRouterProviders';
|
import { AppRouterProviders } from '@/app/components/AppRouterProviders';
|
||||||
import { SettingsRoutes } from '@/app/components/SettingsRoutes';
|
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 { VerifyEmailEffect } from '@/auth/components/VerifyEmailEffect';
|
||||||
import indexAppPath from '@/navigation/utils/indexAppPath';
|
import indexAppPath from '@/navigation/utils/indexAppPath';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { BlankLayout } from '@/ui/layout/page/components/BlankLayout';
|
import { BlankLayout } from '@/ui/layout/page/components/BlankLayout';
|
||||||
import { DefaultLayout } from '@/ui/layout/page/components/DefaultLayout';
|
import { DefaultLayout } from '@/ui/layout/page/components/DefaultLayout';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createBrowserRouter,
|
createBrowserRouter,
|
||||||
createRoutesFromElements,
|
createRoutesFromElements,
|
||||||
@ -38,7 +39,7 @@ export const useCreateAppRouter = (
|
|||||||
loader={async () => Promise.resolve(null)}
|
loader={async () => Promise.resolve(null)}
|
||||||
>
|
>
|
||||||
<Route element={<DefaultLayout />}>
|
<Route element={<DefaultLayout />}>
|
||||||
<Route path={AppPath.Verify} element={<VerifyEffect />} />
|
<Route path={AppPath.Verify} element={<Verify />} />
|
||||||
<Route path={AppPath.VerifyEmail} element={<VerifyEmailEffect />} />
|
<Route path={AppPath.VerifyEmail} element={<VerifyEmailEffect />} />
|
||||||
<Route path={AppPath.SignInUp} element={<SignInUp />} />
|
<Route path={AppPath.SignInUp} element={<SignInUp />} />
|
||||||
<Route path={AppPath.Invite} element={<SignInUp />} />
|
<Route path={AppPath.Invite} element={<SignInUp />} />
|
||||||
|
|||||||
@ -8,10 +8,20 @@ const StyledContent = styled(Modal.Content)`
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type AuthModalProps = { children: React.ReactNode };
|
type AuthModalProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
isOpenAnimated?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export const AuthModal = ({ children }: AuthModalProps) => (
|
export const AuthModal = ({
|
||||||
<Modal padding={'none'} modalVariant="primary">
|
children,
|
||||||
|
isOpenAnimated = true,
|
||||||
|
}: AuthModalProps) => (
|
||||||
|
<Modal
|
||||||
|
padding={'none'}
|
||||||
|
modalVariant="primary"
|
||||||
|
isOpenAnimated={isOpenAnimated}
|
||||||
|
>
|
||||||
<ScrollWrapper componentInstanceId="scroll-wrapper-modal-content">
|
<ScrollWrapper componentInstanceId="scroll-wrapper-modal-content">
|
||||||
<StyledContent>{children}</StyledContent>
|
<StyledContent>{children}</StyledContent>
|
||||||
</ScrollWrapper>
|
</ScrollWrapper>
|
||||||
|
|||||||
@ -6,10 +6,11 @@ import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin';
|
|||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
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 [searchParams] = useSearchParams();
|
||||||
const loginToken = searchParams.get('loginToken');
|
const loginToken = searchParams.get('loginToken');
|
||||||
const errorMessage = searchParams.get('errorMessage');
|
const errorMessage = searchParams.get('errorMessage');
|
||||||
@ -36,5 +37,5 @@ export const VerifyEffect = () => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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 { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
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 { useLingui } from '@lingui/react/macro';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
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 { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||||
|
import { EmailVerificationSent } from '../sign-in-up/components/EmailVerificationSent';
|
||||||
|
|
||||||
export const VerifyEmailEffect = () => {
|
export const VerifyEmailEffect = () => {
|
||||||
const { getLoginTokenFromEmailVerificationToken } = useAuth();
|
const { getLoginTokenFromEmailVerificationToken } = useAuth();
|
||||||
@ -50,6 +50,7 @@ export const VerifyEmailEffect = () => {
|
|||||||
if (workspaceUrl.slice(0, -1) !== window.location.origin) {
|
if (workspaceUrl.slice(0, -1) !== window.location.origin) {
|
||||||
return redirectToWorkspaceDomain(workspaceUrl, AppPath.Verify, {
|
return redirectToWorkspaceDomain(workspaceUrl, AppPath.Verify, {
|
||||||
loginToken: loginToken.token,
|
loginToken: loginToken.token,
|
||||||
|
animateModal: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
verifyLoginToken(loginToken.token);
|
verifyLoginToken(loginToken.token);
|
||||||
|
|||||||
@ -416,6 +416,7 @@ export const useAuth = () => {
|
|||||||
{
|
{
|
||||||
...(!isEmailVerificationRequired && {
|
...(!isEmailVerificationRequired && {
|
||||||
loginToken: signUpResult.data.signUp.loginToken.token,
|
loginToken: signUpResult.data.signUp.loginToken.token,
|
||||||
|
animateModal: false,
|
||||||
}),
|
}),
|
||||||
email,
|
email,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { useRecoilValue } from 'recoil';
|
|||||||
|
|
||||||
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||||
import { AppFullScreenErrorFallback } from '@/error-handler/components/AppFullScreenErrorFallback';
|
import { AppFullScreenErrorFallback } from '@/error-handler/components/AppFullScreenErrorFallback';
|
||||||
|
import { AppPath } from '@/types/AppPath';
|
||||||
|
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||||
|
|
||||||
export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@ -10,8 +12,15 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
clientConfigApiStatusState,
|
clientConfigApiStatusState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { isMatchingLocation } = useIsMatchingLocation();
|
||||||
|
|
||||||
// TODO: Implement a better loading strategy
|
// 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 ? (
|
return isErrored && error instanceof Error ? (
|
||||||
<AppFullScreenErrorFallback
|
<AppFullScreenErrorFallback
|
||||||
|
|||||||
@ -10,7 +10,6 @@ export const ObjectMetadataItemsProvider = ({
|
|||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren) => {
|
}: React.PropsWithChildren) => {
|
||||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||||
|
|
||||||
const shouldDisplayChildren = objectMetadataItems.length > 0;
|
const shouldDisplayChildren = objectMetadataItems.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export const useImpersonationRedirect = () => {
|
|||||||
return redirectToWorkspaceDomain(
|
return redirectToWorkspaceDomain(
|
||||||
getWorkspaceUrl(workspaceUrls),
|
getWorkspaceUrl(workspaceUrls),
|
||||||
AppPath.Verify,
|
AppPath.Verify,
|
||||||
{ loginToken },
|
{ loginToken, animateModal: false },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -56,16 +56,16 @@ const getResult = (isDefaultLayoutAuthModalVisible = true) =>
|
|||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const testCases = [
|
const testCases = [
|
||||||
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, 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: false },
|
{ 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: false },
|
{ 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: false },
|
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.COMPLETED, res: true },
|
||||||
{ loc: AppPath.Verify, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: false },
|
{ loc: AppPath.Verify, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
|
||||||
{ loc: AppPath.Verify, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: false },
|
{ 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: false },
|
{ 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: false },
|
{ 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: false },
|
{ 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: false },
|
{ 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: undefined, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: true },
|
||||||
{ loc: AppPath.VerifyEmail, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.COMPLETED, 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 { AppPath } from '@/types/AppPath';
|
||||||
import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState';
|
import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState';
|
||||||
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
|
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql';
|
import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql';
|
||||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
|
|
||||||
export const useShowAuthModal = () => {
|
export const useShowAuthModal = () => {
|
||||||
const { isMatchingLocation } = useIsMatchingLocation();
|
const { isMatchingLocation } = useIsMatchingLocation();
|
||||||
@ -21,8 +21,11 @@ export const useShowAuthModal = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (isMatchingLocation(AppPath.Verify)) {
|
if (
|
||||||
return false;
|
isMatchingLocation(AppPath.Verify) ||
|
||||||
|
isMatchingLocation(AppPath.VerifyEmail)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
useListenClickOutside,
|
useListenClickOutside,
|
||||||
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
@ -148,6 +149,7 @@ export type ModalProps = React.PropsWithChildren & {
|
|||||||
className?: string;
|
className?: string;
|
||||||
hotkeyScope?: ModalHotkeyScope;
|
hotkeyScope?: ModalHotkeyScope;
|
||||||
onEnter?: () => void;
|
onEnter?: () => void;
|
||||||
|
isOpenAnimated?: boolean;
|
||||||
modalVariant?: ModalVariants;
|
modalVariant?: ModalVariants;
|
||||||
} & (
|
} & (
|
||||||
| { isClosable: true; onClose: () => void }
|
| { isClosable: true; onClose: () => void }
|
||||||
@ -170,6 +172,7 @@ export const Modal = ({
|
|||||||
isClosable = false,
|
isClosable = false,
|
||||||
onClose,
|
onClose,
|
||||||
modalVariant = 'primary',
|
modalVariant = 'primary',
|
||||||
|
isOpenAnimated = true,
|
||||||
}: ModalProps) => {
|
}: ModalProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
@ -223,6 +226,8 @@ export const Modal = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledBackDrop
|
<StyledBackDrop
|
||||||
className="modal-backdrop"
|
className="modal-backdrop"
|
||||||
@ -233,12 +238,13 @@ export const Modal = ({
|
|||||||
ref={modalRef}
|
ref={modalRef}
|
||||||
size={size}
|
size={size}
|
||||||
padding={padding}
|
padding={padding}
|
||||||
initial="hidden"
|
initial={isOpenAnimated ? 'hidden' : 'visible'}
|
||||||
animate="visible"
|
animate="visible"
|
||||||
exit="exit"
|
exit="exit"
|
||||||
layout
|
layout
|
||||||
modalVariant={modalVariant}
|
modalVariant={modalVariant}
|
||||||
variants={modalAnimation}
|
variants={modalAnimation}
|
||||||
|
transition={{ duration: theme.animation.duration.normal }}
|
||||||
className={className}
|
className={className}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
|||||||
import { Global, css, useTheme } from '@emotion/react';
|
import { Global, css, useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
|
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';
|
import { useScreenSize } from 'twenty-ui/utilities';
|
||||||
|
|
||||||
const StyledLayout = styled.div`
|
const StyledLayout = styled.div`
|
||||||
@ -63,6 +63,8 @@ export const DefaultLayout = () => {
|
|||||||
const windowsWidth = useScreenSize().width;
|
const windowsWidth = useScreenSize().width;
|
||||||
const showAuthModal = useShowAuthModal();
|
const showAuthModal = useShowAuthModal();
|
||||||
const useShowFullScreen = useShowFullscreen();
|
const useShowFullScreen = useShowFullscreen();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const animateModal = searchParams.get('animateModal') !== 'false';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -86,7 +88,9 @@ export const DefaultLayout = () => {
|
|||||||
2
|
2
|
||||||
: 0,
|
: 0,
|
||||||
}}
|
}}
|
||||||
transition={{ duration: theme.animation.duration.normal }}
|
transition={{
|
||||||
|
duration: theme.animation.duration.normal,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{!showAuthModal && (
|
{!showAuthModal && (
|
||||||
<>
|
<>
|
||||||
@ -104,7 +108,7 @@ export const DefaultLayout = () => {
|
|||||||
<SignInBackgroundMockPage />
|
<SignInBackgroundMockPage />
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<LayoutGroup>
|
<LayoutGroup>
|
||||||
<AuthModal>
|
<AuthModal isOpenAnimated={animateModal}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</AuthModal>
|
</AuthModal>
|
||||||
</LayoutGroup>
|
</LayoutGroup>
|
||||||
|
|||||||
@ -1,30 +1,27 @@
|
|||||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
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 { 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 { 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 { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
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 { 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 { useColorScheme } from '@/ui/theme/hooks/useColorScheme';
|
||||||
import styled from '@emotion/styled';
|
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 {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
IconDotsVertical,
|
IconDotsVertical,
|
||||||
@ -39,6 +36,9 @@ import {
|
|||||||
MenuItemSelectAvatar,
|
MenuItemSelectAvatar,
|
||||||
UndecoratedLink,
|
UndecoratedLink,
|
||||||
} from 'twenty-ui/navigation';
|
} from 'twenty-ui/navigation';
|
||||||
|
import { useSignUpInNewWorkspaceMutation } from '~/generated/graphql';
|
||||||
|
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||||
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
|
|
||||||
const StyledDescription = styled.div`
|
const StyledDescription = styled.div`
|
||||||
color: ${({ theme }) => theme.font.color.light};
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
@ -79,6 +79,7 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
|
|||||||
AppPath.Verify,
|
AppPath.Verify,
|
||||||
{
|
{
|
||||||
loginToken: data.signUpInNewWorkspace.loginToken.token,
|
loginToken: data.signUpInNewWorkspace.loginToken.token,
|
||||||
|
animateModal: false,
|
||||||
},
|
},
|
||||||
'_blank',
|
'_blank',
|
||||||
);
|
);
|
||||||
|
|||||||
@ -15,6 +15,8 @@ export const UserProvider = ({ children }: React.PropsWithChildren) => {
|
|||||||
const dateTimeFormat = useRecoilValue(dateTimeFormatState);
|
const dateTimeFormat = useRecoilValue(dateTimeFormatState);
|
||||||
|
|
||||||
return !isCurrentUserLoaded &&
|
return !isCurrentUserLoaded &&
|
||||||
|
!isMatchingLocation(AppPath.Verify) &&
|
||||||
|
!isMatchingLocation(AppPath.VerifyEmail) &&
|
||||||
!isMatchingLocation(AppPath.CreateWorkspace) ? (
|
!isMatchingLocation(AppPath.CreateWorkspace) ? (
|
||||||
<UserOrMetadataLoader />
|
<UserOrMetadataLoader />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { useRecoilState, useSetRecoilState } from 'recoil';
|
|||||||
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
|
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
|
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
|
||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
|
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
|
||||||
import { workspacesState } from '@/auth/states/workspaces';
|
import { workspacesState } from '@/auth/states/workspaces';
|
||||||
@ -16,15 +16,18 @@ import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
|
|||||||
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
|
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
|
||||||
import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDateFormatFromWorkspaceDateFormat';
|
import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDateFormatFromWorkspaceDateFormat';
|
||||||
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
|
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
|
||||||
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
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 { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
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 = () => {
|
export const UserProviderEffect = () => {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const { isMatchingLocation } = useIsMatchingLocation();
|
||||||
|
|
||||||
const [isCurrentUserLoaded, setIsCurrentUserLoaded] = useRecoilState(
|
const [isCurrentUserLoaded, setIsCurrentUserLoaded] = useRecoilState(
|
||||||
isCurrentUserLoadedState,
|
isCurrentUserLoadedState,
|
||||||
@ -44,7 +47,10 @@ export const UserProviderEffect = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { loading: queryLoading, data: queryData } = useGetCurrentUserQuery({
|
const { loading: queryLoading, data: queryData } = useGetCurrentUserQuery({
|
||||||
skip: isCurrentUserLoaded,
|
skip:
|
||||||
|
isCurrentUserLoaded ||
|
||||||
|
isMatchingLocation(AppPath.Verify) ||
|
||||||
|
isMatchingLocation(AppPath.VerifyEmail),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
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 styled from '@emotion/styled';
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
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 {
|
import {
|
||||||
BillingPlanKey,
|
BillingPlanKey,
|
||||||
BillingPriceLicensedDto,
|
BillingPriceLicensedDto,
|
||||||
SubscriptionInterval,
|
SubscriptionInterval,
|
||||||
useBillingBaseProductPricesQuery,
|
useBillingBaseProductPricesQuery,
|
||||||
} from '~/generated/graphql';
|
} 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<{
|
const StyledSubscriptionContainer = styled.div<{
|
||||||
withLongerMarginBottom: boolean;
|
withLongerMarginBottom: boolean;
|
||||||
@ -80,6 +80,10 @@ const StyledLinkGroup = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledChooseYourPlanPlaceholder = styled.div`
|
||||||
|
height: 566px;
|
||||||
|
`;
|
||||||
|
|
||||||
export const ChooseYourPlan = () => {
|
export const ChooseYourPlan = () => {
|
||||||
const billing = useRecoilValue(billingState);
|
const billing = useRecoilValue(billingState);
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
@ -182,82 +186,87 @@ export const ChooseYourPlan = () => {
|
|||||||
)?.baseProduct.name;
|
)?.baseProduct.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isDefined(baseProductPrice) &&
|
<>
|
||||||
isDefined(billing) && (
|
{isDefined(baseProductPrice) && isDefined(billing) ? (
|
||||||
<>
|
<>
|
||||||
<Title noMarginTop>
|
<Title noMarginTop>
|
||||||
{hasWithoutCreditCardTrialPeriod
|
{hasWithoutCreditCardTrialPeriod
|
||||||
? t`Choose your Trial`
|
? t`Choose your Trial`
|
||||||
: t`Get your subscription`}
|
: t`Get your subscription`}
|
||||||
</Title>
|
</Title>
|
||||||
{hasWithoutCreditCardTrialPeriod ? (
|
{hasWithoutCreditCardTrialPeriod ? (
|
||||||
<SubTitle>
|
|
||||||
<Trans>Cancel anytime</Trans>
|
|
||||||
</SubTitle>
|
|
||||||
) : (
|
|
||||||
withCreditCardTrialPeriod && (
|
|
||||||
<SubTitle>
|
<SubTitle>
|
||||||
{t`Enjoy a ${withCreditCardTrialPeriodDuration}-days free trial`}
|
<Trans>Cancel anytime</Trans>
|
||||||
</SubTitle>
|
</SubTitle>
|
||||||
)
|
) : (
|
||||||
)}
|
withCreditCardTrialPeriod && (
|
||||||
<StyledSubscriptionContainer
|
<SubTitle>
|
||||||
withLongerMarginBottom={!hasWithoutCreditCardTrialPeriod}
|
{t`Enjoy a ${withCreditCardTrialPeriodDuration}-days free trial`}
|
||||||
>
|
</SubTitle>
|
||||||
<StyledSubscriptionPriceContainer>
|
)
|
||||||
<SubscriptionPrice
|
)}
|
||||||
type={baseProductPrice.recurringInterval}
|
<StyledSubscriptionContainer
|
||||||
price={baseProductPrice.unitAmount / 100}
|
withLongerMarginBottom={!hasWithoutCreditCardTrialPeriod}
|
||||||
/>
|
>
|
||||||
</StyledSubscriptionPriceContainer>
|
<StyledSubscriptionPriceContainer>
|
||||||
<StyledBenefitsContainer>
|
<SubscriptionPrice
|
||||||
{benefits.map((benefit) => (
|
type={baseProductPrice.recurringInterval}
|
||||||
<SubscriptionBenefit key={benefit}>{benefit}</SubscriptionBenefit>
|
price={baseProductPrice.unitAmount / 100}
|
||||||
))}
|
/>
|
||||||
</StyledBenefitsContainer>
|
</StyledSubscriptionPriceContainer>
|
||||||
</StyledSubscriptionContainer>
|
<StyledBenefitsContainer>
|
||||||
{hasWithoutCreditCardTrialPeriod && (
|
{benefits.map((benefit) => (
|
||||||
<StyledChooseTrialContainer>
|
<SubscriptionBenefit key={benefit}>
|
||||||
{billing.trialPeriods.map((trialPeriod) => (
|
{benefit}
|
||||||
<CardPicker
|
</SubscriptionBenefit>
|
||||||
checked={
|
))}
|
||||||
billingCheckoutSession.requirePaymentMethod ===
|
</StyledBenefitsContainer>
|
||||||
trialPeriod.isCreditCardRequired
|
</StyledSubscriptionContainer>
|
||||||
}
|
{hasWithoutCreditCardTrialPeriod && (
|
||||||
handleChange={handleTrialPeriodChange(
|
<StyledChooseTrialContainer>
|
||||||
trialPeriod.isCreditCardRequired,
|
{billing.trialPeriods.map((trialPeriod) => (
|
||||||
)}
|
<CardPicker
|
||||||
key={trialPeriod.duration}
|
checked={
|
||||||
>
|
billingCheckoutSession.requirePaymentMethod ===
|
||||||
<TrialCard
|
trialPeriod.isCreditCardRequired
|
||||||
duration={trialPeriod.duration}
|
}
|
||||||
withCreditCard={trialPeriod.isCreditCardRequired}
|
handleChange={handleTrialPeriodChange(
|
||||||
/>
|
trialPeriod.isCreditCardRequired,
|
||||||
</CardPicker>
|
)}
|
||||||
))}
|
key={trialPeriod.duration}
|
||||||
</StyledChooseTrialContainer>
|
>
|
||||||
)}
|
<TrialCard
|
||||||
<MainButton
|
duration={trialPeriod.duration}
|
||||||
title={t`Continue`}
|
withCreditCard={trialPeriod.isCreditCardRequired}
|
||||||
onClick={handleCheckoutSession}
|
/>
|
||||||
width={200}
|
</CardPicker>
|
||||||
Icon={() => isSubmitting && <Loader />}
|
))}
|
||||||
disabled={isSubmitting}
|
</StyledChooseTrialContainer>
|
||||||
/>
|
)}
|
||||||
<StyledLinkGroup>
|
<MainButton
|
||||||
<ActionLink onClick={signOut}>
|
title={t`Continue`}
|
||||||
<Trans>Log out</Trans>
|
onClick={handleCheckoutSession}
|
||||||
</ActionLink>
|
width={200}
|
||||||
<span />
|
Icon={() => isSubmitting && <Loader />}
|
||||||
<ActionLink onClick={handleSwitchPlan(alternatePlan)}>
|
disabled={isSubmitting}
|
||||||
<Trans>Switch to {alternatePlanName}</Trans>
|
/>
|
||||||
</ActionLink>
|
<StyledLinkGroup>
|
||||||
<span />
|
<ActionLink onClick={signOut}>
|
||||||
<ActionLink href={CAL_LINK} target="_blank" rel="noreferrer">
|
<Trans>Log out</Trans>
|
||||||
<Trans>Book a Call</Trans>
|
</ActionLink>
|
||||||
</ActionLink>
|
<span />
|
||||||
</StyledLinkGroup>
|
<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 { Injectable, Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces';
|
import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces';
|
||||||
import { WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type';
|
import { WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type';
|
||||||
@ -44,7 +44,7 @@ export class DomainManagerService {
|
|||||||
|
|
||||||
private appendSearchParams(
|
private appendSearchParams(
|
||||||
url: URL,
|
url: URL,
|
||||||
searchParams: Record<string, string | number>,
|
searchParams: Record<string, string | number | boolean>,
|
||||||
) {
|
) {
|
||||||
Object.entries(searchParams).forEach(([key, value]) => {
|
Object.entries(searchParams).forEach(([key, value]) => {
|
||||||
url.searchParams.set(key, value.toString());
|
url.searchParams.set(key, value.toString());
|
||||||
@ -78,7 +78,7 @@ export class DomainManagerService {
|
|||||||
}: {
|
}: {
|
||||||
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType;
|
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType;
|
||||||
pathname?: string;
|
pathname?: string;
|
||||||
searchParams?: Record<string, string | number>;
|
searchParams?: Record<string, string | number | boolean>;
|
||||||
}) {
|
}) {
|
||||||
const workspaceUrls = this.getWorkspaceUrls(workspace);
|
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 { 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 { LoggerDriver } from 'src/engine/core-modules/email/drivers/logger.driver';
|
||||||
import { SmtpDriver } from 'src/engine/core-modules/email/drivers/smtp.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 { 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()
|
@Global()
|
||||||
export class EmailModule {
|
export class EmailModule {
|
||||||
|
|||||||
Reference in New Issue
Block a user