Replace the onboarding AI chat feature flag with an environment variable (#23439)

Follow-up to #23199.

The AI-chat onboarding is an instance-level rollout decision, not a
per-workspace experiment, so `IS_ONBOARDING_AI_CHAT_ENABLED` becomes an
instance config variable (default `false`, editable from the admin
panel) exposed to the frontend through `ClientConfig`. The workspace
feature flag is deleted; leftover `featureFlag` rows are inert since the
column is plain text.

`IS_WORKSPACE_COMPANY_ENRICHMENT_ENABLED` is removed as redundant: the
PDL client already skips everything when no API key is set. Enrichment
now runs when the AI chat is on and `PEOPLE_DATA_LABS_API_KEY` is
configured.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23439?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Raphaël Bosi
2026-07-28 16:46:52 +02:00
committed by GitHub
parent 884f470982
commit 9509c737e0
30 changed files with 85 additions and 122 deletions
@@ -24,6 +24,7 @@ import { maintenanceModeState } from '@/client-config/states/maintenanceModeStat
import { isMicrosoftCalendarEnabledState } from '@/client-config/states/isMicrosoftCalendarEnabledState';
import { isMicrosoftMessagingEnabledState } from '@/client-config/states/isMicrosoftMessagingEnabledState';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { isOnboardingAiChatEnabledState } from '@/client-config/states/isOnboardingAiChatEnabledState';
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
import { sentryConfigState } from '@/client-config/states/sentryConfigState';
import { supportChatState } from '@/client-config/states/supportChatState';
@@ -126,6 +127,10 @@ export const useClientConfig = (): UseClientConfigResult => {
const setIsDDLLocked = useSetAtomState(isDDLLockedState);
const setIsOnboardingAiChatEnabled = useSetAtomState(
isOnboardingAiChatEnabledState,
);
const setMaintenanceMode = useSetAtomState(maintenanceModeState);
const setEnterpriseInstanceType = useSetAtomState(
@@ -215,6 +220,9 @@ export const useClientConfig = (): UseClientConfigResult => {
);
setIsClickHouseConfigured(clientConfig?.isClickHouseConfigured ?? false);
setIsDDLLocked(clientConfig?.isWorkspaceSchemaDDLLocked ?? false);
setIsOnboardingAiChatEnabled(
clientConfig?.isOnboardingAiChatEnabled ?? false,
);
setMaintenanceMode(clientConfig?.maintenance ?? null);
setEnterpriseInstanceType(
clientConfig?.enterpriseInstanceType ??
@@ -256,6 +264,7 @@ export const useClientConfig = (): UseClientConfigResult => {
setIsClickHouseConfigured,
setIsCloudflareIntegrationEnabled,
setIsDDLLocked,
setIsOnboardingAiChatEnabled,
setLabPublicFeatureFlags,
setMaintenanceMode,
setEnterpriseInstanceType,
@@ -0,0 +1,6 @@
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
export const isOnboardingAiChatEnabledState = createAtomState<boolean>({
key: 'isOnboardingAiChatEnabledState',
defaultValue: false,
});
@@ -38,6 +38,7 @@ export type ClientConfig = {
isCloudflareIntegrationEnabled: boolean;
isClickHouseConfigured: boolean;
isWorkspaceSchemaDDLLocked: boolean;
isOnboardingAiChatEnabled: boolean;
onboarding: OnboardingConfig;
publicFeatureFlags: Array<PublicFeatureFlag>;
sentry: Sentry;
@@ -3,14 +3,14 @@ import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type WorkspaceCompanyEnrichment } from 'twenty-shared/workspace';
import { isOnboardingAiChatEnabledState } from '@/client-config/states/isOnboardingAiChatEnabledState';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { companyEnrichmentState } from '@/onboarding/states/companyEnrichmentState';
import { hasAttemptedCompanyEnrichmentFetchState } from '@/onboarding/states/hasAttemptedCompanyEnrichmentFetchState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import {
EnrichWorkspaceCompanyDocument,
FeatureFlagKey,
OnboardingStatus,
WorkspaceCompanyEnrichmentOutcome,
} from '~/generated-metadata/graphql';
@@ -25,8 +25,8 @@ export const CompanyEnrichmentOnboardingEffect = () => {
setHasAttemptedCompanyEnrichmentFetch,
] = useAtomState(hasAttemptedCompanyEnrichmentFetchState);
const [enrichWorkspaceCompany] = useMutation(EnrichWorkspaceCompanyDocument);
const isOnboardingAiChatEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_ONBOARDING_AI_CHAT_ENABLED,
const isOnboardingAiChatEnabled = useAtomStateValue(
isOnboardingAiChatEnabledState,
);
const isOnboardingInProgress =
@@ -4,6 +4,7 @@ import { GraphQLError } from 'graphql';
import { Provider as JotaiProvider } from 'jotai';
import { type WorkspaceCompanyEnrichment } from 'twenty-shared/workspace';
import { isOnboardingAiChatEnabledState } from '@/client-config/states/isOnboardingAiChatEnabledState';
import { CompanyEnrichmentOnboardingEffect } from '@/onboarding/effect-components/CompanyEnrichmentOnboardingEffect';
import { companyEnrichmentState } from '@/onboarding/states/companyEnrichmentState';
import { hasAttemptedCompanyEnrichmentFetchState } from '@/onboarding/states/hasAttemptedCompanyEnrichmentFetchState';
@@ -17,16 +18,11 @@ import {
} from '~/generated-metadata/graphql';
const mockOnboardingStatus = jest.fn();
const mockIsOnboardingAiChatEnabled = jest.fn();
jest.mock('@/onboarding/hooks/useOnboardingStatus', () => ({
useOnboardingStatus: () => mockOnboardingStatus(),
}));
jest.mock('@/workspace/hooks/useIsFeatureEnabled', () => ({
useIsFeatureEnabled: () => mockIsOnboardingAiChatEnabled(),
}));
const enrichment: WorkspaceCompanyEnrichment = {
domain: 'acme.com',
enrichedAt: '2026-07-21T10:00:00.000Z',
@@ -89,7 +85,7 @@ describe('CompanyEnrichmentOnboardingEffect', () => {
resetJotaiStore();
localStorage.clear();
mockOnboardingStatus.mockReturnValue(OnboardingStatus.PROFILE_CREATION);
mockIsOnboardingAiChatEnabled.mockReturnValue(true);
jotaiStore.set(isOnboardingAiChatEnabledState.atom, true);
});
afterEach(() => {
@@ -156,7 +152,7 @@ describe('CompanyEnrichmentOnboardingEffect', () => {
);
it('does not fetch when onboarding AI chat is disabled', async () => {
mockIsOnboardingAiChatEnabled.mockReturnValue(false);
jotaiStore.set(isOnboardingAiChatEnabledState.atom, false);
let callCount = 0;
renderEffect([
@@ -6,6 +6,7 @@ import { currentUserState } from '@/auth/states/currentUserState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { billingState } from '@/client-config/states/billingState';
import { isOnboardingAiChatEnabledState } from '@/client-config/states/isOnboardingAiChatEnabledState';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { isWelcomeAnimationVisibleState } from '@/onboarding/states/isWelcomeAnimationVisibleState';
import { shouldOpenAiChatAfterOnboardingState } from '@/onboarding/states/shouldOpenAiChatAfterOnboardingState';
@@ -17,7 +18,7 @@ import {
resetJotaiStore,
} from '@/ui/utilities/state/jotai/jotaiStore';
import { FeatureFlagKey, OnboardingStatus } from '~/generated-metadata/graphql';
import { OnboardingStatus } from '~/generated-metadata/graphql';
import {
mockCurrentWorkspace,
mockedUserData,
@@ -42,6 +43,11 @@ const renderHooks = (
isOnboardingAiChatEnabled = false,
}: RenderHooksOptions = {},
) => {
jotaiStore.set(
isOnboardingAiChatEnabledState.atom,
isOnboardingAiChatEnabled,
);
const { result } = renderHook(
() => {
const [currentUser, setCurrentUser] = useAtomState(currentUserState);
@@ -81,12 +87,6 @@ const renderHooks = (
? mockCurrentWorkspace.billingSubscriptions
: [],
workspaceMembersCount: withOneWorkspaceMember ? 1 : 2,
featureFlags: [
{
key: FeatureFlagKey.IS_ONBOARDING_AI_CHAT_ENABLED,
value: isOnboardingAiChatEnabled,
},
],
});
result.current.setBilling({
__typename: 'Billing',
@@ -257,7 +257,7 @@ describe('useSetNextOnboardingStatus', () => {
expect(shouldOpenAiChatAfterOnboarding).toBe(false);
});
it('should open the ai chat after onboarding when the feature flag is enabled', () => {
it('should open the ai chat after onboarding when it is enabled', () => {
const { isWelcomeAnimationVisible, shouldOpenAiChatAfterOnboarding } =
renderHooks(OnboardingStatus.INVITE_TEAM, {
isOnboardingAiChatEnabled: true,
@@ -266,7 +266,7 @@ describe('useSetNextOnboardingStatus', () => {
expect(shouldOpenAiChatAfterOnboarding).toBe(true);
});
it('should still show the welcome animation when the ai chat feature flag is disabled', () => {
it('should still show the welcome animation when the ai chat is disabled', () => {
const { isWelcomeAnimationVisible, shouldOpenAiChatAfterOnboarding } =
renderHooks(OnboardingStatus.INVITE_TEAM, {
isOnboardingAiChatEnabled: false,
@@ -9,15 +9,15 @@ import {
currentWorkspaceState,
} from '@/auth/states/currentWorkspaceState';
import { billingState } from '@/client-config/states/billingState';
import { isOnboardingAiChatEnabledState } from '@/client-config/states/isOnboardingAiChatEnabledState';
import { isWelcomeAnimationVisibleState } from '@/onboarding/states/isWelcomeAnimationVisibleState';
import { shouldOpenAiChatAfterOnboardingState } from '@/onboarding/states/shouldOpenAiChatAfterOnboardingState';
import { getHasJustCompletedOnboarding } from '@/onboarding/utils/getHasJustCompletedOnboarding';
import { getIsPlanRequired } from '@/onboarding/utils/getIsPlanRequired';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useCallback } from 'react';
import { FeatureFlagKey, OnboardingStatus } from '~/generated-metadata/graphql';
import { OnboardingStatus } from '~/generated-metadata/graphql';
import { useStore } from 'jotai';
type GetNextOnboardingStatusArgs = {
@@ -73,8 +73,8 @@ export const useSetNextOnboardingStatus = () => {
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const billing = useAtomStateValue(billingState);
const isBillingEnabled = billing?.isBillingEnabled ?? false;
const isOnboardingAiChatEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_ONBOARDING_AI_CHAT_ENABLED,
const isOnboardingAiChatEnabled = useAtomStateValue(
isOnboardingAiChatEnabledState,
);
return useCallback(() => {
@@ -1,11 +1,10 @@
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { checkIfFeatureFlagIsEnabledOnWorkspace } from '@/workspace/utils/checkIfFeatureFlagIsEnabledOnWorkspace';
import { isOnboardingAiChatEnabledState } from '@/client-config/states/isOnboardingAiChatEnabledState';
import { isOnboardingCheckoutPendingState } from '@/onboarding/states/isOnboardingCheckoutPendingState';
import { isWelcomeAnimationVisibleState } from '@/onboarding/states/isWelcomeAnimationVisibleState';
import { shouldOpenAiChatAfterOnboardingState } from '@/onboarding/states/shouldOpenAiChatAfterOnboardingState';
import { useStore } from 'jotai';
import { FeatureFlagKey, OnboardingStatus } from '~/generated-metadata/graphql';
import { OnboardingStatus } from '~/generated-metadata/graphql';
export const useShowWelcomeAnimationAfterOnboardingCheckout = () => {
const store = useStore();
@@ -21,9 +20,8 @@ export const useShowWelcomeAnimationAfterOnboardingCheckout = () => {
return;
}
const isOnboardingAiChatEnabled = checkIfFeatureFlagIsEnabledOnWorkspace(
FeatureFlagKey.IS_ONBOARDING_AI_CHAT_ENABLED,
store.get(currentWorkspaceState.atom),
const isOnboardingAiChatEnabled = store.get(
isOnboardingAiChatEnabledState.atom,
);
store.set(isOnboardingCheckoutPendingState.atom, false);
@@ -6,11 +6,10 @@ import { IconButton } from 'twenty-ui/input';
import { useIsMobile } from 'twenty-ui/utilities';
import { aiChatExpandedReturnLocationState } from '@/ai/states/aiChatExpandedReturnLocationState';
import { isOnboardingAiChatEnabledState } from '@/client-config/states/isOnboardingAiChatEnabledState';
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const SidePanelExpandAiChatButton = () => {
const { t } = useLingui();
@@ -18,8 +17,8 @@ export const SidePanelExpandAiChatButton = () => {
const location = useLocation();
const isMobile = useIsMobile();
const sidePanelPage = useAtomStateValue(sidePanelPageState);
const isOnboardingAiChatEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_ONBOARDING_AI_CHAT_ENABLED,
const isOnboardingAiChatEnabled = useAtomStateValue(
isOnboardingAiChatEnabledState,
);
const setAiChatExpandedReturnLocation = useSetAtomState(
aiChatExpandedReturnLocationState,