feat: redirect to Plan Required page if subscription status is not active (#2981)
* feat: redirect to Plan Required page if subscription status is not active Closes #2934 * feat: navigate to Plan Required in PageChangeEffect * feat: add Twenty logo to Plan Required modal * test: add Storybook story * Fix lint --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -1,14 +1,8 @@
|
||||
import { JSX, ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
type SubTitleProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const StyledSubTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export const SubTitle = ({ children }: SubTitleProps): JSX.Element => (
|
||||
<StyledSubTitle>{children}</StyledSubTitle>
|
||||
);
|
||||
export { StyledSubTitle as SubTitle };
|
||||
|
||||
@ -2,6 +2,7 @@ import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
|
||||
import { useIsLogged } from '../hooks/useIsLogged';
|
||||
import {
|
||||
@ -10,13 +11,15 @@ import {
|
||||
} from '../utils/getOnboardingStatus';
|
||||
|
||||
export const useOnboardingStatus = (): OnboardingStatus | undefined => {
|
||||
const billing = useRecoilValue(billingState);
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const isLoggedIn = useIsLogged();
|
||||
|
||||
return getOnboardingStatus(
|
||||
return getOnboardingStatus({
|
||||
isLoggedIn,
|
||||
currentWorkspaceMember,
|
||||
currentWorkspace,
|
||||
);
|
||||
isBillingEnabled: billing?.isBillingEnabled,
|
||||
});
|
||||
};
|
||||
|
||||
@ -6,6 +6,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||
@ -45,8 +46,11 @@ export const useSignInUp = () => {
|
||||
const navigate = useNavigate();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const isMatchingLocation = useIsMatchingLocation();
|
||||
|
||||
const [authProviders] = useRecoilState(authProvidersState);
|
||||
const isSignInPrefilled = useRecoilValue(isSignInPrefilledState);
|
||||
const billing = useRecoilValue(billingState);
|
||||
|
||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||
const [signInUpStep, setSignInUpStep] = useState<SignInUpStep>(
|
||||
SignInUpStep.Init,
|
||||
@ -119,27 +123,33 @@ export const useSignInUp = () => {
|
||||
if (!data.email || !data.password) {
|
||||
throw new Error('Email and password are required');
|
||||
}
|
||||
let currentWorkspace;
|
||||
|
||||
if (signInUpMode === SignInUpMode.SignIn) {
|
||||
const { workspace } = await signInWithCredentials(
|
||||
data.email.toLowerCase(),
|
||||
data.password,
|
||||
);
|
||||
currentWorkspace = workspace;
|
||||
} else {
|
||||
const { workspace } = await signUpWithCredentials(
|
||||
data.email.toLowerCase(),
|
||||
data.password,
|
||||
workspaceInviteHash,
|
||||
);
|
||||
currentWorkspace = workspace;
|
||||
const { workspace: currentWorkspace } =
|
||||
signInUpMode === SignInUpMode.SignIn
|
||||
? await signInWithCredentials(
|
||||
data.email.toLowerCase(),
|
||||
data.password,
|
||||
)
|
||||
: await signUpWithCredentials(
|
||||
data.email.toLowerCase(),
|
||||
data.password,
|
||||
workspaceInviteHash,
|
||||
);
|
||||
|
||||
if (
|
||||
billing?.isBillingEnabled &&
|
||||
currentWorkspace.subscriptionStatus !== 'active'
|
||||
) {
|
||||
navigate('/plan-required');
|
||||
return;
|
||||
}
|
||||
if (currentWorkspace?.displayName) {
|
||||
|
||||
if (currentWorkspace.displayName) {
|
||||
navigate('/');
|
||||
} else {
|
||||
navigate('/create/workspace');
|
||||
return;
|
||||
}
|
||||
|
||||
navigate('/create/workspace');
|
||||
} catch (err: any) {
|
||||
enqueueSnackBar(err?.message, {
|
||||
variant: 'error',
|
||||
@ -151,6 +161,7 @@ export const useSignInUp = () => {
|
||||
signInWithCredentials,
|
||||
signUpWithCredentials,
|
||||
workspaceInviteHash,
|
||||
billing?.isBillingEnabled,
|
||||
navigate,
|
||||
enqueueSnackBar,
|
||||
],
|
||||
|
||||
@ -10,6 +10,7 @@ export type CurrentWorkspace = Pick<
|
||||
| 'displayName'
|
||||
| 'allowImpersonation'
|
||||
| 'featureFlags'
|
||||
| 'subscriptionStatus'
|
||||
>;
|
||||
|
||||
export const currentWorkspaceState = atom<CurrentWorkspace | null>({
|
||||
|
||||
@ -2,17 +2,25 @@ import { CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
|
||||
export enum OnboardingStatus {
|
||||
Incomplete = 'incomplete',
|
||||
Canceled = 'canceled',
|
||||
OngoingUserCreation = 'ongoing_user_creation',
|
||||
OngoingWorkspaceCreation = 'ongoing_workspace_creation',
|
||||
OngoingProfileCreation = 'ongoing_profile_creation',
|
||||
Completed = 'completed',
|
||||
}
|
||||
|
||||
export const getOnboardingStatus = (
|
||||
isLoggedIn: boolean,
|
||||
currentWorkspaceMember: WorkspaceMember | null,
|
||||
currentWorkspace: CurrentWorkspace | null,
|
||||
) => {
|
||||
export const getOnboardingStatus = ({
|
||||
isLoggedIn,
|
||||
currentWorkspaceMember,
|
||||
currentWorkspace,
|
||||
isBillingEnabled,
|
||||
}: {
|
||||
isLoggedIn: boolean;
|
||||
currentWorkspaceMember: WorkspaceMember | null;
|
||||
currentWorkspace: CurrentWorkspace | null;
|
||||
isBillingEnabled?: boolean;
|
||||
}) => {
|
||||
if (!isLoggedIn) {
|
||||
return OnboardingStatus.OngoingUserCreation;
|
||||
}
|
||||
@ -22,6 +30,17 @@ export const getOnboardingStatus = (
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
isBillingEnabled &&
|
||||
currentWorkspace?.subscriptionStatus === 'incomplete'
|
||||
) {
|
||||
return OnboardingStatus.Incomplete;
|
||||
}
|
||||
|
||||
if (isBillingEnabled && currentWorkspace?.subscriptionStatus === 'canceled') {
|
||||
return OnboardingStatus.Canceled;
|
||||
}
|
||||
|
||||
if (!currentWorkspace?.displayName) {
|
||||
return OnboardingStatus.OngoingWorkspaceCreation;
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
@ -16,6 +17,7 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
|
||||
const setIsSignInPrefilled = useSetRecoilState(isSignInPrefilledState);
|
||||
|
||||
const setBilling = useSetRecoilState(billingState);
|
||||
const setTelemetry = useSetRecoilState(telemetryState);
|
||||
const setSupportChat = useSetRecoilState(supportChatState);
|
||||
|
||||
@ -31,6 +33,7 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setIsDebugMode(data?.clientConfig.debugMode);
|
||||
setIsSignInPrefilled(data?.clientConfig.signInPrefilled);
|
||||
|
||||
setBilling(data?.clientConfig.billing);
|
||||
setTelemetry(data?.clientConfig.telemetry);
|
||||
setSupportChat(data?.clientConfig.support);
|
||||
}
|
||||
@ -41,6 +44,7 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setIsSignInPrefilled,
|
||||
setTelemetry,
|
||||
setSupportChat,
|
||||
setBilling,
|
||||
]);
|
||||
|
||||
return loading ? <></> : <>{children}</>;
|
||||
|
||||
@ -7,6 +7,10 @@ export const GET_CLIENT_CONFIG = gql`
|
||||
google
|
||||
password
|
||||
}
|
||||
billing {
|
||||
isBillingEnabled
|
||||
billingUrl
|
||||
}
|
||||
signInPrefilled
|
||||
debugMode
|
||||
telemetry {
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { Billing } from '~/generated/graphql';
|
||||
|
||||
export const billingState = atom<Billing | null>({
|
||||
key: 'billingState',
|
||||
default: null,
|
||||
});
|
||||
@ -2,7 +2,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Account } from '@/accounts/types/account';
|
||||
import { Account } from '@/accounts/types/Account';
|
||||
import { SettingsAccountsRowDropdownMenu } from '@/settings/accounts/components/SettingsAccountsRowDropdownMenu';
|
||||
import { IconAt, IconPlus } from '@/ui/display/icon';
|
||||
import { IconGoogle } from '@/ui/display/icon/components/IconGoogle';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Account } from '@/accounts/types/account';
|
||||
import { Account } from '@/accounts/types/Account';
|
||||
import { IconChevronRight } from '@/ui/display/icon';
|
||||
import { IconGmail } from '@/ui/display/icon/components/IconGmail';
|
||||
import { Status } from '@/ui/display/status/components/Status';
|
||||
|
||||
@ -2,7 +2,7 @@ import { ReactNode } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Account } from '@/accounts/types/account';
|
||||
import { Account } from '@/accounts/types/Account';
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Account } from '@/accounts/types/account';
|
||||
import { Account } from '@/accounts/types/Account';
|
||||
import { IconDotsVertical, IconMail, IconTrash } from '@/ui/display/icon';
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
|
||||
@ -8,6 +8,7 @@ export enum AppPath {
|
||||
// Onboarding
|
||||
CreateWorkspace = '/create/workspace',
|
||||
CreateProfile = '/create/profile',
|
||||
PlanRequired = '/plan-required',
|
||||
|
||||
// Onboarded
|
||||
Index = '/',
|
||||
|
||||
@ -3,6 +3,7 @@ export enum PageHotkeyScope {
|
||||
CreateWokspace = 'create-workspace',
|
||||
SignInUp = 'sign-in-up',
|
||||
CreateProfile = 'create-profile',
|
||||
PlanRequired = 'plan-required',
|
||||
ShowPage = 'show-page',
|
||||
PersonShowPage = 'person-show-page',
|
||||
CompanyShowPage = 'company-show-page',
|
||||
|
||||
@ -19,7 +19,7 @@ const StyledLeftContentWithCheckboxContainer = styled.div`
|
||||
type MenuItemMultiSelectAvatarProps = {
|
||||
avatar?: ReactNode;
|
||||
selected: boolean;
|
||||
isKeySelected: boolean;
|
||||
isKeySelected?: boolean;
|
||||
text: string;
|
||||
className?: string;
|
||||
onSelectChange?: (selected: boolean) => void;
|
||||
|
||||
@ -25,6 +25,7 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
domainName
|
||||
inviteHash
|
||||
allowImpersonation
|
||||
subscriptionStatus
|
||||
featureFlags {
|
||||
id
|
||||
key
|
||||
|
||||
@ -3,36 +3,7 @@ import { gql } from '@apollo/client';
|
||||
export const GET_CURRENT_USER = gql`
|
||||
query GetCurrentUser {
|
||||
currentUser {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
email
|
||||
canImpersonate
|
||||
supportUserHash
|
||||
workspaceMember {
|
||||
id
|
||||
name {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
colorScheme
|
||||
avatarUrl
|
||||
locale
|
||||
}
|
||||
defaultWorkspace {
|
||||
id
|
||||
displayName
|
||||
logo
|
||||
domainName
|
||||
inviteHash
|
||||
allowImpersonation
|
||||
featureFlags {
|
||||
id
|
||||
key
|
||||
value
|
||||
workspaceId
|
||||
}
|
||||
}
|
||||
...UserQueryFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -8,6 +8,7 @@ export const UPDATE_WORKSPACE = gql`
|
||||
displayName
|
||||
logo
|
||||
allowImpersonation
|
||||
subscriptionStatus
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user