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

@ -1,373 +0,0 @@
import { act } from 'react-dom/test-utils';
import { renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { CurrentUser, currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import {
CurrentWorkspace,
currentWorkspaceState,
} from '@/auth/states/currentWorkspaceState';
import { isVerifyPendingState } from '@/auth/states/isVerifyPendingState';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { billingState } from '@/client-config/states/billingState';
import { OnboardingStep } from '~/generated/graphql';
const tokenPair = {
accessToken: { token: 'accessToken', expiresAt: 'expiresAt' },
refreshToken: { token: 'refreshToken', expiresAt: 'expiresAt' },
};
const billing = {
billingUrl: 'testing.com',
isBillingEnabled: true,
};
const currentUser = {
id: '1',
email: 'test@test',
supportUserHash: '1',
canImpersonate: false,
onboardingStep: null,
} as CurrentUser;
const currentWorkspace = {
activationStatus: 'active',
id: '1',
allowImpersonation: true,
currentBillingSubscription: {
status: 'trialing',
},
} as CurrentWorkspace;
const currentWorkspaceMember = {
id: '1',
locale: '',
name: {
firstName: '',
lastName: '',
},
};
const renderHooks = () => {
const { result } = renderHook(
() => {
const onboardingStatus = useOnboardingStatus();
const setBilling = useSetRecoilState(billingState);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
const setCurrentUser = useSetRecoilState(currentUserState);
const setTokenPair = useSetRecoilState(tokenPairState);
const setVerifyPending = useSetRecoilState(isVerifyPendingState);
return {
onboardingStatus,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
setTokenPair,
setVerifyPending,
};
},
{
wrapper: RecoilRoot,
},
);
return { result };
};
describe('useOnboardingStatus', () => {
it('should return "ongoing_user_creation" when user is not logged in', async () => {
const { result } = renderHooks();
expect(result.current.onboardingStatus).toBe('ongoing_user_creation');
});
it('should return "incomplete"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser(currentUser);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'incomplete',
});
setCurrentWorkspaceMember(currentWorkspaceMember);
});
expect(result.current.onboardingStatus).toBe('incomplete');
});
it('should return "canceled"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser(currentUser);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'canceled',
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
name: {
firstName: 'John',
lastName: 'Doe',
},
});
});
expect(result.current.onboardingStatus).toBe('canceled');
});
it('should return "ongoing_workspace_activation"', async () => {
const { result } = renderHooks();
const { setTokenPair, setBilling, setCurrentUser, setCurrentWorkspace } =
result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser(currentUser);
setCurrentWorkspace({
...currentWorkspace,
activationStatus: 'inactive',
subscriptionStatus: 'active',
});
});
expect(result.current.onboardingStatus).toBe(
'ongoing_workspace_activation',
);
});
it('should return "ongoing_profile_creation"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser(currentUser);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'active',
});
setCurrentWorkspaceMember(currentWorkspaceMember);
});
expect(result.current.onboardingStatus).toBe('ongoing_profile_creation');
});
it('should return "ongoing_sync_email"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser({
...currentUser,
onboardingStep: OnboardingStep.SyncEmail,
});
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'active',
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
name: {
firstName: 'John',
lastName: 'Doe',
},
});
});
expect(result.current.onboardingStatus).toBe('ongoing_sync_email');
});
it('should return "ongoing_invite_team"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser({
...currentUser,
onboardingStep: OnboardingStep.InviteTeam,
});
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'active',
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
name: {
firstName: 'John',
lastName: 'Doe',
},
});
});
expect(result.current.onboardingStatus).toBe('ongoing_invite_team');
});
it('should return "completed"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser(currentUser);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'active',
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
name: {
firstName: 'John',
lastName: 'Doe',
},
});
});
expect(result.current.onboardingStatus).toBe('completed');
});
it('should return "past_due"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser(currentUser);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'past_due',
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
name: {
firstName: 'John',
lastName: 'Doe',
},
});
});
expect(result.current.onboardingStatus).toBe('past_due');
});
it('should return "unpaid"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser(currentUser);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'unpaid',
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
name: {
firstName: 'John',
lastName: 'Doe',
},
});
});
expect(result.current.onboardingStatus).toBe('unpaid');
});
it('should return "completed_without_subscription"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser(currentUser);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'trialing',
currentBillingSubscription: null,
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
name: {
firstName: 'John',
lastName: 'Doe',
},
});
});
expect(result.current.onboardingStatus).toBe(
'completed_without_subscription',
);
});
});

