Skip to content

Commit ba0af54

Browse files
authored
Merge pull request Expensify#85570 from mukhrr/fix/83782
Show Create Report CTA for users without a workspace
2 parents 2a12806 + 4adb859 commit ba0af54

6 files changed

Lines changed: 629 additions & 239 deletions

File tree

src/components/Search/SearchPageHeader/SearchActionsBarCreateButton.tsx

Lines changed: 7 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,24 @@ import React, {useCallback, useMemo, useRef, useState} from 'react';
44
import {View} from 'react-native';
55
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
66
import Button from '@components/Button';
7-
import {ModalActions} from '@components/Modal/Global/ModalContext';
87
import type {PopoverMenuItem} from '@components/PopoverMenu';
98
import PopoverMenu from '@components/PopoverMenu';
10-
import useConfirmModal from '@hooks/useConfirmModal';
11-
import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation';
9+
import useCreateReportAction from '@hooks/useCreateReportAction';
1210
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
13-
import useHasEmptyReportsForPolicy from '@hooks/useHasEmptyReportsForPolicy';
1411
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
1512
import useLocalize from '@hooks/useLocalize';
1613
import useOnyx from '@hooks/useOnyx';
1714
import usePermissions from '@hooks/usePermissions';
18-
import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
1915
import usePopoverPosition from '@hooks/usePopoverPosition';
2016
import useThemeStyles from '@hooks/useThemeStyles';
2117
import {startDistanceRequest, startMoneyRequest} from '@libs/actions/IOU';
22-
import {openOldDotLink} from '@libs/actions/Link';
2318
import {createNewReport} from '@libs/actions/Report';
2419
import getIconForAction from '@libs/getIconForAction';
2520
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
2621
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
2722
import Navigation from '@libs/Navigation/Navigation';
28-
import {areAllGroupPoliciesExpenseChatDisabled, getDefaultChatEnabledPolicy} from '@libs/PolicyUtils';
23+
import {getDefaultChatEnabledPolicy} from '@libs/PolicyUtils';
2924
import {generateReportID, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
30-
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
3125
import CONST from '@src/CONST';
3226
import ONYXKEYS from '@src/ONYXKEYS';
3327
import ROUTES from '@src/ROUTES';
@@ -47,7 +41,6 @@ function SearchActionsBarCreateButton() {
4741
const [session] = useOnyx(ONYXKEYS.SESSION);
4842
const [email] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector});
4943
const [allBetas] = useOnyx(ONYXKEYS.BETAS);
50-
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
5144
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
5245
const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector});
5346
const groupPaidPoliciesWithChatEnabledSelector = useCallback((policies: OnyxCollection<OnyxTypes.Policy>) => groupPaidPoliciesWithExpenseChatEnabledSelector(policies, email), [email]);
@@ -58,37 +51,11 @@ function SearchActionsBarCreateButton() {
5851
const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? '');
5952
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
6053
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`);
61-
const [ownerBillingGraceEndPeriod] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
62-
const [userBillingGraceEndPeriods] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
63-
const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses();
64-
const shouldNavigateToUpgradePath = !policyForMovingExpensesID && !shouldSelectPolicy;
65-
const {showConfirmModal} = useConfirmModal();
66-
67-
const shouldRedirectToExpensifyClassic = useMemo(() => {
68-
return areAllGroupPoliciesExpenseChatDisabled((allPolicies as OnyxCollection<OnyxTypes.Policy>) ?? {});
69-
}, [allPolicies]);
7054

7155
const defaultChatEnabledPolicy = useMemo(
7256
() => getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array<OnyxEntry<OnyxTypes.Policy>>, activePolicy),
7357
[activePolicy, groupPoliciesWithChatEnabled],
7458
);
75-
const defaultChatEnabledPolicyID = defaultChatEnabledPolicy?.id;
76-
77-
const hasEmptyReport = useHasEmptyReportsForPolicy(defaultChatEnabledPolicyID);
78-
const [hasDismissedEmptyReportsConfirmation] = useOnyx(ONYXKEYS.NVP_EMPTY_REPORTS_CONFIRMATION_DISMISSED);
79-
const shouldShowEmptyReportConfirmationForDefaultChatEnabledPolicy = hasEmptyReport && hasDismissedEmptyReportsConfirmation !== true;
80-
81-
const showRedirectToExpensifyClassicModal = useCallback(async () => {
82-
const {action} = await showConfirmModal({
83-
title: translate('sidebarScreen.redirectToExpensifyClassicModal.title'),
84-
prompt: translate('sidebarScreen.redirectToExpensifyClassicModal.description'),
85-
confirmText: translate('exitSurvey.goToExpensifyClassic'),
86-
cancelText: translate('common.cancel'),
87-
});
88-
if (action === ModalActions.CONFIRM) {
89-
openOldDotLink(CONST.OLDDOT_URLS.INBOX);
90-
}
91-
}, [showConfirmModal, translate]);
9259

9360
const handleCreateWorkspaceReport = useCallback(
9461
(shouldDismissEmptyReportsConfirmation?: boolean) => {
@@ -116,10 +83,9 @@ function SearchActionsBarCreateButton() {
11683
[currentUserPersonalDetails, hasViolations, defaultChatEnabledPolicy, isASAPSubmitBetaEnabled, allBetas],
11784
);
11885

119-
const {openCreateReportConfirmation} = useCreateEmptyReportConfirmation({
120-
policyID: defaultChatEnabledPolicyID,
121-
policyName: defaultChatEnabledPolicy?.name ?? '',
122-
onConfirm: handleCreateWorkspaceReport,
86+
const {createReportAction} = useCreateReportAction({
87+
onCreateReport: handleCreateWorkspaceReport,
88+
groupPoliciesWithChatEnabled,
12389
});
12490

12591
const hideCreateMenu = useCallback(() => setIsCreateMenuActive(false), []);
@@ -157,71 +123,10 @@ function SearchActionsBarCreateButton() {
157123
{
158124
icon: expensifyIcons.Document,
159125
text: translate('report.newReport.createReport'),
160-
onSelected: () =>
161-
interceptAnonymousUser(() => {
162-
if (shouldRedirectToExpensifyClassic) {
163-
showRedirectToExpensifyClassicModal();
164-
return;
165-
}
166-
167-
// No valid policy at all → upgrade + create workspace flow
168-
if (shouldNavigateToUpgradePath) {
169-
const freshReportID = generateReportID();
170-
const freshTransactionID = generateReportID();
171-
Navigation.navigate(
172-
ROUTES.MONEY_REQUEST_UPGRADE.getRoute({
173-
action: CONST.IOU.ACTION.CREATE,
174-
iouType: CONST.IOU.TYPE.CREATE,
175-
transactionID: freshTransactionID,
176-
reportID: freshReportID,
177-
upgradePath: CONST.UPGRADE_PATHS.REPORTS,
178-
}),
179-
);
180-
return;
181-
}
182-
183-
const workspaceIDForReportCreation = defaultChatEnabledPolicyID;
184-
185-
// No default or restricted with multiple workspaces → workspace selector
186-
if (
187-
!workspaceIDForReportCreation ||
188-
(shouldRestrictUserBillableActions(workspaceIDForReportCreation, userBillingGraceEndPeriods, undefined, ownerBillingGraceEndPeriod) &&
189-
groupPoliciesWithChatEnabled.length > 1)
190-
) {
191-
Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute());
192-
return;
193-
}
194-
195-
// Default workspace is not restricted → create report directly
196-
if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation, userBillingGraceEndPeriods, undefined, ownerBillingGraceEndPeriod)) {
197-
// Check if empty report confirmation should be shown
198-
if (shouldShowEmptyReportConfirmationForDefaultChatEnabledPolicy) {
199-
openCreateReportConfirmation();
200-
} else {
201-
handleCreateWorkspaceReport(false);
202-
}
203-
return;
204-
}
205-
206-
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(workspaceIDForReportCreation));
207-
}),
126+
onSelected: createReportAction,
208127
},
209128
],
210-
[
211-
translate,
212-
expensifyIcons,
213-
draftTransactionIDs,
214-
shouldRedirectToExpensifyClassic,
215-
showRedirectToExpensifyClassicModal,
216-
shouldNavigateToUpgradePath,
217-
groupPoliciesWithChatEnabled.length,
218-
defaultChatEnabledPolicyID,
219-
shouldShowEmptyReportConfirmationForDefaultChatEnabledPolicy,
220-
ownerBillingGraceEndPeriod,
221-
userBillingGraceEndPeriods,
222-
openCreateReportConfirmation,
223-
handleCreateWorkspaceReport,
224-
],
129+
[translate, expensifyIcons, draftTransactionIDs, createReportAction],
225130
);
226131

227132
return (
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import {useCallback, useMemo} from 'react';
2+
import type {OnyxEntry} from 'react-native-onyx';
3+
import {ModalActions} from '@components/Modal/Global/ModalContext';
4+
import {closeReactNativeApp} from '@libs/actions/HybridApp';
5+
import {openOldDotLink} from '@libs/actions/Link';
6+
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
7+
import Navigation from '@libs/Navigation/Navigation';
8+
import {getDefaultChatEnabledPolicy} from '@libs/PolicyUtils';
9+
import {generateReportID} from '@libs/ReportUtils';
10+
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
11+
import CONFIG from '@src/CONFIG';
12+
import CONST from '@src/CONST';
13+
import ONYXKEYS from '@src/ONYXKEYS';
14+
import ROUTES from '@src/ROUTES';
15+
import {isTrackingSelector} from '@src/selectors/GPSDraftDetails';
16+
import {shouldRedirectToExpensifyClassicSelector} from '@src/selectors/Policy';
17+
import type * as OnyxTypes from '@src/types/onyx';
18+
import useConfirmModal from './useConfirmModal';
19+
import useCreateEmptyReportConfirmation from './useCreateEmptyReportConfirmation';
20+
import useHasEmptyReportsForPolicy from './useHasEmptyReportsForPolicy';
21+
import useLocalize from './useLocalize';
22+
import useOnyx from './useOnyx';
23+
24+
type UseCreateReportActionParams = {
25+
/** Callback to create the report and navigate after creation */
26+
onCreateReport: (shouldDismissEmptyReportsConfirmation?: boolean) => void;
27+
/** Group policies with expense chat enabled */
28+
groupPoliciesWithChatEnabled: readonly never[] | Array<OnyxEntry<OnyxTypes.Policy>>;
29+
/** Whether the modal should push a history entry so browser-back dismisses it (default: true) */
30+
shouldHandleNavigationBack?: boolean;
31+
};
32+
33+
type UseCreateReportActionResult = {
34+
/** The action to trigger when the user clicks "Create report" */
35+
createReportAction: () => void;
36+
};
37+
38+
/**
39+
* Hook that encapsulates the shared "create report" branching logic used across
40+
* the FAB, the search Create dropdown, and the empty reports state.
41+
*
42+
* Decision flow:
43+
* 1. Redirect to Expensify Classic if all group policies have expense chat disabled
44+
* 2. Navigate to upgrade path if user has no valid group policies at all
45+
* 3. Navigate to workspace selector if no default workspace or restricted with multiple options
46+
* 4. Show empty report confirmation or create directly if workspace is valid
47+
* 5. Navigate to restricted action if billing restricts the workspace
48+
*/
49+
export default function useCreateReportAction({onCreateReport, groupPoliciesWithChatEnabled, shouldHandleNavigationBack = true}: UseCreateReportActionParams): UseCreateReportActionResult {
50+
const {translate} = useLocalize();
51+
const {showConfirmModal} = useConfirmModal();
52+
53+
const [shouldRedirectToExpensifyClassic, policiesMeta] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: shouldRedirectToExpensifyClassicSelector});
54+
const arePoliciesLoaded = policiesMeta.status === 'loaded';
55+
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
56+
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`);
57+
const [ownerBillingGraceEndPeriod] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
58+
const [userBillingGraceEndPeriods] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
59+
const [isTrackingGPS = false] = useOnyx(ONYXKEYS.GPS_DRAFT_DETAILS, {selector: isTrackingSelector});
60+
61+
// User has no group policies at all (neither with chat enabled nor disabled) → needs upgrade/workspace creation
62+
const shouldNavigateToUpgradePath = !shouldRedirectToExpensifyClassic && groupPoliciesWithChatEnabled.length === 0;
63+
64+
const defaultChatEnabledPolicy = useMemo(
65+
() => getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array<OnyxEntry<OnyxTypes.Policy>>, activePolicy),
66+
[activePolicy, groupPoliciesWithChatEnabled],
67+
);
68+
const defaultChatEnabledPolicyID = defaultChatEnabledPolicy?.id;
69+
70+
const hasEmptyReport = useHasEmptyReportsForPolicy(defaultChatEnabledPolicyID);
71+
const [hasDismissedEmptyReportsConfirmation] = useOnyx(ONYXKEYS.NVP_EMPTY_REPORTS_CONFIRMATION_DISMISSED);
72+
const shouldShowEmptyReportConfirmation = hasEmptyReport && hasDismissedEmptyReportsConfirmation !== true;
73+
74+
const {openCreateReportConfirmation} = useCreateEmptyReportConfirmation({
75+
policyID: defaultChatEnabledPolicyID,
76+
policyName: defaultChatEnabledPolicy?.name ?? '',
77+
onConfirm: onCreateReport,
78+
});
79+
80+
const showRedirectToExpensifyClassicModal = useCallback(async () => {
81+
const {action} = await showConfirmModal({
82+
title: translate('sidebarScreen.redirectToExpensifyClassicModal.title'),
83+
prompt: translate('sidebarScreen.redirectToExpensifyClassicModal.description'),
84+
confirmText: translate('exitSurvey.goToExpensifyClassic'),
85+
cancelText: translate('common.cancel'),
86+
shouldHandleNavigationBack,
87+
});
88+
if (action !== ModalActions.CONFIRM) {
89+
return;
90+
}
91+
if (CONFIG.IS_HYBRID_APP) {
92+
closeReactNativeApp({shouldSetNVP: true, isTrackingGPS});
93+
return;
94+
}
95+
openOldDotLink(CONST.OLDDOT_URLS.INBOX);
96+
}, [showConfirmModal, translate, isTrackingGPS, shouldHandleNavigationBack]);
97+
98+
const createReportAction = useCallback(() => {
99+
interceptAnonymousUser(() => {
100+
// Wait for Onyx to hydrate policy data before making routing decisions
101+
if (!arePoliciesLoaded) {
102+
return;
103+
}
104+
105+
if (shouldRedirectToExpensifyClassic) {
106+
showRedirectToExpensifyClassicModal();
107+
return;
108+
}
109+
110+
// No valid policy at all → upgrade + create workspace flow
111+
if (shouldNavigateToUpgradePath) {
112+
const freshReportID = generateReportID();
113+
const freshTransactionID = generateReportID();
114+
Navigation.navigate(
115+
ROUTES.MONEY_REQUEST_UPGRADE.getRoute({
116+
action: CONST.IOU.ACTION.CREATE,
117+
iouType: CONST.IOU.TYPE.CREATE,
118+
transactionID: freshTransactionID,
119+
reportID: freshReportID,
120+
upgradePath: CONST.UPGRADE_PATHS.REPORTS,
121+
}),
122+
);
123+
return;
124+
}
125+
126+
const workspaceIDForReportCreation = defaultChatEnabledPolicyID;
127+
128+
if (
129+
!workspaceIDForReportCreation ||
130+
(shouldRestrictUserBillableActions(workspaceIDForReportCreation, userBillingGraceEndPeriods, undefined, ownerBillingGraceEndPeriod) &&
131+
groupPoliciesWithChatEnabled.length > 1)
132+
) {
133+
Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute());
134+
return;
135+
}
136+
137+
if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation, userBillingGraceEndPeriods, undefined, ownerBillingGraceEndPeriod)) {
138+
if (shouldShowEmptyReportConfirmation) {
139+
openCreateReportConfirmation();
140+
} else {
141+
onCreateReport(false);
142+
}
143+
return;
144+
}
145+
146+
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(workspaceIDForReportCreation));
147+
});
148+
}, [
149+
arePoliciesLoaded,
150+
shouldRedirectToExpensifyClassic,
151+
showRedirectToExpensifyClassicModal,
152+
shouldNavigateToUpgradePath,
153+
defaultChatEnabledPolicyID,
154+
userBillingGraceEndPeriods,
155+
ownerBillingGraceEndPeriod,
156+
groupPoliciesWithChatEnabled.length,
157+
shouldShowEmptyReportConfirmation,
158+
openCreateReportConfirmation,
159+
onCreateReport,
160+
]);
161+
162+
return {createReportAction};
163+
}

0 commit comments

Comments
 (0)