diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx index bdf99f5bd..9abf50e83 100644 --- a/packages/twenty-front/src/App.tsx +++ b/packages/twenty-front/src/App.tsx @@ -62,7 +62,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 841e0b18b..80ae45b00 100644 --- a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx +++ b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx @@ -39,7 +39,7 @@ export const PageChangeEffect = () => { const [workspaceFromInviteHashQuery] = useGetWorkspaceFromInviteHashLazyQuery(); - const { addToCommandMenu, setToIntitialCommandMenu } = useCommandMenu(); + const { addToCommandMenu, setToInitialCommandMenu } = useCommandMenu(); const openCreateActivity = useOpenCreateActivityDrawer(); @@ -209,7 +209,7 @@ export const PageChangeEffect = () => { }, [isMatchingLocation, setHotkeyScope]); useEffect(() => { - setToIntitialCommandMenu(); + setToInitialCommandMenu(); addToCommandMenu([ { @@ -221,7 +221,7 @@ export const PageChangeEffect = () => { onCommandClick: () => openCreateActivity({ type: 'Task' }), }, ]); - }, [addToCommandMenu, setToIntitialCommandMenu, openCreateActivity]); + }, [addToCommandMenu, setToInitialCommandMenu, openCreateActivity]); useEffect(() => { setTimeout(() => { diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 84b54666b..1973079ae 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -577,6 +577,7 @@ export type Verify = { export type Workspace = { __typename?: 'Workspace'; + activationStatus: Scalars['String']; allowImpersonation: Scalars['Boolean']; createdAt: Scalars['DateTime']; deletedAt?: Maybe; @@ -782,7 +783,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?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, 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 ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: 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']; @@ -813,7 +814,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?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, 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 VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: 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']; @@ -850,7 +851,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?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, 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 UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -867,7 +868,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?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, 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 GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } } }; export type ActivateWorkspaceMutationVariables = Exact<{ input: ActivateWorkspaceInput; @@ -988,6 +989,7 @@ export const UserQueryFragmentFragmentDoc = gql` inviteHash allowImpersonation subscriptionStatus + activationStatus featureFlags { id key @@ -1721,6 +1723,7 @@ export const GetCurrentUserDocument = gql` inviteHash allowImpersonation subscriptionStatus + activationStatus featureFlags { id key diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts new file mode 100644 index 000000000..ec243bb32 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts @@ -0,0 +1,44 @@ +import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; + +import { CurrentWorkspace } from '@/auth/states/currentWorkspaceState.ts'; +import { billingState } from '@/client-config/states/billingState.ts'; +import { AppPath } from '@/types/AppPath.ts'; +import { WorkspaceMember } from '~/generated/graphql.tsx'; + +export const useNavigateAfterSignInUp = () => { + const navigate = useNavigate(); + const billing = useRecoilValue(billingState); + const navigateAfterSignInUp = useCallback( + ( + currentWorkspace: CurrentWorkspace, + currentWorkspaceMember: WorkspaceMember | null, + ) => { + if ( + billing?.isBillingEnabled && + currentWorkspace.subscriptionStatus !== 'active' + ) { + navigate(AppPath.PlanRequired); + return; + } + + if (currentWorkspace.activationStatus !== 'active') { + navigate(AppPath.CreateWorkspace); + return; + } + + if ( + !currentWorkspaceMember?.name.firstName || + !currentWorkspaceMember?.name.lastName + ) { + navigate(AppPath.CreateProfile); + return; + } + + navigate(AppPath.Index); + }, + [billing, navigate], + ); + return { navigateAfterSignInUp }; +}; 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 fdaca81ca..142b78813 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 @@ -1,10 +1,9 @@ import { useCallback, useState } from 'react'; import { SubmitHandler, UseFormReturn } from 'react-hook-form'; -import { useNavigate, useParams } from 'react-router-dom'; -import { useRecoilValue } from 'recoil'; +import { useParams } from 'react-router-dom'; +import { useNavigateAfterSignInUp } from '@/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts'; import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm.ts'; -import { billingState } from '@/client-config/states/billingState'; import { AppPath } from '@/types/AppPath'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; @@ -26,14 +25,18 @@ export enum SignInUpStep { } export const useSignInUp = (form: UseFormReturn
) => { - const navigate = useNavigate(); const { enqueueSnackBar } = useSnackBar(); + const isMatchingLocation = useIsMatchingLocation(); - const billing = useRecoilValue(billingState); + const workspaceInviteHash = useParams().workspaceInviteHash; + + const { navigateAfterSignInUp } = useNavigateAfterSignInUp(); + const [signInUpStep, setSignInUpStep] = useState( SignInUpStep.Init, ); + const [signInUpMode, setSignInUpMode] = useState(() => { if (isMatchingLocation(AppPath.Invite)) { return SignInUpMode.Invite; @@ -43,6 +46,7 @@ export const useSignInUp = (form: UseFormReturn) => { ? SignInUpMode.SignIn : SignInUpMode.SignUp; }); + const { signInWithCredentials, signUpWithCredentials, @@ -84,7 +88,10 @@ export const useSignInUp = (form: UseFormReturn) => { throw new Error('Email and password are required'); } - const { workspace: currentWorkspace } = + const { + workspace: currentWorkspace, + workspaceMember: currentWorkspaceMember, + } = signInUpMode === SignInUpMode.SignIn ? await signInWithCredentials( data.email.toLowerCase().trim(), @@ -96,19 +103,7 @@ export const useSignInUp = (form: UseFormReturn) => { workspaceInviteHash, ); - if ( - billing?.isBillingEnabled && - currentWorkspace.subscriptionStatus !== 'active' - ) { - navigate(AppPath.PlanRequired); - return; - } - if (currentWorkspace.displayName) { - navigate(AppPath.Index); - return; - } - - navigate(AppPath.CreateWorkspace); + navigateAfterSignInUp(currentWorkspace, currentWorkspaceMember); } catch (err: any) { enqueueSnackBar(err?.message, { variant: 'error', @@ -120,8 +115,7 @@ export const useSignInUp = (form: UseFormReturn) => { signInWithCredentials, signUpWithCredentials, workspaceInviteHash, - billing?.isBillingEnabled, - navigate, + navigateAfterSignInUp, enqueueSnackBar, ], ); diff --git a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts index ddbd76c80..594d16e1b 100644 --- a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts +++ b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts @@ -11,6 +11,7 @@ export type CurrentWorkspace = Pick< | 'allowImpersonation' | 'featureFlags' | 'subscriptionStatus' + | 'activationStatus' >; export const currentWorkspaceState = atom({ diff --git a/packages/twenty-front/src/modules/auth/states/selectors/isCurrentWorkspaceActiveSelector.ts b/packages/twenty-front/src/modules/auth/states/selectors/isCurrentWorkspaceActiveSelector.ts deleted file mode 100644 index 22c7fc605..000000000 --- a/packages/twenty-front/src/modules/auth/states/selectors/isCurrentWorkspaceActiveSelector.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { selector } from 'recoil'; - -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; - -export const isCurrentWorkspaceActiveSelector = selector({ - key: 'isCurrentWorkspaceActiveSelector', - get: ({ get }) => { - const currentWorkspaceMember = get(currentWorkspaceMemberState); - return !!currentWorkspaceMember; - }, -}); diff --git a/packages/twenty-front/src/modules/auth/utils/getOnboardingStatus.ts b/packages/twenty-front/src/modules/auth/utils/getOnboardingStatus.ts index 78d86a94b..6bccfb133 100644 --- a/packages/twenty-front/src/modules/auth/utils/getOnboardingStatus.ts +++ b/packages/twenty-front/src/modules/auth/utils/getOnboardingStatus.ts @@ -39,13 +39,13 @@ export const getOnboardingStatus = ({ return OnboardingStatus.Canceled; } - if (!currentWorkspaceMember) { + if (currentWorkspace?.activationStatus !== 'active') { return OnboardingStatus.OngoingWorkspaceActivation; } if ( - !currentWorkspaceMember.name.firstName || - !currentWorkspaceMember.name.lastName + !currentWorkspaceMember?.name.firstName || + !currentWorkspaceMember?.name.lastName ) { return OnboardingStatus.OngoingProfileCreation; } diff --git a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx index 6515a08af..6cf5c7fbf 100644 --- a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx @@ -31,14 +31,14 @@ const meta: Meta = { const setCurrentWorkspaceMember = useSetRecoilState( currentWorkspaceMemberState, ); - const { addToCommandMenu, setToIntitialCommandMenu, openCommandMenu } = + const { addToCommandMenu, setToInitialCommandMenu, openCommandMenu } = useCommandMenu(); setCurrentWorkspace(mockDefaultWorkspace); setCurrentWorkspaceMember(mockedWorkspaceMemberData); useEffect(() => { - setToIntitialCommandMenu(); + setToInitialCommandMenu(); addToCommandMenu([ { id: 'create-task', @@ -58,7 +58,7 @@ const meta: Meta = { }, ]); openCommandMenu(); - }, [addToCommandMenu, setToIntitialCommandMenu, openCommandMenu]); + }, [addToCommandMenu, setToInitialCommandMenu, openCommandMenu]); return ; }, diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx index 80cc60a60..e1ee55013 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx @@ -107,11 +107,11 @@ describe('useCommandMenu', () => { expect(onClickMock).toHaveBeenCalledTimes(1); }); - it('should setToIntitialCommandMenu command menu', () => { + it('should setToInitialCommandMenu command menu', () => { const { result } = renderHooks(); act(() => { - result.current.commandMenu.setToIntitialCommandMenu(); + result.current.commandMenu.setToInitialCommandMenu(); }); expect(result.current.commandMenuCommands.length).toBe(5); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts index f9448debc..3dc1688cd 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts @@ -61,7 +61,7 @@ export const useCommandMenu = () => { [setCommands], ); - const setToIntitialCommandMenu = () => { + const setToInitialCommandMenu = () => { setCommands(commandMenuCommands); }; @@ -87,6 +87,6 @@ export const useCommandMenu = () => { toggleCommandMenu, addToCommandMenu, onItemClick, - setToIntitialCommandMenu, + setToInitialCommandMenu, }; }; diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsProvider.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsProvider.tsx index 3b4db3dce..b8e1eb3d8 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsProvider.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsProvider.tsx @@ -1,6 +1,7 @@ +import React from 'react'; import { useRecoilValue } from 'recoil'; -import { isCurrentWorkspaceActiveSelector } from '@/auth/states/selectors/isCurrentWorkspaceActiveSelector'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState.ts'; import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope'; @@ -9,14 +10,17 @@ export const ObjectMetadataItemsProvider = ({ children, }: React.PropsWithChildren) => { const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - const isCurrentWorkspaceActive = useRecoilValue( - isCurrentWorkspaceActiveSelector, - ); - + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + const shouldDisplayChildren = () => { + if (objectMetadataItems.length) { + return true; + } + return !currentWorkspaceMember; + }; return ( <> - {(!isCurrentWorkspaceActive || !!objectMetadataItems.length) && ( + {shouldDisplayChildren() && ( {children} diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts index 93286d9c5..f25ddeaa3 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts @@ -1,7 +1,7 @@ import { gql } from '@apollo/client'; import { useRecoilValue } from 'recoil'; -import { isCurrentWorkspaceActiveSelector } from '@/auth/states/selectors/isCurrentWorkspaceActiveSelector'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState.ts'; import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError'; import { useGetObjectOrderByField } from '@/object-metadata/hooks/useGetObjectOrderByField'; import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier'; @@ -40,9 +40,8 @@ export const useObjectMetadataItem = ( { objectNameSingular }: ObjectMetadataItemIdentifier, depth?: number, ) => { - const isCurrentWorkspaceActive = useRecoilValue( - isCurrentWorkspaceActiveSelector, - ); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const mockObjectMetadataItems = getObjectMetadataItemsMock(); let objectMetadataItem = useRecoilValue( @@ -54,7 +53,7 @@ export const useObjectMetadataItem = ( let objectMetadataItems = useRecoilValue(objectMetadataItemsState); - if (!isCurrentWorkspaceActive) { + if (currentWorkspace?.activationStatus !== 'active') { objectMetadataItem = mockObjectMetadataItems.find( (objectMetadataItem) => diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemOnly.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemOnly.ts index 61ade984c..d5e5b3f55 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemOnly.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemOnly.ts @@ -1,6 +1,6 @@ import { useRecoilValue } from 'recoil'; -import { isCurrentWorkspaceActiveSelector } from '@/auth/states/selectors/isCurrentWorkspaceActiveSelector'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState.ts'; import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError'; import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; @@ -12,9 +12,8 @@ import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentif export const useObjectMetadataItemOnly = ({ objectNameSingular, }: ObjectMetadataItemIdentifier) => { - const isCurrentWorkspaceActive = useRecoilValue( - isCurrentWorkspaceActiveSelector, - ); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const mockObjectMetadataItems = getObjectMetadataItemsMock(); let objectMetadataItem = useRecoilValue( @@ -26,7 +25,7 @@ export const useObjectMetadataItemOnly = ({ let objectMetadataItems = useRecoilValue(objectMetadataItemsState); - if (!isCurrentWorkspaceActive) { + if (currentWorkspace?.activationStatus !== 'active') { objectMetadataItem = mockObjectMetadataItems.find( (objectMetadataItem) => diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts index aefce4f1f..3cf1c60bb 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts @@ -1,6 +1,6 @@ import { useRecoilValue } from 'recoil'; -import { isCurrentWorkspaceActiveSelector } from '@/auth/states/selectors/isCurrentWorkspaceActiveSelector'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState.ts'; import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; import { isDefined } from '~/utils/isDefined'; @@ -10,9 +10,7 @@ export const useObjectNamePluralFromSingular = ({ }: { objectNameSingular: string; }) => { - const isCurrentWorkspaceActive = useRecoilValue( - isCurrentWorkspaceActiveSelector, - ); + const currentWorkspace = useRecoilValue(currentWorkspaceState); const mockObjectMetadataItems = getObjectMetadataItemsMock(); let objectMetadataItem = useRecoilValue( @@ -22,7 +20,7 @@ export const useObjectNamePluralFromSingular = ({ }), ); - if (!isCurrentWorkspaceActive) { + if (currentWorkspace?.activationStatus !== 'active') { objectMetadataItem = mockObjectMetadataItems.find( (objectMetadataItem) => diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts index 5fc5ee73d..432a3b718 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts @@ -1,6 +1,6 @@ import { useRecoilValue } from 'recoil'; -import { isCurrentWorkspaceActiveSelector } from '@/auth/states/selectors/isCurrentWorkspaceActiveSelector'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState.ts'; import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; import { isDefined } from '~/utils/isDefined'; @@ -10,9 +10,8 @@ export const useObjectNameSingularFromPlural = ({ }: { objectNamePlural: string; }) => { - const isCurrentWorkspaceActive = useRecoilValue( - isCurrentWorkspaceActiveSelector, - ); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const mockObjectMetadataItems = getObjectMetadataItemsMock(); let objectMetadataItem = useRecoilValue( @@ -22,7 +21,7 @@ export const useObjectNameSingularFromPlural = ({ }), ); - if (!isCurrentWorkspaceActive) { + if (currentWorkspace?.activationStatus !== 'active') { objectMetadataItem = mockObjectMetadataItems.find( (objectMetadataItem) => diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts index 807257e4f..9db52bcc7 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts @@ -1,6 +1,6 @@ import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { isCurrentWorkspaceActiveSelector } from '@/auth/states/selectors/isCurrentWorkspaceActiveSelector'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState.ts'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; @@ -14,9 +14,7 @@ export const useLoadRecordIndexTable = (objectNameSingular: string) => { const { setRecordTableData, setIsRecordTableInitialLoading } = useRecordTable(); - const isCurrentWorkspaceActive = useRecoilValue( - isCurrentWorkspaceActiveSelector, - ); + const currentWorkspace = useRecoilValue(currentWorkspaceState); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, }); @@ -58,7 +56,10 @@ export const useLoadRecordIndexTable = (objectNameSingular: string) => { }); return { - records: isCurrentWorkspaceActive ? records : signInBackgroundMockCompanies, + records: + currentWorkspace?.activationStatus === 'active' + ? records + : signInBackgroundMockCompanies, totalCount: totalCount || 0, loading, fetchMoreRecords, 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 0c7894d5f..a19b5ff18 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -26,6 +26,7 @@ export const USER_QUERY_FRAGMENT = gql` inviteHash allowImpersonation subscriptionStatus + activationStatus 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 d7bfacf24..b7b10ce78 100644 --- a/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts +++ b/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts @@ -28,6 +28,7 @@ export const GET_CURRENT_USER = gql` inviteHash allowImpersonation subscriptionStatus + activationStatus featureFlags { id key diff --git a/packages/twenty-front/src/pages/auth/PasswordReset.tsx b/packages/twenty-front/src/pages/auth/PasswordReset.tsx index 72d881695..f134c5bef 100644 --- a/packages/twenty-front/src/pages/auth/PasswordReset.tsx +++ b/packages/twenty-front/src/pages/auth/PasswordReset.tsx @@ -6,15 +6,14 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { zodResolver } from '@hookform/resolvers/zod'; import { motion } from 'framer-motion'; -import { useRecoilValue } from 'recoil'; import { z } from 'zod'; import { Logo } from '@/auth/components/Logo'; import { Title } from '@/auth/components/Title'; import { useAuth } from '@/auth/hooks/useAuth'; import { useIsLogged } from '@/auth/hooks/useIsLogged'; +import { useNavigateAfterSignInUp } from '@/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts'; import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex'; -import { billingState } from '@/client-config/states/billingState'; import { AppPath } from '@/types/AppPath'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { MainButton } from '@/ui/input/button/components/MainButton'; @@ -124,7 +123,7 @@ export const PasswordReset = () => { const { signInWithCredentials } = useAuth(); - const billing = useRecoilValue(billingState); + const { navigateAfterSignInUp } = useNavigateAfterSignInUp(); const onSubmit = async (formData: Form) => { try { @@ -150,25 +149,12 @@ export const PasswordReset = () => { return; } - const { workspace: currentWorkspace } = await signInWithCredentials( - email || '', - formData.newPassword, - ); + const { + workspace: currentWorkspace, + workspaceMember: currentWorkspaceMember, + } = await signInWithCredentials(email || '', formData.newPassword); - if ( - billing?.isBillingEnabled && - currentWorkspace.subscriptionStatus !== 'active' - ) { - navigate(AppPath.PlanRequired); - return; - } - - if (currentWorkspace.displayName) { - navigate(AppPath.Index); - return; - } - - navigate(AppPath.CreateWorkspace); + navigateAfterSignInUp(currentWorkspace, currentWorkspaceMember); } catch (err) { logError(err); enqueueSnackBar( diff --git a/packages/twenty-front/src/pages/auth/VerifyEffect.tsx b/packages/twenty-front/src/pages/auth/VerifyEffect.tsx index 05ced046d..cda60b1bc 100644 --- a/packages/twenty-front/src/pages/auth/VerifyEffect.tsx +++ b/packages/twenty-front/src/pages/auth/VerifyEffect.tsx @@ -1,6 +1,5 @@ import { useEffect } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; -import { isNonEmptyString } from '@sniptt/guards'; import { useRecoilValue } from 'recoil'; import { useAuth } from '@/auth/hooks/useAuth'; @@ -25,7 +24,7 @@ export const VerifyEffect = () => { } else { await verify(loginToken); - if (isNonEmptyString(currentWorkspace?.displayName)) { + if (currentWorkspace?.activationStatus === 'active') { navigate(AppPath.Index); } else { navigate(AppPath.CreateWorkspace); diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts index f7b27713e..5e7e33118 100644 --- a/packages/twenty-front/src/testing/mock-data/users.ts +++ b/packages/twenty-front/src/testing/mock-data/users.ts @@ -29,6 +29,7 @@ export const mockDefaultWorkspace: Workspace = { logo: workspaceLogoUrl, allowImpersonation: true, subscriptionStatus: 'active', + activationStatus: 'active', featureFlags: [], createdAt: '2023-04-26T10:23:42.33625+00:00', updatedAt: '2023-04-26T10:23:42.33625+00:00', diff --git a/packages/twenty-server/src/core/workspace/services/workspace.service.ts b/packages/twenty-server/src/core/workspace/services/workspace.service.ts index 0c4a8f6eb..18166c4cc 100644 --- a/packages/twenty-server/src/core/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/core/workspace/services/workspace.service.ts @@ -35,6 +35,10 @@ export class WorkspaceService extends TypeOrmQueryService { return user.defaultWorkspace; } + async isWorkspaceActivated(id: string): Promise { + return await this.workspaceManagerService.doesDataSourceExist(id); + } + async deleteWorkspace(id: string, shouldDeleteCoreWorkspace = true) { const workspace = await this.workspaceRepository.findOneBy({ id }); diff --git a/packages/twenty-server/src/core/workspace/workspace.entity.ts b/packages/twenty-server/src/core/workspace/workspace.entity.ts index ca4299656..7ae201a9b 100644 --- a/packages/twenty-server/src/core/workspace/workspace.entity.ts +++ b/packages/twenty-server/src/core/workspace/workspace.entity.ts @@ -62,4 +62,7 @@ export class Workspace { @Field() @Column({ default: 'incomplete' }) subscriptionStatus: 'incomplete' | 'active' | 'canceled'; + + @Field() + activationStatus: 'active' | 'inactive'; } diff --git a/packages/twenty-server/src/core/workspace/workspace.resolver.ts b/packages/twenty-server/src/core/workspace/workspace.resolver.ts index 4bff02259..58085cbbc 100644 --- a/packages/twenty-server/src/core/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/core/workspace/workspace.resolver.ts @@ -1,4 +1,11 @@ -import { Resolver, Query, Args, Mutation } from '@nestjs/graphql'; +import { + Resolver, + Query, + Args, + Mutation, + ResolveField, + Parent, +} from '@nestjs/graphql'; import { ForbiddenException, UseGuards } from '@nestjs/common'; import { FileUpload, GraphQLUpload } from 'graphql-upload'; @@ -90,4 +97,15 @@ export class WorkspaceResolver { return this.workspaceService.deleteWorkspace(id); } + + @ResolveField(() => String) + async activationStatus( + @Parent() workspace: Workspace, + ): Promise<'active' | 'inactive'> { + if (await this.workspaceService.isWorkspaceActivated(workspace.id)) { + return 'active'; + } + + return 'inactive'; + } } diff --git a/packages/twenty-server/src/workspace/workspace-manager/workspace-manager.service.ts b/packages/twenty-server/src/workspace/workspace-manager/workspace-manager.service.ts index abe4ef232..b71238f42 100644 --- a/packages/twenty-server/src/workspace/workspace-manager/workspace-manager.service.ts +++ b/packages/twenty-server/src/workspace/workspace-manager/workspace-manager.service.ts @@ -80,6 +80,22 @@ export class WorkspaceManagerService { await this.prefillWorkspaceWithDemoObjects(dataSourceMetadata, workspaceId); } + /** + * + * Check if the workspace schema has already been created or not + * + * @param workspaceId + * @Returns Promise + */ + public async doesDataSourceExist(workspaceId: string): Promise { + const dataSource = + await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId( + workspaceId, + ); + + return dataSource.length > 0; + } + /** * * We are updating the pg_graphql max_rows from 30 (default value) to 60