diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json
index 6af91f50e..e560af364 100644
--- a/packages/twenty-front/package.json
+++ b/packages/twenty-front/package.json
@@ -8,6 +8,7 @@
"start:clean": "yarn start --force",
"build": "tsc && vite build && yarn build:inject-runtime-env",
"build:inject-runtime-env": "sh ./scripts/inject-runtime-env.sh",
+ "tsc": "tsc --watch",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"fmt:fix": "prettier --cache --write \"src/**/*.ts\" \"src/**/*.tsx\"",
diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx
index fa0843d05..df2cfc310 100644
--- a/packages/twenty-front/src/App.tsx
+++ b/packages/twenty-front/src/App.tsx
@@ -10,6 +10,7 @@ import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect';
import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect';
import { CreateProfile } from '~/pages/auth/CreateProfile';
import { CreateWorkspace } from '~/pages/auth/CreateWorkspace';
+import { PlanRequired } from '~/pages/auth/PlanRequired';
import { SignInUp } from '~/pages/auth/SignInUp';
import { VerifyEffect } from '~/pages/auth/VerifyEffect';
import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect';
@@ -50,6 +51,7 @@ export const App = () => {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx
index f21b541d8..dc001dcce 100644
--- a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx
+++ b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx
@@ -57,12 +57,10 @@ export const PageChangeEffect = () => {
isMatchingLocation(AppPath.Verify);
const isMatchingOnboardingRoute =
- isMatchingLocation(AppPath.SignUp) ||
- isMatchingLocation(AppPath.SignIn) ||
- isMatchingLocation(AppPath.Invite) ||
- isMatchingLocation(AppPath.Verify) ||
+ isMachinOngoingUserCreationRoute ||
isMatchingLocation(AppPath.CreateWorkspace) ||
- isMatchingLocation(AppPath.CreateProfile);
+ isMatchingLocation(AppPath.CreateProfile) ||
+ isMatchingLocation(AppPath.PlanRequired);
const navigateToSignUp = () => {
enqueueSnackBar('workspace does not exist', {
@@ -76,6 +74,14 @@ export const PageChangeEffect = () => {
!isMachinOngoingUserCreationRoute
) {
navigate(AppPath.SignIn);
+ } else if (
+ onboardingStatus &&
+ [OnboardingStatus.Canceled, OnboardingStatus.Incomplete].includes(
+ onboardingStatus,
+ ) &&
+ !isMatchingLocation(AppPath.PlanRequired)
+ ) {
+ navigate(AppPath.PlanRequired);
} else if (
onboardingStatus === OnboardingStatus.OngoingWorkspaceCreation &&
!isMatchingLocation(AppPath.CreateWorkspace)
@@ -170,6 +176,10 @@ export const PageChangeEffect = () => {
setHotkeyScope(PageHotkeyScope.CreateWokspace);
break;
}
+ case isMatchingLocation(AppPath.PlanRequired): {
+ setHotkeyScope(PageHotkeyScope.PlanRequired);
+ break;
+ }
case isMatchingLocation(SettingsPath.ProfilePage, AppBasePath.Settings): {
setHotkeyScope(PageHotkeyScope.ProfilePage, {
goto: true,
diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index 0c0143550..e369c98bf 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -41,6 +41,12 @@ export type AuthTokenPair = {
refreshToken: AuthToken;
};
+export type Billing = {
+ __typename?: 'Billing';
+ billingUrl: Scalars['String']['output'];
+ isBillingEnabled: Scalars['Boolean']['output'];
+};
+
export type BooleanFieldComparison = {
is?: InputMaybe;
isNot?: InputMaybe;
@@ -484,6 +490,7 @@ export type Workspace = {
id: Scalars['ID']['output'];
inviteHash?: Maybe;
logo?: Maybe;
+ subscriptionStatus: Scalars['String']['output'];
updatedAt: Scalars['DateTime']['output'];
};
diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx
index f48bbf587..33cd54fe0 100644
--- a/packages/twenty-front/src/generated/graphql.tsx
+++ b/packages/twenty-front/src/generated/graphql.tsx
@@ -54,6 +54,12 @@ export type AuthTokens = {
tokens: AuthTokenPair;
};
+export type Billing = {
+ __typename?: 'Billing';
+ billingUrl: Scalars['String'];
+ isBillingEnabled: Scalars['Boolean'];
+};
+
export type BooleanFieldComparison = {
is?: InputMaybe;
isNot?: InputMaybe;
@@ -62,6 +68,7 @@ export type BooleanFieldComparison = {
export type ClientConfig = {
__typename?: 'ClientConfig';
authProviders: AuthProviders;
+ billing: Billing;
debugMode: Scalars['Boolean'];
signInPrefilled: Scalars['Boolean'];
support: Support;
@@ -507,6 +514,7 @@ export type Workspace = {
id: Scalars['ID'];
inviteHash?: Maybe;
logo?: Maybe;
+ subscriptionStatus: Scalars['String'];
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<{
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<{
email: Scalars['String'];
@@ -695,7 +703,7 @@ export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __
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<{
file: Scalars['Upload'];
@@ -713,7 +721,7 @@ export type UploadImageMutationVariables = Exact<{
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; }>;
@@ -730,7 +738,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
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; }>;
@@ -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<{
file: Scalars['Upload'];
@@ -799,6 +807,7 @@ export const UserQueryFragmentFragmentDoc = gql`
domainName
inviteHash
allowImpersonation
+ subscriptionStatus
featureFlags {
id
key
@@ -1142,6 +1151,10 @@ export const GetClientConfigDocument = gql`
google
password
}
+ billing {
+ isBillingEnabled
+ billingUrl
+ }
signInPrefilled
debugMode
telemetry {
@@ -1312,39 +1325,10 @@ export type UploadProfilePictureMutationOptions = Apollo.BaseMutationOptions theme.font.color.secondary};
+ text-align: center;
`;
-export const SubTitle = ({ children }: SubTitleProps): JSX.Element => (
- {children}
-);
+export { StyledSubTitle as SubTitle };
diff --git a/packages/twenty-front/src/modules/auth/hooks/useOnboardingStatus.ts b/packages/twenty-front/src/modules/auth/hooks/useOnboardingStatus.ts
index 2cee5104e..91a8a60e8 100644
--- a/packages/twenty-front/src/modules/auth/hooks/useOnboardingStatus.ts
+++ b/packages/twenty-front/src/modules/auth/hooks/useOnboardingStatus.ts
@@ -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,
+ });
};
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx
index d3e77fbf2..d8c3a5651 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx
@@ -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.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,
],
diff --git a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts
index 95da94336..ddbd76c80 100644
--- a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts
+++ b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts
@@ -10,6 +10,7 @@ export type CurrentWorkspace = Pick<
| 'displayName'
| 'allowImpersonation'
| 'featureFlags'
+ | 'subscriptionStatus'
>;
export const currentWorkspaceState = atom({
diff --git a/packages/twenty-front/src/modules/auth/utils/getOnboardingStatus.ts b/packages/twenty-front/src/modules/auth/utils/getOnboardingStatus.ts
index dd8f23b4f..309ad1fd2 100644
--- a/packages/twenty-front/src/modules/auth/utils/getOnboardingStatus.ts
+++ b/packages/twenty-front/src/modules/auth/utils/getOnboardingStatus.ts
@@ -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;
}
diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx
index 94d104e00..aa114b34f 100644
--- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx
+++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx
@@ -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 = ({
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 = ({
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 = ({
setIsSignInPrefilled,
setTelemetry,
setSupportChat,
+ setBilling,
]);
return loading ? <>> : <>{children}>;
diff --git a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts
index a5502c901..2c2e5aabc 100644
--- a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts
+++ b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts
@@ -7,6 +7,10 @@ export const GET_CLIENT_CONFIG = gql`
google
password
}
+ billing {
+ isBillingEnabled
+ billingUrl
+ }
signInPrefilled
debugMode
telemetry {
diff --git a/packages/twenty-front/src/modules/client-config/states/billingState.ts b/packages/twenty-front/src/modules/client-config/states/billingState.ts
new file mode 100644
index 000000000..4f0982b49
--- /dev/null
+++ b/packages/twenty-front/src/modules/client-config/states/billingState.ts
@@ -0,0 +1,8 @@
+import { atom } from 'recoil';
+
+import { Billing } from '~/generated/graphql';
+
+export const billingState = atom({
+ key: 'billingState',
+ default: null,
+});
diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCard.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCard.tsx
index c2b5f7fd5..b6fa3224d 100644
--- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCard.tsx
+++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCard.tsx
@@ -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';
diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsCard.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsCard.tsx
index d9eae1a9e..742e479f9 100644
--- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsCard.tsx
+++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsCard.tsx
@@ -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';
diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRow.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRow.tsx
index 716f99e00..c28cb16be 100644
--- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRow.tsx
+++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRow.tsx
@@ -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';
diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx
index f5919b53b..a3913a43b 100644
--- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx
+++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx
@@ -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';
diff --git a/packages/twenty-front/src/modules/types/AppPath.ts b/packages/twenty-front/src/modules/types/AppPath.ts
index 686ef7f83..f3e646a9f 100644
--- a/packages/twenty-front/src/modules/types/AppPath.ts
+++ b/packages/twenty-front/src/modules/types/AppPath.ts
@@ -8,6 +8,7 @@ export enum AppPath {
// Onboarding
CreateWorkspace = '/create/workspace',
CreateProfile = '/create/profile',
+ PlanRequired = '/plan-required',
// Onboarded
Index = '/',
diff --git a/packages/twenty-front/src/modules/types/PageHotkeyScope.ts b/packages/twenty-front/src/modules/types/PageHotkeyScope.ts
index 5882480f6..db175a792 100644
--- a/packages/twenty-front/src/modules/types/PageHotkeyScope.ts
+++ b/packages/twenty-front/src/modules/types/PageHotkeyScope.ts
@@ -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',
diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar.tsx
index 796869994..369f7b205 100644
--- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar.tsx
+++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar.tsx
@@ -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;
diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts
index f3ce60fed..0c7894d5f 100644
--- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts
+++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts
@@ -25,6 +25,7 @@ export const USER_QUERY_FRAGMENT = gql`
domainName
inviteHash
allowImpersonation
+ subscriptionStatus
featureFlags {
id
key
diff --git a/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts b/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts
index 891afbc63..8b1a6eac5 100644
--- a/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts
+++ b/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts
@@ -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
}
}
`;
diff --git a/packages/twenty-front/src/modules/workspace/graphql/mutations/updateWorkspace.ts b/packages/twenty-front/src/modules/workspace/graphql/mutations/updateWorkspace.ts
index 1d9a9b9fb..c244ce041 100644
--- a/packages/twenty-front/src/modules/workspace/graphql/mutations/updateWorkspace.ts
+++ b/packages/twenty-front/src/modules/workspace/graphql/mutations/updateWorkspace.ts
@@ -8,6 +8,7 @@ export const UPDATE_WORKSPACE = gql`
displayName
logo
allowImpersonation
+ subscriptionStatus
}
}
`;
diff --git a/packages/twenty-front/src/pages/auth/CreateWorkspace.tsx b/packages/twenty-front/src/pages/auth/CreateWorkspace.tsx
index 93e1bd8a5..e38d21376 100644
--- a/packages/twenty-front/src/pages/auth/CreateWorkspace.tsx
+++ b/packages/twenty-front/src/pages/auth/CreateWorkspace.tsx
@@ -77,6 +77,8 @@ export const CreateWorkspace = () => {
setCurrentWorkspace({
id: result.data?.updateWorkspace?.id ?? '',
displayName: data.name,
+ subscriptionStatus:
+ result.data?.updateWorkspace?.subscriptionStatus ?? 'incomplete',
allowImpersonation:
result.data?.updateWorkspace?.allowImpersonation ?? false,
});
diff --git a/packages/twenty-front/src/pages/auth/PlanRequired.tsx b/packages/twenty-front/src/pages/auth/PlanRequired.tsx
new file mode 100644
index 000000000..69dc681fa
--- /dev/null
+++ b/packages/twenty-front/src/pages/auth/PlanRequired.tsx
@@ -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 (
+ <>
+
+
+
+ Plan required
+
+ Please select a subscription plan before proceeding to sign in.
+
+
+
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/pages/auth/__stories__/PlanRequired.stories.tsx b/packages/twenty-front/src/pages/auth/__stories__/PlanRequired.stories.tsx
new file mode 100644
index 000000000..2289be551
--- /dev/null
+++ b/packages/twenty-front/src/pages/auth/__stories__/PlanRequired.stories.tsx
@@ -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 = {
+ title: 'Pages/Auth/PlanRequired',
+ component: PlanRequired,
+ decorators: [PageDecorator],
+ args: { routePath: AppPath.PlanRequired },
+ parameters: {
+ msw: graphqlMocks,
+ cookie: {
+ tokenPair: '{}',
+ },
+ },
+};
+
+export default meta;
+
+export type Story = StoryObj;
+
+export const Default: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ await canvas.findByRole('button', { name: 'Get started' });
+ },
+};
diff --git a/packages/twenty-front/src/testing/mock-data/accounts.ts b/packages/twenty-front/src/testing/mock-data/accounts.ts
index 8c7a73f79..0e19d7ef1 100644
--- a/packages/twenty-front/src/testing/mock-data/accounts.ts
+++ b/packages/twenty-front/src/testing/mock-data/accounts.ts
@@ -1,4 +1,4 @@
-import { Account } from '@/accounts/types/account';
+import { Account } from '@/accounts/types/Account';
export const mockedAccounts: Account[] = [
{
diff --git a/packages/twenty-server/.env.example b/packages/twenty-server/.env.example
index f3246091e..e6bd28fc3 100644
--- a/packages/twenty-server/.env.example
+++ b/packages/twenty-server/.env.example
@@ -19,6 +19,8 @@ SIGN_IN_PREFILLED=true
# FRONT_AUTH_CALLBACK_URL=http://localhost:3001/verify
# AUTH_GOOGLE_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_SECRET=replace_me_with_google_client_secret
# AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect
diff --git a/packages/twenty-server/src/core/auth/services/auth.service.ts b/packages/twenty-server/src/core/auth/services/auth.service.ts
index 37d6a72b8..cce4daa62 100644
--- a/packages/twenty-server/src/core/auth/services/auth.service.ts
+++ b/packages/twenty-server/src/core/auth/services/auth.service.ts
@@ -116,6 +116,7 @@ export class AuthService {
displayName: '',
domainName: '',
inviteHash: v4(),
+ subscriptionStatus: 'incomplete',
});
workspace = await this.workspaceRepository.save(workspaceToCreate);
diff --git a/packages/twenty-server/src/core/client-config/client-config.entity.ts b/packages/twenty-server/src/core/client-config/client-config.entity.ts
index 079a76008..404ea24c9 100644
--- a/packages/twenty-server/src/core/client-config/client-config.entity.ts
+++ b/packages/twenty-server/src/core/client-config/client-config.entity.ts
@@ -21,6 +21,15 @@ class Telemetry {
anonymizationEnabled: boolean;
}
+@ObjectType()
+class Billing {
+ @Field(() => Boolean)
+ isBillingEnabled: boolean;
+
+ @Field(() => String)
+ billingUrl: string;
+}
+
@ObjectType()
class Support {
@Field(() => String)
@@ -38,6 +47,9 @@ export class ClientConfig {
@Field(() => Telemetry, { nullable: false })
telemetry: Telemetry;
+ @Field(() => Billing, { nullable: false })
+ billing: Billing;
+
@Field(() => Boolean)
signInPrefilled: boolean;
diff --git a/packages/twenty-server/src/core/client-config/client-config.resolver.ts b/packages/twenty-server/src/core/client-config/client-config.resolver.ts
index 1f4d753ba..bb1a5674a 100644
--- a/packages/twenty-server/src/core/client-config/client-config.resolver.ts
+++ b/packages/twenty-server/src/core/client-config/client-config.resolver.ts
@@ -21,6 +21,10 @@ export class ClientConfigResolver {
anonymizationEnabled:
this.environmentService.isTelemetryAnonymizationEnabled(),
},
+ billing: {
+ isBillingEnabled: this.environmentService.isBillingEnabled(),
+ billingUrl: this.environmentService.getBillingUrl(),
+ },
signInPrefilled: this.environmentService.isSignInPrefilled(),
debugMode: this.environmentService.isDebugMode(),
support: {
diff --git a/packages/twenty-server/src/core/workspace/workspace.entity.ts b/packages/twenty-server/src/core/workspace/workspace.entity.ts
index 78afeb7ff..ca4299656 100644
--- a/packages/twenty-server/src/core/workspace/workspace.entity.ts
+++ b/packages/twenty-server/src/core/workspace/workspace.entity.ts
@@ -58,4 +58,8 @@ export class Workspace {
@OneToMany(() => FeatureFlagEntity, (featureFlag) => featureFlag.workspace)
featureFlags: FeatureFlagEntity[];
+
+ @Field()
+ @Column({ default: 'incomplete' })
+ subscriptionStatus: 'incomplete' | 'active' | 'canceled';
}
diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/demo/workspaces.ts b/packages/twenty-server/src/database/typeorm-seeds/core/demo/workspaces.ts
index 8778e721a..adf4f886e 100644
--- a/packages/twenty-server/src/database/typeorm-seeds/core/demo/workspaces.ts
+++ b/packages/twenty-server/src/database/typeorm-seeds/core/demo/workspaces.ts
@@ -16,6 +16,7 @@ export const seedWorkspaces = async (
'domainName',
'inviteHash',
'logo',
+ 'subscriptionStatus',
])
.orIgnore()
.values([
@@ -25,6 +26,7 @@ export const seedWorkspaces = async (
domainName: 'demo.dev',
inviteHash: 'demo.dev-invite-hash',
logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAAAXNSR0IArs4c6QAAELpJREFUeF7tnXnwr3MVx1+WS5Hthoss1R0lNSTUKGQdt6EscVVCKVPKUqmUKcu0WJI2ppJUt0kNwmRN0dBCtolKpqIVIdl3qXnX8xs/1+937/d5vp/lnOc5n3++/zyfzznnfc77+zyf7ZxFiBYIBALTIrBIYBMIBALTIxAEiegIBBaAQBAkwiMQCIJEDIyJgP5IlwRmAreOOZar7vEGceWuYsrOBg4E5gKzgMlxMqiYGZSxxcLLnyDFwU7AF4A1FqL+osB//JnYTeMgSDfc+tLrhcBZwHotDFoMeLLF864fDYK4dl9n5XcBTm3mFW0HWRz4d9tOXp8Pgnj1XDe9d2+Ioc+kri3eIF2Ri35mEdgAuLzjG2N+o2IOYtbNoVhbBGYA1wPrtO24gOcH9dUxKGMTBomHofYE5iVWVKtX43yeJVYn/3BBkPwY15BwXcuVqVF1fBB4zqgP9+G5IEgfvPiUDSsAdwBaacrRrgY2zjGw1TGDIFY9016vjYCr2ndr1eMQ4NhWPZw/HARx7sBG/Z2BMwuY8nzgLwXkmBERBDHjis6KaG/je517t+s4uHgZnMHt4sH809sCFxXS8l5g+UKyzIgJgphxRWtFdI7qpta9unc4Ejiie3efPYMgPv2muxmPFFZdy7ta5h1UC4L4dPcDwNIFVX8YWKqgPDOigiBmXDGyIhcAc0Z+Os2D+wFfSTOUr1GCIL78tTXw4woqD+qA4mR8gyAVoq2jSAVpjXsY2l95Y0ed3XcLgvhx4S+BV1ZQdwng8QpyTYgMgphww0KVeDFw40KfSv/AD4Ad0w/rZ8QgiA9fPQQ8u4Kqg7o9OBW+QZAKUddSpLKNKLFC6XYocFRpodbkBUGseeSZ+uj7P9fx9ems1yZkjTeWOW8EQcy55GkKab9D+x6l2+rALaWFWpQXBLHolad0Kr1jLsknAAfYhqWcdkGQcli3lbQKcFvbTmM+fxew4phj9Kp7EMSuOy8Btiys3qD3PKbCOghSOAJbiCud//alwA0t9BvEo0EQm25+GfDrgqrtC5xcUJ4bUUEQm666ENiukGrK6P6+QrLciQmC2HTZE4B2sXO305saILnluB0/CGLPdSKGCJK7nQu8PrcQ7+MHQex5cENACdpytu8Cb8kpoC9jB0HsefI44OCMaul8lc5ZRRsBgSDICCAVfuQ3gJZcczQVzqlx8DGHLUXGDIIUgbmVEGUOSZ0gQSXTdL6q9M58K8MtPhwEsecVBXNKvyjTuwrolN54tIdsB41SOqKD+OgyBQIpA/kNwDmBcncEgiDdscvVMwVBzmuWcFOMlctOF+MGQey5aZyg/hWwGaBj8tESIBAESQBi4iG6EOQ0YO8K6UgTm25vuCCIPZ+MMkkXiZQGSBebcm8q2kOooEZBkIJgjyhKV11nNitZIoI+l/4O/Aw4A7gCeHTEscZ5TLExcRdeeihpXZe32zg6VO87ZILoctDzgPWbZdC1gdWAZRqv3An8Dris+bfWHkLfAkQEWA94E7A9oJIKz1pIVAoDJXX4M/DTZuNRNdhVP6R3bSgEUSAor+37m0ls1404ff4ogdvnmqpO3ibDKpugNKIfbXbrU/tfxPkRcHTzphNerltqgCyBMQs4vjnOnSttjv5N9S8q4l1ryfhJumgHXee7di10hH4yDDoVINnHACqh4K71jSAigv69dAGoxH2K+R3+W+Cdzb9nzWBYFfgqsEPiXflxbBJZDgJO8fSp2heCKCBUFmDdcTyYuO9PgL2aCXbioaccTnOHTzVBWOPPoY2Nyvk7t9BiQxu9nvGsd4JoUq3a4Pq12rT6oznLYRk+M0QEvbH0GaMSad6aVua2sUwUrwTRZPPKZgXGU1BoUi+iKDlb15ICqhOiVSeRbmVPxi9A1283b1tz5ngkyKebVRhzYLZUSGTRP/9nRzgaoiXpPZv5VV8Tu2nBQyuN+jQ10zwRRP+WWnvvY1JlBYdWwU5s5lLaU9ClKS02KLu7CDKUphMCm1iZyHshyOFDrNE9FEZMYafmbWtZSKBtnSDS7w/A7AEHy5BNf3OzIVsNA8sEWRZQMuVcm3zVQA/BrRComtjOKkFq1eRr5bl4uBgC1SrtWiTIps3xjWLohyAXCFQpKGqNIFtYW+ZzETrDUfIbwD4lzbVEkI2aXfGS9ocsfwh8oNkkLaK5FYLoXoYuBUULBEZBQH+m14zy4LjPWCCINsF0j8CCLuPiGf3LIaDDmdlvVloIyvudHrQrFwohaSoE7m6uJmdFpzZBdERd52+iBQJtECh2bqsmQXYEzm6DSjwbCDQ5ArTaWSQ/QC2CzAAeC3cHAi0R0N2Ri1v2GevxWgS5qcmgMZby0XkwCNwDKMdA8T/VGgSZA1wwGNeGoeMioDeG3hxVWg2CjJI5sAoYIdQcAp8EPl5Tq9IE+Vpzh7qmzSHbBwLKNTyvtqolCaINwewbO7UBDflJEFByO53grd5KEuRSYPPqFocC1hHYAzjVipKlCKJjAS4z61lx1ED00HxD8w4zrRRBLgS2M2N1KGIRgfObBNqmdCtBEOVx0iX8aIHAdAgUOVfVBf4SBImMJF08M6w+SgRYfBNwFIhLEOSJSomkR7E/nqmPwM6Wz+TlJojS9fyxvg9CA6MI3Gw9pVNugig58WuMOifUqo+AsmTqspzZlpsgRY4km0U3FFsQAqoT8g7rEOUkyDpNjT/rGIR+dRDQ6qb5P9CcBDnX4rp2nVgIqfMhoOpX7/aASk6CxOqVhwioo6PSybrYG8tFkDiYWCfwPEi9vim97UHXbKl2VDzyHBcIhJKlEVDe5d+XFtpVXq43iG6BbdVVqejXWwQ0Kdfk3E3LRRCtbev4QLRAYDICKq/m6o8zF0HML99F3FZBYGPg6iqSOwrNQZBlgPs66hPd+o2Ai72PyS7IQZAtgUv67eewrgMCOq3r7rM7B0FU2vjgDgBGl34j8HNAxZFctRwE0Tfmhq5QCGVLILB/U+a6hKxkMnIQRFnwlkumYQzUFwRe1FQsdmVPDoLoCIGrtW5XHvOrbJF6HqnhyUGQWOJN7aV+jJcj1rIjk0PpIEh2t7kUkCPWsgORWunFAJ3ijRYITEbA3RGTCeVTE0Tr3KavUEbcVkFACcv15+mupSZI7KK7C4EiCuurQkWT3LXUBFkJuMMdCqFwbgSCIA3CqwK35kY7xneHQHxiNS5bDbjFnftC4dwIxCS9QXgV4LbcaMf4LhFI/TlfBITUSsccpIjbXApxd9RdKKcmyLLAvS7dF0rnRsB8FsWpAEhNkCiUkzvM/I7vKlnDBMypCRI76X4DOLfm7wG+nFtI6vFTE0TjaUkvWiAwPwKXAa/1Bktqgsj+OKzoLQrK6KsKx/oEd9VyEERvkBzjugI2lJ0SAXcrWTkCOXJiBTumQ2Bdbxn/cxBEO+naUY8WCMyPwBnAbp5gyUGQSDvqKQLK6qrr2Mrs7qblIMhhwJFuEAhFSyOg0xb/LC20q7wcBNkcuLSrQtGv9wicDaiyrYuWgyDLAyoMHy0QmA6BHHGXBe0cisZmYRZX9WrQvYF5HizKQRDZHXshHrxfT0c3NwxzEUR3QnQ3JFogMB0CmodoPmK65SLIyR5qYJv2TP+Vc3ENNxdBYiWr/wGewsKjgENTDJRrjFwEifxYuTzWv3GXAh62alYugsjeONVr1eu29Lrd8nw1J0H+BqxuyxehjVEEjrB6+iInQQ4HZHi0QGAUBEzWD8lJkEgiN0pYxDMTCOiTXHPXxy1BkpMgMQ+x5Gkfumiyrkm7mZabIP8AZpmxNhTxgIA2mc3cJ8pNkA8Cn/HgldDRFAI3A7MtaJSbIJFIzoKXfeqgVdA1a6uemyCyL4p61vayX/m6NvHcmntqJQhyEbCtXx+F5pUR0Mlf3TF6sIYeJQjyEuCGGsaFzF4h8Grg8tIWlSBILPeW9mp/5emU+L4lzStFkGuBDUoaFrJ6i8ADzeS9yLXuUgTZCLiqty4Lw2ogcBLwrtyCSxEkPrNye3KY4+t4ylxACemytJIEUXbvzbJYEYMOHQGd3xJRkl/hLUmQFwDaIY0WCORCQG8UbU5rnpKklSSIFI5NwyRui0GmQSB5tpTSBDkOODjcGwhkQuD41PFVmiBLACqkEi0QyIFA8kKhpQkiUP4KrJEDnRhz0AhoX2RmagRqEOQVwDWpDYnxBo/ALsBZqVGoQRDZoMmUKuJGCwRSIZAllrMMOoLFHwaOGeG5eCQQGAWBC4HXjfJg22dqESQywLf1VDy/IARWAO7JAVEtgsiW07zVq8vhgBhzbAT+1VyqGnugqQaoSZBY8s3i0sENmvWeSE2CyJO/ADYZnEvD4FQIJN85n1+x2gRZOuW5mVSoxzhuEHgr8J2c2tYmiGy7Gtgwp5Exdi8R0MHERXNbZoEg8RbJ7eV+jr8/cGJu0ywQRDaqbLSK7kQLBEZBoMjbQ4pYIcgM4LFRkIlnAoGmvN8pJZCwQhDZKoPfXsLokOEagewrV5PRsUQQ6RVVqVzHbhHltwYuKSLJ0CfWhL17Ad8qZXzIcYdA1l3zqdCw9gaRjvcBy7hzXShcAgGVRVB5hGLNIkGiMlUx97sSpM8qfV4VbRYJIgB08WWnokiEMOsI6P7Qk6WVtEoQ4RAZUEpHg115ewCn1lDPMkEiXWmNiLAns2pJNssEkasuBray57PQqCACKuqp4p5VmnWCCJS4v14lNEwIPQA4oaYmHggSKUtrRkg92UoPtVY98f+X7IEg0lOVclUxN9pwEND5PH09VG1eCCKQ7gBWqopWCC+FwBbNCe9S8qaV44kgceK3ergUUeB8YPsikkYQ4okgMkf313WPPVo/EdBqlVatzDRvBBFw3wT2NoNgKJISgeWas3gpxxxrLI8EkcG3AyuPZXl0tobA7k2uNFN6eSWIzuWo7JZX/U0FgQFlTM07JuPhOcBif8RAZCdQQSlDlTrUZPNMEAGquYjmJNF8IqAbpEs2XwMmLfBOEIGqG4i6iRjNHwKzrRd27QNBFBbXAev5i49Ba7wr8H3rCPSFIML5TmBF64CHfv9D4CjgUA9Y9IkgskX1sU1tNHkIgsI6ng7MLSyzs7g+EUQgKFfrQ83ErzMo0TEbAlXulY9jTd8IMkGS++NNMk5YZOn7Q2BOlpEzDtpHgggu2XUrsEpG7HIMraQE+kzUfEqnlx9shCwLzGpOM6sWuDe/nQzsmwOw3GN6A7otHio3rbLTFpvyf50JnARcCzzaUkltru0AvBfYuEQpgJb6TTx+EPDFjn2rd+s7QQTw5wE5qXbT2+Ec4CPAjRmU0YabNk4/Yeic2vrA9RlsLTbkEAgiMF8FXFEM1acLOq/5vCiZEVCLFfs0fw6qv1K6/QlYu0ndVFp2UnlDIYhAU9Dok+vlSRGcerC7AJUHU/3u2k2fYvOaz7HcuujoyG4eNgBHBWJIBJnARK/9ywFNdlM23Z/+UrMB9kjKgROOtQ3wdWDNhGNODHUscEiGcasOOUSCTAC+aTMnWH4MD2hirYD7GHD3GOOU7iq/K8/tcc0Rna5xoIWG/WplPSwBWldgSuhWSoZ23j8EHAjMXIhQXQm9DDi+qVFRPetGIpA0T9EexdsA1R3XZ9lUsaFN2Csb+y+wkHUkkf3TDhMEeSY0izcBoqDRypM2HfVPqVzBQ2yKkcEWNgqCDDHkw+aREQiCjAxVPDhEBIIgQ/R62DwyAkGQkaGKB4eIwH8BiW3y2J/F45oAAAAASUVORK5CYII=',
+ subscriptionStatus: 'incomplete',
},
])
.execute();
diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/workspaces.ts b/packages/twenty-server/src/database/typeorm-seeds/core/workspaces.ts
index 7a7ddd868..0cef73330 100644
--- a/packages/twenty-server/src/database/typeorm-seeds/core/workspaces.ts
+++ b/packages/twenty-server/src/database/typeorm-seeds/core/workspaces.ts
@@ -18,6 +18,7 @@ export const seedWorkspaces = async (
'domainName',
'inviteHash',
'logo',
+ 'subscriptionStatus',
])
.orIgnore()
.values([
@@ -27,6 +28,7 @@ export const seedWorkspaces = async (
domainName: 'apple.dev',
inviteHash: 'apple.dev-invite-hash',
logo: 'data:image/png;base64,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',
+ subscriptionStatus: 'incomplete',
},
])
.execute();
diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1702479005171-addSubscriptionStatusOnWorkspace.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1702479005171-addSubscriptionStatusOnWorkspace.ts
new file mode 100644
index 000000000..5d5a2fcdf
--- /dev/null
+++ b/packages/twenty-server/src/database/typeorm/core/migrations/1702479005171-addSubscriptionStatusOnWorkspace.ts
@@ -0,0 +1,19 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class AddSubscriptionStatusOnWorkspace1702479005171
+ implements MigrationInterface
+{
+ name = 'AddSubscriptionStatusOnWorkspace1702479005171';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "core"."workspace" ADD "subscriptionStatus" character varying NOT NULL DEFAULT 'incomplete'`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "core"."workspace" DROP COLUMN "subscriptionStatus"`,
+ );
+ }
+}
diff --git a/packages/twenty-server/src/integrations/environment/environment.service.ts b/packages/twenty-server/src/integrations/environment/environment.service.ts
index 00ca9ed39..80e2036af 100644
--- a/packages/twenty-server/src/integrations/environment/environment.service.ts
+++ b/packages/twenty-server/src/integrations/environment/environment.service.ts
@@ -22,6 +22,14 @@ export class EnvironmentService {
return this.configService.get('SIGN_IN_PREFILLED') ?? false;
}
+ isBillingEnabled() {
+ return this.configService.get('IS_BILLING_ENABLED') ?? false;
+ }
+
+ getBillingUrl() {
+ return this.configService.get('BILLING_PLAN_REQUIRED_LINK') ?? '';
+ }
+
isTelemetryEnabled(): boolean {
return this.configService.get('TELEMETRY_ENABLED') ?? true;
}
diff --git a/packages/twenty-server/src/integrations/environment/environment.validation.ts b/packages/twenty-server/src/integrations/environment/environment.validation.ts
index 9e8705bb5..ddf69c52e 100644
--- a/packages/twenty-server/src/integrations/environment/environment.validation.ts
+++ b/packages/twenty-server/src/integrations/environment/environment.validation.ts
@@ -38,6 +38,15 @@ export class EnvironmentVariables {
@IsBoolean()
SIGN_IN_PREFILLED?: boolean;
+ @CastToBoolean()
+ @IsOptional()
+ @IsBoolean()
+ IS_BILLING_ENABLED?: boolean;
+
+ @IsOptional()
+ @IsString()
+ BILLING_URL?: string;
+
@CastToBoolean()
@IsOptional()
@IsBoolean()