diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx
index e26ca00539..d2e3e73ace 100644
--- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx
+++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx
@@ -415,28 +415,39 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
element={}
/>
} />
- } />
- } />
}
- />
- }
- />
- }
- />
- }
- />
- }
- />
+ element={
+
+ }
+ >
+ } />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+
{
const subscriptionStatus = useSubscriptionStatus();
+ const permissionMap = usePermissionFlagMap();
+ const isAccountSyncEnabled =
+ permissionMap[PermissionFlagType.CONNECTED_ACCOUNTS];
const isWorkspaceSuspended = useIsWorkspaceActivationStatusEqualsTo(
WorkspaceActivationStatus.SUSPENDED,
);
@@ -43,8 +52,12 @@ export const InformationBannerWrapper = () => {
return (
-
-
+ {isAccountSyncEnabled && (
+
+ )}
+ {isAccountSyncEnabled && (
+
+ )}
{displayBillingSubscriptionPausedBanner && (
// TODO: remove this once paused subscriptions are deprecated
)}
diff --git a/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useSetNextOnboardingStatus.test.ts b/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useSetNextOnboardingStatus.test.ts
index ffd9be1f9c..14ec4f09cb 100644
--- a/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useSetNextOnboardingStatus.test.ts
+++ b/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useSetNextOnboardingStatus.test.ts
@@ -3,10 +3,13 @@ import { RecoilRoot, useRecoilState, useSetRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { currentUserState } from '@/auth/states/currentUserState';
+import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
+
import {
OnboardingStatus,
+ PermissionFlagType,
SubscriptionStatus,
} from '~/generated-metadata/graphql';
import {
@@ -18,16 +21,21 @@ const renderHooks = (
onboardingStatus: OnboardingStatus,
withCurrentBillingSubscription: boolean,
withOneWorkspaceMember = true,
+ permissionFlags = mockedUserData.currentUserWorkspace.permissionFlags,
) => {
const { result } = renderHook(
() => {
const [currentUser, setCurrentUser] = useRecoilState(currentUserState);
+ const setCurrentUserWorkspace = useSetRecoilState(
+ currentUserWorkspaceState,
+ );
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setNextOnboardingStatus = useSetNextOnboardingStatus();
return {
currentUser,
setCurrentUser,
setCurrentWorkspace,
+ setCurrentUserWorkspace,
setNextOnboardingStatus,
};
},
@@ -37,6 +45,10 @@ const renderHooks = (
);
act(() => {
result.current.setCurrentUser({ ...mockedUserData, onboardingStatus });
+ result.current.setCurrentUserWorkspace({
+ ...mockedUserData.currentUserWorkspace,
+ permissionFlags,
+ });
result.current.setCurrentWorkspace({
...mockCurrentWorkspace,
currentBillingSubscription: withCurrentBillingSubscription
@@ -75,6 +87,16 @@ describe('useSetNextOnboardingStatus', () => {
expect(nextOnboardingStatus).toEqual(OnboardingStatus.COMPLETED);
});
+ it('should skip SyncEmail when account sync is disabled', () => {
+ const nextOnboardingStatus = renderHooks(
+ OnboardingStatus.PROFILE_CREATION,
+ false,
+ true,
+ [PermissionFlagType.WORKSPACE_MEMBERS],
+ );
+ expect(nextOnboardingStatus).toEqual(OnboardingStatus.INVITE_TEAM);
+ });
+
it('should set next onboarding status for SyncEmail', () => {
const nextOnboardingStatus = renderHooks(
OnboardingStatus.SYNC_EMAIL,
diff --git a/packages/twenty-front/src/modules/onboarding/hooks/useSetNextOnboardingStatus.ts b/packages/twenty-front/src/modules/onboarding/hooks/useSetNextOnboardingStatus.ts
index 674899cdd2..33653861d6 100644
--- a/packages/twenty-front/src/modules/onboarding/hooks/useSetNextOnboardingStatus.ts
+++ b/packages/twenty-front/src/modules/onboarding/hooks/useSetNextOnboardingStatus.ts
@@ -1,5 +1,7 @@
import { useRecoilCallback, useRecoilValue } from 'recoil';
+import { isDefined } from 'twenty-shared/utils';
+
import {
type CurrentUser,
currentUserState,
@@ -9,21 +11,36 @@ import {
currentWorkspaceState,
} from '@/auth/states/currentWorkspaceState';
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
-import { isDefined } from 'twenty-shared/utils';
-import { OnboardingStatus } from '~/generated-metadata/graphql';
+import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
-const getNextOnboardingStatus = (
- currentUser: CurrentUser | null,
- currentWorkspace: CurrentWorkspace | null,
- calendarBookingPageId: string | null,
-) => {
+import {
+ OnboardingStatus,
+ PermissionFlagType,
+} from '~/generated-metadata/graphql';
+
+type GetNextOnboardingStatusArgs = {
+ currentUser: CurrentUser | null;
+ currentWorkspace: CurrentWorkspace | null;
+ calendarBookingPageId: string | null;
+ isAccountSyncEnabled: boolean;
+};
+
+const getNextOnboardingStatus = ({
+ currentUser,
+ currentWorkspace,
+ calendarBookingPageId,
+ isAccountSyncEnabled,
+}: GetNextOnboardingStatusArgs) => {
if (currentUser?.onboardingStatus === OnboardingStatus.WORKSPACE_ACTIVATION) {
return OnboardingStatus.PROFILE_CREATION;
}
if (currentUser?.onboardingStatus === OnboardingStatus.PROFILE_CREATION) {
if (currentWorkspace?.workspaceMembersCount === 1) {
- return OnboardingStatus.SYNC_EMAIL;
+ if (isAccountSyncEnabled) {
+ return OnboardingStatus.SYNC_EMAIL;
+ }
+ return OnboardingStatus.INVITE_TEAM;
}
return OnboardingStatus.COMPLETED;
}
@@ -48,15 +65,19 @@ export const useSetNextOnboardingStatus = () => {
const currentUser = useRecoilValue(currentUserState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const calendarBookingPageId = useRecoilValue(calendarBookingPageIdState);
+ const permissionMap = usePermissionFlagMap();
+ const isAccountSyncEnabled =
+ permissionMap[PermissionFlagType.CONNECTED_ACCOUNTS];
return useRecoilCallback(
({ set }) =>
() => {
- const nextOnboardingStatus = getNextOnboardingStatus(
+ const nextOnboardingStatus = getNextOnboardingStatus({
currentUser,
currentWorkspace,
calendarBookingPageId,
- );
+ isAccountSyncEnabled,
+ });
set(currentUserState, (current) => {
if (isDefined(current)) {
return {
@@ -67,6 +88,11 @@ export const useSetNextOnboardingStatus = () => {
return current;
});
},
- [currentWorkspace, currentUser, calendarBookingPageId],
+ [
+ currentUser,
+ currentWorkspace,
+ calendarBookingPageId,
+ isAccountSyncEnabled,
+ ],
);
};
diff --git a/packages/twenty-front/src/modules/settings/hooks/__tests__/useSettingsNavigationItems.test.tsx b/packages/twenty-front/src/modules/settings/hooks/__tests__/useSettingsNavigationItems.test.tsx
index c88e7fb0d8..ccd70fb2d3 100644
--- a/packages/twenty-front/src/modules/settings/hooks/__tests__/useSettingsNavigationItems.test.tsx
+++ b/packages/twenty-front/src/modules/settings/hooks/__tests__/useSettingsNavigationItems.test.tsx
@@ -4,6 +4,7 @@ import { renderHook } from '@testing-library/react';
import { type ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { type MutableSnapshot, RecoilRoot } from 'recoil';
+import { SettingsPath } from 'twenty-shared/types';
import {
type Billing,
OnboardingStatus,
@@ -78,6 +79,7 @@ describe('useSettingsNavigationItems', () => {
[PermissionFlagType.API_KEYS_AND_WEBHOOKS]: false,
[PermissionFlagType.ROLES]: false,
[PermissionFlagType.SECURITY]: false,
+ [PermissionFlagType.CONNECTED_ACCOUNTS]: false,
}));
const { result } = renderHook(() => useSettingsNavigationItems(), {
@@ -99,6 +101,7 @@ describe('useSettingsNavigationItems', () => {
[PermissionFlagType.API_KEYS_AND_WEBHOOKS]: true,
[PermissionFlagType.ROLES]: true,
[PermissionFlagType.SECURITY]: true,
+ [PermissionFlagType.CONNECTED_ACCOUNTS]: true,
}));
const { result } = renderHook(() => useSettingsNavigationItems(), {
@@ -113,6 +116,16 @@ describe('useSettingsNavigationItems', () => {
});
it('should show user section items regardless of permissions', () => {
+ (usePermissionFlagMap as jest.Mock).mockImplementation(() => ({
+ [PermissionFlagType.WORKSPACE]: false,
+ [PermissionFlagType.WORKSPACE_MEMBERS]: false,
+ [PermissionFlagType.DATA_MODEL]: false,
+ [PermissionFlagType.API_KEYS_AND_WEBHOOKS]: false,
+ [PermissionFlagType.ROLES]: false,
+ [PermissionFlagType.SECURITY]: false,
+ [PermissionFlagType.CONNECTED_ACCOUNTS]: false,
+ }));
+
const { result } = renderHook(() => useSettingsNavigationItems(), {
wrapper: Wrapper,
});
@@ -120,7 +133,13 @@ describe('useSettingsNavigationItems', () => {
const userSection = result.current.find(
(section) => section.label === 'User',
);
- expect(userSection?.items.length).toBeGreaterThan(0);
- expect(userSection?.items.every((item) => !item.isHidden)).toBe(true);
+ expect(
+ userSection?.items.filter((item) => !item.isHidden).length,
+ ).toBeGreaterThan(0);
+ expect(
+ userSection?.items
+ .filter((item) => item.path !== SettingsPath.Accounts)
+ .every((item) => !item.isHidden),
+ ).toBe(true);
});
});
diff --git a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx
index cab015b94f..7525b2131e 100644
--- a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx
+++ b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx
@@ -99,17 +99,20 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
label: t`Accounts`,
path: SettingsPath.Accounts,
Icon: IconAt,
+ isHidden: !permissionMap[PermissionFlagType.CONNECTED_ACCOUNTS],
subItems: [
{
label: t`Emails`,
path: SettingsPath.AccountsEmails,
Icon: IconMail,
+ isHidden: !permissionMap[PermissionFlagType.CONNECTED_ACCOUNTS],
indentationLevel: 2,
},
{
label: t`Calendars`,
path: SettingsPath.AccountsCalendars,
Icon: IconCalendarEvent,
+ isHidden: !permissionMap[PermissionFlagType.CONNECTED_ACCOUNTS],
indentationLevel: 2,
},
],
diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts
index feaee59b31..6ada3aac14 100644
--- a/packages/twenty-front/src/testing/mock-data/users.ts
+++ b/packages/twenty-front/src/testing/mock-data/users.ts
@@ -192,7 +192,10 @@ export const mockedUserData: MockedUser = {
workspaceMember: mockedWorkspaceMemberData,
currentWorkspace: mockCurrentWorkspace,
currentUserWorkspace: {
- permissionFlags: [PermissionFlagType.WORKSPACE_MEMBERS],
+ permissionFlags: [
+ PermissionFlagType.WORKSPACE_MEMBERS,
+ PermissionFlagType.CONNECTED_ACCOUNTS,
+ ],
twoFactorAuthenticationMethodSummary: [],
objectsPermissions: generatedMockObjectMetadataItems.map((item) => ({
objectMetadataId: item.id,
diff --git a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.resolver.ts b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.resolver.ts
index 1c9f9b0474..954eac2cd7 100644
--- a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.resolver.ts
+++ b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.resolver.ts
@@ -24,7 +24,6 @@ import { ImapSmtpCalDavAPIService } from 'src/modules/connected-account/services
@MetadataResolver()
@UsePipes(ResolverValidationPipe)
@UseFilters(AuthGraphqlApiExceptionFilter, PermissionsGraphqlApiExceptionFilter)
-@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKSPACE))
export class ImapSmtpCaldavResolver {
constructor(
private readonly ImapSmtpCaldavConnectionService: ImapSmtpCaldavService,
@@ -33,7 +32,10 @@ export class ImapSmtpCaldavResolver {
) {}
@Query(() => ConnectedImapSmtpCaldavAccountDTO)
- @UseGuards(WorkspaceAuthGuard)
+ @UseGuards(
+ WorkspaceAuthGuard,
+ SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS),
+ )
async getConnectedImapSmtpCaldavAccount(
@Args('id', { type: () => UUIDScalarType }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -60,7 +62,10 @@ export class ImapSmtpCaldavResolver {
}
@Mutation(() => ImapSmtpCaldavConnectionSuccessDTO)
- @UseGuards(WorkspaceAuthGuard)
+ @UseGuards(
+ WorkspaceAuthGuard,
+ SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS),
+ )
async saveImapSmtpCaldavAccount(
@Args('accountOwnerId', { type: () => UUIDScalarType })
accountOwnerId: string,
diff --git a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.module.ts b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.module.ts
index 66fd54050b..cfe1a28392 100644
--- a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.module.ts
+++ b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.module.ts
@@ -6,9 +6,17 @@ import { TimelineMessagingResolver } from 'src/engine/core-modules/messaging/tim
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
+import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
+import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
@Module({
- imports: [WorkspaceDataSourceModule, UserModule, ConnectedAccountModule],
+ imports: [
+ WorkspaceDataSourceModule,
+ UserModule,
+ ConnectedAccountModule,
+ FeatureFlagModule,
+ PermissionsModule,
+ ],
exports: [],
providers: [
TimelineMessagingResolver,
diff --git a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.resolver.ts b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.resolver.ts
index a2d725fcaa..2bb0a4cc3c 100644
--- a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.resolver.ts
+++ b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.resolver.ts
@@ -2,6 +2,7 @@ import { UseGuards } from '@nestjs/common';
import { Args, ArgsType, Field, Int, Mutation, Query } from '@nestjs/graphql';
import { Max } from 'class-validator';
+import { PermissionFlagType } from 'twenty-shared/constants';
import { CoreResolver } from 'src/engine/api/graphql/graphql-config/decorators/core-resolver.decorator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@@ -16,6 +17,7 @@ import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
+import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
@@ -149,6 +151,7 @@ export class TimelineMessagingResolver {
return timelineThreads;
}
+ @UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
@Mutation(() => Boolean)
async dismissReconnectAccountBanner(
@AuthUser() user: UserEntity,