Skip to content

Commit dc5b94b

Browse files
authored
Merge pull request Expensify#85330 from Expensify/claude-autoCreateWorkspaceForTrackSignups
Auto-create workspace for Track signups during onboarding
2 parents 8aa7fc3 + d74e7cf commit dc5b94b

4 files changed

Lines changed: 154 additions & 4 deletions

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import {hasSeenTourSelector} from '@selectors/Onboarding';
2+
import {useCallback, useMemo} from 'react';
3+
import type {OnyxCollection} from 'react-native-onyx';
4+
import {navigateAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding';
5+
import {createDisplayName} from '@libs/PersonalDetailsUtils';
6+
import {isPaidGroupPolicy, isPolicyAdmin} from '@libs/PolicyUtils';
7+
import {createWorkspace, generateDefaultWorkspaceName, generatePolicyID} from '@userActions/Policy/Policy';
8+
import {completeOnboarding} from '@userActions/Report';
9+
import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@userActions/Welcome';
10+
import CONST from '@src/CONST';
11+
import ONYXKEYS from '@src/ONYXKEYS';
12+
import type {OnboardingPurpose, Policy} from '@src/types/onyx';
13+
import useArchivedReportsIdSet from './useArchivedReportsIdSet';
14+
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
15+
import useHasActiveAdminPolicies from './useHasActiveAdminPolicies';
16+
import useLocalize from './useLocalize';
17+
import useOnboardingMessages from './useOnboardingMessages';
18+
import useOnyx from './useOnyx';
19+
import usePermissions from './usePermissions';
20+
import usePreferredPolicy from './usePreferredPolicy';
21+
import useResponsiveLayout from './useResponsiveLayout';
22+
23+
/**
24+
* Hook that provides a function to auto-create a workspace for Track (PERSONAL_SPEND)
25+
* users during onboarding and complete the onboarding flow.
26+
*
27+
* Shared by BaseOnboardingPersonalDetails and BaseOnboardingPurpose.
28+
*/
29+
function useAutoCreateTrackWorkspace() {
30+
const [onboardingPolicyID] = useOnyx(ONYXKEYS.ONBOARDING_POLICY_ID);
31+
const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID);
32+
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
33+
const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
34+
const [betas] = useOnyx(ONYXKEYS.BETAS);
35+
const [session] = useOnyx(ONYXKEYS.SESSION);
36+
const paidGroupPolicySelector = useMemo(
37+
() => (policies: OnyxCollection<Policy>) => Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, session?.email)),
38+
[session?.email],
39+
);
40+
const [hasPaidGroupAdminPolicy] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: paidGroupPolicySelector});
41+
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
42+
const [conciergeChatReportID = ''] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);
43+
const [onboardingValues] = useOnyx(ONYXKEYS.NVP_ONBOARDING);
44+
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
45+
const archivedReportsIdSet = useArchivedReportsIdSet();
46+
const {isBetaEnabled} = usePermissions();
47+
const {formatPhoneNumber} = useLocalize();
48+
const {isRestrictedPolicyCreation} = usePreferredPolicy();
49+
const hasActiveAdminPolicies = useHasActiveAdminPolicies();
50+
const {onboardingMessages} = useOnboardingMessages();
51+
52+
// We use isSmallScreenWidth instead of shouldUseNarrowLayout because navigateAfterOnboarding
53+
// relies on actual device screen width to handle navigation stack differences: on small screens,
54+
// removing OnboardingModalNavigator redirects to HOME, requiring explicit navigation to the last
55+
// accessed report. This behavior is tied to screen size, not responsive layout mode.
56+
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
57+
const {isSmallScreenWidth} = useResponsiveLayout();
58+
59+
const mergedAccountConciergeReportID = !onboardingValues?.shouldRedirectToClassicAfterMerge && onboardingValues?.shouldValidate ? conciergeChatReportID : undefined;
60+
61+
const autoCreateTrackWorkspace = useCallback(
62+
(firstName: string, lastName: string, onboardingPurposeSelected: OnboardingPurpose) => {
63+
const shouldCreateWorkspace = !isRestrictedPolicyCreation && !onboardingPolicyID && !hasPaidGroupAdminPolicy;
64+
const displayName = createDisplayName(session?.email ?? '', {firstName, lastName}, formatPhoneNumber);
65+
66+
const {adminsChatReportID: newAdminsChatReportID, policyID: newPolicyID} = shouldCreateWorkspace
67+
? createWorkspace({
68+
policyOwnerEmail: undefined,
69+
makeMeAdmin: true,
70+
policyName: generateDefaultWorkspaceName(session?.email, displayName),
71+
policyID: generatePolicyID(),
72+
engagementChoice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE,
73+
currency: currentUserPersonalDetails.localCurrencyCode ?? CONST.CURRENCY.USD,
74+
file: undefined,
75+
shouldAddOnboardingTasks: false,
76+
introSelected,
77+
activePolicyID,
78+
currentUserAccountIDParam: session?.accountID ?? CONST.DEFAULT_NUMBER_ID,
79+
currentUserEmailParam: session?.email ?? '',
80+
shouldAddGuideWelcomeMessage: false,
81+
onboardingPurposeSelected,
82+
betas,
83+
isSelfTourViewed,
84+
hasActiveAdminPolicies,
85+
})
86+
: {adminsChatReportID: onboardingAdminsChatReportID, policyID: onboardingPolicyID};
87+
88+
completeOnboarding({
89+
engagementChoice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE,
90+
onboardingMessage: onboardingMessages[CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE],
91+
firstName,
92+
lastName,
93+
adminsChatReportID: newAdminsChatReportID,
94+
onboardingPolicyID: newPolicyID,
95+
introSelected,
96+
isSelfTourViewed,
97+
betas,
98+
});
99+
100+
setOnboardingAdminsChatReportID();
101+
setOnboardingPolicyID();
102+
103+
navigateAfterOnboardingWithMicrotaskQueue(
104+
isSmallScreenWidth,
105+
isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS),
106+
conciergeChatReportID,
107+
archivedReportsIdSet,
108+
newPolicyID,
109+
mergedAccountConciergeReportID,
110+
false,
111+
);
112+
},
113+
[
114+
session?.email,
115+
session?.accountID,
116+
formatPhoneNumber,
117+
isRestrictedPolicyCreation,
118+
onboardingPolicyID,
119+
hasPaidGroupAdminPolicy,
120+
onboardingAdminsChatReportID,
121+
currentUserPersonalDetails.localCurrencyCode,
122+
introSelected,
123+
activePolicyID,
124+
isSelfTourViewed,
125+
onboardingMessages,
126+
betas,
127+
hasActiveAdminPolicies,
128+
isSmallScreenWidth,
129+
isBetaEnabled,
130+
conciergeChatReportID,
131+
archivedReportsIdSet,
132+
mergedAccountConciergeReportID,
133+
],
134+
);
135+
136+
return autoCreateTrackWorkspace;
137+
}
138+
139+
export default useAutoCreateTrackWorkspace;

