Skip to content

Commit 4775ecf

Browse files
authored
Merge pull request Expensify#64998 from thelullabyy/fix/63881
Onboarding - Signing-up with a + operator email address doesn't imitate the accurate onboarding flow
2 parents 6201585 + 4e9f503 commit 4775ecf

5 files changed

Lines changed: 311 additions & 3 deletions

File tree

src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat
5555
const isPrivateDomainAndHasAccessiblePolicies = !account?.isFromPublicDomain && !!account?.hasAccessibleDomainPolicies;
5656
const isValidated = isCurrentUserValidated(loginList);
5757

58+
const isVsb = onboardingValues?.signupQualifier === CONST.ONBOARDING_SIGNUP_QUALIFIERS.VSB;
59+
const isSmb = onboardingValues?.signupQualifier === CONST.ONBOARDING_SIGNUP_QUALIFIERS.SMB;
60+
5861
useEffect(() => {
5962
setOnboardingErrorMessage('');
6063
}, []);
@@ -90,7 +93,7 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat
9093
clearPersonalDetailsDraft();
9194
setPersonalDetails(firstName, lastName);
9295

93-
if (isPrivateDomainAndHasAccessiblePolicies && !onboardingPurposeSelected) {
96+
if (isPrivateDomainAndHasAccessiblePolicies && (!onboardingPurposeSelected || isVsb || isSmb)) {
9497
const nextRoute = isValidated ? ROUTES.ONBOARDING_WORKSPACES : ROUTES.ONBOARDING_PRIVATE_DOMAIN;
9598
Navigation.navigate(nextRoute.getRoute(route.params?.backTo));
9699
return;
@@ -104,7 +107,7 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat
104107

105108
completeOnboarding(firstName, lastName);
106109
},
107-
[isPrivateDomainAndHasAccessiblePolicies, onboardingPurposeSelected, isValidated, route.params?.backTo, completeOnboarding],
110+
[isPrivateDomainAndHasAccessiblePolicies, onboardingPurposeSelected, isValidated, route.params?.backTo, completeOnboarding, isVsb, isSmb],
108111
);
109112

