Skip to content

Commit dc22562

Browse files
authored
Merge pull request Expensify#83975 from mukhrr/fix/create-reports
Improve create report flow from search page with workspace validation
2 parents 77f397d + e1df428 commit dc22562

3 files changed

Lines changed: 417 additions & 19 deletions

File tree

src/components/Search/SearchPageHeader/SearchFiltersBarCreateButton.tsx

Lines changed: 147 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
11
import {emailSelector} from '@selectors/Session';
22
import React, {useCallback, useMemo, useRef, useState} from 'react';
33
import {View} from 'react-native';
4-
import type {OnyxCollection} from 'react-native-onyx';
4+
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
55
import Button from '@components/Button';
6+
import {ModalActions} from '@components/Modal/Global/ModalContext';
67
import type {PopoverMenuItem} from '@components/PopoverMenu';
78
import PopoverMenu from '@components/PopoverMenu';
9+
import useConfirmModal from '@hooks/useConfirmModal';
10+
import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation';
11+
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
12+
import useHasEmptyReportsForPolicy from '@hooks/useHasEmptyReportsForPolicy';
813
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
914
import useLocalize from '@hooks/useLocalize';
1015
import useOnyx from '@hooks/useOnyx';
16+
import usePermissions from '@hooks/usePermissions';
17+
import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
1118
import usePopoverPosition from '@hooks/usePopoverPosition';
1219
import useThemeStyles from '@hooks/useThemeStyles';
1320
import {startDistanceRequest, startMoneyRequest} from '@libs/actions/IOU';
21+
import {openOldDotLink} from '@libs/actions/Link';
22+
import {createNewReport} from '@libs/actions/Report';
1423
import getIconForAction from '@libs/getIconForAction';
1524
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
25+
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
1626
import Navigation from '@libs/Navigation/Navigation';
17-
import {generateReportID} from '@libs/ReportUtils';
27+
import {areAllGroupPoliciesExpenseChatDisabled, getDefaultChatEnabledPolicy} from '@libs/PolicyUtils';
28+
import {generateReportID, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
29+
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
1830
import CONST from '@src/CONST';
1931
import ONYXKEYS from '@src/ONYXKEYS';
2032
import ROUTES from '@src/ROUTES';
21-
import {groupPaidPoliciesWithExpenseChatEnabledSelector, shouldRedirectToExpensifyClassicSelector} from '@src/selectors/Policy';
33+
import {groupPaidPoliciesWithExpenseChatEnabledSelector} from '@src/selectors/Policy';
2234
import type * as OnyxTypes from '@src/types/onyx';
2335

2436
function SearchFiltersBarCreateButton() {
@@ -31,11 +43,80 @@ function SearchFiltersBarCreateButton() {
3143
const [createMenuPosition, setCreateMenuPosition] = useState<{horizontal: number; vertical: number}>({horizontal: 0, vertical: 0});
3244
const {calculatePopoverPosition} = usePopoverPosition();
3345

46+
const [session] = useOnyx(ONYXKEYS.SESSION);
3447
const [email] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector});
48+
const [allBetas] = useOnyx(ONYXKEYS.BETAS);
49+
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
50+
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
3551
const groupPaidPoliciesWithChatEnabledSelector = useCallback((policies: OnyxCollection<OnyxTypes.Policy>) => groupPaidPoliciesWithExpenseChatEnabledSelector(policies, email), [email]);
3652
const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPaidPoliciesWithChatEnabledSelector}, [email]);
37-
const [shouldRedirectToExpensifyClassic = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: shouldRedirectToExpensifyClassicSelector});
38-
const shouldShowCreateReportOption = shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0;
53+
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
54+
const {isBetaEnabled} = usePermissions();
55+
const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
56+
const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? '');
57+
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
58+
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`);
59+
const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses();
60+
const shouldNavigateToUpgradePath = !policyForMovingExpensesID && !shouldSelectPolicy;
61+
const {showConfirmModal} = useConfirmModal();
62+
63+
const shouldRedirectToExpensifyClassic = useMemo(() => {
64+
return areAllGroupPoliciesExpenseChatDisabled((allPolicies as OnyxCollection<OnyxTypes.Policy>) ?? {});
65+
}, [allPolicies]);
66+
67+
const defaultChatEnabledPolicy = useMemo(
68+
() => getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array<OnyxEntry<OnyxTypes.Policy>>, activePolicy),
69+
[activePolicy, groupPoliciesWithChatEnabled],
70+
);
71+
const defaultChatEnabledPolicyID = defaultChatEnabledPolicy?.id;
72+
73+
const hasEmptyReport = useHasEmptyReportsForPolicy(defaultChatEnabledPolicyID);
74+
const [hasDismissedEmptyReportsConfirmation] = useOnyx(ONYXKEYS.NVP_EMPTY_REPORTS_CONFIRMATION_DISMISSED);
75+
const shouldShowEmptyReportConfirmationForDefaultChatEnabledPolicy = hasEmptyReport && hasDismissedEmptyReportsConfirmation !== true;
76+
77+
const showRedirectToExpensifyClassicModal = useCallback(async () => {
78+
const {action} = await showConfirmModal({
79+
title: translate('sidebarScreen.redirectToExpensifyClassicModal.title'),
80+
prompt: translate('sidebarScreen.redirectToExpensifyClassicModal.description'),
81+
confirmText: translate('exitSurvey.goToExpensifyClassic'),
82+
cancelText: translate('common.cancel'),
83+
});
84+
if (action === ModalActions.CONFIRM) {
85+
openOldDotLink(CONST.OLDDOT_URLS.INBOX);
86+
}
87+
}, [showConfirmModal, translate]);
88+
89+
const handleCreateWorkspaceReport = useCallback(
90+
(shouldDismissEmptyReportsConfirmation?: boolean) => {
91+
if (!defaultChatEnabledPolicy?.id) {
92+
return;
93+
}
94+
95+
const {reportID: createdReportID} = createNewReport(
96+
currentUserPersonalDetails,
97+
hasViolations,
98+
isASAPSubmitBetaEnabled,
99+
defaultChatEnabledPolicy,
100+
allBetas,
101+
false,
102+
shouldDismissEmptyReportsConfirmation,
103+
);
104+
Navigation.setNavigationActionToMicrotaskQueue(() => {
105+
Navigation.navigate(
106+
isSearchTopmostFullScreenRoute()
107+
? ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: createdReportID, backTo: Navigation.getActiveRoute()})
108+
: ROUTES.REPORT_WITH_ID.getRoute(createdReportID, undefined, undefined, Navigation.getActiveRoute()),
109+
);
110+
});
111+
},
112+
[currentUserPersonalDetails, hasViolations, defaultChatEnabledPolicy, isASAPSubmitBetaEnabled, allBetas],
113+
);
114+
115+
const {openCreateReportConfirmation, CreateReportConfirmationModal} = useCreateEmptyReportConfirmation({
116+
policyID: defaultChatEnabledPolicyID,
117+
policyName: defaultChatEnabledPolicy?.name ?? '',
118+
onConfirm: handleCreateWorkspaceReport,
119+
});
39120

40121
const hideCreateMenu = useCallback(() => setIsCreateMenuActive(false), []);
41122
const showCreateMenu = useCallback(() => {
@@ -69,24 +150,72 @@ function SearchFiltersBarCreateButton() {
69150
startDistanceRequest(CONST.IOU.TYPE.CREATE, generateReportID());
70151
}),
71152
},
72-
...(shouldShowCreateReportOption
73-
? [
74-
{
75-
icon: expensifyIcons.Document,
76-
text: translate('report.newReport.createReport'),
77-
onSelected: () =>
78-
interceptAnonymousUser(() => {
79-
Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute());
80-
}),
81-
},
82-
]
83-
: []),
153+
{
154+
icon: expensifyIcons.Document,
155+
text: translate('report.newReport.createReport'),
156+
onSelected: () =>
157+
interceptAnonymousUser(() => {
158+
if (shouldRedirectToExpensifyClassic) {
159+
showRedirectToExpensifyClassicModal();
160+
return;
161+
}
162+
163+
// No valid policy at all → upgrade + create workspace flow
164+
if (shouldNavigateToUpgradePath) {
165+
const freshReportID = generateReportID();
166+
const freshTransactionID = generateReportID();
167+
Navigation.navigate(
168+
ROUTES.MONEY_REQUEST_UPGRADE.getRoute({
169+
action: CONST.IOU.ACTION.CREATE,
170+
iouType: CONST.IOU.TYPE.CREATE,
171+
transactionID: freshTransactionID,
172+
reportID: freshReportID,
173+
upgradePath: CONST.UPGRADE_PATHS.REPORTS,
174+
}),
175+
);
176+
return;
177+
}
178+
179+
const workspaceIDForReportCreation = defaultChatEnabledPolicyID;
180+
181+
// No default or restricted with multiple workspaces → workspace selector
182+
if (!workspaceIDForReportCreation || (shouldRestrictUserBillableActions(workspaceIDForReportCreation) && groupPoliciesWithChatEnabled.length > 1)) {
183+
Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute());
184+
return;
185+
}
186+
187+
// Default workspace is not restricted → create report directly
188+
if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) {
189+
// Check if empty report confirmation should be shown
190+
if (shouldShowEmptyReportConfirmationForDefaultChatEnabledPolicy) {
191+
openCreateReportConfirmation();
192+
} else {
193+
handleCreateWorkspaceReport(false);
194+
}
195+
return;
196+
}
197+
198+
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(workspaceIDForReportCreation));
199+
}),
200+
},
201+
],
202+
[
203+
translate,
204+
expensifyIcons,
205+
shouldRedirectToExpensifyClassic,
206+
showRedirectToExpensifyClassicModal,
207+
shouldNavigateToUpgradePath,
208+
groupPoliciesWithChatEnabled.length,
209+
defaultChatEnabledPolicyID,
210+
shouldShowEmptyReportConfirmationForDefaultChatEnabledPolicy,
211+
openCreateReportConfirmation,
212+
handleCreateWorkspaceReport,
84213
],
85-
[translate, expensifyIcons, shouldShowCreateReportOption],
86214
);
87215

88216
return (
89217
<View style={[styles.pr5, styles.searchFiltersBarCreateButton]}>
218+
{CreateReportConfirmationModal}
90219
<PopoverMenu
91220
onClose={hideCreateMenu}
92221
isVisible={isCreateMenuActive}

src/pages/iou/request/step/IOURequestStepUpgrade.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,12 @@ function IOURequestStepUpgrade({
117117
}
118118
case CONST.UPGRADE_PATHS.REPORTS:
119119
Navigation.goBack();
120-
navigateWithMicrotask(ROUTES.MONEY_REQUEST_STEP_REPORT.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, reportID));
120+
if (action === CONST.IOU.ACTION.CREATE) {
121+
// Coming from "Create report" button (no workspace) → go to workspace selection which creates the report
122+
navigateWithMicrotask(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute());
123+
} else {
124+
navigateWithMicrotask(ROUTES.MONEY_REQUEST_STEP_REPORT.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, reportID));
125+
}
121126

122127
break;
123128
case CONST.UPGRADE_PATHS.CATEGORIES:

0 commit comments

Comments
 (0)