View File

@ -1,28 +0,0 @@
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { billingState } from '@/client-config/states/billingState';
import { useIsLogged } from '../hooks/useIsLogged';
import {
getOnboardingStatus,
OnboardingStatus,
} from '../utils/getOnboardingStatus';
export const useOnboardingStatus = (): OnboardingStatus | undefined => {
const billing = useRecoilValue(billingState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const currentUser = useRecoilValue(currentUserState);
const isLoggedIn = useIsLogged();
return getOnboardingStatus({
isLoggedIn,
currentWorkspaceMember,
currentWorkspace,
currentUser,
isBillingEnabled: billing?.isBillingEnabled || false,
});
};

View File

@ -4,7 +4,7 @@ import { User } from '~/generated/graphql';
export type CurrentUser = Pick<
User,
'id' | 'email' | 'supportUserHash' | 'canImpersonate' | 'onboardingStep'
'id' | 'email' | 'supportUserHash' | 'canImpersonate' | 'onboardingStatus'
>;
export const currentUserState = createState<CurrentUser | null>({

View File

@ -10,7 +10,6 @@ export type CurrentWorkspace = Pick<
| 'displayName'
| 'allowImpersonation'
| 'featureFlags'
| 'subscriptionStatus'
| 'activationStatus'
| 'currentBillingSubscription'
| 'currentCacheVersion'

View File

@ -1,174 +0,0 @@
import { CurrentUser } from '@/auth/states/currentUserState';
import { CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { OnboardingStep } from '~/generated/graphql';
import { getOnboardingStatus } from '../getOnboardingStatus';
describe('getOnboardingStatus', () => {
it('should return the correct status', () => {
const ongoingUserCreation = getOnboardingStatus({
isLoggedIn: false,
currentWorkspaceMember: null,
currentWorkspace: null,
currentUser: null,
isBillingEnabled: false,
});
const ongoingWorkspaceActivation = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: null,
currentWorkspace: {
id: '1',
activationStatus: 'inactive',
} as CurrentWorkspace,
currentUser: {
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: false,
});
const ongoingProfileCreation = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
id: '1',
name: {},
} as WorkspaceMember,
currentWorkspace: {
id: '1',
activationStatus: 'active',
} as CurrentWorkspace,
currentUser: {
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: false,
});
const ongoingSyncEmail = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
id: '1',
name: {
firstName: 'John',
lastName: 'Doe',
},
} as WorkspaceMember,
currentWorkspace: {
id: '1',
activationStatus: 'active',
} as CurrentWorkspace,
currentUser: {
onboardingStep: OnboardingStep.SyncEmail,
} as CurrentUser,
isBillingEnabled: false,
});
const ongoingInviteTeam = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
id: '1',
name: {
firstName: 'John',
lastName: 'Doe',
},
} as WorkspaceMember,
currentWorkspace: {
id: '1',
activationStatus: 'active',
} as CurrentWorkspace,
currentUser: {
onboardingStep: OnboardingStep.InviteTeam,
} as CurrentUser,
isBillingEnabled: false,
});
const completed = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
id: '1',
name: {
firstName: 'John',
lastName: 'Doe',
},
} as WorkspaceMember,
currentWorkspace: {
id: '1',
activationStatus: 'active',
} as CurrentWorkspace,
currentUser: {
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: false,
});
const incomplete = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
id: '1',
name: {
firstName: 'John',
lastName: 'Doe',
},
} as WorkspaceMember,
currentWorkspace: {
id: '1',
activationStatus: 'active',
subscriptionStatus: 'incomplete',
} as CurrentWorkspace,
currentUser: {
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: true,
});
const incompleteButBillingDisabled = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
id: '1',
name: {
firstName: 'John',
lastName: 'Doe',
},
} as WorkspaceMember,
currentWorkspace: {
id: '1',
activationStatus: 'active',
subscriptionStatus: 'incomplete',
} as CurrentWorkspace,
currentUser: {
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: false,
});
const canceled = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
id: '1',
name: {
firstName: 'John',
lastName: 'Doe',
},
} as WorkspaceMember,
currentWorkspace: {
id: '1',
activationStatus: 'active',
subscriptionStatus: 'canceled',
} as CurrentWorkspace,
currentUser: {
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: true,
});
expect(ongoingUserCreation).toBe('ongoing_user_creation');
expect(ongoingWorkspaceActivation).toBe('ongoing_workspace_activation');
expect(ongoingProfileCreation).toBe('ongoing_profile_creation');
expect(ongoingSyncEmail).toBe('ongoing_sync_email');
expect(ongoingInviteTeam).toBe('ongoing_invite_team');
expect(completed).toBe('completed');
expect(incomplete).toBe('incomplete');
expect(canceled).toBe('canceled');
expect(incompleteButBillingDisabled).toBe('completed');
});
});

