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:
Thaïs
2023-12-14 12:39:22 +01:00
committed by GitHub
parent 8916dee352
commit a10f353a4c
37 changed files with 285 additions and 110 deletions

View File

@ -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 };

View File

@ -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,
});
};

View File

@ -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,
],

View File

@ -10,6 +10,7 @@ export type CurrentWorkspace = Pick<
| 'displayName'
| 'allowImpersonation'
| 'featureFlags'
| 'subscriptionStatus'
>;
export const currentWorkspaceState = atom<CurrentWorkspace | null>({

View File

@ -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;
}

View File

@ -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}</>;

View File

@ -7,6 +7,10 @@ export const GET_CLIENT_CONFIG = gql`
google
password
}
billing {
isBillingEnabled
billingUrl
}
signInPrefilled
debugMode
telemetry {

View File

@ -0,0 +1,8 @@
import { atom } from 'recoil';
import { Billing } from '~/generated/graphql';
export const billingState = atom<Billing | null>({
key: 'billingState',
default: null,
});

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -8,6 +8,7 @@ export enum AppPath {
// Onboarding
CreateWorkspace = '/create/workspace',
CreateProfile = '/create/profile',
PlanRequired = '/plan-required',
// Onboarded
Index = '/',

View File

@ -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',

View File

@ -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;

View File

@ -25,6 +25,7 @@ export const USER_QUERY_FRAGMENT = gql`
domainName
inviteHash
allowImpersonation
subscriptionStatus
featureFlags {
id
key

View File

@ -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
}
}
`;

View File

@ -8,6 +8,7 @@ export const UPDATE_WORKSPACE = gql`
displayName
logo
allowImpersonation
subscriptionStatus
}
}
`;