Skip to content

Commit cbde538

Browse files
authored
Merge pull request Expensify#88374 from mukhrr/revert-revert-85570-create-report-cta
Reapply: Show Create Report CTA for users without a workspace
2 parents 732ac22 + aede2dd commit cbde538

9 files changed

Lines changed: 683 additions & 157 deletions

File tree

src/ROUTES.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1201,7 +1201,12 @@ const ROUTES = {
12011201
NEW_REPORT_WORKSPACE_SELECTION: {
12021202
route: 'new-report-workspace-selection',
12031203
getRoute: (isMovingExpenses?: boolean, backTo?: string) => {
1204-
const baseRoute = `new-report-workspace-selection${isMovingExpenses ? '?isMovingExpenses=true' : ''}` as const;
1204+
const params = new URLSearchParams();
1205+
if (isMovingExpenses) {
1206+
params.set('isMovingExpenses', 'true');
1207+
}
1208+
const query = params.toString();
1209+
const baseRoute = `new-report-workspace-selection${query ? `?${query}` : ''}` as const;
12051210

12061211
// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
12071212
return getUrlWithBackToParam(baseRoute, backTo);

src/hooks/useCreateNewReport.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {accountIDSelector, emailSelector} from '@selectors/Session';
2+
import {useCallback} from 'react';
3+
import {createNewReport} from '@libs/actions/Report';
4+
import {hasViolations as hasViolationsUtil} from '@libs/ReportUtils';
5+
import CONST from '@src/CONST';
6+
import ONYXKEYS from '@src/ONYXKEYS';
7+
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
8+
import useOnyx from './useOnyx';
9+
import usePermissions from './usePermissions';
10+
11+
/**
12+
* Returns a stable callback that creates a new expense report for a given policyID.
13+
* Returns the full optimistic report data so callers can navigate or use the result as needed.
14+
*/
15+
function useCreateNewReport() {
16+
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
17+
const {isBetaEnabled} = usePermissions();
18+
const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
19+
const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector});
20+
const [email] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector});
21+
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
22+
const [betas] = useOnyx(ONYXKEYS.BETAS);
23+
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
24+
25+
const hasViolations = hasViolationsUtil(undefined, transactionViolations, accountID ?? CONST.DEFAULT_NUMBER_ID, email ?? '');
26+
27+
return useCallback(
28+
(policyID: string, shouldDismissEmptyReportsConfirmation = false) => {
29+
const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
30+
return createNewReport(currentUserPersonalDetails, hasViolations, isASAPSubmitBetaEnabled, policy, betas, false, shouldDismissEmptyReportsConfirmation);
31+
},
32+
[betas, currentUserPersonalDetails, hasViolations, isASAPSubmitBetaEnabled, policies],
33+
);
34+
}
35+
36+
export default useCreateNewReport;

