Skip to content

Commit 960631a

Browse files
authored
Merge pull request #87283 from abzokhattab/akhattab/87261-submit-workspace-onboarding
feat: Submit workspace creation + onboarding flow (Wave 2)
2 parents 8526bac + 3094680 commit 960631a

44 files changed

Lines changed: 1314 additions & 101 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/CONST/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,7 @@ const CONST = {
887887
BULK_DUPLICATE_REPORT: 'bulkDuplicateReport',
888888
BULK_EDIT: 'bulkEdit',
889889
NEW_MANUAL_EXPENSE_FLOW: 'newManualExpenseFlow',
890+
SUBMIT_2026: 'submit2026',
890891
BULK_SUBMIT_APPROVE_PAY: 'bulkSubmitApprovePay',
891892
},
892893
BUTTON_STATES: {
@@ -3516,6 +3517,8 @@ const CONST = {
35163517

35173518
// Often referred to as "collect" workspaces
35183519
TEAM: 'team',
3520+
3521+
SUBMIT: 'submit2026',
35193522
},
35203523
RULE_CONDITIONS: {
35213524
MATCHES: 'matches',
@@ -3534,6 +3537,7 @@ const CONST = {
35343537
ADMIN: 'admin',
35353538
AUDITOR: 'auditor',
35363539
USER: 'user',
3540+
EDITOR: 'editor',
35373541
},
35383542
AUTO_REIMBURSEMENT_MAX_LIMIT_CENTS: 2000000,
35393543

src/components/SidePanel/SidePanelContextProvider.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
1010
import SidePanelActions from '@libs/actions/SidePanel';
1111
import DateUtils from '@libs/DateUtils';
1212
import focusComposerWithDelay from '@libs/focusComposerWithDelay';
13-
import {isPolicyAdmin, shouldShowPolicy} from '@libs/PolicyUtils';
13+
import {canEditWorkspaceSettings, shouldShowPolicy} from '@libs/PolicyUtils';
1414
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
1515
import variables from '@styles/variables';
1616
import CONST from '@src/CONST';
@@ -82,7 +82,7 @@ function SidePanelContextProvider({children}: PropsWithChildren) {
8282

8383
const isRHPAdminsRoom = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM;
8484
const isRHPHomePage = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_HOME_PAGE;
85-
const isUserAdmin = isPolicyAdmin(activePolicy, sessionEmail);
85+
const isUserAdmin = canEditWorkspaceSettings(activePolicy);
8686
const isPolicyActive = shouldShowPolicy(activePolicy, false, sessionEmail ?? '');
8787
const adminsChatReportID = activePolicy?.chatReportIDAdmins?.toString();
8888

src/components/WorkspaceMemberRoleList.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import {emailSelector} from '@selectors/Session';
12
import React from 'react';
23
import {View} from 'react-native';
34
import type {OnyxEntry} from 'react-native-onyx';
45
import type {ValueOf} from 'type-fest';
56
import useLocalize from '@hooks/useLocalize';
7+
import useOnyx from '@hooks/useOnyx';
68
import useThemeStyles from '@hooks/useThemeStyles';
79
import Navigation from '@libs/Navigation/Navigation';
8-
import {isControlPolicy} from '@libs/PolicyUtils';
10+
import {isControlPolicy, isPolicyAdmin} from '@libs/PolicyUtils';
911
import CONST from '@src/CONST';
12+
import ONYXKEYS from '@src/ONYXKEYS';
1013
import type {Route} from '@src/ROUTES';
1114
import type {Policy} from '@src/types/onyx';
1215
import HeaderWithBackButton from './HeaderWithBackButton';
@@ -32,6 +35,7 @@ type WorkspaceMemberRoleListProps = {
3235
function WorkspaceMemberRoleList({role, policy, navigateBackTo = undefined, isLoading = false, onSelectRole = () => {}}: WorkspaceMemberRoleListProps) {
3336
const {translate} = useLocalize();
3437
const styles = useThemeStyles();
38+
const [currentUserEmail] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector});
3539

3640
const workspaceRoles: ListItemType[] = [
3741
{
@@ -58,7 +62,18 @@ function WorkspaceMemberRoleList({role, policy, navigateBackTo = undefined, isLo
5862
];
5963

6064
const isPolicyControl = isControlPolicy(policy);
61-
const availableRoleItems: ListItemType[] = workspaceRoles.filter((item) => isPolicyControl || item.value !== CONST.POLICY.ROLE.AUDITOR);
65+
// Only strict admins can assign the ADMIN role. Editors (e.g. Submit workspace owners) can
66+
// invite/manage members but must not be able to escalate anyone to admin.
67+
const canAssignAdminRole = isPolicyAdmin(policy, currentUserEmail);
68+
const availableRoleItems: ListItemType[] = workspaceRoles.filter((item) => {
69+
if (item.value === CONST.POLICY.ROLE.AUDITOR && !isPolicyControl) {
70+
return false;
71+
}
72+
if (item.value === CONST.POLICY.ROLE.ADMIN && !canAssignAdminRole) {
73+
return false;
74+
}
75+
return true;
76+
});
6277

6378
return (
6479
<>
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import {useCallback, useMemo} from 'react';
2+
import type {OnyxCollection} from 'react-native-onyx';
3+
import {navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding';
4+
import {createDisplayName} from '@libs/PersonalDetailsUtils';
5+
import {canEditWorkspaceSettings, isGroupPolicy} from '@libs/PolicyUtils';
6+
import {createWorkspace, generateDefaultWorkspaceName, generatePolicyID} from '@userActions/Policy/Policy';
7+
import {completeOnboarding} from '@userActions/Report';
8+
import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@userActions/Welcome';
9+
import CONST from '@src/CONST';
10+
import ONYXKEYS from '@src/ONYXKEYS';
11+
import type {Policy} from '@src/types/onyx';
12+
import useOnboardingWorkspaceCreationState from './useOnboardingWorkspaceCreationState';
13+
import useOnyx from './useOnyx';
14+
15+
/**
16+
* Hook that provides a function to auto-create a Submit workspace for EMPLOYER
17+
* users during onboarding and complete the onboarding flow.
18+
*
19+
* Shared by BaseOnboardingPersonalDetails, BaseOnboardingPurpose, and BaseOnboardingWorkspaces.
20+
*/
21+
function useAutoCreateSubmitWorkspace() {
22+
const {
23+
onboardingPolicyID,
24+
onboardingAdminsChatReportID,
25+
introSelected,
26+
isSelfTourViewed,
27+
betas,
28+
currentUserEmail,
29+
currentUserAccountID,
30+
localCurrencyCode,
31+
activePolicy,
32+
translate,
33+
formatPhoneNumber,
34+
isRestrictedPolicyCreation,
35+
hasActiveAdminPolicies,
36+
onboardingMessages,
37+
lastWorkspaceNumber,
38+
isSmallScreenWidth,
39+
} = useOnboardingWorkspaceCreationState();
40+
41+
const groupPolicySelector = useMemo(
42+
() => (policies: OnyxCollection<Policy>) => Object.values(policies ?? {}).some((policy) => isGroupPolicy(policy) && canEditWorkspaceSettings(policy)),
43+
[],
44+
);
45+
const [hasEditableGroupPolicy] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPolicySelector});
46+
47+
const autoCreateSubmitWorkspace = useCallback(
48+
(firstName: string, lastName: string) => {
49+
const shouldCreateWorkspace = !isRestrictedPolicyCreation && !onboardingPolicyID && !hasEditableGroupPolicy;
50+
const displayName = createDisplayName(currentUserEmail, {firstName, lastName}, formatPhoneNumber);
51+
52+
const {adminsChatReportID: newAdminsChatReportID, policyID: newPolicyID} = shouldCreateWorkspace
53+
? createWorkspace({
54+
policyOwnerEmail: undefined,
55+
makeMeAdmin: true,
56+
policyName: generateDefaultWorkspaceName(currentUserEmail, lastWorkspaceNumber, translate, displayName),
57+
policyID: generatePolicyID(),
58+
engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER,
59+
currency: localCurrencyCode,
60+
file: undefined,
61+
shouldAddOnboardingTasks: false,
62+
introSelected,
63+
activePolicy,
64+
currentUserAccountIDParam: currentUserAccountID,
65+
currentUserEmailParam: currentUserEmail,
66+
shouldAddGuideWelcomeMessage: false,
67+
type: CONST.POLICY.TYPE.SUBMIT,
68+
betas,
69+
isSelfTourViewed,
70+
hasActiveAdminPolicies,
71+
})
72+
: {adminsChatReportID: onboardingAdminsChatReportID, policyID: onboardingPolicyID};
73+
74+
completeOnboarding({
75+
engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER,
76+
onboardingMessage: onboardingMessages[CONST.ONBOARDING_CHOICES.EMPLOYER],
77+
firstName,
78+
lastName,
79+
adminsChatReportID: newAdminsChatReportID,
80+
onboardingPolicyID: newPolicyID,
81+
introSelected,
82+
isSelfTourViewed,
83+
betas,
84+
});
85+
86+
setOnboardingAdminsChatReportID();
87+
setOnboardingPolicyID();
88+
89+
navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(newPolicyID, isSmallScreenWidth);
90+
},
91+
[
92+
currentUserEmail,
93+
currentUserAccountID,
94+
lastWorkspaceNumber,
95+
translate,
96+
formatPhoneNumber,
97+
isRestrictedPolicyCreation,
98+
onboardingPolicyID,
99+
hasEditableGroupPolicy,
100+
onboardingAdminsChatReportID,
101+
localCurrencyCode,
102+
introSelected,
103+
activePolicy,
104+
isSelfTourViewed,
105+
onboardingMessages,
106+
betas,
107+
hasActiveAdminPolicies,
108+
isSmallScreenWidth,
109+
],
110+
);
111+
112+
return autoCreateSubmitWorkspace;
113+
}
114+
115+
export default useAutoCreateSubmitWorkspace;

src/hooks/useAutoCreateTrackWorkspace.ts

Lines changed: 31 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {hasSeenTourSelector} from '@selectors/Onboarding';
21
import {useCallback, useMemo} from 'react';
32
import type {OnyxCollection} from 'react-native-onyx';
43
import isSidePanelReportSupported from '@components/SidePanel/isSidePanelReportSupported';
@@ -12,17 +11,10 @@ import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@userActio
1211
import CONST from '@src/CONST';
1312
import ONYXKEYS from '@src/ONYXKEYS';
1413
import type {OnboardingPurpose, OnboardingRHPVariant, Policy} from '@src/types/onyx';
15-
import useActivePolicy from './useActivePolicy';
1614
import useArchivedReportsIdSet from './useArchivedReportsIdSet';
17-
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
18-
import useHasActiveAdminPolicies from './useHasActiveAdminPolicies';
19-
import useLastWorkspaceNumber from './useLastWorkspaceNumber';
20-
import useLocalize from './useLocalize';
21-
import useOnboardingMessages from './useOnboardingMessages';
15+
import useOnboardingWorkspaceCreationState from './useOnboardingWorkspaceCreationState';
2216
import useOnyx from './useOnyx';
2317
import usePermissions from './usePermissions';
24-
import usePreferredPolicy from './usePreferredPolicy';
25-
import useResponsiveLayout from './useResponsiveLayout';
2618

2719
/**
2820
* Hook that provides a function to auto-create a workspace for Track (PERSONAL_SPEND)
@@ -31,57 +23,57 @@ import useResponsiveLayout from './useResponsiveLayout';
3123
* Shared by BaseOnboardingPersonalDetails and BaseOnboardingPurpose.
3224
*/
3325
function useAutoCreateTrackWorkspace() {
34-
const [onboardingPolicyID] = useOnyx(ONYXKEYS.ONBOARDING_POLICY_ID);
35-
const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID);
36-
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
37-
const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
38-
const [betas] = useOnyx(ONYXKEYS.BETAS);
39-
const [session] = useOnyx(ONYXKEYS.SESSION);
26+
const {
27+
onboardingPolicyID,
28+
onboardingAdminsChatReportID,
29+
introSelected,
30+
isSelfTourViewed,
31+
betas,
32+
currentUserEmail,
33+
currentUserAccountID,
34+
localCurrencyCode,
35+
activePolicy,
36+
translate,
37+
formatPhoneNumber,
38+
isRestrictedPolicyCreation,
39+
hasActiveAdminPolicies,
40+
onboardingMessages,
41+
lastWorkspaceNumber,
42+
isSmallScreenWidth,
43+
} = useOnboardingWorkspaceCreationState();
44+
4045
const paidGroupPolicySelector = useMemo(
41-
() => (policies: OnyxCollection<Policy>) => Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, session?.email)),
42-
[session?.email],
46+
() => (policies: OnyxCollection<Policy>) => Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, currentUserEmail)),
47+
[currentUserEmail],
4348
);
4449
const [hasPaidGroupAdminPolicy] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: paidGroupPolicySelector});
50+
4551
const [conciergeChatReportID = ''] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);
4652
const [onboardingValues] = useOnyx(ONYXKEYS.NVP_ONBOARDING);
47-
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
4853
const archivedReportsIdSet = useArchivedReportsIdSet();
4954
const {isBetaEnabled} = usePermissions();
50-
const {translate, formatPhoneNumber} = useLocalize();
51-
const activePolicy = useActivePolicy();
52-
const {isRestrictedPolicyCreation} = usePreferredPolicy();
53-
const hasActiveAdminPolicies = useHasActiveAdminPolicies();
54-
const lastWorkspaceNumber = useLastWorkspaceNumber();
55-
const {onboardingMessages} = useOnboardingMessages();
56-
57-
// We use isSmallScreenWidth instead of shouldUseNarrowLayout because navigateAfterOnboarding
58-
// relies on actual device screen width to handle navigation stack differences: on small screens,
59-
// removing OnboardingModalNavigator redirects to HOME, requiring explicit navigation to the last
60-
// accessed report. This behavior is tied to screen size, not responsive layout mode.
61-
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
62-
const {isSmallScreenWidth} = useResponsiveLayout();
6355