110113
const validate = (values: FormOnyxValues<'onboardingPersonalDetailsForm'>) => {

src/pages/OnboardingPrivateDomain/BaseOnboardingPrivateDomain.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Navigation from '@libs/Navigation/Navigation';
1313
import {isCurrentUserValidated} from '@libs/UserUtils';
1414
import {clearGetAccessiblePoliciesErrors, getAccessiblePolicies} from '@userActions/Policy/Policy';
1515
import {resendValidateCode} from '@userActions/User';
16+
import CONST from '@src/CONST';
1617
import ONYXKEYS from '@src/ONYXKEYS';
1718
import ROUTES from '@src/ROUTES';
1819
import type {BaseOnboardingPrivateDomainProps} from './types';
@@ -36,6 +37,10 @@ function BaseOnboardingPrivateDomain({shouldUseNativeStyles, route}: BaseOnboard
3637

3738
const isValidated = isCurrentUserValidated(loginList);
3839

40+
const [onboardingValues] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {canBeMissing: true});
41+
const isVsb = onboardingValues?.signupQualifier === CONST.ONBOARDING_SIGNUP_QUALIFIERS.VSB;
42+
const isSmb = onboardingValues?.signupQualifier === CONST.ONBOARDING_SIGNUP_QUALIFIERS.SMB;
43+
3944
const sendValidateCode = useCallback(() => {
4045
if (!email) {
4146
return;
@@ -92,7 +97,18 @@ function BaseOnboardingPrivateDomain({shouldUseNativeStyles, route}: BaseOnboard
9297
validateError={getAccessiblePoliciesAction?.errors}
9398
hasMagicCodeBeenSent={hasMagicCodeBeenSent}
9499
shouldShowSkipButton
95-
handleSkipButtonPress={() => Navigation.navigate(ROUTES.ONBOARDING_PURPOSE.getRoute(route.params?.backTo))}
100+
handleSkipButtonPress={() => {
101+
if (isVsb) {
102+
Navigation.navigate(ROUTES.ONBOARDING_ACCOUNTING.getRoute(route.params?.backTo));
103+
return;
104+
}
105+
106+
if (isSmb) {
107+
Navigation.navigate(ROUTES.ONBOARDING_EMPLOYEES.getRoute(route.params?.backTo));
108+
return;
109+
}
110+
Navigation.navigate(ROUTES.ONBOARDING_PURPOSE.getRoute(route.params?.backTo));
111+
}}
96112
buttonStyles={[styles.flex2, styles.justifyContentEnd]}
97113
isLoading={getAccessiblePoliciesAction?.loading}
98114
/>

src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ function BaseOnboardingWorkspaces({route, shouldUseNativeStyles}: BaseOnboarding
5252

5353
const {isBetaEnabled} = usePermissions();
5454

55+
const [onboardingValues] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {canBeMissing: true});
56+
const isVsb = onboardingValues?.signupQualifier === CONST.ONBOARDING_SIGNUP_QUALIFIERS.VSB;
57+
const isSmb = onboardingValues?.signupQualifier === CONST.ONBOARDING_SIGNUP_QUALIFIERS.SMB;
58+
5559
const handleJoinWorkspace = useCallback(
5660
(policy: JoinablePolicy) => {
5761
if (policy.automaticJoiningEnabled) {
@@ -152,7 +156,17 @@ function BaseOnboardingWorkspaces({route, shouldUseNativeStyles}: BaseOnboarding
152156
success={false}
153157
large
154158
text={translate('common.skip')}
159+
testID="onboardingWorkSpaceSkipButton"
155160
onPress={() => {
161+
if (isVsb) {
162+
Navigation.navigate(ROUTES.ONBOARDING_ACCOUNTING.getRoute(route.params?.backTo));
163+
return;
164+
}
165+
166+
if (isSmb) {
167+
Navigation.navigate(ROUTES.ONBOARDING_EMPLOYEES.getRoute(route.params?.backTo));
168+
return;
169+
}
156170
Navigation.navigate(ROUTES.ONBOARDING_PURPOSE.getRoute(route.params?.backTo));
157171
}}
158172
style={[styles.mt5]}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import {PortalProvider} from '@gorhom/portal';
2+
import {NavigationContainer} from '@react-navigation/native';
3+
import {act, fireEvent, render, screen, waitFor} from '@testing-library/react-native';
4+
import React from 'react';
5+
import Onyx from 'react-native-onyx';
6+
import ComposeProviders from '@components/ComposeProviders';
7+
import {LocaleContextProvider} from '@components/LocaleContextProvider';
8+
import OnyxProvider from '@components/OnyxProvider';
9+
import {CurrentReportIDContextProvider} from '@hooks/useCurrentReportID';
10+
import * as useResponsiveLayoutModule from '@hooks/useResponsiveLayout';
11+
import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types';
12+
import {translateLocal} from '@libs/Localize';
13+
import Navigation from '@libs/Navigation/Navigation';
14+
import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator';
15+
import type {OnboardingModalNavigatorParamList} from '@navigation/types';
16+
import OnboardingPersonalDetails from '@pages/OnboardingPersonalDetails';
17+
import CONST from '@src/CONST';
18+
import ONYXKEYS from '@src/ONYXKEYS';
19+
import ROUTES from '@src/ROUTES';
20+
import SCREENS from '@src/SCREENS';
21+
import * as TestHelper from '../utils/TestHelper';
22+
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
23+
24+
TestHelper.setupGlobalFetchMock();
25+
26+
const Stack = createPlatformStackNavigator<OnboardingModalNavigatorParamList>();
27+
28+
const navigate = jest.spyOn(Navigation, 'navigate');
29+
30+
const fakeEmail = 'fake@gmail.com';
31+
const mockLoginList = {
32+
[fakeEmail]: {
33+
partnerName: 'expensify.com',
34+
partnerUserID: fakeEmail,
35+
validatedDate: 'fake-validatedDate',
36+
},
37+
};
38+
39+
const renderOnboardingPersonalDetailsPage = (
40+
initialRouteName: typeof SCREENS.ONBOARDING.PERSONAL_DETAILS,
41+
initialParams: OnboardingModalNavigatorParamList[typeof SCREENS.ONBOARDING.PERSONAL_DETAILS],
42+
) => {
43+
return render(
44+
<ComposeProviders components={[OnyxProvider, LocaleContextProvider, CurrentReportIDContextProvider]}>
45+
<PortalProvider>
46+
<NavigationContainer>
47+
<Stack.Navigator initialRouteName={initialRouteName}>
48+
<Stack.Screen
49+
name={SCREENS.ONBOARDING.PERSONAL_DETAILS}
50+
component={OnboardingPersonalDetails}
51+
initialParams={initialParams}
52+
/>
53+
</Stack.Navigator>
54+
</NavigationContainer>
55+
</PortalProvider>
56+
</ComposeProviders>,
57+
);
58+
};
59+
60+
describe('OnboardingPersonalDetails Page', () => {
61+
beforeAll(() => {
62+
Onyx.init({
63+
keys: ONYXKEYS,
64+
});
65+
});
66+
67+
beforeEach(() => {
68+
jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({
69+
isSmallScreenWidth: false,
70+
shouldUseNarrowLayout: false,
71+
} as ResponsiveLayoutResult);
72+
});
73+
74+
afterEach(async () => {
75+
await act(async () => {
76+
await Onyx.clear();
77+
});
78+
jest.clearAllMocks();
79+
});
80+
81+
it('should navigate to Onboarding Private Domain page when submitting form and user is routed app via VSB with unvalidated account and private domain', async () => {
82+
await TestHelper.signInWithTestUser();
83+
84+
// Setup account as private domain and has accessible policies
85+
await act(async () => {
86+
await Onyx.merge(ONYXKEYS.ACCOUNT, {
87+
isFromPublicDomain: false,
88+
hasAccessibleDomainPolicies: true,
89+
});
90+
await Onyx.merge(ONYXKEYS.LOGIN_LIST, mockLoginList);
91+
await Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {
92+
signupQualifier: CONST.ONBOARDING_SIGNUP_QUALIFIERS.VSB,
93+
});
94+
await Onyx.merge(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, CONST.ONBOARDING_CHOICES.MANAGE_TEAM);
95+
});
96+
97+
const {unmount} = renderOnboardingPersonalDetailsPage(SCREENS.ONBOARDING.PERSONAL_DETAILS, {backTo: ''});
98+
99+
await waitForBatchedUpdatesWithAct();
100+
101+
// Submit the form
102+
fireEvent.press(screen.getByText(translateLocal('common.continue')));
103+
104+
await waitFor(() => {
105+
expect(navigate).toHaveBeenCalledWith(ROUTES.ONBOARDING_PRIVATE_DOMAIN.getRoute());
106+
});
107+
108+
unmount();
109+
await waitForBatchedUpdatesWithAct();
110+
});
111+
112+
it('should navigate to Onboarding workspaces page when submitting form and user is routed app via SMB with unvalidated account and private domain', async () => {
113+
await TestHelper.signInWithTestUser();
114+
115+
// Setup account as private domain and has accessible policies
116+
await act(async () => {
117+
await Onyx.merge(ONYXKEYS.ACCOUNT, {
118+
isFromPublicDomain: false,
119+
hasAccessibleDomainPolicies: true,
120+
});
121+
await Onyx.merge(ONYXKEYS.LOGIN_LIST, mockLoginList);
122+
await Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {
123+
signupQualifier: CONST.ONBOARDING_SIGNUP_QUALIFIERS.SMB,
124+
});
125+
await Onyx.merge(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, CONST.ONBOARDING_CHOICES.MANAGE_TEAM);
126+
});
127+
128+
const {unmount} = renderOnboardingPersonalDetailsPage(SCREENS.ONBOARDING.PERSONAL_DETAILS, {backTo: ''});
129+
130+
await waitForBatchedUpdatesWithAct();
131+
132+
// Submit the form
133+
fireEvent.press(screen.getByText(translateLocal('common.continue')));
134+
135+
await waitFor(() => {
136+
expect(navigate).toHaveBeenCalledWith(ROUTES.ONBOARDING_PRIVATE_DOMAIN.getRoute());
137+
});
138+
139+
unmount();
140+
await waitForBatchedUpdatesWithAct();
141+
});
142+
});

tests/ui/WorkspaceOnboarding.tsx

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import {PortalProvider} from '@gorhom/portal';
2+
import {NavigationContainer} from '@react-navigation/native';
3+
import {act, fireEvent, render, screen, waitFor} from '@testing-library/react-native';
4+
import React from 'react';
5+
import Onyx from 'react-native-onyx';
6+
import ComposeProviders from '@components/ComposeProviders';
7+
import {LocaleContextProvider} from '@components/LocaleContextProvider';
8+
import OnyxProvider from '@components/OnyxProvider';
9+
import {CurrentReportIDContextProvider} from '@hooks/useCurrentReportID';
10+
import * as useResponsiveLayoutModule from '@hooks/useResponsiveLayout';
11+
import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types';
12+
import Navigation from '@libs/Navigation/Navigation';
13+
import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator';
14+
import type {OnboardingModalNavigatorParamList} from '@libs/Navigation/types';
15+
import OnboardingWorkspaces from '@pages/OnboardingWorkspaces';
16+
import CONST from '@src/CONST';
17+
import ONYXKEYS from '@src/ONYXKEYS';
18+
import ROUTES from '@src/ROUTES';
19+
import SCREENS from '@src/SCREENS';
20+
import * as TestHelper from '../utils/TestHelper';
21+
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
22+
23+
TestHelper.setupGlobalFetchMock();
24+
25+
const Stack = createPlatformStackNavigator<OnboardingModalNavigatorParamList>();
26+
27+
const navigate = jest.spyOn(Navigation, 'navigate');
28+
29+
const renderOnboardingWorkspacesPage = (initialRouteName: typeof SCREENS.ONBOARDING.WORKSPACES, initialParams: OnboardingModalNavigatorParamList[typeof SCREENS.ONBOARDING.WORKSPACES]) => {
30+
return render(
31+
<ComposeProviders components={[OnyxProvider, LocaleContextProvider, CurrentReportIDContextProvider]}>
32+
<PortalProvider>
33+
<NavigationContainer>
34+
<Stack.Navigator initialRouteName={initialRouteName}>
35+
<Stack.Screen
36+
name={SCREENS.ONBOARDING.WORKSPACES}
37+
component={OnboardingWorkspaces}
38+
initialParams={initialParams}
39+
/>
40+
</Stack.Navigator>
41+
</NavigationContainer>
42+
</PortalProvider>
43+
</ComposeProviders>,
44+
);
45+
};
46+
47+
describe('OnboardingWorkspaces Page', () => {
48+
beforeAll(() => {
49+
Onyx.init({
50+
keys: ONYXKEYS,
51+
});
52+
});
53+
54+
beforeEach(() => {
55+
jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({
56+
isSmallScreenWidth: false,
57+
shouldUseNarrowLayout: false,
58+
} as ResponsiveLayoutResult);
59+
});
60+
61+
afterEach(async () => {
62+
await act(async () => {
63+
await Onyx.clear();
64+
});
65+
jest.clearAllMocks();
66+
});
67+
68+
it('should navigate to Onboarding employee page when skip is pressed and user is routed app via SMB', async () => {
69+
await TestHelper.signInWithTestUser();
70+
71+
await act(async () => {
72+
await Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {
73+
hasCompletedGuidedSetupFlow: false,
74+
signupQualifier: CONST.ONBOARDING_SIGNUP_QUALIFIERS.SMB,
75+
});
76+
});
77+
78+
const {unmount} = renderOnboardingWorkspacesPage(SCREENS.ONBOARDING.WORKSPACES, {backTo: ''});
79+
80+
await waitForBatchedUpdatesWithAct();
81+
82+
const skipButton = screen.getByTestId('onboardingWorkSpaceSkipButton');
83+
84+
const mockEvent = {
85+
nativeEvent: {},
86+
type: 'press',
87+
target: skipButton,
88+
currentTarget: skipButton,
89+
};
90+
91+
fireEvent.press(skipButton, mockEvent);
92+
93+
await waitFor(() => {
94+
expect(navigate).toHaveBeenCalledWith(ROUTES.ONBOARDING_EMPLOYEES.getRoute());
95+
});
96+
97+
unmount();
98+
await waitForBatchedUpdatesWithAct();
99+
});
100+
101+
it('should navigate to Onboarding accounting page when skip is pressed and user is routed app via VSB', async () => {
102+
await TestHelper.signInWithTestUser();
103+
104+
await act(async () => {
105+
await Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {
106+
hasCompletedGuidedSetupFlow: false,
107+
signupQualifier: CONST.ONBOARDING_SIGNUP_QUALIFIERS.VSB,
108+
});
109+
});
110+
111+
const {unmount} = renderOnboardingWorkspacesPage(SCREENS.ONBOARDING.WORKSPACES, {backTo: ''});
112+
113+
await waitForBatchedUpdatesWithAct();
114+
115+
const skipButton = screen.getByTestId('onboardingWorkSpaceSkipButton');
116+
117+
const mockEvent = {
118+
nativeEvent: {},
119+
type: 'press',
120+
target: skipButton,
121+
currentTarget: skipButton,
122+
};
123+
124+
fireEvent.press(skipButton, mockEvent);
125+
126+
await waitFor(() => {
127+
expect(navigate).toHaveBeenCalledWith(ROUTES.ONBOARDING_ACCOUNTING.getRoute());
128+
});
129+
130+
unmount();
131+
await waitForBatchedUpdatesWithAct();
132+
});
133+
});

0 commit comments

Comments
 (0)