Files
twenty/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts
T
nitinandGitHub 904be95148 Fix calendar booking step in onboarding when env var is not configured (#14707)
## The Issue

When CALENDAR_BOOKING_PAGE_ID env variable is not configured, the
onboarding flow still sets the booking step as pending in the database.
This causes users to get stuck on a broken booking page after
logout/login, as the Cal.com iframe tries to load with an empty calendar
link.

 ## The Fix

Made the booking step handling idempotent across the stack:

  Backend:
- setOnboardingBookOnboardingPending now checks if calendar is actually
configured before setting the step as pending
- getOnboardingStatus auto-cleans invalid booking states when detected
(booking pending but no calendar configured)
  - Empty strings in env are now treated as undefined in client config

  Frontend:
- Added navigation protection to redirect away from booking pages when
calendar isn't configured
- Existing defensive logic in useSetNextOnboardingStatus already skips
booking when no calendar ID

  Result

  - New users won't get invalid booking states
  - Existing bad data self-heals when users interact with the system
  - Backend and frontend stay in sync about when booking should be shown

Fixes the issue Felix reported where users saw a broken booking page in
production.
  
I think we should keep the old CAL_LINK constant for now - while we
could remove the booking onboarding step entirely, it would break the
plan/pricing modal which uses it as a fallback when no calendar is
configured. Open for discussion! -- May be we dont show the `Book a
Call` button if the env is not set -- but we should keep it as it is if
we want two different behaviors :)
  
  closes https://github.com/twentyhq/core-team-issues/issues/1558
2025-09-25 10:19:09 +02:00

156 lines
4.8 KiB
TypeScript

import { verifyEmailRedirectPathState } from '@/app/states/verifyEmailRedirectPathState';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
import { useLocation, useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { AppPath, SettingsPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { OnboardingStatus } from '~/generated/graphql';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
export const usePageChangeEffectNavigateLocation = () => {
const isLoggedIn = useIsLogged();
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
const onboardingStatus = useOnboardingStatus();
const isWorkspaceSuspended = useIsWorkspaceActivationStatusEqualsTo(
WorkspaceActivationStatus.SUSPENDED,
);
const { defaultHomePagePath } = useDefaultHomePagePath();
const location = useLocation();
const calendarBookingPageId = useRecoilValue(calendarBookingPageIdState);
const someMatchingLocationOf = (appPaths: AppPath[]): boolean =>
appPaths.some((appPath) => isMatchingLocation(location, appPath));
const onGoingUserCreationPaths = [
AppPath.Invite,
AppPath.SignInUp,
AppPath.VerifyEmail,
AppPath.Verify,
];
const onboardingPaths = [
AppPath.CreateWorkspace,
AppPath.CreateProfile,
AppPath.SyncEmails,
AppPath.InviteTeam,
AppPath.PlanRequired,
AppPath.PlanRequiredSuccess,
AppPath.BookCallDecision,
AppPath.BookCall,
];
const objectNamePlural = useParams().objectNamePlural ?? '';
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const objectMetadataItem = objectMetadataItems?.find(
(objectMetadataItem) => objectMetadataItem.namePlural === objectNamePlural,
);
const verifyEmailRedirectPath = useRecoilValue(verifyEmailRedirectPathState);
if (
(!isLoggedIn || (isLoggedIn && !isOnAWorkspace)) &&
!someMatchingLocationOf([
...onGoingUserCreationPaths,
AppPath.ResetPassword,
])
) {
return AppPath.SignInUp;
}
if (
onboardingStatus === OnboardingStatus.PLAN_REQUIRED &&
!someMatchingLocationOf([
AppPath.PlanRequired,
AppPath.PlanRequiredSuccess,
AppPath.BookCall,
AppPath.BookCallDecision,
])
) {
if (
isMatchingLocation(location, AppPath.VerifyEmail) &&
isDefined(verifyEmailRedirectPath)
) {
return verifyEmailRedirectPath;
}
return AppPath.PlanRequired;
}
if (isWorkspaceSuspended) {
if (!isMatchingLocation(location, AppPath.SettingsCatchAll)) {
return `${AppPath.SettingsCatchAll.replace('/*', '')}/${
SettingsPath.Billing
}`;
}
return;
}
if (
onboardingStatus === OnboardingStatus.WORKSPACE_ACTIVATION &&
!someMatchingLocationOf([
AppPath.CreateWorkspace,
AppPath.BookCallDecision,
AppPath.BookCall,
])
) {
return AppPath.CreateWorkspace;
}
if (
onboardingStatus === OnboardingStatus.PROFILE_CREATION &&
!isMatchingLocation(location, AppPath.CreateProfile)
) {
return AppPath.CreateProfile;
}
if (
onboardingStatus === OnboardingStatus.SYNC_EMAIL &&
!isMatchingLocation(location, AppPath.SyncEmails)
) {
return AppPath.SyncEmails;
}
if (
onboardingStatus === OnboardingStatus.INVITE_TEAM &&
!isMatchingLocation(location, AppPath.InviteTeam)
) {
return AppPath.InviteTeam;
}
if (
onboardingStatus === OnboardingStatus.BOOK_ONBOARDING &&
!someMatchingLocationOf([AppPath.BookCallDecision, AppPath.BookCall])
) {
if (!isDefined(calendarBookingPageId)) {
return defaultHomePagePath;
}
return AppPath.BookCallDecision;
}
if (
onboardingStatus === OnboardingStatus.COMPLETED &&
someMatchingLocationOf([...onboardingPaths, ...onGoingUserCreationPaths]) &&
!isMatchingLocation(location, AppPath.ResetPassword) &&
isLoggedIn
) {
return defaultHomePagePath;
}
if (isMatchingLocation(location, AppPath.Index) && isLoggedIn) {
return defaultHomePagePath;
}
if (
isMatchingLocation(location, AppPath.RecordIndexPage) &&
!isDefined(objectMetadataItem)
) {
return AppPath.NotFound;
}
return;
};