[REQUIRED FOR 2.18 RELEASE] Show the upgrade-plan step at the end of V1 onboarding (#22368)
## What On V1 onboarding the upgrade-plan step (`ChooseYourPlan`) only appeared later, once the user happened to create a record, instead of right after Invite team. ## Why The frontend advances the onboarding status optimistically in `getNextOnboardingStatus()` without refetching, and it never emitted `PLAN_REQUIRED`. So after Invite team the user was locally marked `COMPLETED` and dropped into the app; the backend's real `PLAN_REQUIRED` only surfaced on a later `GetCurrentUser` refetch. ## Fix Make `getNextOnboardingStatus()` billing-aware so it mirrors the backend: return `PLAN_REQUIRED` in the terminal branches when `isBillingEnabled && billingSubscriptions.length === 0` (using `billingSubscriptions` to match the backend's any-subscription check). The navigate hook already routes `PLAN_REQUIRED` to `/plan-required`, so no routing change is needed. Self-hosted and existing-subscription flows are unchanged. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22368?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. --> --------- Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
+72
-33
@@ -1,11 +1,11 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { createElement } from 'react';
|
||||
import { Provider as JotaiProvider } from 'jotai';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
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 { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
@@ -14,10 +14,7 @@ import {
|
||||
resetJotaiStore,
|
||||
} from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
|
||||
import {
|
||||
OnboardingStatus,
|
||||
SubscriptionStatus,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { OnboardingStatus } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
mockCurrentWorkspace,
|
||||
mockedUserData,
|
||||
@@ -26,10 +23,19 @@ import {
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) =>
|
||||
createElement(JotaiProvider, { store: jotaiStore }, children);
|
||||
|
||||
type RenderHooksOptions = {
|
||||
withSubscription?: boolean;
|
||||
isBillingEnabled?: boolean;
|
||||
withOneWorkspaceMember?: boolean;
|
||||
};
|
||||
|
||||
const renderHooks = (
|
||||
onboardingStatus: OnboardingStatus,
|
||||
withCurrentBillingSubscription: boolean,
|
||||
withOneWorkspaceMember = true,
|
||||
{
|
||||
withSubscription = false,
|
||||
isBillingEnabled = false,
|
||||
withOneWorkspaceMember = true,
|
||||
}: RenderHooksOptions = {},
|
||||
) => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
@@ -38,12 +44,14 @@ const renderHooks = (
|
||||
currentUserWorkspaceState,
|
||||
);
|
||||
const setCurrentWorkspace = useSetAtomState(currentWorkspaceState);
|
||||
const setBilling = useSetAtomState(billingState);
|
||||
const setNextOnboardingStatus = useSetNextOnboardingStatus();
|
||||
return {
|
||||
currentUser,
|
||||
setCurrentUser,
|
||||
setCurrentWorkspace,
|
||||
setCurrentUserWorkspace,
|
||||
setBilling,
|
||||
setNextOnboardingStatus,
|
||||
};
|
||||
},
|
||||
@@ -56,16 +64,16 @@ const renderHooks = (
|
||||
result.current.setCurrentUserWorkspace(mockedUserData.currentUserWorkspace);
|
||||
result.current.setCurrentWorkspace({
|
||||
...mockCurrentWorkspace,
|
||||
currentBillingSubscription: withCurrentBillingSubscription
|
||||
? {
|
||||
id: v4(),
|
||||
status: SubscriptionStatus.Active,
|
||||
metadata: {},
|
||||
phases: [],
|
||||
}
|
||||
: undefined,
|
||||
billingSubscriptions: withSubscription
|
||||
? mockCurrentWorkspace.billingSubscriptions
|
||||
: [],
|
||||
workspaceMembersCount: withOneWorkspaceMember ? 1 : 2,
|
||||
});
|
||||
result.current.setBilling({
|
||||
__typename: 'Billing',
|
||||
isBillingEnabled,
|
||||
trialPeriods: [],
|
||||
});
|
||||
});
|
||||
act(() => {
|
||||
result.current.setNextOnboardingStatus();
|
||||
@@ -81,45 +89,76 @@ describe('useSetNextOnboardingStatus', () => {
|
||||
it('should sync emails right after workspace activation', () => {
|
||||
const nextOnboardingStatus = renderHooks(
|
||||
OnboardingStatus.WORKSPACE_ACTIVATION,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
expect(nextOnboardingStatus).toEqual(OnboardingStatus.SYNC_EMAIL);
|
||||
});
|
||||
|
||||
it('should create profile after syncing emails', () => {
|
||||
const nextOnboardingStatus = renderHooks(
|
||||
OnboardingStatus.SYNC_EMAIL,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
const nextOnboardingStatus = renderHooks(OnboardingStatus.SYNC_EMAIL);
|
||||
expect(nextOnboardingStatus).toEqual(OnboardingStatus.PROFILE_CREATION);
|
||||
});
|
||||
|
||||
it('should invite the team right after profile creation', () => {
|
||||
const nextOnboardingStatus = renderHooks(
|
||||
OnboardingStatus.PROFILE_CREATION,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
const nextOnboardingStatus = renderHooks(OnboardingStatus.PROFILE_CREATION);
|
||||
expect(nextOnboardingStatus).toEqual(OnboardingStatus.INVITE_TEAM);
|
||||
});
|
||||
|
||||
it('should complete after profile creation when more than 1 workspaceMember exist', () => {
|
||||
const nextOnboardingStatus = renderHooks(
|
||||
OnboardingStatus.PROFILE_CREATION,
|
||||
false,
|
||||
false,
|
||||
{
|
||||
withOneWorkspaceMember: false,
|
||||
},
|
||||
);
|
||||
expect(nextOnboardingStatus).toEqual(OnboardingStatus.COMPLETED);
|
||||
});
|
||||
|
||||
it('should set next onboarding status for Completed', () => {
|
||||
it('should require a plan after profile creation when billing is enabled and the workspace has no subscription', () => {
|
||||
const nextOnboardingStatus = renderHooks(
|
||||
OnboardingStatus.INVITE_TEAM,
|
||||
true,
|
||||
true,
|
||||
OnboardingStatus.PROFILE_CREATION,
|
||||
{
|
||||
withOneWorkspaceMember: false,
|
||||
isBillingEnabled: true,
|
||||
withSubscription: false,
|
||||
},
|
||||
);
|
||||
expect(nextOnboardingStatus).toEqual(OnboardingStatus.PLAN_REQUIRED);
|
||||
});
|
||||
|
||||
it('should complete after inviting the team when billing is disabled', () => {
|
||||
const nextOnboardingStatus = renderHooks(OnboardingStatus.INVITE_TEAM);
|
||||
expect(nextOnboardingStatus).toEqual(OnboardingStatus.COMPLETED);
|
||||
});
|
||||
|
||||
it('should complete after inviting the team when the workspace already has a subscription', () => {
|
||||
const nextOnboardingStatus = renderHooks(OnboardingStatus.INVITE_TEAM, {
|
||||
isBillingEnabled: true,
|
||||
withSubscription: true,
|
||||
});
|
||||
expect(nextOnboardingStatus).toEqual(OnboardingStatus.COMPLETED);
|
||||
});
|
||||
|
||||
it('should require a plan after inviting the team when billing is enabled and the workspace has no subscription', () => {
|
||||
const nextOnboardingStatus = renderHooks(OnboardingStatus.INVITE_TEAM, {
|
||||
isBillingEnabled: true,
|
||||
withSubscription: false,
|
||||
});
|
||||
expect(nextOnboardingStatus).toEqual(OnboardingStatus.PLAN_REQUIRED);
|
||||
});
|
||||
|
||||
it('should complete after booking onboarding when the workspace already has a subscription', () => {
|
||||
const nextOnboardingStatus = renderHooks(OnboardingStatus.BOOK_ONBOARDING, {
|
||||
isBillingEnabled: true,
|
||||
withSubscription: true,
|
||||
});
|
||||
expect(nextOnboardingStatus).toEqual(OnboardingStatus.COMPLETED);
|
||||
});
|
||||
|
||||
it('should require a plan after booking onboarding when billing is enabled and the workspace has no subscription', () => {
|
||||
const nextOnboardingStatus = renderHooks(OnboardingStatus.BOOK_ONBOARDING, {
|
||||
isBillingEnabled: true,
|
||||
withSubscription: false,
|
||||
});
|
||||
expect(nextOnboardingStatus).toEqual(OnboardingStatus.PLAN_REQUIRED);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type CurrentWorkspace,
|
||||
currentWorkspaceState,
|
||||
} from '@/auth/states/currentWorkspaceState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
@@ -19,13 +20,19 @@ type GetNextOnboardingStatusArgs = {
|
||||
currentUser: CurrentUser | null;
|
||||
currentWorkspace: CurrentWorkspace | null;
|
||||
calendarBookingPageId: string | null;
|
||||
isBillingEnabled: boolean;
|
||||
};
|
||||
|
||||
const getNextOnboardingStatus = ({
|
||||
currentUser,
|
||||
currentWorkspace,
|
||||
calendarBookingPageId,
|
||||
isBillingEnabled,
|
||||
}: GetNextOnboardingStatusArgs) => {
|
||||
const isPlanRequired =
|
||||
isBillingEnabled &&
|
||||
(currentWorkspace?.billingSubscriptions?.length ?? 0) === 0;
|
||||
|
||||
if (currentUser?.onboardingStatus === OnboardingStatus.WORKSPACE_ACTIVATION) {
|
||||
return OnboardingStatus.SYNC_EMAIL;
|
||||
}
|
||||
@@ -35,17 +42,25 @@ const getNextOnboardingStatus = ({
|
||||
}
|
||||
|
||||
if (currentUser?.onboardingStatus === OnboardingStatus.PROFILE_CREATION) {
|
||||
return currentWorkspace?.workspaceMembersCount === 1
|
||||
? OnboardingStatus.INVITE_TEAM
|
||||
if (currentWorkspace?.workspaceMembersCount === 1) {
|
||||
return OnboardingStatus.INVITE_TEAM;
|
||||
}
|
||||
return isPlanRequired
|
||||
? OnboardingStatus.PLAN_REQUIRED
|
||||
: OnboardingStatus.COMPLETED;
|
||||
}
|
||||
if (currentUser?.onboardingStatus === OnboardingStatus.INVITE_TEAM) {
|
||||
if (isPlanRequired) {
|
||||
return OnboardingStatus.PLAN_REQUIRED;
|
||||
}
|
||||
return isDefined(calendarBookingPageId)
|
||||
? OnboardingStatus.BOOK_ONBOARDING
|
||||
: OnboardingStatus.COMPLETED;
|
||||
}
|
||||
if (currentUser?.onboardingStatus === OnboardingStatus.BOOK_ONBOARDING) {
|
||||
return OnboardingStatus.COMPLETED;
|
||||
return isPlanRequired
|
||||
? OnboardingStatus.PLAN_REQUIRED
|
||||
: OnboardingStatus.COMPLETED;
|
||||
}
|
||||
return OnboardingStatus.COMPLETED;
|
||||
};
|
||||
@@ -55,12 +70,15 @@ export const useSetNextOnboardingStatus = () => {
|
||||
const currentUser = useAtomStateValue(currentUserState);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const calendarBookingPageId = useAtomStateValue(calendarBookingPageIdState);
|
||||
const billing = useAtomStateValue(billingState);
|
||||
const isBillingEnabled = billing?.isBillingEnabled ?? false;
|
||||
|
||||
return useCallback(() => {
|
||||
const nextOnboardingStatus = getNextOnboardingStatus({
|
||||
currentUser,
|
||||
currentWorkspace,
|
||||
calendarBookingPageId,
|
||||
isBillingEnabled,
|
||||
});
|
||||
store.set(currentUserState.atom, (current) => {
|
||||
if (isDefined(current)) {
|
||||
@@ -71,5 +89,11 @@ export const useSetNextOnboardingStatus = () => {
|
||||
}
|
||||
return current;
|
||||
});
|
||||
}, [currentUser, currentWorkspace, calendarBookingPageId, store]);
|
||||
}, [
|
||||
currentUser,
|
||||
currentWorkspace,
|
||||
calendarBookingPageId,
|
||||
isBillingEnabled,
|
||||
store,
|
||||
]);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user