View File

@ -1,89 +0,0 @@
import { CurrentUser } from '@/auth/states/currentUserState';
import { CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { OnboardingStep } from '~/generated/graphql';
export enum OnboardingStatus {
Incomplete = 'incomplete',
Canceled = 'canceled',
Unpaid = 'unpaid',
PastDue = 'past_due',
OngoingUserCreation = 'ongoing_user_creation',
OngoingWorkspaceActivation = 'ongoing_workspace_activation',
OngoingProfileCreation = 'ongoing_profile_creation',
OngoingSyncEmail = 'ongoing_sync_email',
OngoingInviteTeam = 'ongoing_invite_team',
Completed = 'completed',
CompletedWithoutSubscription = 'completed_without_subscription',
}
export const getOnboardingStatus = ({
isLoggedIn,
currentWorkspaceMember,
currentWorkspace,
currentUser,
isBillingEnabled,
}: {
isLoggedIn: boolean;
currentWorkspaceMember: Omit<
WorkspaceMember,
'createdAt' | 'updatedAt' | 'userId' | 'userEmail' | '__typename'
> | null;
currentWorkspace: CurrentWorkspace | null;
currentUser: CurrentUser | null;
isBillingEnabled: boolean;
}) => {
if (!isLoggedIn) {
return OnboardingStatus.OngoingUserCreation;
}
// After SignInUp, the user should have a current workspace assigned.
// If not, it indicates that the data is still being requested.
if (!currentWorkspace || !currentUser) {
return undefined;
}
if (
isBillingEnabled &&
currentWorkspace.subscriptionStatus === 'incomplete'
) {
return OnboardingStatus.Incomplete;
}
if (currentWorkspace.activationStatus !== 'active') {
return OnboardingStatus.OngoingWorkspaceActivation;
}
if (
!currentWorkspaceMember?.name.firstName ||
!currentWorkspaceMember?.name.lastName
) {
return OnboardingStatus.OngoingProfileCreation;
}
if (currentUser.onboardingStep === OnboardingStep.SyncEmail) {
return OnboardingStatus.OngoingSyncEmail;
}
if (currentUser.onboardingStep === OnboardingStep.InviteTeam) {
return OnboardingStatus.OngoingInviteTeam;
}
if (isBillingEnabled && currentWorkspace.subscriptionStatus === 'canceled') {
return OnboardingStatus.Canceled;
}
if (isBillingEnabled && currentWorkspace.subscriptionStatus === 'past_due') {
return OnboardingStatus.PastDue;
}
if (isBillingEnabled && currentWorkspace.subscriptionStatus === 'unpaid') {
return OnboardingStatus.Unpaid;
}
if (isBillingEnabled && !currentWorkspace.currentBillingSubscription) {
return OnboardingStatus.CompletedWithoutSubscription;
}
return OnboardingStatus.Completed;
};