5095 move onboardingstatus computation from frontend to backend (#5954)

- move front `onboardingStatus` computing to server side
- add logic to `useSetNextOnboardingStatus`
- update some missing redirections in
`usePageChangeEffectNavigateLocation`
- separate subscriptionStatus from onboardingStatus
This commit is contained in:
martmull
2024-06-28 17:32:02 +02:00
committed by GitHub
parent 1a66db5bff
commit b8f33f6f59
78 changed files with 1767 additions and 1763 deletions

View File

@ -0,0 +1,61 @@
import { act } from 'react-dom/test-utils';
import { renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { CurrentUser, currentUserState } from '@/auth/states/currentUserState';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { OnboardingStatus } from '~/generated/graphql';
const tokenPair = {
accessToken: { token: 'accessToken', expiresAt: 'expiresAt' },
refreshToken: { token: 'refreshToken', expiresAt: 'expiresAt' },
};
const currentUser = {
id: '1',
onboardingStatus: null,
} as CurrentUser;
const renderHooks = () => {
const { result } = renderHook(
() => {
const onboardingStatus = useOnboardingStatus();
const setCurrentUser = useSetRecoilState(currentUserState);
const setTokenPair = useSetRecoilState(tokenPairState);
return {
onboardingStatus,
setCurrentUser,
setTokenPair,
};
},
{
wrapper: RecoilRoot,
},
);
return { result };
};
describe('useOnboardingStatus', () => {
it(`should return "undefined" when user is not logged in`, async () => {
const { result } = renderHooks();
expect(result.current.onboardingStatus).toBe(undefined);
});
Object.values(OnboardingStatus).forEach((onboardingStatus) => {
it(`should return "${onboardingStatus}"`, async () => {
const { result } = renderHooks();
const { setTokenPair, setCurrentUser } = result.current;
act(() => {
setTokenPair(tokenPair);
setCurrentUser({
...currentUser,
onboardingStatus,
});
});
expect(result.current.onboardingStatus).toBe(onboardingStatus);
});
});
});

View File

@ -0,0 +1,87 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useRecoilState, useSetRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql';
import {
mockDefaultWorkspace,
mockedUserData,
} from '~/testing/mock-data/users';
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
useFindManyRecords: jest.fn(),
}));
const setupMockWorkspaceMembers = (withManyWorkspaceMembers = false) => {
jest
.requireMock('@/object-record/hooks/useFindManyRecords')
.useFindManyRecords.mockReturnValue({
records: withManyWorkspaceMembers ? [{}, {}] : [{}],
});
};
const renderHooks = (
onboardingStatus: OnboardingStatus,
withCurrentBillingSubscription: boolean,
) => {
const { result } = renderHook(
() => {
const [currentUser, setCurrentUser] = useRecoilState(currentUserState);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setNextOnboardingStatus = useSetNextOnboardingStatus();
return {
currentUser,
setCurrentUser,
setCurrentWorkspace,
setNextOnboardingStatus,
};
},
{
wrapper: RecoilRoot,
},
);
act(() => {
result.current.setCurrentUser({ ...mockedUserData, onboardingStatus });
result.current.setCurrentWorkspace({
...mockDefaultWorkspace,
currentBillingSubscription: withCurrentBillingSubscription
? { id: v4(), status: SubscriptionStatus.Active }
: undefined,
});
});
act(() => {
result.current.setNextOnboardingStatus();
});
return result.current.currentUser?.onboardingStatus;
};
describe('useSetNextOnboardingStatus', () => {
it('should set next onboarding status for ProfileCreation', () => {
setupMockWorkspaceMembers();
const nextOnboardingStatus = renderHooks(
OnboardingStatus.ProfileCreation,
false,
);
expect(nextOnboardingStatus).toEqual(OnboardingStatus.SyncEmail);
});
it('should set next onboarding status for SyncEmail', () => {
setupMockWorkspaceMembers();
const nextOnboardingStatus = renderHooks(OnboardingStatus.SyncEmail, false);
expect(nextOnboardingStatus).toEqual(OnboardingStatus.InviteTeam);
});
it('should skip invite when workspaceMembers exist', () => {
setupMockWorkspaceMembers(true);
const nextOnboardingStatus = renderHooks(OnboardingStatus.SyncEmail, true);
expect(nextOnboardingStatus).toEqual(OnboardingStatus.Completed);
});
it('should set next onboarding status for Completed', () => {
setupMockWorkspaceMembers();
const nextOnboardingStatus = renderHooks(OnboardingStatus.InviteTeam, true);
expect(nextOnboardingStatus).toEqual(OnboardingStatus.Completed);
});
});

View File

@ -0,0 +1,9 @@
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { OnboardingStatus } from '~/generated/graphql';
export const useOnboardingStatus = (): OnboardingStatus | null | undefined => {
const currentUser = useRecoilValue(currentUserState);
return currentUser?.onboardingStatus;
};

View File

@ -0,0 +1,51 @@
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { CurrentUser, currentUserState } from '@/auth/states/currentUserState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { OnboardingStatus } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
const getNextOnboardingStatus = (
currentUser: CurrentUser | null,
workspaceMembers: WorkspaceMember[],
) => {
if (currentUser?.onboardingStatus === OnboardingStatus.ProfileCreation) {
return OnboardingStatus.SyncEmail;
}
if (
currentUser?.onboardingStatus === OnboardingStatus.SyncEmail &&
workspaceMembers.length === 1
) {
return OnboardingStatus.InviteTeam;
}
return OnboardingStatus.Completed;
};
export const useSetNextOnboardingStatus = () => {
const { records: workspaceMembers } = useFindManyRecords<WorkspaceMember>({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
});
const currentUser = useRecoilValue(currentUserState);
return useRecoilCallback(
({ set }) =>
() => {
const nextOnboardingStatus = getNextOnboardingStatus(
currentUser,
workspaceMembers,
);
set(currentUserState, (current) => {
if (isDefined(current)) {
return {
...current,
onboardingStatus: nextOnboardingStatus,
};
}
return current;
});
},
[workspaceMembers, currentUser],
);
};

View File

@ -1,41 +0,0 @@
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { OnboardingStep } from '~/generated/graphql';
const getNextOnboardingStep = (
currentOnboardingStep: OnboardingStep,
workspaceMembers: WorkspaceMember[],
) => {
if (currentOnboardingStep === OnboardingStep.SyncEmail) {
return workspaceMembers && workspaceMembers.length > 1
? null
: OnboardingStep.InviteTeam;
}
return null;
};
export const useSetNextOnboardingStep = () => {
const setCurrentUser = useSetRecoilState(currentUserState);
const { records: workspaceMembers } = useFindManyRecords<WorkspaceMember>({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
});
return useRecoilCallback(
() => (currentOnboardingStep: OnboardingStep) => {
setCurrentUser(
(current) =>
({
...current,
onboardingStep: getNextOnboardingStep(
currentOnboardingStep,
workspaceMembers,
),
}) as any,
);
},
[setCurrentUser, workspaceMembers],
);
};