src/hooks/useCreateReport.tsx

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import {useCallback} from 'react';
2+
import type {OnyxEntry} from 'react-native-onyx';
3+
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
4+
import Navigation from '@libs/Navigation/Navigation';
5+
import {getDefaultChatEnabledPolicy, isPaidGroupPolicy} from '@libs/PolicyUtils';
6+
import {generateReportID} from '@libs/ReportUtils';
7+
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
8+
import CONST from '@src/CONST';
9+
import ONYXKEYS from '@src/ONYXKEYS';
10+
import ROUTES from '@src/ROUTES';
11+
import type * as OnyxTypes from '@src/types/onyx';
12+
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
13+
import useCreateEmptyReportConfirmation from './useCreateEmptyReportConfirmation';
14+
import useHasEmptyReportsForPolicy from './useHasEmptyReportsForPolicy';
15+
import useOnyx from './useOnyx';
16+
17+
type UseCreateReportParams = {
18+
/** Callback that creates the report and navigates after creation */
19+
onCreateReport: (shouldDismissEmptyReportsConfirmation?: boolean) => void;
20+
/** Group paid policies with expense chat enabled */
21+
groupPoliciesWithChatEnabled: readonly never[] | Array<OnyxEntry<OnyxTypes.Policy>>;
22+
/** Whether the empty-report confirmation modal should push a history entry so browser-back dismisses it (default: true) */
23+
shouldHandleNavigationBack?: boolean;
24+
};
25+
26+
type UseCreateReportResult = {
27+
/** The callback to trigger when the user clicks "Create report" */
28+
createReport: () => void;
29+
/** Whether the menu item/button should be visible */
30+
isVisible: boolean;
31+
};
32+
33+
/**
34+
* Hook that encapsulates the shared "create report" branching logic used across
35+
* the FAB, the search Create dropdown, and the empty reports state.
36+
*
37+
* Decision flow:
38+
* 1. Navigate to upgrade path if user has no valid group policies at all
39+
* 2. Navigate to workspace selector if default is personal AND there are at least 2 non-personal workspaces, or if the chosen default is billing-restricted and alternatives exist
40+
* 3. Show empty report confirmation or create directly if workspace is valid
41+
* 4. Navigate to restricted action if billing restricts the workspace
42+
*/
43+
export default function useCreateReport({onCreateReport, groupPoliciesWithChatEnabled, shouldHandleNavigationBack = true}: UseCreateReportParams): UseCreateReportResult {
44+
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
45+
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`);
46+
const [, policiesLoadStatus] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
47+
const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
48+
const [userBillingGracePeriodEnds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
49+
const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED);
50+
const [hasDismissedEmptyReportsConfirmation] = useOnyx(ONYXKEYS.NVP_EMPTY_REPORTS_CONFIRMATION_DISMISSED);
51+
const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.accountID});
52+
53+
// Gate visibility and routing on policy hydration. Without this, during Onyx cold-start
54+
// groupPoliciesWithChatEnabled.length === 0 would be true even for users who actually have
55+
// workspaces, sending them to MONEY_REQUEST_UPGRADE as if they had none.
56+
const arePoliciesLoaded = !isLoadingOnyxValue(policiesLoadStatus);
57+
const isVisible = arePoliciesLoaded;
58+
const shouldNavigateToUpgradePath = groupPoliciesWithChatEnabled.length === 0;
59+
60+
const defaultChatEnabledPolicy = getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array<OnyxEntry<OnyxTypes.Policy>>, activePolicy);
61+
const defaultChatEnabledPolicyID = defaultChatEnabledPolicy?.id;
62+
63+
const hasEmptyReport = useHasEmptyReportsForPolicy(defaultChatEnabledPolicyID);
64+
const shouldShowEmptyReportConfirmation = hasEmptyReport && hasDismissedEmptyReportsConfirmation !== true;
65+
66+
const {openCreateReportConfirmation} = useCreateEmptyReportConfirmation({
67+
policyID: defaultChatEnabledPolicyID,
68+
policyName: defaultChatEnabledPolicy?.name ?? '',
69+
onConfirm: onCreateReport,
70+
shouldHandleNavigationBack,
71+
});
72+
73+
const createReport = useCallback(() => {
74+
interceptAnonymousUser(() => {
75+
if (!arePoliciesLoaded) {
76+
return;
77+
}
78+
79+
// No valid policy at all → upgrade + create workspace flow
80+
if (shouldNavigateToUpgradePath) {
81+
const freshReportID = generateReportID();
82+
const freshTransactionID = generateReportID();
83+
Navigation.navigate(
84+
ROUTES.MONEY_REQUEST_UPGRADE.getRoute({
85+
action: CONST.IOU.ACTION.CREATE,
86+
iouType: CONST.IOU.TYPE.CREATE,
87+
transactionID: freshTransactionID,
88+
reportID: freshReportID,
89+
upgradePath: CONST.UPGRADE_PATHS.REPORTS,
90+
}),
91+
);
92+
return;
93+
}
94+
95+
const workspaceIDForReportCreation = defaultChatEnabledPolicyID;
96+
97+
// Show the workspace selector only when the default workspace is personal and there are
98+
// at least 2 non-personal workspaces to choose between. Also fall back to the selector if
99+
// the default is billing-restricted and alternatives exist, so the user isn't dead-ended
100+
// on the restricted-action page.
101+
const isDefaultPersonal = !activePolicy || activePolicy.type === CONST.POLICY.TYPE.PERSONAL || !isPaidGroupPolicy(activePolicy);
102+
const hasMultipleNonPersonalWorkspaces = groupPoliciesWithChatEnabled.length > 1;
103+
const isDefaultBillingRestricted =
104+
!!workspaceIDForReportCreation && shouldRestrictUserBillableActions(defaultChatEnabledPolicy, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, accountID);
105+
106+
if (!workspaceIDForReportCreation || (isDefaultPersonal && hasMultipleNonPersonalWorkspaces) || (isDefaultBillingRestricted && hasMultipleNonPersonalWorkspaces)) {
107+
Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute());
108+
return;
109+
}
110+
111+
// Default workspace is not restricted → create report directly (or show empty-report confirmation)
112+
if (!shouldRestrictUserBillableActions(defaultChatEnabledPolicy, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, accountID)) {
113+
if (shouldShowEmptyReportConfirmation) {
114+
openCreateReportConfirmation();
115+
} else {
116+
onCreateReport(false);
117+
}
118+
return;
119+
}
120+
121+
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(workspaceIDForReportCreation));
122+
});
123+
}, [
124+
arePoliciesLoaded,
125+
shouldNavigateToUpgradePath,
126+
activePolicy,
127+
defaultChatEnabledPolicy,
128+
defaultChatEnabledPolicyID,
129+
ownerBillingGracePeriodEnd,
130+
userBillingGracePeriodEnds,
131+
amountOwed,
132+
accountID,
133+
groupPoliciesWithChatEnabled.length,
134+
shouldShowEmptyReportConfirmation,
135+
openCreateReportConfirmation,
136+
onCreateReport,
137+
]);
138+
139+
return {createReport, isVisible};
140+
}

src/pages/NewReportWorkspaceSelectionPage.tsx

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {accountIDSelector, emailSelector} from '@selectors/Session';
2-
import React, {useEffect, useState} from 'react';
1+
import {policyIDsWithEmptyReportsSelector} from '@selectors/Report';
2+
import {accountIDSelector} from '@selectors/Session';
3+
import React, {useEffect, useMemo, useState} from 'react';
34
import {View} from 'react-native';
4-
import type {OnyxCollection} from 'react-native-onyx';
55
import ActivityIndicator from '@components/ActivityIndicator';
66
import HeaderWithBackButton from '@components/HeaderWithBackButton';
77
import ScreenWrapper from '@components/ScreenWrapper';
@@ -11,6 +11,7 @@ import UserListItem from '@components/SelectionList/ListItem/UserListItem';
1111
import type {ListItem} from '@components/SelectionList/types';
1212
import Text from '@components/Text';
1313
import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation';
14+
import useCreateNewReport from '@hooks/useCreateNewReport';
1415
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
1516
import useDebouncedState from '@hooks/useDebouncedState';
1617
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
@@ -20,14 +21,13 @@ import useOnyx from '@hooks/useOnyx';
2021
import usePermissions from '@hooks/usePermissions';
2122
import useResponsiveLayout from '@hooks/useResponsiveLayout';
2223
import useThemeStyles from '@hooks/useThemeStyles';
23-
import {createNewReport} from '@libs/actions/Report';
2424
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
2525
import setNavigationActionToMicrotaskQueue from '@libs/Navigation/helpers/setNavigationActionToMicrotaskQueue';
2626
import Navigation from '@libs/Navigation/Navigation';
2727
import type {NewReportWorkspaceSelectionNavigatorParamList} from '@libs/Navigation/types';
2828
import {getHeaderMessageForNonUserList} from '@libs/OptionsListUtils';
2929
import {canSubmitPerDiemExpenseFromWorkspace, isPolicyAdmin, shouldShowPolicy} from '@libs/PolicyUtils';
30-
import {getDefaultWorkspaceAvatar, getPolicyIDsWithEmptyReportsForAccount, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
30+
import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils';
3131
import {buildCannedSearchQuery} from '@libs/SearchQueryUtils';
3232
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
3333
import {isPerDiemRequest} from '@libs/TransactionUtils';
@@ -39,7 +39,6 @@ import CONST from '@src/CONST';
3939
import ONYXKEYS from '@src/ONYXKEYS';
4040
import ROUTES from '@src/ROUTES';
4141
import type SCREENS from '@src/SCREENS';
42-
import type * as OnyxTypes from '@src/types/onyx';
4342
import {isEmptyObject} from '@src/types/utils/EmptyObject';
4443

4544
type WorkspaceListItem = {
@@ -62,15 +61,12 @@ function NewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelectionPag
6261
const {shouldUseNarrowLayout} = useResponsiveLayout();
6362
const [allReportNextSteps] = useOnyx(ONYXKEYS.COLLECTION.NEXT_STEP);
6463
const isRHPOnReportInSearch = isRHPOnSearchMoneyRequestReportPage();
65-
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
6664
const {isBetaEnabled} = usePermissions();
6765
const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
6866
const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector});
69-
const [email] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector});
70-
const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, accountID ?? CONST.DEFAULT_NUMBER_ID, email ?? '');
7167
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
7268
const [hasDismissedEmptyReportsConfirmation] = useOnyx(ONYXKEYS.NVP_EMPTY_REPORTS_CONFIRMATION_DISMISSED);
73-
const [betas] = useOnyx(ONYXKEYS.BETAS);
69+
const createNewReport = useCreateNewReport();
7470
const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
7571
const [userBillingGracePeriods] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
7672
const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED);
@@ -87,19 +83,8 @@ function NewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelectionPag
8783
const [todos] = useOnyx(ONYXKEYS.DERIVED.TODOS);
8884
const transactionsByReportID = todos?.transactionsByReportID;
8985

90-
const [policiesWithEmptyReports] = useOnyx(
91-
ONYXKEYS.COLLECTION.REPORT,
92-
{
93-
selector: (reports: OnyxCollection<OnyxTypes.Report>) => {
94-
if (!accountID) {
95-
return {};
96-
}
97-
98-
return getPolicyIDsWithEmptyReportsForAccount(reports, accountID, transactionsByReportID ?? {});
99-
},
100-
},
101-
[accountID, transactionsByReportID],
102-
);
86+
const policiesWithEmptyReportsForAccountSelector = useMemo(() => policyIDsWithEmptyReportsSelector(accountID, transactionsByReportID ?? {}), [accountID, transactionsByReportID]);
87+
const [policiesWithEmptyReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: policiesWithEmptyReportsForAccountSelector});
10388

10489
const navigateToNewReport = (optimisticReportID: string) => {
10590
if (isRHPOnReportInSearch) {
@@ -117,15 +102,7 @@ function NewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelectionPag
117102
};
118103

119104
const createReport = (policyID: string, shouldDismissEmptyReportsConfirmation?: boolean) => {
120-
const optimisticReport = createNewReport(
121-
currentUserPersonalDetails,
122-
isASAPSubmitBetaEnabled,
123-
hasViolations,
124-
policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`],
125-
betas,
126-
false,
127-
shouldDismissEmptyReportsConfirmation,
128-
);
105+
const optimisticReport = createNewReport(policyID, shouldDismissEmptyReportsConfirmation);
129106
const selectedTransactionsKeys = Object.keys(selectedTransactions);
130107

131108
if (isMovingExpenses && (!!selectedTransactionsKeys.length || !!selectedTransactionIDs.length)) {

0 commit comments

Comments
 (0)