From a10f353a4cb177d9766d78c6ea86baa5b88fac54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Thu, 14 Dec 2023 12:39:22 +0100 Subject: [PATCH] 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 --- packages/twenty-front/package.json | 1 + packages/twenty-front/src/App.tsx | 2 + .../effect-components/PageChangeEffect.tsx | 20 +++++-- .../src/generated-metadata/graphql.ts | 7 +++ .../twenty-front/src/generated/graphql.tsx | 59 +++++++------------ .../src/modules/auth/components/SubTitle.tsx | 10 +--- .../modules/auth/hooks/useOnboardingStatus.ts | 7 ++- .../auth/sign-in-up/hooks/useSignInUp.tsx | 45 ++++++++------ .../auth/states/currentWorkspaceState.ts | 1 + .../modules/auth/utils/getOnboardingStatus.ts | 29 +++++++-- .../components/ClientConfigProvider.tsx | 4 ++ .../graphql/queries/getClientConfig.ts | 4 ++ .../client-config/states/billingState.ts | 8 +++ .../components/SettingsAccountsCard.tsx | 2 +- .../components/SettingsAccountsEmailsCard.tsx | 2 +- .../components/SettingsAccountsRow.tsx | 2 +- .../SettingsAccountsRowDropdownMenu.tsx | 2 +- .../twenty-front/src/modules/types/AppPath.ts | 1 + .../src/modules/types/PageHotkeyScope.ts | 1 + .../components/MenuItemMultiSelectAvatar.tsx | 2 +- .../graphql/fragments/userQueryFragment.ts | 1 + .../users/graphql/queries/getCurrentUser.ts | 31 +--------- .../graphql/mutations/updateWorkspace.ts | 1 + .../src/pages/auth/CreateWorkspace.tsx | 2 + .../src/pages/auth/PlanRequired.tsx | 50 ++++++++++++++++ .../auth/__stories__/PlanRequired.stories.tsx | 36 +++++++++++ .../src/testing/mock-data/accounts.ts | 2 +- packages/twenty-server/.env.example | 2 + .../src/core/auth/services/auth.service.ts | 1 + .../client-config/client-config.entity.ts | 12 ++++ .../client-config/client-config.resolver.ts | 4 ++ .../src/core/workspace/workspace.entity.ts | 4 ++ .../typeorm-seeds/core/demo/workspaces.ts | 2 + .../database/typeorm-seeds/core/workspaces.ts | 2 + ...005171-addSubscriptionStatusOnWorkspace.ts | 19 ++++++ .../environment/environment.service.ts | 8 +++ .../environment/environment.validation.ts | 9 +++ 37 files changed, 285 insertions(+), 110 deletions(-) create mode 100644 packages/twenty-front/src/modules/client-config/states/billingState.ts create mode 100644 packages/twenty-front/src/pages/auth/PlanRequired.tsx create mode 100644 packages/twenty-front/src/pages/auth/__stories__/PlanRequired.stories.tsx create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/1702479005171-addSubscriptionStatusOnWorkspace.ts diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index 6af91f50e0..e560af364a 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 fa0843d05e..df2cfc3108 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 f21b541d80..dc001dcce2 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 0c01435502..e369c98bf5 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 f48bbf5875..33cd54fe04 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 2cee5104ef..91a8a60e84 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 d3e77fbf2b..d8c3a5651b 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 95da943368..ddbd76c80c 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 dd8f23b4f9..309ad1fd27 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 94d104e00f..aa114b34f3 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 a5502c9017..2c2e5aabc5 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 0000000000..4f0982b497 --- /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 c2b5f7fd5d..b6fa3224d1 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 d9eae1a9e4..742e479f99 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 716f99e003..c28cb16be4 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 f5919b53bb..a3913a43b7 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 686ef7f832..f3e646a9f5 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 5882480f67..db175a7925 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 7968699948..369f7b2059 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 f3ce60fedb..0c7894d5fd 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 891afbc638..8b1a6eac54 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 1d9a9b9fbe..c244ce0410 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 93e1bd8a5d..e38d21376b 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 0000000000..69dc681faa --- /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 0000000000..2289be5519 --- /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 8c7a73f798..0e19d7ef10 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 f3246091e4..e6bd28fc3c 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 37d6a72b81..cce4daa62f 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 079a76008e..404ea24c96 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 1f4d753ba1..bb1a5674ac 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 78afeb7ff8..ca42996566 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 8778e721a3..adf4f886e9 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,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', + 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 7a7ddd8682..0cef733301 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 0000000000..5d5a2fcdf8 --- /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 00ca9ed393..80e2036af5 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 9e8705bb5d..ddf69c52e8 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()