6456
const mergedAccountConciergeReportID = !onboardingValues?.shouldRedirectToClassicAfterMerge && onboardingValues?.shouldValidate ? conciergeChatReportID : undefined;
6557

6658
const autoCreateTrackWorkspace = useCallback(
6759
async (firstName: string, lastName: string, onboardingPurposeSelected: OnboardingPurpose) => {
6860
const shouldCreateWorkspace = !isRestrictedPolicyCreation && !onboardingPolicyID && !hasPaidGroupAdminPolicy;
69-
const displayName = createDisplayName(session?.email ?? '', {firstName, lastName}, formatPhoneNumber);
61+
const displayName = createDisplayName(currentUserEmail, {firstName, lastName}, formatPhoneNumber);
7062

7163
const {adminsChatReportID: newAdminsChatReportID, policyID: newPolicyID} = shouldCreateWorkspace
7264
? createWorkspace({
7365
policyOwnerEmail: undefined,
7466
makeMeAdmin: true,
75-
policyName: generateDefaultWorkspaceName(session?.email ?? '', lastWorkspaceNumber, translate, displayName),
67+
policyName: generateDefaultWorkspaceName(currentUserEmail, lastWorkspaceNumber, translate, displayName),
7668
policyID: generatePolicyID(),
7769
engagementChoice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE,
78-
currency: currentUserPersonalDetails.localCurrencyCode ?? CONST.CURRENCY.USD,
70+
currency: localCurrencyCode,
7971
file: undefined,
8072
shouldAddOnboardingTasks: false,
8173
introSelected,
8274
activePolicy,
83-
currentUserAccountIDParam: session?.accountID ?? CONST.DEFAULT_NUMBER_ID,
84-
currentUserEmailParam: session?.email ?? '',
75+
currentUserAccountIDParam: currentUserAccountID,
76+
currentUserEmailParam: currentUserEmail,
8577
shouldAddGuideWelcomeMessage: false,
8678
onboardingPurposeSelected,
8779
betas,
@@ -129,16 +121,16 @@ function useAutoCreateTrackWorkspace() {
129121
}
130122
},
131123
[
132-
session?.email,
133-
session?.accountID,
124+
currentUserEmail,
125+
currentUserAccountID,
134126
lastWorkspaceNumber,
135127
translate,
136128
formatPhoneNumber,
137129
isRestrictedPolicyCreation,
138130
onboardingPolicyID,
139131
hasPaidGroupAdminPolicy,
140132
onboardingAdminsChatReportID,
141-
currentUserPersonalDetails.localCurrencyCode,
133+
localCurrencyCode,
142134
introSelected,
143135
activePolicy,
144136
isSelfTourViewed,
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {hasSeenTourSelector} from '@selectors/Onboarding';
2+
import CONST from '@src/CONST';
3+
import ONYXKEYS from '@src/ONYXKEYS';
4+
import useActivePolicy from './useActivePolicy';
5+
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
6+
import useHasActiveAdminPolicies from './useHasActiveAdminPolicies';
7+
import useLastWorkspaceNumber from './useLastWorkspaceNumber';
8+
import useLocalize from './useLocalize';
9+
import useOnboardingMessages from './useOnboardingMessages';
10+
import useOnyx from './useOnyx';
11+
import usePreferredPolicy from './usePreferredPolicy';
12+
import useResponsiveLayout from './useResponsiveLayout';
13+
14+
/**
15+
* Shared state for the onboarding workspace auto-creation hooks
16+
* (`useAutoCreateSubmitWorkspace`, `useAutoCreateTrackWorkspace`).
17+
*
18+
* Email and accountID come from `ONYXKEYS.SESSION` because session is hydrated
19+
* earlier in onboarding than personal details.
20+
*/
21+
function useOnboardingWorkspaceCreationState() {
22+
const [onboardingPolicyID] = useOnyx(ONYXKEYS.ONBOARDING_POLICY_ID);
23+
const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID);
24+
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
25+
const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
26+
const [betas] = useOnyx(ONYXKEYS.BETAS);
27+
const [session] = useOnyx(ONYXKEYS.SESSION);
28+
29+
const currentUserEmail = session?.email ?? '';
30+
const currentUserAccountID = session?.accountID ?? CONST.DEFAULT_NUMBER_ID;
31+
32+
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
33+
const localCurrencyCode = currentUserPersonalDetails.localCurrencyCode ?? CONST.CURRENCY.USD;
34+
35+
const activePolicy = useActivePolicy();
36+
const {translate, formatPhoneNumber} = useLocalize();
37+
const {isRestrictedPolicyCreation} = usePreferredPolicy();
38+
const hasActiveAdminPolicies = useHasActiveAdminPolicies();
39+
const {onboardingMessages} = useOnboardingMessages();
40+
const lastWorkspaceNumber = useLastWorkspaceNumber();
41+
42+
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
43+
const {isSmallScreenWidth} = useResponsiveLayout();
44+
45+
return {
46+
onboardingPolicyID,
47+
onboardingAdminsChatReportID,
48+
introSelected,
49+
isSelfTourViewed,
50+
betas,
51+
currentUserPersonalDetails,
52+
currentUserEmail,
53+
currentUserAccountID,
54+
localCurrencyCode,
55+
activePolicy,
56+
translate,
57+
formatPhoneNumber,
58+
isRestrictedPolicyCreation,
59+
hasActiveAdminPolicies,
60+
onboardingMessages,
61+
lastWorkspaceNumber,
62+
isSmallScreenWidth,
63+
};
64+
}
65+
66+
export default useOnboardingWorkspaceCreationState;

0 commit comments

Comments
 (0)