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:
@ -8,6 +8,7 @@
|
|||||||
"start:clean": "yarn start --force",
|
"start:clean": "yarn start --force",
|
||||||
"build": "tsc && vite build && yarn build:inject-runtime-env",
|
"build": "tsc && vite build && yarn build:inject-runtime-env",
|
||||||
"build:inject-runtime-env": "sh ./scripts/inject-runtime-env.sh",
|
"build:inject-runtime-env": "sh ./scripts/inject-runtime-env.sh",
|
||||||
|
"tsc": "tsc --watch",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"fmt:fix": "prettier --cache --write \"src/**/*.ts\" \"src/**/*.tsx\"",
|
"fmt:fix": "prettier --cache --write \"src/**/*.ts\" \"src/**/*.tsx\"",
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect';
|
|||||||
import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect';
|
import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect';
|
||||||
import { CreateProfile } from '~/pages/auth/CreateProfile';
|
import { CreateProfile } from '~/pages/auth/CreateProfile';
|
||||||
import { CreateWorkspace } from '~/pages/auth/CreateWorkspace';
|
import { CreateWorkspace } from '~/pages/auth/CreateWorkspace';
|
||||||
|
import { PlanRequired } from '~/pages/auth/PlanRequired';
|
||||||
import { SignInUp } from '~/pages/auth/SignInUp';
|
import { SignInUp } from '~/pages/auth/SignInUp';
|
||||||
import { VerifyEffect } from '~/pages/auth/VerifyEffect';
|
import { VerifyEffect } from '~/pages/auth/VerifyEffect';
|
||||||
import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect';
|
import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect';
|
||||||
@ -50,6 +51,7 @@ export const App = () => {
|
|||||||
<Route path={AppPath.Invite} element={<SignInUp />} />
|
<Route path={AppPath.Invite} element={<SignInUp />} />
|
||||||
<Route path={AppPath.CreateWorkspace} element={<CreateWorkspace />} />
|
<Route path={AppPath.CreateWorkspace} element={<CreateWorkspace />} />
|
||||||
<Route path={AppPath.CreateProfile} element={<CreateProfile />} />
|
<Route path={AppPath.CreateProfile} element={<CreateProfile />} />
|
||||||
|
<Route path={AppPath.PlanRequired} element={<PlanRequired />} />
|
||||||
<Route path="/" element={<Navigate to="/objects/companies" />} />
|
<Route path="/" element={<Navigate to="/objects/companies" />} />
|
||||||
<Route path={AppPath.TasksPage} element={<Tasks />} />
|
<Route path={AppPath.TasksPage} element={<Tasks />} />
|
||||||
<Route path={AppPath.Impersonate} element={<ImpersonateEffect />} />
|
<Route path={AppPath.Impersonate} element={<ImpersonateEffect />} />
|
||||||
|
|||||||
@ -57,12 +57,10 @@ export const PageChangeEffect = () => {
|
|||||||
isMatchingLocation(AppPath.Verify);
|
isMatchingLocation(AppPath.Verify);
|
||||||
|
|
||||||
const isMatchingOnboardingRoute =
|
const isMatchingOnboardingRoute =
|
||||||
isMatchingLocation(AppPath.SignUp) ||
|
isMachinOngoingUserCreationRoute ||
|
||||||
isMatchingLocation(AppPath.SignIn) ||
|
|
||||||
isMatchingLocation(AppPath.Invite) ||
|
|
||||||
isMatchingLocation(AppPath.Verify) ||
|
|
||||||
isMatchingLocation(AppPath.CreateWorkspace) ||
|
isMatchingLocation(AppPath.CreateWorkspace) ||
|
||||||
isMatchingLocation(AppPath.CreateProfile);
|
isMatchingLocation(AppPath.CreateProfile) ||
|
||||||
|
isMatchingLocation(AppPath.PlanRequired);
|
||||||
|
|
||||||
const navigateToSignUp = () => {
|
const navigateToSignUp = () => {
|
||||||
enqueueSnackBar('workspace does not exist', {
|
enqueueSnackBar('workspace does not exist', {
|
||||||
@ -76,6 +74,14 @@ export const PageChangeEffect = () => {
|
|||||||
!isMachinOngoingUserCreationRoute
|
!isMachinOngoingUserCreationRoute
|
||||||
) {
|
) {
|
||||||
navigate(AppPath.SignIn);
|
navigate(AppPath.SignIn);
|
||||||
|
} else if (
|
||||||
|
onboardingStatus &&
|
||||||
|
[OnboardingStatus.Canceled, OnboardingStatus.Incomplete].includes(
|
||||||
|
onboardingStatus,
|
||||||
|
) &&
|
||||||
|
!isMatchingLocation(AppPath.PlanRequired)
|
||||||
|
) {
|
||||||
|
navigate(AppPath.PlanRequired);
|
||||||
} else if (
|
} else if (
|
||||||
onboardingStatus === OnboardingStatus.OngoingWorkspaceCreation &&
|
onboardingStatus === OnboardingStatus.OngoingWorkspaceCreation &&
|
||||||
!isMatchingLocation(AppPath.CreateWorkspace)
|
!isMatchingLocation(AppPath.CreateWorkspace)
|
||||||
@ -170,6 +176,10 @@ export const PageChangeEffect = () => {
|
|||||||
setHotkeyScope(PageHotkeyScope.CreateWokspace);
|
setHotkeyScope(PageHotkeyScope.CreateWokspace);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case isMatchingLocation(AppPath.PlanRequired): {
|
||||||
|
setHotkeyScope(PageHotkeyScope.PlanRequired);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case isMatchingLocation(SettingsPath.ProfilePage, AppBasePath.Settings): {
|
case isMatchingLocation(SettingsPath.ProfilePage, AppBasePath.Settings): {
|
||||||
setHotkeyScope(PageHotkeyScope.ProfilePage, {
|
setHotkeyScope(PageHotkeyScope.ProfilePage, {
|
||||||
goto: true,
|
goto: true,
|
||||||
|
|||||||
@ -41,6 +41,12 @@ export type AuthTokenPair = {
|
|||||||
refreshToken: AuthToken;
|
refreshToken: AuthToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Billing = {
|
||||||
|
__typename?: 'Billing';
|
||||||
|
billingUrl: Scalars['String']['output'];
|
||||||
|
isBillingEnabled: Scalars['Boolean']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
export type BooleanFieldComparison = {
|
export type BooleanFieldComparison = {
|
||||||
is?: InputMaybe<Scalars['Boolean']['input']>;
|
is?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
isNot?: InputMaybe<Scalars['Boolean']['input']>;
|
isNot?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
@ -484,6 +490,7 @@ export type Workspace = {
|
|||||||
id: Scalars['ID']['output'];
|
id: Scalars['ID']['output'];
|
||||||
inviteHash?: Maybe<Scalars['String']['output']>;
|
inviteHash?: Maybe<Scalars['String']['output']>;
|
||||||
logo?: Maybe<Scalars['String']['output']>;
|
logo?: Maybe<Scalars['String']['output']>;
|
||||||
|
subscriptionStatus: Scalars['String']['output'];
|
||||||
updatedAt: Scalars['DateTime']['output'];
|
updatedAt: Scalars['DateTime']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,12 @@ export type AuthTokens = {
|
|||||||
tokens: AuthTokenPair;
|
tokens: AuthTokenPair;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Billing = {
|
||||||
|
__typename?: 'Billing';
|
||||||
|
billingUrl: Scalars['String'];
|
||||||
|
isBillingEnabled: Scalars['Boolean'];
|
||||||
|
};
|
||||||
|
|
||||||
export type BooleanFieldComparison = {
|
export type BooleanFieldComparison = {
|
||||||
is?: InputMaybe<Scalars['Boolean']>;
|
is?: InputMaybe<Scalars['Boolean']>;
|
||||||
isNot?: InputMaybe<Scalars['Boolean']>;
|
isNot?: InputMaybe<Scalars['Boolean']>;
|
||||||
@ -62,6 +68,7 @@ export type BooleanFieldComparison = {
|
|||||||
export type ClientConfig = {
|
export type ClientConfig = {
|
||||||
__typename?: 'ClientConfig';
|
__typename?: 'ClientConfig';
|
||||||
authProviders: AuthProviders;
|
authProviders: AuthProviders;
|
||||||
|
billing: Billing;
|
||||||
debugMode: Scalars['Boolean'];
|
debugMode: Scalars['Boolean'];
|
||||||
signInPrefilled: Scalars['Boolean'];
|
signInPrefilled: Scalars['Boolean'];
|
||||||
support: Support;
|
support: Support;
|
||||||
@ -507,6 +514,7 @@ export type Workspace = {
|
|||||||
id: Scalars['ID'];
|
id: Scalars['ID'];
|
||||||
inviteHash?: Maybe<Scalars['String']>;
|
inviteHash?: Maybe<Scalars['String']>;
|
||||||
logo?: Maybe<Scalars['String']>;
|
logo?: Maybe<Scalars['String']>;
|
||||||
|
subscriptionStatus: Scalars['String'];
|
||||||
updatedAt: Scalars['DateTime'];
|
updatedAt: Scalars['DateTime'];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -660,7 +668,7 @@ export type ImpersonateMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||||
|
|
||||||
export type RenewTokenMutationVariables = Exact<{
|
export type RenewTokenMutationVariables = Exact<{
|
||||||
refreshToken: Scalars['String'];
|
refreshToken: Scalars['String'];
|
||||||
@ -683,7 +691,7 @@ export type VerifyMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||||
|
|
||||||
export type CheckUserExistsQueryVariables = Exact<{
|
export type CheckUserExistsQueryVariables = Exact<{
|
||||||
email: Scalars['String'];
|
email: Scalars['String'];
|
||||||
@ -695,7 +703,7 @@ export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __
|
|||||||
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null } } };
|
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl: string }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null } } };
|
||||||
|
|
||||||
export type UploadFileMutationVariables = Exact<{
|
export type UploadFileMutationVariables = Exact<{
|
||||||
file: Scalars['Upload'];
|
file: Scalars['Upload'];
|
||||||
@ -713,7 +721,7 @@ export type UploadImageMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type UploadImageMutation = { __typename?: 'Mutation', uploadImage: string };
|
export type UploadImageMutation = { __typename?: 'Mutation', uploadImage: string };
|
||||||
|
|
||||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } };
|
export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } };
|
||||||
|
|
||||||
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
@ -730,7 +738,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
|
|||||||
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } } };
|
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } } };
|
||||||
|
|
||||||
export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>;
|
export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
@ -742,7 +750,7 @@ export type UpdateWorkspaceMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type UpdateWorkspaceMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, allowImpersonation: boolean } };
|
export type UpdateWorkspaceMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, allowImpersonation: boolean, subscriptionStatus: string } };
|
||||||
|
|
||||||
export type UploadWorkspaceLogoMutationVariables = Exact<{
|
export type UploadWorkspaceLogoMutationVariables = Exact<{
|
||||||
file: Scalars['Upload'];
|
file: Scalars['Upload'];
|
||||||
@ -799,6 +807,7 @@ export const UserQueryFragmentFragmentDoc = gql`
|
|||||||
domainName
|
domainName
|
||||||
inviteHash
|
inviteHash
|
||||||
allowImpersonation
|
allowImpersonation
|
||||||
|
subscriptionStatus
|
||||||
featureFlags {
|
featureFlags {
|
||||||
id
|
id
|
||||||
key
|
key
|
||||||
@ -1142,6 +1151,10 @@ export const GetClientConfigDocument = gql`
|
|||||||
google
|
google
|
||||||
password
|
password
|
||||||
}
|
}
|
||||||
|
billing {
|
||||||
|
isBillingEnabled
|
||||||
|
billingUrl
|
||||||
|
}
|
||||||
signInPrefilled
|
signInPrefilled
|
||||||
debugMode
|
debugMode
|
||||||
telemetry {
|
telemetry {
|
||||||
@ -1312,39 +1325,10 @@ export type UploadProfilePictureMutationOptions = Apollo.BaseMutationOptions<Upl
|
|||||||
export const GetCurrentUserDocument = gql`
|
export const GetCurrentUserDocument = gql`
|
||||||
query GetCurrentUser {
|
query GetCurrentUser {
|
||||||
currentUser {
|
currentUser {
|
||||||
id
|
...UserQueryFragment
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
${UserQueryFragmentFragmentDoc}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* __useGetCurrentUserQuery__
|
* __useGetCurrentUserQuery__
|
||||||
@ -1412,6 +1396,7 @@ export const UpdateWorkspaceDocument = gql`
|
|||||||
displayName
|
displayName
|
||||||
logo
|
logo
|
||||||
allowImpersonation
|
allowImpersonation
|
||||||
|
subscriptionStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,14 +1,8 @@
|
|||||||
import { JSX, ReactNode } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
type SubTitleProps = {
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledSubTitle = styled.div`
|
const StyledSubTitle = styled.div`
|
||||||
color: ${({ theme }) => theme.font.color.secondary};
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
|
text-align: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SubTitle = ({ children }: SubTitleProps): JSX.Element => (
|
export { StyledSubTitle as SubTitle };
|
||||||
<StyledSubTitle>{children}</StyledSubTitle>
|
|
||||||
);
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useRecoilValue } from 'recoil';
|
|||||||
|
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
|
import { billingState } from '@/client-config/states/billingState';
|
||||||
|
|
||||||
import { useIsLogged } from '../hooks/useIsLogged';
|
import { useIsLogged } from '../hooks/useIsLogged';
|
||||||
import {
|
import {
|
||||||
@ -10,13 +11,15 @@ import {
|
|||||||
} from '../utils/getOnboardingStatus';
|
} from '../utils/getOnboardingStatus';
|
||||||
|
|
||||||
export const useOnboardingStatus = (): OnboardingStatus | undefined => {
|
export const useOnboardingStatus = (): OnboardingStatus | undefined => {
|
||||||
|
const billing = useRecoilValue(billingState);
|
||||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||||
const isLoggedIn = useIsLogged();
|
const isLoggedIn = useIsLogged();
|
||||||
|
|
||||||
return getOnboardingStatus(
|
return getOnboardingStatus({
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
currentWorkspaceMember,
|
currentWorkspaceMember,
|
||||||
currentWorkspace,
|
currentWorkspace,
|
||||||
);
|
isBillingEnabled: billing?.isBillingEnabled,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||||
|
import { billingState } from '@/client-config/states/billingState';
|
||||||
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||||
@ -45,8 +46,11 @@ export const useSignInUp = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
const isMatchingLocation = useIsMatchingLocation();
|
const isMatchingLocation = useIsMatchingLocation();
|
||||||
|
|
||||||
const [authProviders] = useRecoilState(authProvidersState);
|
const [authProviders] = useRecoilState(authProvidersState);
|
||||||
const isSignInPrefilled = useRecoilValue(isSignInPrefilledState);
|
const isSignInPrefilled = useRecoilValue(isSignInPrefilledState);
|
||||||
|
const billing = useRecoilValue(billingState);
|
||||||
|
|
||||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||||
const [signInUpStep, setSignInUpStep] = useState<SignInUpStep>(
|
const [signInUpStep, setSignInUpStep] = useState<SignInUpStep>(
|
||||||
SignInUpStep.Init,
|
SignInUpStep.Init,
|
||||||
@ -119,27 +123,33 @@ export const useSignInUp = () => {
|
|||||||
if (!data.email || !data.password) {
|
if (!data.email || !data.password) {
|
||||||
throw new Error('Email and password are required');
|
throw new Error('Email and password are required');
|
||||||
}
|
}
|
||||||
let currentWorkspace;
|
|
||||||
|
|
||||||
if (signInUpMode === SignInUpMode.SignIn) {
|
const { workspace: currentWorkspace } =
|
||||||
const { workspace } = await signInWithCredentials(
|
signInUpMode === SignInUpMode.SignIn
|
||||||
data.email.toLowerCase(),
|
? await signInWithCredentials(
|
||||||
data.password,
|
data.email.toLowerCase(),
|
||||||
);
|
data.password,
|
||||||
currentWorkspace = workspace;
|
)
|
||||||
} else {
|
: await signUpWithCredentials(
|
||||||
const { workspace } = await signUpWithCredentials(
|
data.email.toLowerCase(),
|
||||||
data.email.toLowerCase(),
|
data.password,
|
||||||
data.password,
|
workspaceInviteHash,
|
||||||
workspaceInviteHash,
|
);
|
||||||
);
|
|
||||||
currentWorkspace = workspace;
|
if (
|
||||||
|
billing?.isBillingEnabled &&
|
||||||
|
currentWorkspace.subscriptionStatus !== 'active'
|
||||||
|
) {
|
||||||
|
navigate('/plan-required');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (currentWorkspace?.displayName) {
|
|
||||||
|
if (currentWorkspace.displayName) {
|
||||||
navigate('/');
|
navigate('/');
|
||||||
} else {
|
return;
|
||||||
navigate('/create/workspace');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigate('/create/workspace');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
enqueueSnackBar(err?.message, {
|
enqueueSnackBar(err?.message, {
|
||||||
variant: 'error',
|
variant: 'error',
|
||||||
@ -151,6 +161,7 @@ export const useSignInUp = () => {
|
|||||||
signInWithCredentials,
|
signInWithCredentials,
|
||||||
signUpWithCredentials,
|
signUpWithCredentials,
|
||||||
workspaceInviteHash,
|
workspaceInviteHash,
|
||||||
|
billing?.isBillingEnabled,
|
||||||
navigate,
|
navigate,
|
||||||
enqueueSnackBar,
|
enqueueSnackBar,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export type CurrentWorkspace = Pick<
|
|||||||
| 'displayName'
|
| 'displayName'
|
||||||
| 'allowImpersonation'
|
| 'allowImpersonation'
|
||||||
| 'featureFlags'
|
| 'featureFlags'
|
||||||
|
| 'subscriptionStatus'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const currentWorkspaceState = atom<CurrentWorkspace | null>({
|
export const currentWorkspaceState = atom<CurrentWorkspace | null>({
|
||||||
|
|||||||
@ -2,17 +2,25 @@ import { CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
|
|||||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||||
|
|
||||||
export enum OnboardingStatus {
|
export enum OnboardingStatus {
|
||||||
|
Incomplete = 'incomplete',
|
||||||
|
Canceled = 'canceled',
|
||||||
OngoingUserCreation = 'ongoing_user_creation',
|
OngoingUserCreation = 'ongoing_user_creation',
|
||||||
OngoingWorkspaceCreation = 'ongoing_workspace_creation',
|
OngoingWorkspaceCreation = 'ongoing_workspace_creation',
|
||||||
OngoingProfileCreation = 'ongoing_profile_creation',
|
OngoingProfileCreation = 'ongoing_profile_creation',
|
||||||
Completed = 'completed',
|
Completed = 'completed',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOnboardingStatus = (
|
export const getOnboardingStatus = ({
|
||||||
isLoggedIn: boolean,
|
isLoggedIn,
|
||||||
currentWorkspaceMember: WorkspaceMember | null,
|
currentWorkspaceMember,
|
||||||
currentWorkspace: CurrentWorkspace | null,
|
currentWorkspace,
|
||||||
) => {
|
isBillingEnabled,
|
||||||
|
}: {
|
||||||
|
isLoggedIn: boolean;
|
||||||
|
currentWorkspaceMember: WorkspaceMember | null;
|
||||||
|
currentWorkspace: CurrentWorkspace | null;
|
||||||
|
isBillingEnabled?: boolean;
|
||||||
|
}) => {
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
return OnboardingStatus.OngoingUserCreation;
|
return OnboardingStatus.OngoingUserCreation;
|
||||||
}
|
}
|
||||||
@ -22,6 +30,17 @@ export const getOnboardingStatus = (
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isBillingEnabled &&
|
||||||
|
currentWorkspace?.subscriptionStatus === 'incomplete'
|
||||||
|
) {
|
||||||
|
return OnboardingStatus.Incomplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBillingEnabled && currentWorkspace?.subscriptionStatus === 'canceled') {
|
||||||
|
return OnboardingStatus.Canceled;
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentWorkspace?.displayName) {
|
if (!currentWorkspace?.displayName) {
|
||||||
return OnboardingStatus.OngoingWorkspaceCreation;
|
return OnboardingStatus.OngoingWorkspaceCreation;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useEffect } from 'react';
|
|||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||||
|
import { billingState } from '@/client-config/states/billingState';
|
||||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||||
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
||||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||||
@ -16,6 +17,7 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
|
|
||||||
const setIsSignInPrefilled = useSetRecoilState(isSignInPrefilledState);
|
const setIsSignInPrefilled = useSetRecoilState(isSignInPrefilledState);
|
||||||
|
|
||||||
|
const setBilling = useSetRecoilState(billingState);
|
||||||
const setTelemetry = useSetRecoilState(telemetryState);
|
const setTelemetry = useSetRecoilState(telemetryState);
|
||||||
const setSupportChat = useSetRecoilState(supportChatState);
|
const setSupportChat = useSetRecoilState(supportChatState);
|
||||||
|
|
||||||
@ -31,6 +33,7 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setIsDebugMode(data?.clientConfig.debugMode);
|
setIsDebugMode(data?.clientConfig.debugMode);
|
||||||
setIsSignInPrefilled(data?.clientConfig.signInPrefilled);
|
setIsSignInPrefilled(data?.clientConfig.signInPrefilled);
|
||||||
|
|
||||||
|
setBilling(data?.clientConfig.billing);
|
||||||
setTelemetry(data?.clientConfig.telemetry);
|
setTelemetry(data?.clientConfig.telemetry);
|
||||||
setSupportChat(data?.clientConfig.support);
|
setSupportChat(data?.clientConfig.support);
|
||||||
}
|
}
|
||||||
@ -41,6 +44,7 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setIsSignInPrefilled,
|
setIsSignInPrefilled,
|
||||||
setTelemetry,
|
setTelemetry,
|
||||||
setSupportChat,
|
setSupportChat,
|
||||||
|
setBilling,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return loading ? <></> : <>{children}</>;
|
return loading ? <></> : <>{children}</>;
|
||||||
|
|||||||
@ -7,6 +7,10 @@ export const GET_CLIENT_CONFIG = gql`
|
|||||||
google
|
google
|
||||||
password
|
password
|
||||||
}
|
}
|
||||||
|
billing {
|
||||||
|
isBillingEnabled
|
||||||
|
billingUrl
|
||||||
|
}
|
||||||
signInPrefilled
|
signInPrefilled
|
||||||
debugMode
|
debugMode
|
||||||
telemetry {
|
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 { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
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 { SettingsAccountsRowDropdownMenu } from '@/settings/accounts/components/SettingsAccountsRowDropdownMenu';
|
||||||
import { IconAt, IconPlus } from '@/ui/display/icon';
|
import { IconAt, IconPlus } from '@/ui/display/icon';
|
||||||
import { IconGoogle } from '@/ui/display/icon/components/IconGoogle';
|
import { IconGoogle } from '@/ui/display/icon/components/IconGoogle';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { Account } from '@/accounts/types/account';
|
import { Account } from '@/accounts/types/Account';
|
||||||
import { IconChevronRight } from '@/ui/display/icon';
|
import { IconChevronRight } from '@/ui/display/icon';
|
||||||
import { IconGmail } from '@/ui/display/icon/components/IconGmail';
|
import { IconGmail } from '@/ui/display/icon/components/IconGmail';
|
||||||
import { Status } from '@/ui/display/status/components/Status';
|
import { Status } from '@/ui/display/status/components/Status';
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { ReactNode } from 'react';
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
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 { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
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 { IconDotsVertical, IconMail, IconTrash } from '@/ui/display/icon';
|
||||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export enum AppPath {
|
|||||||
// Onboarding
|
// Onboarding
|
||||||
CreateWorkspace = '/create/workspace',
|
CreateWorkspace = '/create/workspace',
|
||||||
CreateProfile = '/create/profile',
|
CreateProfile = '/create/profile',
|
||||||
|
PlanRequired = '/plan-required',
|
||||||
|
|
||||||
// Onboarded
|
// Onboarded
|
||||||
Index = '/',
|
Index = '/',
|
||||||
|
|||||||
@ -3,6 +3,7 @@ export enum PageHotkeyScope {
|
|||||||
CreateWokspace = 'create-workspace',
|
CreateWokspace = 'create-workspace',
|
||||||
SignInUp = 'sign-in-up',
|
SignInUp = 'sign-in-up',
|
||||||
CreateProfile = 'create-profile',
|
CreateProfile = 'create-profile',
|
||||||
|
PlanRequired = 'plan-required',
|
||||||
ShowPage = 'show-page',
|
ShowPage = 'show-page',
|
||||||
PersonShowPage = 'person-show-page',
|
PersonShowPage = 'person-show-page',
|
||||||
CompanyShowPage = 'company-show-page',
|
CompanyShowPage = 'company-show-page',
|
||||||
|
|||||||
@ -19,7 +19,7 @@ const StyledLeftContentWithCheckboxContainer = styled.div`
|
|||||||
type MenuItemMultiSelectAvatarProps = {
|
type MenuItemMultiSelectAvatarProps = {
|
||||||
avatar?: ReactNode;
|
avatar?: ReactNode;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
isKeySelected: boolean;
|
isKeySelected?: boolean;
|
||||||
text: string;
|
text: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onSelectChange?: (selected: boolean) => void;
|
onSelectChange?: (selected: boolean) => void;
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export const USER_QUERY_FRAGMENT = gql`
|
|||||||
domainName
|
domainName
|
||||||
inviteHash
|
inviteHash
|
||||||
allowImpersonation
|
allowImpersonation
|
||||||
|
subscriptionStatus
|
||||||
featureFlags {
|
featureFlags {
|
||||||
id
|
id
|
||||||
key
|
key
|
||||||
|
|||||||
@ -3,36 +3,7 @@ import { gql } from '@apollo/client';
|
|||||||
export const GET_CURRENT_USER = gql`
|
export const GET_CURRENT_USER = gql`
|
||||||
query GetCurrentUser {
|
query GetCurrentUser {
|
||||||
currentUser {
|
currentUser {
|
||||||
id
|
...UserQueryFragment
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export const UPDATE_WORKSPACE = gql`
|
|||||||
displayName
|
displayName
|
||||||
logo
|
logo
|
||||||
allowImpersonation
|
allowImpersonation
|
||||||
|
subscriptionStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -77,6 +77,8 @@ export const CreateWorkspace = () => {
|
|||||||
setCurrentWorkspace({
|
setCurrentWorkspace({
|
||||||
id: result.data?.updateWorkspace?.id ?? '',
|
id: result.data?.updateWorkspace?.id ?? '',
|
||||||
displayName: data.name,
|
displayName: data.name,
|
||||||
|
subscriptionStatus:
|
||||||
|
result.data?.updateWorkspace?.subscriptionStatus ?? 'incomplete',
|
||||||
allowImpersonation:
|
allowImpersonation:
|
||||||
result.data?.updateWorkspace?.allowImpersonation ?? false,
|
result.data?.updateWorkspace?.allowImpersonation ?? false,
|
||||||
});
|
});
|
||||||
|
|||||||
50
packages/twenty-front/src/pages/auth/PlanRequired.tsx
Normal file
50
packages/twenty-front/src/pages/auth/PlanRequired.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { Logo } from '@/auth/components/Logo';
|
||||||
|
import { SubTitle } from '@/auth/components/SubTitle';
|
||||||
|
import { Title } from '@/auth/components/Title';
|
||||||
|
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
||||||
|
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
||||||
|
import { billingState } from '@/client-config/states/billingState';
|
||||||
|
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||||
|
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||||
|
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
||||||
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
|
||||||
|
const StyledButtonContainer = styled.div`
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||||
|
width: 200px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const PlanRequired = () => {
|
||||||
|
const onboardingStatus = useOnboardingStatus();
|
||||||
|
const billing = useRecoilValue(billingState);
|
||||||
|
|
||||||
|
const handleButtonClick = () => {
|
||||||
|
billing?.billingUrl && window.location.replace(billing.billingUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
useScopedHotkeys('enter', handleButtonClick, PageHotkeyScope.PlanRequired, [
|
||||||
|
handleButtonClick,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (onboardingStatus === OnboardingStatus.Completed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AnimatedEaseIn>
|
||||||
|
<Logo />
|
||||||
|
</AnimatedEaseIn>
|
||||||
|
<Title>Plan required</Title>
|
||||||
|
<SubTitle>
|
||||||
|
Please select a subscription plan before proceeding to sign in.
|
||||||
|
</SubTitle>
|
||||||
|
<StyledButtonContainer>
|
||||||
|
<MainButton title="Get started" onClick={handleButtonClick} fullWidth />
|
||||||
|
</StyledButtonContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { within } from '@storybook/test';
|
||||||
|
|
||||||
|
import { AppPath } from '@/types/AppPath';
|
||||||
|
import {
|
||||||
|
PageDecorator,
|
||||||
|
PageDecoratorArgs,
|
||||||
|
} from '~/testing/decorators/PageDecorator';
|
||||||
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
|
|
||||||
|
import { PlanRequired } from '../PlanRequired';
|
||||||
|
|
||||||
|
const meta: Meta<PageDecoratorArgs> = {
|
||||||
|
title: 'Pages/Auth/PlanRequired',
|
||||||
|
component: PlanRequired,
|
||||||
|
decorators: [PageDecorator],
|
||||||
|
args: { routePath: AppPath.PlanRequired },
|
||||||
|
parameters: {
|
||||||
|
msw: graphqlMocks,
|
||||||
|
cookie: {
|
||||||
|
tokenPair: '{}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
export type Story = StoryObj<typeof PlanRequired>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
await canvas.findByRole('button', { name: 'Get started' });
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Account } from '@/accounts/types/account';
|
import { Account } from '@/accounts/types/Account';
|
||||||
|
|
||||||
export const mockedAccounts: Account[] = [
|
export const mockedAccounts: Account[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -19,6 +19,8 @@ SIGN_IN_PREFILLED=true
|
|||||||
# FRONT_AUTH_CALLBACK_URL=http://localhost:3001/verify
|
# FRONT_AUTH_CALLBACK_URL=http://localhost:3001/verify
|
||||||
# AUTH_GOOGLE_ENABLED=false
|
# AUTH_GOOGLE_ENABLED=false
|
||||||
# MESSAGING_PROVIDER_GMAIL_ENABLED=false
|
# MESSAGING_PROVIDER_GMAIL_ENABLED=false
|
||||||
|
# IS_BILLING_ENABLED=false
|
||||||
|
# BILLING_PLAN_REQUIRED_LINK=https://twenty.com/stripe-redirection
|
||||||
# AUTH_GOOGLE_CLIENT_ID=replace_me_with_google_client_id
|
# AUTH_GOOGLE_CLIENT_ID=replace_me_with_google_client_id
|
||||||
# AUTH_GOOGLE_CLIENT_SECRET=replace_me_with_google_client_secret
|
# AUTH_GOOGLE_CLIENT_SECRET=replace_me_with_google_client_secret
|
||||||
# AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect
|
# AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect
|
||||||
|
|||||||
@ -116,6 +116,7 @@ export class AuthService {
|
|||||||
displayName: '',
|
displayName: '',
|
||||||
domainName: '',
|
domainName: '',
|
||||||
inviteHash: v4(),
|
inviteHash: v4(),
|
||||||
|
subscriptionStatus: 'incomplete',
|
||||||
});
|
});
|
||||||
|
|
||||||
workspace = await this.workspaceRepository.save(workspaceToCreate);
|
workspace = await this.workspaceRepository.save(workspaceToCreate);
|
||||||
|
|||||||
@ -21,6 +21,15 @@ class Telemetry {
|
|||||||
anonymizationEnabled: boolean;
|
anonymizationEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
class Billing {
|
||||||
|
@Field(() => Boolean)
|
||||||
|
isBillingEnabled: boolean;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
billingUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
class Support {
|
class Support {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@ -38,6 +47,9 @@ export class ClientConfig {
|
|||||||
@Field(() => Telemetry, { nullable: false })
|
@Field(() => Telemetry, { nullable: false })
|
||||||
telemetry: Telemetry;
|
telemetry: Telemetry;
|
||||||
|
|
||||||
|
@Field(() => Billing, { nullable: false })
|
||||||
|
billing: Billing;
|
||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
signInPrefilled: boolean;
|
signInPrefilled: boolean;
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,10 @@ export class ClientConfigResolver {
|
|||||||
anonymizationEnabled:
|
anonymizationEnabled:
|
||||||
this.environmentService.isTelemetryAnonymizationEnabled(),
|
this.environmentService.isTelemetryAnonymizationEnabled(),
|
||||||
},
|
},
|
||||||
|
billing: {
|
||||||
|
isBillingEnabled: this.environmentService.isBillingEnabled(),
|
||||||
|
billingUrl: this.environmentService.getBillingUrl(),
|
||||||
|
},
|
||||||
signInPrefilled: this.environmentService.isSignInPrefilled(),
|
signInPrefilled: this.environmentService.isSignInPrefilled(),
|
||||||
debugMode: this.environmentService.isDebugMode(),
|
debugMode: this.environmentService.isDebugMode(),
|
||||||
support: {
|
support: {
|
||||||
|
|||||||
@ -58,4 +58,8 @@ export class Workspace {
|
|||||||
|
|
||||||
@OneToMany(() => FeatureFlagEntity, (featureFlag) => featureFlag.workspace)
|
@OneToMany(() => FeatureFlagEntity, (featureFlag) => featureFlag.workspace)
|
||||||
featureFlags: FeatureFlagEntity[];
|
featureFlags: FeatureFlagEntity[];
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
@Column({ default: 'incomplete' })
|
||||||
|
subscriptionStatus: 'incomplete' | 'active' | 'canceled';
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,19 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddSubscriptionStatusOnWorkspace1702479005171
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'AddSubscriptionStatusOnWorkspace1702479005171';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."workspace" ADD "subscriptionStatus" character varying NOT NULL DEFAULT 'incomplete'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."workspace" DROP COLUMN "subscriptionStatus"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,6 +22,14 @@ export class EnvironmentService {
|
|||||||
return this.configService.get<boolean>('SIGN_IN_PREFILLED') ?? false;
|
return this.configService.get<boolean>('SIGN_IN_PREFILLED') ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isBillingEnabled() {
|
||||||
|
return this.configService.get<boolean>('IS_BILLING_ENABLED') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBillingUrl() {
|
||||||
|
return this.configService.get<string>('BILLING_PLAN_REQUIRED_LINK') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
isTelemetryEnabled(): boolean {
|
isTelemetryEnabled(): boolean {
|
||||||
return this.configService.get<boolean>('TELEMETRY_ENABLED') ?? true;
|
return this.configService.get<boolean>('TELEMETRY_ENABLED') ?? true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,15 @@ export class EnvironmentVariables {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
SIGN_IN_PREFILLED?: boolean;
|
SIGN_IN_PREFILLED?: boolean;
|
||||||
|
|
||||||
|
@CastToBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
IS_BILLING_ENABLED?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
BILLING_URL?: string;
|
||||||
|
|
||||||
@CastToBoolean()
|
@CastToBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
|||||||
Reference in New Issue
Block a user