src/libs/actions/Policy/Policy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2139,15 +2139,15 @@ function clearDuplicateWorkspace() {
21392139
* Generate a policy name based on an email and policy list.
21402140
* @param [email] the email to base the workspace name on. If not passed, will use the logged-in user's email instead
21412141
*/
2142-
function generateDefaultWorkspaceName(email = ''): string {
2142+
function generateDefaultWorkspaceName(email = '', displayNameOverride?: string): string {
21432143
const emailParts = email ? email.split('@') : deprecatedSessionEmail.split('@');
21442144
if (!emailParts || emailParts.length !== 2) {
21452145
return '';
21462146
}
21472147
const username = emailParts.at(0) ?? '';
21482148
const domain = emailParts.at(1) ?? '';
21492149
const userDetails = PersonalDetailsUtils.getPersonalDetailByEmail(email || deprecatedSessionEmail);
2150-
const displayName = userDetails?.displayName?.trim();
2150+
const displayName = displayNameOverride?.trim() ?? userDetails?.displayName?.trim();
21512151
let displayNameForWorkspace = '';
21522152

21532153
if (!PUBLIC_DOMAINS_SET.has(domain.toLowerCase())) {

src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Text from '@components/Text';
1010
import TextInput from '@components/TextInput';
1111
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
1212
import useArchivedReportsIdSet from '@hooks/useArchivedReportsIdSet';
13+
import useAutoCreateTrackWorkspace from '@hooks/useAutoCreateTrackWorkspace';
1314
import useAutoFocusInput from '@hooks/useAutoFocusInput';
1415
import useLocalize from '@hooks/useLocalize';
1516
import useOnboardingMessages from '@hooks/useOnboardingMessages';
@@ -52,6 +53,7 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat
5253
const [session] = useOnyx(ONYXKEYS.SESSION);
5354
const [onboardingPersonalDetailsForm] = useOnyx(ONYXKEYS.FORMS.ONBOARDING_PERSONAL_DETAILS_FORM);
5455
const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
56+
const autoCreateTrackWorkspace = useAutoCreateTrackWorkspace();
5557

5658
// When we merge public email with work email, we now want to navigate to the
5759
// concierge chat report of the new work email and not the last accessed report.
@@ -135,7 +137,13 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat
135137
return;
136138
}
137139

138-
if (onboardingPurposeSelected === CONST.ONBOARDING_CHOICES.PERSONAL_SPEND || onboardingPurposeSelected === CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE) {
140+
if (onboardingPurposeSelected === CONST.ONBOARDING_CHOICES.PERSONAL_SPEND) {
141+
updateDisplayName(firstName, lastName, formatPhoneNumber, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? '');
142+
autoCreateTrackWorkspace(firstName, lastName, onboardingPurposeSelected);
143+
return;
144+
}
145+
146+
if (onboardingPurposeSelected === CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE) {
139147
updateDisplayName(firstName, lastName, formatPhoneNumber, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? '');
140148
Navigation.navigate(ROUTES.ONBOARDING_WORKSPACE.getRoute(route.params?.backTo));
141149
return;
@@ -154,6 +162,7 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat
154162
completeOnboarding,
155163
isValidated,
156164
route.params?.backTo,
165+
autoCreateTrackWorkspace,
157166
],
158167
);
159168

src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {MenuItemProps} from '@components/MenuItem';
99
import MenuItemList from '@components/MenuItemList';
1010
import ScreenWrapper from '@components/ScreenWrapper';
1111
import Text from '@components/Text';
12+
import useAutoCreateTrackWorkspace from '@hooks/useAutoCreateTrackWorkspace';
1213
import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
1314
import useLocalize from '@hooks/useLocalize';
1415
import useOnboardingMessages from '@hooks/useOnboardingMessages';
@@ -73,6 +74,7 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, ro
7374
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
7475
const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
7576
const [betas] = useOnyx(ONYXKEYS.BETAS);
77+
const autoCreateTrackWorkspace = useAutoCreateTrackWorkspace();
7678
const paddingHorizontal = onboardingIsMediumOrLargerScreenWidth ? styles.ph8 : styles.ph5;
7779

7880
const [customChoices = getEmptyArray<OnboardingPurpose>()] = useOnyx(ONYXKEYS.ONBOARDING_CUSTOM_CHOICES);
@@ -102,7 +104,7 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, ro
102104

103105
if (isPrivateDomainAndHasAccessiblePolicies && personalDetailsForm?.firstName) {
104106
if (choice === CONST.ONBOARDING_CHOICES.PERSONAL_SPEND) {
105-
Navigation.navigate(ROUTES.ONBOARDING_WORKSPACE.getRoute(route.params?.backTo));
107+
autoCreateTrackWorkspace(personalDetailsForm.firstName, personalDetailsForm.lastName ?? '', choice);
106108
return;
107109
}
108110
completeOnboarding({

0 commit comments

Comments
 (0)