From b13d7bbc1139370befa516d90c88e37514a8bb65 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 23 Feb 2026 12:35:55 +0100 Subject: [PATCH 01/54] Refactor FloatingActionButtonAndPopover into composable pieces Decompose the 765-line monolith into focused hooks and components: - Extract 9 menu item hooks that each own their Onyx subscriptions - Add FABPopoverContent guard/inner pattern for lazy mount/unmount - Move dragover listener into the component, eliminating imperative ref - Simplify NavigationTabBarFloatingActionButton to a re-export - Move PolicySelector type to FABPopoverContent/types.ts --- src/libs/actions/Policy/Policy.ts | 2 +- .../FABPopoverContent/FABPopoverContent.tsx | 54 ++ .../FABPopoverContentInner.tsx | 114 +++ .../inbox/sidebar/FABPopoverContent/index.ts | 1 + .../menuItems/useCreateReportMenuItem.ts | 166 ++++ .../menuItems/useExpenseMenuItem.ts | 46 ++ .../menuItems/useInvoiceMenuItem.ts | 52 ++ .../menuItems/useNewChatMenuItem.ts | 31 + .../menuItems/useNewWorkspaceMenuItem.ts | 64 ++ .../menuItems/useQuickActionMenuItem.ts | 225 ++++++ .../menuItems/useTestDriveMenuItem.ts | 45 ++ .../menuItems/useTrackDistanceMenuItem.ts | 45 ++ .../menuItems/useTravelMenuItem.ts | 67 ++ .../inbox/sidebar/FABPopoverContent/types.ts | 37 + .../useRedirectToExpensifyClassic.ts | 48 ++ .../FABPopoverContent/useScanActions.ts | 67 ++ .../FloatingActionButtonAndPopover.tsx | 736 ++---------------- .../index.tsx | 40 +- .../types.ts | 5 - 19 files changed, 1110 insertions(+), 735 deletions(-) create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/index.ts create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useCreateReportMenuItem.ts create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useExpenseMenuItem.ts create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useInvoiceMenuItem.ts create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useNewChatMenuItem.ts create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useNewWorkspaceMenuItem.ts create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTestDriveMenuItem.ts create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTrackDistanceMenuItem.ts create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTravelMenuItem.ts create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/types.ts create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts delete mode 100644 src/pages/inbox/sidebar/NavigationTabBarFloatingActionButton/types.ts diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index b5e26c82ab552..9c1c8b13bfbfb 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -93,7 +93,7 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import {getCustomUnitsForDuplication, getMemberAccountIDsForWorkspace, goBackWhenEnableFeature, isControlPolicy, navigateToExpensifyCardPage} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import {hasValidModifiedAmount} from '@libs/TransactionUtils'; -import type {PolicySelector} from '@pages/inbox/sidebar/FloatingActionButtonAndPopover'; +import type {PolicySelector} from '@pages/inbox/sidebar/FABPopoverContent/types'; import type {Feature} from '@pages/OnboardingInterestedFeatures/types'; import * as PaymentMethods from '@userActions/PaymentMethods'; import * as PersistedRequests from '@userActions/PersistedRequests'; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx new file mode 100644 index 0000000000000..2137c1ceac5b4 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import type {OnyxCollection} from 'react-native-onyx'; +import type * as OnyxTypes from '@src/types/onyx'; +import FABPopoverContentInner from './FABPopoverContentInner'; +import type {FABPopoverContentProps} from './types'; + +type FABPopoverContentExtraProps = FABPopoverContentProps & { + reportID: string; + activePolicyID: string | undefined; + session: {email?: string; accountID?: number} | undefined; + policyChatForActivePolicy: OnyxTypes.Report | undefined; + allTransactionDrafts: OnyxCollection; +}; + +function FABPopoverContent({ + isMenuMounted, + isVisible, + onClose, + onItemSelected, + onModalHide, + anchorPosition, + anchorRef, + shouldUseNarrowLayout, + reportID, + activePolicyID, + session, + policyChatForActivePolicy, + allTransactionDrafts, +}: FABPopoverContentExtraProps) { + if (!isMenuMounted) { + return null; + } + + return ( + + ); +} + +FABPopoverContent.displayName = 'FABPopoverContent'; + +export default FABPopoverContent; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx new file mode 100644 index 0000000000000..0b0d927e2e0e5 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx @@ -0,0 +1,114 @@ +import React, {useMemo} from 'react'; +import type {OnyxCollection} from 'react-native-onyx'; +import PopoverMenu from '@components/PopoverMenu'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction'; +import CONST from '@src/CONST'; +import type * as OnyxTypes from '@src/types/onyx'; +import useCreateReportMenuItem from './menuItems/useCreateReportMenuItem'; +import useExpenseMenuItem from './menuItems/useExpenseMenuItem'; +import useInvoiceMenuItem from './menuItems/useInvoiceMenuItem'; +import useNewChatMenuItem from './menuItems/useNewChatMenuItem'; +import useNewWorkspaceMenuItem from './menuItems/useNewWorkspaceMenuItem'; +import useQuickActionMenuItem from './menuItems/useQuickActionMenuItem'; +import useTestDriveMenuItem from './menuItems/useTestDriveMenuItem'; +import useTrackDistanceMenuItem from './menuItems/useTrackDistanceMenuItem'; +import useTravelMenuItem from './menuItems/useTravelMenuItem'; +import type {FABPopoverContentInnerProps} from './types'; + +type FABPopoverContentInnerExtraProps = FABPopoverContentInnerProps & { + reportID: string; + activePolicyID: string | undefined; + session: {email?: string; accountID?: number} | undefined; + policyChatForActivePolicy: OnyxTypes.Report | undefined; + allTransactionDrafts: OnyxCollection; +}; + +function FABPopoverContentInner({ + isVisible, + onClose, + onItemSelected, + onModalHide, + anchorPosition, + anchorRef, + shouldUseNarrowLayout, + reportID, + activePolicyID, + session, + policyChatForActivePolicy, + allTransactionDrafts, +}: FABPopoverContentInnerExtraProps) { + const icons = useMemoizedLazyExpensifyIcons([ + 'CalendarSolid', + 'Document', + 'NewWorkspace', + 'NewWindow', + 'Binoculars', + 'Car', + 'Location', + 'Suitcase', + 'Task', + 'InvoiceGeneric', + 'ReceiptScan', + 'ChatBubble', + 'Coins', + 'Receipt', + 'Cash', + 'Transfer', + 'MoneyCircle', + 'Clock', + ] as const); + + const expenseItem = useExpenseMenuItem({shouldUseNarrowLayout, icons, reportID}); + const trackDistanceItem = useTrackDistanceMenuItem({shouldUseNarrowLayout, icons, reportID}); + const {menuItem: createReportItem, confirmationModal} = useCreateReportMenuItem({shouldUseNarrowLayout, icons, activePolicyID}); + const newChatItem = useNewChatMenuItem({shouldUseNarrowLayout, icons}); + const invoiceItem = useInvoiceMenuItem({shouldUseNarrowLayout, icons, reportID, allTransactionDrafts}); + const travelItem = useTravelMenuItem({icons, activePolicyID}); + const testDriveItem = useTestDriveMenuItem({icons}); + const newWorkspaceItem = useNewWorkspaceMenuItem({shouldUseNarrowLayout, icons}); + const quickActionItem = useQuickActionMenuItem({ + shouldUseNarrowLayout, + icons, + reportID, + session, + policyChatForActivePolicy, + allTransactionDrafts, + }); + + const menuItems = useMemo( + () => [...expenseItem, ...trackDistanceItem, ...createReportItem, ...newChatItem, ...invoiceItem, ...travelItem, ...testDriveItem, ...newWorkspaceItem, ...quickActionItem], + [expenseItem, trackDistanceItem, createReportItem, newChatItem, invoiceItem, travelItem, testDriveItem, newWorkspaceItem, quickActionItem], + ); + + return ( + <> + {confirmationModal} + ({ + ...item, + onSelected: () => { + if (!item.onSelected) { + return; + } + navigateAfterInteraction(item.onSelected); + }, + }))} + anchorRef={anchorRef} + /> + + ); +} + +FABPopoverContentInner.displayName = 'FABPopoverContentInner'; + +export default FABPopoverContentInner; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/index.ts b/src/pages/inbox/sidebar/FABPopoverContent/index.ts new file mode 100644 index 0000000000000..b11f5764e3e09 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/index.ts @@ -0,0 +1 @@ +export {default} from './FABPopoverContent'; // eslint-disable-line no-restricted-exports diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useCreateReportMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useCreateReportMenuItem.ts new file mode 100644 index 0000000000000..ce8972e998b00 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useCreateReportMenuItem.ts @@ -0,0 +1,166 @@ +import type {ReactNode} from 'react'; +import {useCallback, useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; +import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useHasEmptyReportsForPolicy from '@hooks/useHasEmptyReportsForPolicy'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; +import {createNewReport} from '@libs/actions/Report'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; +import Navigation from '@libs/Navigation/Navigation'; +import {getDefaultChatEnabledPolicy} from '@libs/PolicyUtils'; +import {hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; +import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; +import {groupPaidPoliciesWithExpenseChatEnabledSelector} from '@selectors/Policy'; +import isOnSearchMoneyRequestReportPage from '@navigation/helpers/isOnSearchMoneyRequestReportPage'; +import {clearLastSearchParams} from '@userActions/ReportNavigation'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; + +type UseCreateReportMenuItemParams = { + shouldUseNarrowLayout: boolean; + icons: MenuItemIcons; + activePolicyID: string | undefined; +}; + +type UseCreateReportMenuItemResult = { + menuItem: PopoverMenuItem[]; + confirmationModal: ReactNode; +}; + +const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); + +function useCreateReportMenuItem({shouldUseNarrowLayout, icons, activePolicyID}: UseCreateReportMenuItemParams): UseCreateReportMenuItemResult { + const {translate} = useLocalize(); + const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); + const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); + const [hasDismissedEmptyReportsConfirmation] = useOnyx(ONYXKEYS.NVP_EMPTY_REPORTS_CONFIRMATION_DISMISSED, {canBeMissing: true}); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const {isBetaEnabled} = usePermissions(); + const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); + const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); + + const groupPaidPoliciesWithChatEnabled = useCallback( + (policies: Parameters[0]) => groupPaidPoliciesWithExpenseChatEnabledSelector(policies, session?.email), + [session?.email], + ); + + const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPaidPoliciesWithChatEnabled, canBeMissing: true}, [session?.email]); + + const shouldShowCreateReportOption = shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; + + const defaultChatEnabledPolicy = useMemo( + () => getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array>, activePolicy), + [activePolicy, groupPoliciesWithChatEnabled], + ); + + const defaultChatEnabledPolicyID = defaultChatEnabledPolicy?.id; + const hasEmptyReport = useHasEmptyReportsForPolicy(defaultChatEnabledPolicyID); + const shouldShowEmptyReportConfirmation = hasEmptyReport && hasDismissedEmptyReportsConfirmation !== true; + + const isReportInSearch = isOnSearchMoneyRequestReportPage(); + + const handleCreateWorkspaceReport = useCallback( + (shouldDismissEmptyReportsConfirmation?: boolean) => { + if (!defaultChatEnabledPolicy?.id) { + return; + } + + if (isReportInSearch) { + clearLastSearchParams(); + } + + const {reportID: createdReportID} = createNewReport( + currentUserPersonalDetails, + hasViolations, + isASAPSubmitBetaEnabled, + defaultChatEnabledPolicy, + allBetas, + false, + shouldDismissEmptyReportsConfirmation, + ); + Navigation.setNavigationActionToMicrotaskQueue(() => { + Navigation.navigate( + isSearchTopmostFullScreenRoute() + ? ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: createdReportID, backTo: Navigation.getActiveRoute()}) + : ROUTES.REPORT_WITH_ID.getRoute(createdReportID, undefined, undefined, Navigation.getActiveRoute()), + {forceReplace: isReportInSearch}, + ); + }); + }, + [currentUserPersonalDetails, hasViolations, defaultChatEnabledPolicy, isASAPSubmitBetaEnabled, isReportInSearch, allBetas], + ); + + const {openCreateReportConfirmation, CreateReportConfirmationModal} = useCreateEmptyReportConfirmation({ + policyID: defaultChatEnabledPolicyID, + policyName: defaultChatEnabledPolicy?.name ?? '', + onConfirm: handleCreateWorkspaceReport, + }); + + const menuItem = useMemo(() => { + if (!shouldShowCreateReportOption) { + return []; + } + return [ + { + icon: icons.Document, + text: translate('report.newReport.createReport'), + shouldCallAfterModalHide: shouldUseNarrowLayout, + onSelected: () => { + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + + const workspaceIDForReportCreation = defaultChatEnabledPolicyID; + + if (!workspaceIDForReportCreation || (shouldRestrictUserBillableActions(workspaceIDForReportCreation) && groupPoliciesWithChatEnabled.length > 1)) { + Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute()); + return; + } + + if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { + if (shouldShowEmptyReportConfirmation) { + openCreateReportConfirmation(); + } else { + handleCreateWorkspaceReport(false); + } + return; + } + + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(workspaceIDForReportCreation)); + }); + }, + sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.CREATE_REPORT, + }, + ]; + }, [ + shouldShowCreateReportOption, + icons.Document, + translate, + shouldUseNarrowLayout, + shouldRedirectToExpensifyClassic, + showRedirectToExpensifyClassicModal, + defaultChatEnabledPolicyID, + groupPoliciesWithChatEnabled.length, + shouldShowEmptyReportConfirmation, + openCreateReportConfirmation, + handleCreateWorkspaceReport, + ]); + + return {menuItem, confirmationModal: CreateReportConfirmationModal}; +} + +export default useCreateReportMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useExpenseMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useExpenseMenuItem.ts new file mode 100644 index 0000000000000..e98a15c3bd110 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useExpenseMenuItem.ts @@ -0,0 +1,46 @@ +import {useMemo} from 'react'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import {startMoneyRequest} from '@libs/actions/IOU'; +import getIconForAction from '@libs/getIconForAction'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; + +type UseExpenseMenuItemParams = { + shouldUseNarrowLayout: boolean; + icons: MenuItemIcons; + reportID: string; +}; + +function useExpenseMenuItem({shouldUseNarrowLayout, icons, reportID}: UseExpenseMenuItemParams): PopoverMenuItem[] { + const {translate} = useLocalize(); + const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); + const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); + + return useMemo( + () => [ + { + icon: getIconForAction(CONST.IOU.TYPE.CREATE, icons as Parameters[1]), + text: translate('iou.createExpense'), + testID: 'create-expense', + shouldCallAfterModalHide: shouldRedirectToExpensifyClassic || shouldUseNarrowLayout, + onSelected: () => + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); + }), + sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.CREATE_EXPENSE, + }, + ], + [translate, shouldRedirectToExpensifyClassic, shouldUseNarrowLayout, allTransactionDrafts, reportID, icons, showRedirectToExpensifyClassicModal], + ); +} + +export default useExpenseMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useInvoiceMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useInvoiceMenuItem.ts new file mode 100644 index 0000000000000..ef8296bbcd37d --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useInvoiceMenuItem.ts @@ -0,0 +1,52 @@ +import {useMemo} from 'react'; +import type {OnyxCollection} from 'react-native-onyx'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import {startMoneyRequest} from '@libs/actions/IOU'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import {canSendInvoice as canSendInvoicePolicyUtils} from '@libs/PolicyUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; + +type UseInvoiceMenuItemParams = { + shouldUseNarrowLayout: boolean; + icons: MenuItemIcons; + reportID: string; + allTransactionDrafts: OnyxCollection; +}; + +function useInvoiceMenuItem({shouldUseNarrowLayout, icons, reportID, allTransactionDrafts}: UseInvoiceMenuItemParams): PopoverMenuItem[] { + const {translate} = useLocalize(); + const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal, allPolicies} = useRedirectToExpensifyClassic(); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); + + const canSendInvoice = useMemo(() => canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email), [allPolicies, session?.email]); + + return useMemo(() => { + if (!canSendInvoice) { + return []; + } + return [ + { + icon: icons.InvoiceGeneric, + text: translate('workspace.invoices.sendInvoice'), + shouldCallAfterModalHide: shouldRedirectToExpensifyClassic || shouldUseNarrowLayout, + onSelected: () => + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + startMoneyRequest(CONST.IOU.TYPE.INVOICE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); + }), + sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.SEND_INVOICE, + }, + ]; + }, [canSendInvoice, icons.InvoiceGeneric, translate, shouldRedirectToExpensifyClassic, shouldUseNarrowLayout, showRedirectToExpensifyClassicModal, reportID, allTransactionDrafts]); +} + +export default useInvoiceMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useNewChatMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useNewChatMenuItem.ts new file mode 100644 index 0000000000000..3adfd632341c5 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useNewChatMenuItem.ts @@ -0,0 +1,31 @@ +import {useMemo} from 'react'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; +import useLocalize from '@hooks/useLocalize'; +import {startNewChat} from '@libs/actions/Report'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import CONST from '@src/CONST'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; + +type UseNewChatMenuItemParams = { + shouldUseNarrowLayout: boolean; + icons: MenuItemIcons; +}; + +function useNewChatMenuItem({shouldUseNarrowLayout, icons}: UseNewChatMenuItemParams): PopoverMenuItem[] { + const {translate} = useLocalize(); + + return useMemo( + () => [ + { + icon: icons.ChatBubble, + text: translate('sidebarScreen.fabNewChat'), + shouldCallAfterModalHide: shouldUseNarrowLayout, + onSelected: () => interceptAnonymousUser(startNewChat), + sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.START_CHAT, + }, + ], + [icons.ChatBubble, translate, shouldUseNarrowLayout], + ); +} + +export default useNewChatMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useNewWorkspaceMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useNewWorkspaceMenuItem.ts new file mode 100644 index 0000000000000..e2bea5b4f0082 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useNewWorkspaceMenuItem.ts @@ -0,0 +1,64 @@ +import {useMemo} from 'react'; +import type {ImageContentFit} from 'expo-image'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; +import useLocalize from '@hooks/useLocalize'; +import useMappedPolicies from '@hooks/useMappedPolicies'; +import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; +import usePreferredPolicy from '@hooks/usePreferredPolicy'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import Navigation from '@libs/Navigation/Navigation'; +import {shouldShowPolicy} from '@libs/PolicyUtils'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import {policyMapper} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; + +type UseNewWorkspaceMenuItemParams = { + shouldUseNarrowLayout: boolean; + icons: MenuItemIcons; +}; + +function useNewWorkspaceMenuItem({shouldUseNarrowLayout, icons}: UseNewWorkspaceMenuItemParams): PopoverMenuItem[] { + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); + const [isLoading = false] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); + const [allPolicies] = useMappedPolicies(policyMapper); + const {isRestrictedPolicyCreation} = usePreferredPolicy(); + + const shouldShowNewWorkspaceButton = useMemo(() => { + if (isRestrictedPolicyCreation) { + return false; + } + const isOfflineBool = !!isOffline; + const email = session?.email; + return Object.values(allPolicies ?? {}).every((policy) => !shouldShowPolicy(policy as OnyxEntry, isOfflineBool, email)); + }, [isRestrictedPolicyCreation, allPolicies, isOffline, session?.email]); + + return useMemo(() => { + if (isLoading || !shouldShowNewWorkspaceButton) { + return []; + } + return [ + { + displayInDefaultIconColor: true, + contentFit: 'contain' as ImageContentFit, + icon: icons.NewWorkspace, + iconWidth: variables.w46, + iconHeight: variables.h40, + text: translate('workspace.new.newWorkspace'), + description: translate('workspace.new.getTheExpensifyCardAndMore'), + shouldCallAfterModalHide: shouldUseNarrowLayout, + onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(Navigation.getActiveRoute()))), + sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.NEW_WORKSPACE, + }, + ]; + }, [isLoading, shouldShowNewWorkspaceButton, icons.NewWorkspace, translate, shouldUseNarrowLayout]); +} + +export default useNewWorkspaceMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts new file mode 100644 index 0000000000000..48b739e544258 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts @@ -0,0 +1,225 @@ +import {useCallback, useMemo} from 'react'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import usePreferredPolicy from '@hooks/usePreferredPolicy'; +import useReportIsArchived from '@hooks/useReportIsArchived'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {startMoneyRequest} from '@libs/actions/IOU'; +import {navigateToQuickAction} from '@libs/actions/QuickActionNavigation'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import Navigation from '@libs/Navigation/Navigation'; +import {getQuickActionIcon, getQuickActionTitle, isQuickActionAllowed} from '@libs/QuickActionUtils'; +import { + getDisplayNameForParticipant, + getIcons, + // Will be fixed in https://github.com/Expensify/App/issues/76852 + // eslint-disable-next-line @typescript-eslint/no-deprecated + getReportName, + isPolicyExpenseChat, +} from '@libs/ReportUtils'; +import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {QuickActionName} from '@src/types/onyx/QuickAction'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type UseQuickActionMenuItemParams = { + shouldUseNarrowLayout: boolean; + icons: MenuItemIcons; + reportID: string; + session: {email?: string; accountID?: number} | undefined; + policyChatForActivePolicy: OnyxEntry; + allTransactionDrafts: OnyxCollection; +}; + +function useQuickActionMenuItem({shouldUseNarrowLayout, icons, reportID, session, policyChatForActivePolicy, allTransactionDrafts}: UseQuickActionMenuItemParams): PopoverMenuItem[] { + const styles = useThemeStyles(); + const {translate, formatPhoneNumber} = useLocalize(); + const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, {canBeMissing: true}); + const [quickActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`, {canBeMissing: true}); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); + const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); + const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const {isDelegateAccessRestricted} = useDelegateNoAccessState(); + const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); + const isReportArchived = useReportIsArchived(quickActionReport?.reportID); + const {isRestrictedToPreferredPolicy} = usePreferredPolicy(); + + const quickActionPolicyID = quickAction?.action === CONST.QUICK_ACTIONS.TRACK_PER_DIEM && quickAction?.perDiemPolicyID ? quickAction?.perDiemPolicyID : quickActionReport?.policyID; + const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionPolicyID}`, {canBeMissing: true}); + + const isValidReport = !(isEmptyObject(quickActionReport) || isReportArchived); + + const selectOption = useCallback( + (onSelected: () => void, shouldRestrictAction: boolean) => { + if (shouldRestrictAction && quickActionReport?.policyID && shouldRestrictUserBillableActions(quickActionReport.policyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(quickActionReport.policyID)); + return; + } + onSelected(); + }, + [quickActionReport?.policyID], + ); + + const quickActionAvatars = useMemo(() => { + if (isValidReport) { + const avatars = getIcons(quickActionReport, formatPhoneNumber, personalDetails, null, undefined, undefined, undefined, undefined, isReportArchived); + return avatars.length <= 1 || isPolicyExpenseChat(quickActionReport) ? avatars : avatars.filter((avatar) => avatar.id !== session?.accountID); + } + if (!isEmptyObject(policyChatForActivePolicy)) { + return getIcons(policyChatForActivePolicy, formatPhoneNumber, personalDetails, null, undefined, undefined, undefined, undefined, isReportArchived); + } + return []; + // Policy is needed as a dependency in order to update the shortcut details when the workspace changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [personalDetails, session?.accountID, quickActionReport, quickActionPolicy, policyChatForActivePolicy, isReportArchived, isValidReport]); + + const quickActionTitle = useMemo(() => { + if (isEmptyObject(quickActionReport)) { + return ''; + } + if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && quickActionAvatars.length > 0) { + const accountID = quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID; + const name = getDisplayNameForParticipant({accountID: Number(accountID), shouldUseShortForm: true, formatPhoneNumber}) ?? ''; + return translate('quickAction.paySomeone', name); + } + const titleKey = getQuickActionTitle(quickAction?.action ?? ('' as QuickActionName)); + return titleKey ? translate(titleKey) : ''; + }, [quickAction?.action, translate, quickActionAvatars, quickActionReport, formatPhoneNumber]); + + const hideQABSubtitle = useMemo(() => { + if (!isValidReport) { + return true; + } + if (quickActionAvatars.length === 0) { + return false; + } + const displayName = personalDetails?.[quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID]?.firstName ?? ''; + return quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && displayName.length === 0; + }, [isValidReport, quickActionAvatars, personalDetails, quickAction?.action]); + + const quickActionSubtitle = useMemo(() => { + // eslint-disable-next-line @typescript-eslint/no-deprecated + return !hideQABSubtitle ? (getReportName(quickActionReport, quickActionPolicy, undefined, personalDetails) ?? translate('quickAction.updateDestination')) : ''; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [hideQABSubtitle, personalDetails, quickAction?.action, quickActionPolicy?.name, quickActionReport, translate]); + + return useMemo(() => { + const baseQuickAction = { + label: translate('quickAction.header'), + labelStyle: [styles.pt3, styles.pb2], + isLabelHoverable: false, + numberOfLinesDescription: 1, + tooltipAnchorAlignment: { + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, + }, + shouldTeleportPortalToModalLayer: true, + }; + + if (quickAction?.action && quickActionReport) { + if (!isQuickActionAllowed(quickAction, quickActionReport, quickActionPolicy, isReportArchived, allBetas, isRestrictedToPreferredPolicy)) { + return []; + } + const onSelected = () => { + interceptAnonymousUser(() => { + if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + const targetAccountPersonalDetails = { + ...personalDetails?.[quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID], + accountID: quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID, + }; + + navigateToQuickAction({ + isValidReport, + quickAction, + selectOption, + lastDistanceExpenseType, + targetAccountPersonalDetails, + currentUserAccountID: currentUserPersonalDetails.accountID, + isFromFloatingActionButton: true, + }); + }); + }; + return [ + { + ...baseQuickAction, + icon: getQuickActionIcon(icons as Parameters[0], quickAction?.action), + text: quickActionTitle, + rightIconAccountID: quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID, + description: quickActionSubtitle, + onSelected, + shouldCallAfterModalHide: shouldUseNarrowLayout, + rightIconReportID: quickActionReport?.reportID, + sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.QUICK_ACTION, + }, + ]; + } + if (!isEmptyObject(policyChatForActivePolicy)) { + const onSelected = () => { + interceptAnonymousUser(() => { + if (policyChatForActivePolicy?.policyID && shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatForActivePolicy.policyID)); + return; + } + + const quickActionReportID = policyChatForActivePolicy?.reportID || reportID; + startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, allTransactionDrafts, true); + }); + }; + + return [ + { + ...baseQuickAction, + icon: icons.ReceiptScan, + text: translate('quickAction.scanReceipt'), + // eslint-disable-next-line @typescript-eslint/no-deprecated + description: getReportName(policyChatForActivePolicy), + shouldCallAfterModalHide: shouldUseNarrowLayout, + onSelected, + rightIconReportID: policyChatForActivePolicy?.reportID, + sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.QUICK_ACTION, + }, + ]; + } + + return []; + }, [ + icons, + translate, + styles.pt3, + styles.pb2, + quickAction, + policyChatForActivePolicy, + quickActionReport, + quickActionPolicy, + isReportArchived, + isRestrictedToPreferredPolicy, + quickActionTitle, + quickActionAvatars, + quickActionSubtitle, + shouldUseNarrowLayout, + isDelegateAccessRestricted, + isValidReport, + selectOption, + lastDistanceExpenseType, + personalDetails, + currentUserPersonalDetails.accountID, + showDelegateNoAccessModal, + reportID, + allTransactionDrafts, + allBetas, + ]); +} + +export default useQuickActionMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTestDriveMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTestDriveMenuItem.ts new file mode 100644 index 0000000000000..0d2ff4eb3d423 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTestDriveMenuItem.ts @@ -0,0 +1,45 @@ +import {useMemo} from 'react'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; +import useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {startTestDrive} from '@libs/actions/Tour'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import {hasSeenTourSelector, tryNewDotOnyxSelector} from '@selectors/Onboarding'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; + +type UseTestDriveMenuItemParams = { + icons: MenuItemIcons; +}; + +function useTestDriveMenuItem({icons}: UseTestDriveMenuItemParams): PopoverMenuItem[] { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const theme = useTheme(); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); + const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector, canBeMissing: true}); + const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true}); + const isUserPaidPolicyMember = useIsPaidPolicyAdmin(); + + return useMemo(() => { + if (hasSeenTour) { + return []; + } + return [ + { + icon: icons.Binoculars, + iconStyles: styles.popoverIconCircle, + iconFill: theme.icon, + text: translate('testDrive.quickAction.takeATwoMinuteTestDrive'), + onSelected: () => interceptAnonymousUser(() => startTestDrive(introSelected, tryNewDot?.hasBeenAddedToNudgeMigration ?? false, isUserPaidPolicyMember)), + sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.TEST_DRIVE, + }, + ]; + }, [hasSeenTour, icons.Binoculars, styles.popoverIconCircle, theme.icon, translate, introSelected, tryNewDot?.hasBeenAddedToNudgeMigration, isUserPaidPolicyMember]); +} + +export default useTestDriveMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTrackDistanceMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTrackDistanceMenuItem.ts new file mode 100644 index 0000000000000..f8810c98de5dc --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTrackDistanceMenuItem.ts @@ -0,0 +1,45 @@ +import {useMemo} from 'react'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import {startDistanceRequest} from '@libs/actions/IOU'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; + +type UseTrackDistanceMenuItemParams = { + shouldUseNarrowLayout: boolean; + icons: MenuItemIcons; + reportID: string; +}; + +function useTrackDistanceMenuItem({shouldUseNarrowLayout, icons, reportID}: UseTrackDistanceMenuItemParams): PopoverMenuItem[] { + const {translate} = useLocalize(); + const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); + const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); + + return useMemo( + () => [ + { + icon: icons.Location, + text: translate('iou.trackDistance'), + shouldCallAfterModalHide: shouldUseNarrowLayout, + onSelected: () => { + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + startDistanceRequest(CONST.IOU.TYPE.CREATE, reportID, lastDistanceExpenseType, undefined, undefined, true); + }); + }, + sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.TRACK_DISTANCE, + }, + ], + [icons.Location, translate, shouldUseNarrowLayout, shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal, reportID, lastDistanceExpenseType], + ); +} + +export default useTrackDistanceMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTravelMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTravelMenuItem.ts new file mode 100644 index 0000000000000..ecbdecc3c7a89 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTravelMenuItem.ts @@ -0,0 +1,67 @@ +import {Str} from 'expensify-common'; +import {useCallback, useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import Navigation from '@libs/Navigation/Navigation'; +import {openTravelDotLink, shouldOpenTravelDotLinkWeb} from '@libs/openTravelDotLink'; +import Permissions from '@libs/Permissions'; +import {isPaidGroupPolicy} from '@libs/PolicyUtils'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; + +type UseTravelMenuItemParams = { + icons: MenuItemIcons; + activePolicyID: string | undefined; +}; + +const accountPrimaryLoginSelector = (account: OnyxEntry) => account?.primaryLogin; + +function useTravelMenuItem({icons, activePolicyID}: UseTravelMenuItemParams): PopoverMenuItem[] { + const {translate} = useLocalize(); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); + const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS, {canBeMissing: true}); + const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: accountPrimaryLoginSelector, canBeMissing: true}); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); + const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const isBlockedFromSpotnanaTravel = Permissions.isBetaEnabled(CONST.BETAS.PREVENT_SPOTNANA_TRAVEL, allBetas); + const primaryContactMethod = primaryLogin ?? session?.email ?? ''; + + const isTravelEnabled = useMemo(() => { + if (!!isBlockedFromSpotnanaTravel || !primaryContactMethod || Str.isSMSLogin(primaryContactMethod) || !isPaidGroupPolicy(activePolicy)) { + return false; + } + const isPolicyProvisioned = activePolicy?.travelSettings?.spotnanaCompanyID ?? activePolicy?.travelSettings?.associatedTravelDomainAccountID; + return activePolicy?.travelSettings?.hasAcceptedTerms ?? (travelSettings?.hasAcceptedTerms && isPolicyProvisioned); + }, [activePolicy, isBlockedFromSpotnanaTravel, primaryContactMethod, travelSettings?.hasAcceptedTerms]); + + const openTravel = useCallback(() => { + if (isTravelEnabled) { + openTravelDotLink(activePolicy?.id); + return; + } + Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS.getRoute(activePolicy?.id)); + }, [activePolicy?.id, isTravelEnabled]); + + return useMemo(() => { + if (!activePolicy?.isTravelEnabled) { + return []; + } + return [ + { + icon: icons.Suitcase, + text: translate('travel.bookTravel'), + rightIcon: isTravelEnabled && shouldOpenTravelDotLinkWeb() ? icons.NewWindow : undefined, + onSelected: () => interceptAnonymousUser(() => openTravel()), + sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.BOOK_TRAVEL, + }, + ]; + }, [activePolicy?.isTravelEnabled, icons.Suitcase, icons.NewWindow, translate, isTravelEnabled, openTravel]); +} + +export default useTravelMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/types.ts b/src/pages/inbox/sidebar/FABPopoverContent/types.ts new file mode 100644 index 0000000000000..1b41450df3608 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/types.ts @@ -0,0 +1,37 @@ +import type {RefObject} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {AnchorPosition} from '@src/styles'; +import type * as OnyxTypes from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; + +type PolicySelector = Pick; + +const policyMapper = (policy: OnyxEntry): PolicySelector => + (policy && { + type: policy.type, + role: policy.role, + id: policy.id, + isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, + pendingAction: policy.pendingAction, + avatarURL: policy.avatarURL, + name: policy.name, + areInvoicesEnabled: policy.areInvoicesEnabled, + }) as PolicySelector; + +type FABPopoverContentProps = { + isMenuMounted: boolean; + isVisible: boolean; + onClose: () => void; + onItemSelected: () => void; + onModalHide: () => void; + anchorPosition: AnchorPosition; + anchorRef: RefObject; + shouldUseNarrowLayout: boolean; +}; + +type FABPopoverContentInnerProps = Omit; + +type MenuItemIcons = Record; + +export type {PolicySelector, FABPopoverContentProps, FABPopoverContentInnerProps, MenuItemIcons}; +export {policyMapper}; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts b/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts new file mode 100644 index 0000000000000..abc7634d227e0 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts @@ -0,0 +1,48 @@ +import {useCallback, useMemo} from 'react'; +import type {OnyxCollection} from 'react-native-onyx'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; +import useConfirmModal from '@hooks/useConfirmModal'; +import useLocalize from '@hooks/useLocalize'; +import useMappedPolicies from '@hooks/useMappedPolicies'; +import useOnyx from '@hooks/useOnyx'; +import {openOldDotLink} from '@libs/actions/Link'; +import {areAllGroupPoliciesExpenseChatDisabled} from '@libs/PolicyUtils'; +import {closeReactNativeApp} from '@userActions/HybridApp'; +import CONFIG from '@src/CONFIG'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {isTrackingSelector} from '@src/selectors/GPSDraftDetails'; +import type * as OnyxTypes from '@src/types/onyx'; +import {policyMapper} from './types'; + +function useRedirectToExpensifyClassic() { + const {translate} = useLocalize(); + const {showConfirmModal} = useConfirmModal(); + const [isTrackingGPS = false] = useOnyx(ONYXKEYS.GPS_DRAFT_DETAILS, {canBeMissing: true, selector: isTrackingSelector}); + const [allPolicies] = useMappedPolicies(policyMapper); + + const shouldRedirectToExpensifyClassic = useMemo(() => { + return areAllGroupPoliciesExpenseChatDisabled((allPolicies as OnyxCollection) ?? {}); + }, [allPolicies]); + + const showRedirectToExpensifyClassicModal = useCallback(async () => { + const {action} = await showConfirmModal({ + title: translate('sidebarScreen.redirectToExpensifyClassicModal.title'), + prompt: translate('sidebarScreen.redirectToExpensifyClassicModal.description'), + confirmText: translate('exitSurvey.goToExpensifyClassic'), + cancelText: translate('common.cancel'), + }); + if (action !== ModalActions.CONFIRM) { + return; + } + if (CONFIG.IS_HYBRID_APP) { + closeReactNativeApp({shouldSetNVP: true, isTrackingGPS}); + return; + } + openOldDotLink(CONST.OLDDOT_URLS.INBOX); + }, [showConfirmModal, translate, isTrackingGPS]); + + return {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal, allPolicies}; +} + +export default useRedirectToExpensifyClassic; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts new file mode 100644 index 0000000000000..935cef7f73e98 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts @@ -0,0 +1,67 @@ +import {useCallback, useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import useOnyx from '@hooks/useOnyx'; +import {startMoneyRequest} from '@libs/actions/IOU'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import Navigation from '@libs/Navigation/Navigation'; +import {generateReportID, getWorkspaceChats} from '@libs/ReportUtils'; +import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; +import Tab from '@userActions/Tab'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import useRedirectToExpensifyClassic from './useRedirectToExpensifyClassic'; + +const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); + +function useScanActions() { + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); + const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); + const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); + + const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); + + const reportID = useMemo(() => generateReportID(), []); + + const policyChatForActivePolicy = useMemo(() => { + if (isEmptyObject(activePolicy) || !activePolicy?.isPolicyExpenseChatEnabled) { + return {} as OnyxTypes.Report; + } + const policyChats = getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], allReports); + return policyChats.length > 0 ? policyChats.at(0) : ({} as OnyxTypes.Report); + }, [activePolicy, activePolicyID, session?.accountID, allReports]); + + const startScan = useCallback(() => { + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, CONST.IOU.REQUEST_TYPE.SCAN, false, undefined, allTransactionDrafts, true); + }); + }, [shouldRedirectToExpensifyClassic, allTransactionDrafts, reportID, showRedirectToExpensifyClassicModal]); + + const policyChatPolicyID = policyChatForActivePolicy?.policyID; + const policyChatReportID = policyChatForActivePolicy?.reportID; + + const startQuickScan = useCallback(() => { + interceptAnonymousUser(() => { + if (policyChatPolicyID && shouldRestrictUserBillableActions(policyChatPolicyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatPolicyID)); + return; + } + + const quickActionReportID = policyChatReportID ?? reportID; + Tab.setSelectedTab(CONST.TAB.IOU_REQUEST_TYPE, CONST.IOU.REQUEST_TYPE.SCAN); + startMoneyRequest(CONST.IOU.TYPE.CREATE, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, !!policyChatReportID, undefined, allTransactionDrafts, true); + }); + }, [policyChatPolicyID, policyChatReportID, reportID, allTransactionDrafts]); + + return {startScan, startQuickScan, reportID, activePolicyID, session, policyChatForActivePolicy, allTransactionDrafts}; +} + +export default useScanActions; diff --git a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx index 197a9000d680f..8007b2ae0a02f 100644 --- a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx @@ -1,422 +1,55 @@ import {useIsFocused} from '@react-navigation/native'; -import {hasSeenTourSelector, tryNewDotOnyxSelector} from '@selectors/Onboarding'; -import {groupPaidPoliciesWithExpenseChatEnabledSelector} from '@selectors/Policy'; -import {Str} from 'expensify-common'; -import type {ImageContentFit} from 'expo-image'; -import type {ForwardedRef} from 'react'; -import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import {Platform, View} from 'react-native'; import FloatingActionButton from '@components/FloatingActionButton'; import FloatingReceiptButton from '@components/FloatingReceiptButton'; -import {ModalActions} from '@components/Modal/Global/ModalContext'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; -import PopoverMenu from '@components/PopoverMenu'; -import useConfirmModal from '@hooks/useConfirmModal'; -import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useHasEmptyReportsForPolicy from '@hooks/useHasEmptyReportsForPolicy'; -import useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import useMappedPolicies from '@hooks/useMappedPolicies'; -import useNetwork from '@hooks/useNetwork'; -import useOnyx from '@hooks/useOnyx'; -import usePermissions from '@hooks/usePermissions'; -import usePreferredPolicy from '@hooks/usePreferredPolicy'; import usePrevious from '@hooks/usePrevious'; -import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {startDistanceRequest, startMoneyRequest} from '@libs/actions/IOU'; -import {openOldDotLink} from '@libs/actions/Link'; -import {navigateToQuickAction} from '@libs/actions/QuickActionNavigation'; -import {createNewReport, startNewChat} from '@libs/actions/Report'; -import {startTestDrive} from '@libs/actions/Tour'; -import getIconForAction from '@libs/getIconForAction'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; -import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction'; -import Navigation from '@libs/Navigation/Navigation'; -import {openTravelDotLink, shouldOpenTravelDotLinkWeb} from '@libs/openTravelDotLink'; -import Permissions from '@libs/Permissions'; -import {areAllGroupPoliciesExpenseChatDisabled, canSendInvoice as canSendInvoicePolicyUtils, getDefaultChatEnabledPolicy, isPaidGroupPolicy, shouldShowPolicy} from '@libs/PolicyUtils'; -import {getQuickActionIcon, getQuickActionTitle, isQuickActionAllowed} from '@libs/QuickActionUtils'; -import { - generateReportID, - getDisplayNameForParticipant, - getIcons, - // Will be fixed in https://github.com/Expensify/App/issues/76852 - // eslint-disable-next-line @typescript-eslint/no-deprecated - getReportName, - getWorkspaceChats, - hasViolations as hasViolationsReportUtils, - isPolicyExpenseChat, -} from '@libs/ReportUtils'; -import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; -import isOnSearchMoneyRequestReportPage from '@navigation/helpers/isOnSearchMoneyRequestReportPage'; -import variables from '@styles/variables'; -import {closeReactNativeApp} from '@userActions/HybridApp'; -import {clearLastSearchParams} from '@userActions/ReportNavigation'; -import Tab from '@userActions/Tab'; -import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import {isTrackingSelector} from '@src/selectors/GPSDraftDetails'; -import type * as OnyxTypes from '@src/types/onyx'; -import type {QuickActionName} from '@src/types/onyx/QuickAction'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import getEmptyArray from '@src/types/utils/getEmptyArray'; - -type PolicySelector = Pick; - -type FloatingActionButtonAndPopoverProps = { - /* Callback function when the menu is shown */ - onShowCreateMenu?: () => void; - - /* Callback function before the menu is hidden */ - onHideCreateMenu?: () => void; - - /** Reference to the outer element */ - ref?: ForwardedRef; -}; - -type FloatingActionButtonAndPopoverRef = { - hideCreateMenu: () => void; -}; - -const policyMapper = (policy: OnyxEntry): PolicySelector => - (policy && { - type: policy.type, - role: policy.role, - id: policy.id, - isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, - pendingAction: policy.pendingAction, - avatarURL: policy.avatarURL, - name: policy.name, - areInvoicesEnabled: policy.areInvoicesEnabled, - }) as PolicySelector; - -const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); - -const accountPrimaryLoginSelector = (account: OnyxEntry) => account?.primaryLogin; +import FABPopoverContent from './FABPopoverContent'; +import useScanActions from './FABPopoverContent/useScanActions'; /** - * Responsible for rendering the {@link PopoverMenu}, and the accompanying + * Responsible for rendering the {@link FABPopoverContent}, and the accompanying * FAB that can open or close the menu. */ -function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref}: FloatingActionButtonAndPopoverProps) { - const icons = useMemoizedLazyExpensifyIcons([ - 'CalendarSolid', - 'Document', - 'NewWorkspace', - 'NewWindow', - 'Binoculars', - 'Car', - 'Location', - 'Suitcase', - 'Task', - 'InvoiceGeneric', - 'ReceiptScan', - 'ChatBubble', - 'Coins', - 'Receipt', - 'Cash', - 'Transfer', - 'MoneyCircle', - 'Clock', - ] as const); +function FloatingActionButtonAndPopover() { const styles = useThemeStyles(); - const theme = useTheme(); - const {translate, formatPhoneNumber} = useLocalize(); - const [isLoading = false] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); - const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); - const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); - const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, {canBeMissing: true}); - const [quickActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`, {canBeMissing: true}); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); - const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); - const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); - const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); - const {isRestrictedToPreferredPolicy, isRestrictedPolicyCreation} = usePreferredPolicy(); - - const workspaceChatsSelector = useCallback( - (reports: OnyxCollection) => { - return getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports); - }, - [activePolicyID, session?.accountID], - ); - - const [policyChatsForActivePolicy = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true, selector: workspaceChatsSelector}, [ - activePolicyID, - session?.accountID, - ]); - - const policyChatForActivePolicy = useMemo(() => { - if (isEmptyObject(activePolicy) || !activePolicy?.isPolicyExpenseChatEnabled) { - return {} as OnyxTypes.Report; - } - return policyChatsForActivePolicy.length > 0 ? policyChatsForActivePolicy.at(0) : ({} as OnyxTypes.Report); - }, [activePolicy, policyChatsForActivePolicy]); - - const quickActionPolicyID = quickAction?.action === CONST.QUICK_ACTIONS.TRACK_PER_DIEM && quickAction?.perDiemPolicyID ? quickAction?.perDiemPolicyID : quickActionReport?.policyID; - const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionPolicyID}`, {canBeMissing: true}); - const [allPolicies] = useMappedPolicies(policyMapper); - const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); - const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {isDelegateAccessRestricted} = useDelegateNoAccessState(); - const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); - const {showConfirmModal} = useConfirmModal(); - - const [isCreateMenuActive, setIsCreateMenuActive] = useState(false); - const fabRef = useRef(null); + const {translate} = useLocalize(); const {windowHeight} = useWindowDimensions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const isFocused = useIsFocused(); const prevIsFocused = usePrevious(isFocused); - const isReportArchived = useReportIsArchived(quickActionReport?.reportID); - const {isOffline} = useNetwork(); - const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); - const isBlockedFromSpotnanaTravel = Permissions.isBetaEnabled(CONST.BETAS.PREVENT_SPOTNANA_TRAVEL, allBetas); - const {isBetaEnabled} = usePermissions(); - const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: accountPrimaryLoginSelector, canBeMissing: true}); - const primaryContactMethod = primaryLogin ?? session?.email ?? ''; - const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS, {canBeMissing: true}); - const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); - const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); - - const canSendInvoice = useMemo(() => canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email), [allPolicies, session?.email]); - const isValidReport = !(isEmptyObject(quickActionReport) || isReportArchived); - const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); - const [isTrackingGPS = false] = useOnyx(ONYXKEYS.GPS_DRAFT_DETAILS, {canBeMissing: true, selector: isTrackingSelector}); - const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { - selector: hasSeenTourSelector, - canBeMissing: true, - }); - const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true}); - - const isUserPaidPolicyMember = useIsPaidPolicyAdmin(); - const reportID = useMemo(() => generateReportID(), []); - - const groupPaidPoliciesWithChatEnabled = useCallback( - (policies: OnyxCollection) => groupPaidPoliciesWithExpenseChatEnabledSelector(policies, session?.email), - [session?.email], - ); - - const isReportInSearch = isOnSearchMoneyRequestReportPage(); - const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx( - ONYXKEYS.COLLECTION.POLICY, - { - selector: groupPaidPoliciesWithChatEnabled, - canBeMissing: true, - }, - [session?.email], - ); - - /** - * There are scenarios where users who have not yet had their group workspace-chats in NewDot (isPolicyExpenseChatEnabled). In those scenarios, things can get confusing if they try to submit/track expenses. To address this, we block them from Creating, Tracking, Submitting expenses from NewDot if they are: - * 1. on at least one group policy - * 2. none of the group policies they are a member of have isPolicyExpenseChatEnabled=true - */ - const shouldRedirectToExpensifyClassic = useMemo(() => { - return areAllGroupPoliciesExpenseChatDisabled((allPolicies as OnyxCollection) ?? {}); - }, [allPolicies]); - const shouldShowCreateReportOption = shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; - - const defaultChatEnabledPolicy = useMemo( - () => getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array>, activePolicy), - [activePolicy, groupPoliciesWithChatEnabled], - ); - - const defaultChatEnabledPolicyID = defaultChatEnabledPolicy?.id; - - const hasEmptyReport = useHasEmptyReportsForPolicy(defaultChatEnabledPolicyID); - const [hasDismissedEmptyReportsConfirmation] = useOnyx(ONYXKEYS.NVP_EMPTY_REPORTS_CONFIRMATION_DISMISSED, {canBeMissing: true}); - - const shouldShowEmptyReportConfirmationForDefaultChatEnabledPolicy = hasEmptyReport && hasDismissedEmptyReportsConfirmation !== true; - - const handleCreateWorkspaceReport = useCallback( - (shouldDismissEmptyReportsConfirmation?: boolean) => { - if (!defaultChatEnabledPolicy?.id) { - return; - } - - if (isReportInSearch) { - clearLastSearchParams(); - } - - const {reportID: createdReportID} = createNewReport( - currentUserPersonalDetails, - hasViolations, - isASAPSubmitBetaEnabled, - defaultChatEnabledPolicy, - allBetas, - false, - shouldDismissEmptyReportsConfirmation, - ); - Navigation.setNavigationActionToMicrotaskQueue(() => { - Navigation.navigate( - isSearchTopmostFullScreenRoute() - ? ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: createdReportID, backTo: Navigation.getActiveRoute()}) - : ROUTES.REPORT_WITH_ID.getRoute(createdReportID, undefined, undefined, Navigation.getActiveRoute()), - {forceReplace: isReportInSearch}, - ); - }); - }, - [currentUserPersonalDetails, hasViolations, defaultChatEnabledPolicy, isASAPSubmitBetaEnabled, isReportInSearch, allBetas], - ); - - const {openCreateReportConfirmation: openFabCreateReportConfirmation, CreateReportConfirmationModal: FabCreateReportConfirmationModal} = useCreateEmptyReportConfirmation({ - policyID: defaultChatEnabledPolicyID, - policyName: defaultChatEnabledPolicy?.name ?? '', - onConfirm: handleCreateWorkspaceReport, - }); - - const shouldShowNewWorkspaceButton = - !isRestrictedPolicyCreation && Object.values(allPolicies ?? {}).every((policy) => !shouldShowPolicy(policy as OnyxEntry, !!isOffline, session?.email)); - - const quickActionAvatars = useMemo(() => { - if (isValidReport) { - const avatars = getIcons(quickActionReport, formatPhoneNumber, personalDetails, null, undefined, undefined, undefined, undefined, isReportArchived); - return avatars.length <= 1 || isPolicyExpenseChat(quickActionReport) ? avatars : avatars.filter((avatar) => avatar.id !== session?.accountID); - } - if (!isEmptyObject(policyChatForActivePolicy)) { - return getIcons(policyChatForActivePolicy, formatPhoneNumber, personalDetails, null, undefined, undefined, undefined, undefined, isReportArchived); - } - return []; - // Policy is needed as a dependency in order to update the shortcut details when the workspace changes - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [personalDetails, session?.accountID, quickActionReport, quickActionPolicy, policyChatForActivePolicy, isReportArchived, isValidReport]); - - const quickActionTitle = useMemo(() => { - if (isEmptyObject(quickActionReport)) { - return ''; - } - if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && quickActionAvatars.length > 0) { - const accountID = quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID; - const name = getDisplayNameForParticipant({accountID: Number(accountID), shouldUseShortForm: true, formatPhoneNumber}) ?? ''; - return translate('quickAction.paySomeone', name); - } - const titleKey = getQuickActionTitle(quickAction?.action ?? ('' as QuickActionName)); - return titleKey ? translate(titleKey) : ''; - }, [quickAction?.action, translate, quickActionAvatars, quickActionReport, formatPhoneNumber]); - - const hideQABSubtitle = useMemo(() => { - if (!isValidReport) { - return true; - } - if (quickActionAvatars.length === 0) { - return false; - } - const displayName = personalDetails?.[quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID]?.firstName ?? ''; - return quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && displayName.length === 0; - }, [isValidReport, quickActionAvatars, personalDetails, quickAction?.action]); - const quickActionSubtitle = useMemo(() => { - // eslint-disable-next-line @typescript-eslint/no-deprecated - return !hideQABSubtitle ? (getReportName(quickActionReport, quickActionPolicy, undefined, personalDetails) ?? translate('quickAction.updateDestination')) : ''; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hideQABSubtitle, personalDetails, quickAction?.action, quickActionPolicy?.name, quickActionReport, translate]); + const [isCreateMenuActive, setIsCreateMenuActive] = useState(false); + const [isMenuMounted, setIsMenuMounted] = useState(false); + const fabRef = useRef(null); - const selectOption = useCallback( - (onSelected: () => void, shouldRestrictAction: boolean) => { - if (shouldRestrictAction && quickActionReport?.policyID && shouldRestrictUserBillableActions(quickActionReport.policyID)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(quickActionReport.policyID)); - return; - } - onSelected(); - }, - [quickActionReport?.policyID], - ); + const {startScan, startQuickScan, reportID, activePolicyID, session, policyChatForActivePolicy, allTransactionDrafts} = useScanActions(); - const showRedirectToExpensifyClassicModal = useCallback(async () => { - const {action} = await showConfirmModal({ - title: translate('sidebarScreen.redirectToExpensifyClassicModal.title'), - prompt: translate('sidebarScreen.redirectToExpensifyClassicModal.description'), - confirmText: translate('exitSurvey.goToExpensifyClassic'), - cancelText: translate('common.cancel'), - }); - if (action !== ModalActions.CONFIRM) { + const showCreateMenu = useCallback(() => { + if (!isFocused && shouldUseNarrowLayout) { return; } - if (CONFIG.IS_HYBRID_APP) { - closeReactNativeApp({shouldSetNVP: true, isTrackingGPS}); + setIsMenuMounted(true); + setIsCreateMenuActive(true); + }, [isFocused, shouldUseNarrowLayout]); + + const hideCreateMenu = useCallback(() => { + if (!isCreateMenuActive) { return; } - openOldDotLink(CONST.OLDDOT_URLS.INBOX); - }, [showConfirmModal, translate, isTrackingGPS]); - - const startScan = useCallback(() => { - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - - // Start the scan flow directly - startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, CONST.IOU.REQUEST_TYPE.SCAN, false, undefined, allTransactionDrafts, true); - }); - }, [shouldRedirectToExpensifyClassic, allTransactionDrafts, reportID, showRedirectToExpensifyClassicModal]); + setIsCreateMenuActive(false); + }, [isCreateMenuActive]); - const startQuickScan = useCallback(() => { - interceptAnonymousUser(() => { - if (policyChatForActivePolicy?.policyID && shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatForActivePolicy.policyID)); - return; - } - - const quickActionReportID = policyChatForActivePolicy?.reportID ?? reportID; - Tab.setSelectedTab(CONST.TAB.IOU_REQUEST_TYPE, CONST.IOU.REQUEST_TYPE.SCAN); - startMoneyRequest(CONST.IOU.TYPE.CREATE, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, !!policyChatForActivePolicy?.reportID, undefined, allTransactionDrafts, true); - }); - }, [policyChatForActivePolicy?.policyID, policyChatForActivePolicy?.reportID, reportID, allTransactionDrafts]); - - /** - * Check if LHN status changed from active to inactive. - * Used to close already opened FAB menu when open any other pages (i.e. Press Command + K on web). - */ - const didScreenBecomeInactive = useCallback( - (): boolean => - // When any other page is opened over LHN - !isFocused && prevIsFocused, - [isFocused, prevIsFocused], - ); - - /** - * Method called when we click the floating action button - */ - const showCreateMenu = useCallback( - () => { - if (!isFocused && shouldUseNarrowLayout) { - return; - } - setIsCreateMenuActive(true); - onShowCreateMenu?.(); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [isFocused, shouldUseNarrowLayout], - ); + const handleMenuModalHide = useCallback(() => { + setIsMenuMounted(false); + }, []); - /** - * Method called either when: - * - Pressing the floating action button to open the CreateMenu modal - * - Selecting an item on CreateMenu or closing it by clicking outside of the modal component - */ - const hideCreateMenu = useCallback( - () => { - if (!isCreateMenuActive) { - return; - } - setIsCreateMenuActive(false); - onHideCreateMenu?.(); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [isCreateMenuActive], - ); + const didScreenBecomeInactive = useCallback((): boolean => !isFocused && prevIsFocused, [isFocused, prevIsFocused]); useEffect(() => { if (!didScreenBecomeInactive()) { @@ -424,14 +57,19 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref } // Hide menu manually when other pages are opened using shortcut key + // eslint-disable-next-line react-hooks/set-state-in-effect hideCreateMenu(); }, [didScreenBecomeInactive, hideCreateMenu]); - useImperativeHandle(ref, () => ({ - hideCreateMenu() { - hideCreateMenu(); - }, - })); + // Close menu on dragover (web only — prevents popover from staying open during file drag) + useEffect(() => { + if (Platform.OS !== 'web' || !isCreateMenuActive) { + return; + } + const handler = () => hideCreateMenu(); + document.addEventListener('dragover', handler); + return () => document.removeEventListener('dragover', handler); + }, [isCreateMenuActive, hideCreateMenu]); const toggleCreateMenu = () => { if (isCreateMenuActive) { @@ -441,302 +79,22 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref } }; - const expenseMenuItems = useMemo((): PopoverMenuItem[] => { - return [ - { - icon: getIconForAction(CONST.IOU.TYPE.CREATE, icons), - text: translate('iou.createExpense'), - testID: 'create-expense', - shouldCallAfterModalHide: shouldRedirectToExpensifyClassic || shouldUseNarrowLayout, - onSelected: () => - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); - }), - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.CREATE_EXPENSE, - }, - ]; - }, [translate, shouldRedirectToExpensifyClassic, shouldUseNarrowLayout, allTransactionDrafts, reportID, icons, showRedirectToExpensifyClassicModal]); - - const quickActionMenuItems = useMemo(() => { - // Define common properties in baseQuickAction - const baseQuickAction = { - label: translate('quickAction.header'), - labelStyle: [styles.pt3, styles.pb2], - isLabelHoverable: false, - numberOfLinesDescription: 1, - tooltipAnchorAlignment: { - vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, - horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, - }, - shouldTeleportPortalToModalLayer: true, - }; - - if (quickAction?.action && quickActionReport) { - if (!isQuickActionAllowed(quickAction, quickActionReport, quickActionPolicy, isReportArchived, allBetas, isRestrictedToPreferredPolicy)) { - return []; - } - const onSelected = () => { - interceptAnonymousUser(() => { - if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && isDelegateAccessRestricted) { - showDelegateNoAccessModal(); - return; - } - const targetAccountPersonalDetails = { - ...personalDetails?.[quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID], - accountID: quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID, - }; - - navigateToQuickAction({ - isValidReport, - quickAction, - selectOption, - lastDistanceExpenseType, - targetAccountPersonalDetails, - currentUserAccountID: currentUserPersonalDetails.accountID, - isFromFloatingActionButton: true, - }); - }); - }; - return [ - { - ...baseQuickAction, - icon: getQuickActionIcon(icons, quickAction?.action), - text: quickActionTitle, - rightIconAccountID: quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID, - description: quickActionSubtitle, - onSelected, - shouldCallAfterModalHide: shouldUseNarrowLayout, - rightIconReportID: quickActionReport?.reportID, - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.QUICK_ACTION, - }, - ]; - } - if (!isEmptyObject(policyChatForActivePolicy)) { - const onSelected = () => { - interceptAnonymousUser(() => { - if (policyChatForActivePolicy?.policyID && shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatForActivePolicy.policyID)); - return; - } - - const quickActionReportID = policyChatForActivePolicy?.reportID || reportID; - startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, allTransactionDrafts, true); - }); - }; - - return [ - { - ...baseQuickAction, - icon: icons.ReceiptScan, - text: translate('quickAction.scanReceipt'), - // eslint-disable-next-line @typescript-eslint/no-deprecated - description: getReportName(policyChatForActivePolicy), - shouldCallAfterModalHide: shouldUseNarrowLayout, - onSelected, - rightIconReportID: policyChatForActivePolicy?.reportID, - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.QUICK_ACTION, - }, - ]; - } - - return []; - }, [ - icons, - translate, - styles.pt3, - styles.pb2, - quickAction, - policyChatForActivePolicy, - quickActionReport, - quickActionPolicy, - isReportArchived, - isRestrictedToPreferredPolicy, - quickActionTitle, - quickActionAvatars, - quickActionSubtitle, - shouldUseNarrowLayout, - isDelegateAccessRestricted, - isValidReport, - selectOption, - lastDistanceExpenseType, - personalDetails, - currentUserPersonalDetails.accountID, - showDelegateNoAccessModal, - reportID, - allTransactionDrafts, - allBetas, - ]); - - const isTravelEnabled = useMemo(() => { - if (!!isBlockedFromSpotnanaTravel || !primaryContactMethod || Str.isSMSLogin(primaryContactMethod) || !isPaidGroupPolicy(activePolicy)) { - return false; - } - - const isPolicyProvisioned = activePolicy?.travelSettings?.spotnanaCompanyID ?? activePolicy?.travelSettings?.associatedTravelDomainAccountID; - - return activePolicy?.travelSettings?.hasAcceptedTerms ?? (travelSettings?.hasAcceptedTerms && isPolicyProvisioned); - }, [activePolicy, isBlockedFromSpotnanaTravel, primaryContactMethod, travelSettings?.hasAcceptedTerms]); - - const openTravel = useCallback(() => { - if (isTravelEnabled) { - openTravelDotLink(activePolicy?.id); - return; - } - Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS.getRoute(activePolicy?.id)); - }, [activePolicy?.id, isTravelEnabled]); - - const menuItems = [ - ...expenseMenuItems, - { - icon: icons.Location, - text: translate('iou.trackDistance'), - shouldCallAfterModalHide: shouldUseNarrowLayout, - onSelected: () => { - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - // Start the flow to start tracking a distance request - startDistanceRequest(CONST.IOU.TYPE.CREATE, reportID, lastDistanceExpenseType, undefined, undefined, true); - }); - }, - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.TRACK_DISTANCE, - }, - ...(shouldShowCreateReportOption - ? [ - { - icon: icons.Document, - text: translate('report.newReport.createReport'), - shouldCallAfterModalHide: shouldUseNarrowLayout, - onSelected: () => { - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - - const workspaceIDForReportCreation = defaultChatEnabledPolicyID; - - if (!workspaceIDForReportCreation || (shouldRestrictUserBillableActions(workspaceIDForReportCreation) && groupPoliciesWithChatEnabled.length > 1)) { - // If we couldn't guess the workspace to create the report, or a guessed workspace is past it's grace period and we have other workspaces to choose from - Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute()); - return; - } - - if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { - // Check if empty report confirmation should be shown - if (shouldShowEmptyReportConfirmationForDefaultChatEnabledPolicy) { - openFabCreateReportConfirmation(); - } else { - handleCreateWorkspaceReport(false); - } - return; - } - - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(workspaceIDForReportCreation)); - }); - }, - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.CREATE_REPORT, - }, - ] - : []), - { - icon: icons.ChatBubble, - text: translate('sidebarScreen.fabNewChat'), - shouldCallAfterModalHide: shouldUseNarrowLayout, - onSelected: () => interceptAnonymousUser(startNewChat), - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.START_CHAT, - }, - ...(canSendInvoice - ? [ - { - icon: icons.InvoiceGeneric, - text: translate('workspace.invoices.sendInvoice'), - shouldCallAfterModalHide: shouldRedirectToExpensifyClassic || shouldUseNarrowLayout, - onSelected: () => - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - - startMoneyRequest(CONST.IOU.TYPE.INVOICE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); - }), - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.SEND_INVOICE, - }, - ] - : []), - ...(activePolicy?.isTravelEnabled - ? [ - { - icon: icons.Suitcase, - text: translate('travel.bookTravel'), - rightIcon: isTravelEnabled && shouldOpenTravelDotLinkWeb() ? icons.NewWindow : undefined, - onSelected: () => interceptAnonymousUser(() => openTravel()), - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.BOOK_TRAVEL, - }, - ] - : []), - ...(!hasSeenTour - ? [ - { - icon: icons.Binoculars, - iconStyles: styles.popoverIconCircle, - iconFill: theme.icon, - text: translate('testDrive.quickAction.takeATwoMinuteTestDrive'), - onSelected: () => interceptAnonymousUser(() => startTestDrive(introSelected, tryNewDot?.hasBeenAddedToNudgeMigration ?? false, isUserPaidPolicyMember)), - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.TEST_DRIVE, - }, - ] - : []), - ...(!isLoading && shouldShowNewWorkspaceButton - ? [ - { - displayInDefaultIconColor: true, - contentFit: 'contain' as ImageContentFit, - icon: icons.NewWorkspace, - iconWidth: variables.w46, - iconHeight: variables.h40, - text: translate('workspace.new.newWorkspace'), - description: translate('workspace.new.getTheExpensifyCardAndMore'), - shouldCallAfterModalHide: shouldUseNarrowLayout, - onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(Navigation.getActiveRoute()))), - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.NEW_WORKSPACE, - }, - ] - : []), - ...quickActionMenuItems, - ]; - return ( - {FabCreateReportConfirmationModal} - { - return { - ...item, - onSelected: () => { - if (!item.onSelected) { - return; - } - navigateAfterInteraction(item.onSelected); - }, - }; - })} + onModalHide={handleMenuModalHide} + anchorPosition={styles.createMenuPositionSidebar(windowHeight)} anchorRef={fabRef} + shouldUseNarrowLayout={shouldUseNarrowLayout} + reportID={reportID} + activePolicyID={activePolicyID} + session={session} + policyChatForActivePolicy={policyChatForActivePolicy} + allTransactionDrafts={allTransactionDrafts} /> {!shouldUseNarrowLayout && ( (null); - - /** - * Method to hide popover when dragover. - */ - const hidePopoverOnDragOver = useCallback(() => { - if (!popoverModal.current) { - return; - } - popoverModal.current.hideCreateMenu(); - }, []); - - /** - * Method create event listener - */ - const createDragoverListener = () => { - document.addEventListener('dragover', hidePopoverOnDragOver); - }; - - /** - * Method remove event listener. - */ - const removeDragoverListener = () => { - document.removeEventListener('dragover', hidePopoverOnDragOver); - }; - - return ( - - ); -} - -export default NavigationTabBarFloatingActionButton; +export default FloatingActionButtonAndPopover; diff --git a/src/pages/inbox/sidebar/NavigationTabBarFloatingActionButton/types.ts b/src/pages/inbox/sidebar/NavigationTabBarFloatingActionButton/types.ts deleted file mode 100644 index e8fd03ee2adc5..0000000000000 --- a/src/pages/inbox/sidebar/NavigationTabBarFloatingActionButton/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -type FloatingActionButtonPopoverMenuRef = { - hideCreateMenu: () => void; -}; - -export default FloatingActionButtonPopoverMenuRef; From f3e1f87bde1cd3096f52e56ff1a19d36e882ef47 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 23 Feb 2026 13:04:23 +0100 Subject: [PATCH 02/54] extract platform dragover dismiss into platform-specific hook --- src/hooks/useDragoverDismiss.native.ts | 3 +++ src/hooks/useDragoverDismiss.ts | 14 ++++++++++++++ .../sidebar/FloatingActionButtonAndPopover.tsx | 14 ++++---------- 3 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 src/hooks/useDragoverDismiss.native.ts create mode 100644 src/hooks/useDragoverDismiss.ts diff --git a/src/hooks/useDragoverDismiss.native.ts b/src/hooks/useDragoverDismiss.native.ts new file mode 100644 index 0000000000000..b7fd2d71a5549 --- /dev/null +++ b/src/hooks/useDragoverDismiss.native.ts @@ -0,0 +1,3 @@ +const useDragoverDismiss: (isActive: boolean, dismiss: () => void) => void = () => {}; + +export default useDragoverDismiss; diff --git a/src/hooks/useDragoverDismiss.ts b/src/hooks/useDragoverDismiss.ts new file mode 100644 index 0000000000000..7557cb08fc29b --- /dev/null +++ b/src/hooks/useDragoverDismiss.ts @@ -0,0 +1,14 @@ +import {useEffect} from 'react'; + +function useDragoverDismiss(isActive: boolean, dismiss: () => void) { + useEffect(() => { + if (!isActive) { + return; + } + const handler = () => dismiss(); + document.addEventListener('dragover', handler); + return () => document.removeEventListener('dragover', handler); + }, [isActive, dismiss]); +} + +export default useDragoverDismiss; diff --git a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx index 8007b2ae0a02f..7ee05d626d7d4 100644 --- a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx @@ -1,8 +1,9 @@ import {useIsFocused} from '@react-navigation/native'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {Platform, View} from 'react-native'; +import {View} from 'react-native'; import FloatingActionButton from '@components/FloatingActionButton'; import FloatingReceiptButton from '@components/FloatingReceiptButton'; +import useDragoverDismiss from '@hooks/useDragoverDismiss'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -61,15 +62,8 @@ function FloatingActionButtonAndPopover() { hideCreateMenu(); }, [didScreenBecomeInactive, hideCreateMenu]); - // Close menu on dragover (web only — prevents popover from staying open during file drag) - useEffect(() => { - if (Platform.OS !== 'web' || !isCreateMenuActive) { - return; - } - const handler = () => hideCreateMenu(); - document.addEventListener('dragover', handler); - return () => document.removeEventListener('dragover', handler); - }, [isCreateMenuActive, hideCreateMenu]); + // Close menu on dragover — prevents popover from staying open during file drag + useDragoverDismiss(isCreateMenuActive, hideCreateMenu); const toggleCreateMenu = () => { if (isCreateMenuActive) { From c604841058920c67ad73c69f153b7c4830c75ee0 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 23 Feb 2026 13:09:22 +0100 Subject: [PATCH 03/54] =?UTF-8?q?remove=20data=20intermediary=20=E2=80=94?= =?UTF-8?q?=20hooks=20fetch=20own=20session/policy/transaction=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FABPopoverContent/FABPopoverContent.tsx | 11 --------- .../FABPopoverContentInner.tsx | 19 ++------------- .../menuItems/useInvoiceMenuItem.ts | 9 ++++---- .../menuItems/useQuickActionMenuItem.ts | 23 +++++++++++++++---- .../FABPopoverContent/useScanActions.ts | 2 +- .../FloatingActionButtonAndPopover.tsx | 5 +--- 6 files changed, 26 insertions(+), 43 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx index 2137c1ceac5b4..4563de096ecb0 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx @@ -1,15 +1,10 @@ import React from 'react'; -import type {OnyxCollection} from 'react-native-onyx'; -import type * as OnyxTypes from '@src/types/onyx'; import FABPopoverContentInner from './FABPopoverContentInner'; import type {FABPopoverContentProps} from './types'; type FABPopoverContentExtraProps = FABPopoverContentProps & { reportID: string; activePolicyID: string | undefined; - session: {email?: string; accountID?: number} | undefined; - policyChatForActivePolicy: OnyxTypes.Report | undefined; - allTransactionDrafts: OnyxCollection; }; function FABPopoverContent({ @@ -23,9 +18,6 @@ function FABPopoverContent({ shouldUseNarrowLayout, reportID, activePolicyID, - session, - policyChatForActivePolicy, - allTransactionDrafts, }: FABPopoverContentExtraProps) { if (!isMenuMounted) { return null; @@ -42,9 +34,6 @@ function FABPopoverContent({ shouldUseNarrowLayout={shouldUseNarrowLayout} reportID={reportID} activePolicyID={activePolicyID} - session={session} - policyChatForActivePolicy={policyChatForActivePolicy} - allTransactionDrafts={allTransactionDrafts} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx index 0b0d927e2e0e5..fab466e90fcfd 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx @@ -1,10 +1,8 @@ import React, {useMemo} from 'react'; -import type {OnyxCollection} from 'react-native-onyx'; import PopoverMenu from '@components/PopoverMenu'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction'; import CONST from '@src/CONST'; -import type * as OnyxTypes from '@src/types/onyx'; import useCreateReportMenuItem from './menuItems/useCreateReportMenuItem'; import useExpenseMenuItem from './menuItems/useExpenseMenuItem'; import useInvoiceMenuItem from './menuItems/useInvoiceMenuItem'; @@ -19,9 +17,6 @@ import type {FABPopoverContentInnerProps} from './types'; type FABPopoverContentInnerExtraProps = FABPopoverContentInnerProps & { reportID: string; activePolicyID: string | undefined; - session: {email?: string; accountID?: number} | undefined; - policyChatForActivePolicy: OnyxTypes.Report | undefined; - allTransactionDrafts: OnyxCollection; }; function FABPopoverContentInner({ @@ -34,9 +29,6 @@ function FABPopoverContentInner({ shouldUseNarrowLayout, reportID, activePolicyID, - session, - policyChatForActivePolicy, - allTransactionDrafts, }: FABPopoverContentInnerExtraProps) { const icons = useMemoizedLazyExpensifyIcons([ 'CalendarSolid', @@ -63,18 +55,11 @@ function FABPopoverContentInner({ const trackDistanceItem = useTrackDistanceMenuItem({shouldUseNarrowLayout, icons, reportID}); const {menuItem: createReportItem, confirmationModal} = useCreateReportMenuItem({shouldUseNarrowLayout, icons, activePolicyID}); const newChatItem = useNewChatMenuItem({shouldUseNarrowLayout, icons}); - const invoiceItem = useInvoiceMenuItem({shouldUseNarrowLayout, icons, reportID, allTransactionDrafts}); + const invoiceItem = useInvoiceMenuItem({shouldUseNarrowLayout, icons, reportID}); const travelItem = useTravelMenuItem({icons, activePolicyID}); const testDriveItem = useTestDriveMenuItem({icons}); const newWorkspaceItem = useNewWorkspaceMenuItem({shouldUseNarrowLayout, icons}); - const quickActionItem = useQuickActionMenuItem({ - shouldUseNarrowLayout, - icons, - reportID, - session, - policyChatForActivePolicy, - allTransactionDrafts, - }); + const quickActionItem = useQuickActionMenuItem({shouldUseNarrowLayout, icons, reportID}); const menuItems = useMemo( () => [...expenseItem, ...trackDistanceItem, ...createReportItem, ...newChatItem, ...invoiceItem, ...travelItem, ...testDriveItem, ...newWorkspaceItem, ...quickActionItem], diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useInvoiceMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useInvoiceMenuItem.ts index ef8296bbcd37d..ff3f058d053f1 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useInvoiceMenuItem.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useInvoiceMenuItem.ts @@ -1,28 +1,27 @@ import {useMemo} from 'react'; -import type {OnyxCollection} from 'react-native-onyx'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import {startMoneyRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {canSendInvoice as canSendInvoicePolicyUtils} from '@libs/PolicyUtils'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; type UseInvoiceMenuItemParams = { shouldUseNarrowLayout: boolean; icons: MenuItemIcons; reportID: string; - allTransactionDrafts: OnyxCollection; }; -function useInvoiceMenuItem({shouldUseNarrowLayout, icons, reportID, allTransactionDrafts}: UseInvoiceMenuItemParams): PopoverMenuItem[] { +function useInvoiceMenuItem({shouldUseNarrowLayout, icons, reportID}: UseInvoiceMenuItemParams): PopoverMenuItem[] { const {translate} = useLocalize(); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal, allPolicies} = useRedirectToExpensifyClassic(); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); + const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); const canSendInvoice = useMemo(() => canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email), [allPolicies, session?.email]); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts index 48b739e544258..66dd92436b150 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts @@ -1,5 +1,5 @@ import {useCallback, useMemo} from 'react'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -19,6 +19,7 @@ import { // Will be fixed in https://github.com/Expensify/App/issues/76852 // eslint-disable-next-line @typescript-eslint/no-deprecated getReportName, + getWorkspaceChats, isPolicyExpenseChat, } from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; @@ -30,18 +31,22 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {QuickActionName} from '@src/types/onyx/QuickAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); + type UseQuickActionMenuItemParams = { shouldUseNarrowLayout: boolean; icons: MenuItemIcons; reportID: string; - session: {email?: string; accountID?: number} | undefined; - policyChatForActivePolicy: OnyxEntry; - allTransactionDrafts: OnyxCollection; }; -function useQuickActionMenuItem({shouldUseNarrowLayout, icons, reportID, session, policyChatForActivePolicy, allTransactionDrafts}: UseQuickActionMenuItemParams): PopoverMenuItem[] { +function useQuickActionMenuItem({shouldUseNarrowLayout, icons, reportID}: UseQuickActionMenuItemParams): PopoverMenuItem[] { const styles = useThemeStyles(); const {translate, formatPhoneNumber} = useLocalize(); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); + const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); + const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, {canBeMissing: true}); const [quickActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`, {canBeMissing: true}); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); @@ -58,6 +63,14 @@ function useQuickActionMenuItem({shouldUseNarrowLayout, icons, reportID, session const isValidReport = !(isEmptyObject(quickActionReport) || isReportArchived); + const policyChatForActivePolicy = useMemo(() => { + if (isEmptyObject(activePolicy) || !activePolicy?.isPolicyExpenseChatEnabled) { + return {} as OnyxTypes.Report; + } + const policyChats = getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], allReports); + return policyChats.length > 0 ? policyChats.at(0) : ({} as OnyxTypes.Report); + }, [activePolicy, activePolicyID, session?.accountID, allReports]); + const selectOption = useCallback( (onSelected: () => void, shouldRestrictAction: boolean) => { if (shouldRestrictAction && quickActionReport?.policyID && shouldRestrictUserBillableActions(quickActionReport.policyID)) { diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts index 935cef7f73e98..7a188e6c4e913 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts @@ -61,7 +61,7 @@ function useScanActions() { }); }, [policyChatPolicyID, policyChatReportID, reportID, allTransactionDrafts]); - return {startScan, startQuickScan, reportID, activePolicyID, session, policyChatForActivePolicy, allTransactionDrafts}; + return {startScan, startQuickScan, reportID, activePolicyID}; } export default useScanActions; diff --git a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx index 7ee05d626d7d4..5bde665e943e5 100644 --- a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx @@ -29,7 +29,7 @@ function FloatingActionButtonAndPopover() { const [isMenuMounted, setIsMenuMounted] = useState(false); const fabRef = useRef(null); - const {startScan, startQuickScan, reportID, activePolicyID, session, policyChatForActivePolicy, allTransactionDrafts} = useScanActions(); + const {startScan, startQuickScan, reportID, activePolicyID} = useScanActions(); const showCreateMenu = useCallback(() => { if (!isFocused && shouldUseNarrowLayout) { @@ -86,9 +86,6 @@ function FloatingActionButtonAndPopover() { shouldUseNarrowLayout={shouldUseNarrowLayout} reportID={reportID} activePolicyID={activePolicyID} - session={session} - policyChatForActivePolicy={policyChatForActivePolicy} - allTransactionDrafts={allTransactionDrafts} /> {!shouldUseNarrowLayout && ( Date: Mon, 23 Feb 2026 13:14:33 +0100 Subject: [PATCH 04/54] use selector on REPORT collection to avoid full collection re-renders --- .../menuItems/useQuickActionMenuItem.ts | 11 +++++++---- .../inbox/sidebar/FABPopoverContent/useScanActions.ts | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts index 66dd92436b150..acd9ba08b78e9 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts @@ -1,5 +1,5 @@ import {useCallback, useMemo} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -46,7 +46,11 @@ function useQuickActionMenuItem({shouldUseNarrowLayout, icons, reportID}: UseQui const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); - const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); + const workspaceChatsSelector = useCallback( + (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports), + [activePolicyID, session?.accountID], + ); + const [policyChats = []] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector, canBeMissing: true}); const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, {canBeMissing: true}); const [quickActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`, {canBeMissing: true}); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); @@ -67,9 +71,8 @@ function useQuickActionMenuItem({shouldUseNarrowLayout, icons, reportID}: UseQui if (isEmptyObject(activePolicy) || !activePolicy?.isPolicyExpenseChatEnabled) { return {} as OnyxTypes.Report; } - const policyChats = getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], allReports); return policyChats.length > 0 ? policyChats.at(0) : ({} as OnyxTypes.Report); - }, [activePolicy, activePolicyID, session?.accountID, allReports]); + }, [activePolicy, policyChats]); const selectOption = useCallback( (onSelected: () => void, shouldRestrictAction: boolean) => { diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts index 7a188e6c4e913..37a7eaa090700 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts @@ -1,5 +1,5 @@ import {useCallback, useMemo} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import useOnyx from '@hooks/useOnyx'; import {startMoneyRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; @@ -21,7 +21,11 @@ function useScanActions() { const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); - const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); + const workspaceChatsSelector = useCallback( + (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports), + [activePolicyID, session?.accountID], + ); + const [policyChats = []] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector, canBeMissing: true}); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); @@ -31,9 +35,8 @@ function useScanActions() { if (isEmptyObject(activePolicy) || !activePolicy?.isPolicyExpenseChatEnabled) { return {} as OnyxTypes.Report; } - const policyChats = getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], allReports); return policyChats.length > 0 ? policyChats.at(0) : ({} as OnyxTypes.Report); - }, [activePolicy, activePolicyID, session?.accountID, allReports]); + }, [activePolicy, policyChats]); const startScan = useCallback(() => { interceptAnonymousUser(() => { From 8beca63e0f23536b8d86a2cac9637389df44b893 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 23 Feb 2026 13:16:18 +0100 Subject: [PATCH 05/54] add justification comments to eslint-disable lines --- .../FABPopoverContent/menuItems/useQuickActionMenuItem.ts | 2 ++ src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts index acd9ba08b78e9..9ca54c4a2ae81 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts @@ -125,6 +125,8 @@ function useQuickActionMenuItem({shouldUseNarrowLayout, icons, reportID}: UseQui const quickActionSubtitle = useMemo(() => { // eslint-disable-next-line @typescript-eslint/no-deprecated return !hideQABSubtitle ? (getReportName(quickActionReport, quickActionPolicy, undefined, personalDetails) ?? translate('quickAction.updateDestination')) : ''; + // Intentionally using property accessors (quickAction?.action, quickActionPolicy?.name) instead of the + // full objects to prevent recomputation when unrelated properties on those objects change // eslint-disable-next-line react-hooks/exhaustive-deps }, [hideQABSubtitle, personalDetails, quickAction?.action, quickActionPolicy?.name, quickActionReport, translate]); diff --git a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx index 5bde665e943e5..adcabf7a32c74 100644 --- a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx @@ -57,7 +57,8 @@ function FloatingActionButtonAndPopover() { return; } - // Hide menu manually when other pages are opened using shortcut key + // Intentionally calling setState inside useEffect — we need to imperatively respond to + // navigation focus changes (an external event) which can't be expressed as an event handler // eslint-disable-next-line react-hooks/set-state-in-effect hideCreateMenu(); }, [didScreenBecomeInactive, hideCreateMenu]); From 2224d4dc2683cb7e1811b5fde7d36631b8f72dd2 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 23 Feb 2026 13:30:50 +0100 Subject: [PATCH 06/54] remove useMemo from modified files --- .../FABPopoverContentInner.tsx | 7 +- .../menuItems/useInvoiceMenuItem.ts | 44 ++- .../menuItems/useQuickActionMenuItem.ts | 255 ++++++++---------- .../FABPopoverContent/useScanActions.ts | 12 +- 4 files changed, 133 insertions(+), 185 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx index fab466e90fcfd..e11ff8357fd77 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React from 'react'; import PopoverMenu from '@components/PopoverMenu'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction'; @@ -61,10 +61,7 @@ function FABPopoverContentInner({ const newWorkspaceItem = useNewWorkspaceMenuItem({shouldUseNarrowLayout, icons}); const quickActionItem = useQuickActionMenuItem({shouldUseNarrowLayout, icons, reportID}); - const menuItems = useMemo( - () => [...expenseItem, ...trackDistanceItem, ...createReportItem, ...newChatItem, ...invoiceItem, ...travelItem, ...testDriveItem, ...newWorkspaceItem, ...quickActionItem], - [expenseItem, trackDistanceItem, createReportItem, newChatItem, invoiceItem, travelItem, testDriveItem, newWorkspaceItem, quickActionItem], - ); + const menuItems = [...expenseItem, ...trackDistanceItem, ...createReportItem, ...newChatItem, ...invoiceItem, ...travelItem, ...testDriveItem, ...newWorkspaceItem, ...quickActionItem]; return ( <> diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useInvoiceMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useInvoiceMenuItem.ts index ff3f058d053f1..41a8d2a1af2ae 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useInvoiceMenuItem.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useInvoiceMenuItem.ts @@ -1,4 +1,4 @@ -import {useMemo} from 'react'; +import type {OnyxCollection} from 'react-native-onyx'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -23,29 +23,27 @@ function useInvoiceMenuItem({shouldUseNarrowLayout, icons, reportID}: UseInvoice const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); - const canSendInvoice = useMemo(() => canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email), [allPolicies, session?.email]); + const canSendInvoice = canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email); - return useMemo(() => { - if (!canSendInvoice) { - return []; - } - return [ - { - icon: icons.InvoiceGeneric, - text: translate('workspace.invoices.sendInvoice'), - shouldCallAfterModalHide: shouldRedirectToExpensifyClassic || shouldUseNarrowLayout, - onSelected: () => - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - startMoneyRequest(CONST.IOU.TYPE.INVOICE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); - }), - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.SEND_INVOICE, - }, - ]; - }, [canSendInvoice, icons.InvoiceGeneric, translate, shouldRedirectToExpensifyClassic, shouldUseNarrowLayout, showRedirectToExpensifyClassicModal, reportID, allTransactionDrafts]); + if (!canSendInvoice) { + return []; + } + return [ + { + icon: icons.InvoiceGeneric, + text: translate('workspace.invoices.sendInvoice'), + shouldCallAfterModalHide: shouldRedirectToExpensifyClassic || shouldUseNarrowLayout, + onSelected: () => + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + startMoneyRequest(CONST.IOU.TYPE.INVOICE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); + }), + sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.SEND_INVOICE, + }, + ]; } export default useInvoiceMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts index 9ca54c4a2ae81..79c3081642b73 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts @@ -1,4 +1,4 @@ -import {useCallback, useMemo} from 'react'; +import {useCallback} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import type {PopoverMenuItem} from '@components/PopoverMenu'; @@ -67,12 +67,8 @@ function useQuickActionMenuItem({shouldUseNarrowLayout, icons, reportID}: UseQui const isValidReport = !(isEmptyObject(quickActionReport) || isReportArchived); - const policyChatForActivePolicy = useMemo(() => { - if (isEmptyObject(activePolicy) || !activePolicy?.isPolicyExpenseChatEnabled) { - return {} as OnyxTypes.Report; - } - return policyChats.length > 0 ? policyChats.at(0) : ({} as OnyxTypes.Report); - }, [activePolicy, policyChats]); + const policyChatForActivePolicy: OnyxTypes.Report = + !isEmptyObject(activePolicy) && activePolicy?.isPolicyExpenseChatEnabled && policyChats.length > 0 ? (policyChats.at(0) ?? ({} as OnyxTypes.Report)) : ({} as OnyxTypes.Report); const selectOption = useCallback( (onSelected: () => void, shouldRestrictAction: boolean) => { @@ -85,159 +81,120 @@ function useQuickActionMenuItem({shouldUseNarrowLayout, icons, reportID}: UseQui [quickActionReport?.policyID], ); - const quickActionAvatars = useMemo(() => { - if (isValidReport) { - const avatars = getIcons(quickActionReport, formatPhoneNumber, personalDetails, null, undefined, undefined, undefined, undefined, isReportArchived); - return avatars.length <= 1 || isPolicyExpenseChat(quickActionReport) ? avatars : avatars.filter((avatar) => avatar.id !== session?.accountID); - } - if (!isEmptyObject(policyChatForActivePolicy)) { - return getIcons(policyChatForActivePolicy, formatPhoneNumber, personalDetails, null, undefined, undefined, undefined, undefined, isReportArchived); - } - return []; - // Policy is needed as a dependency in order to update the shortcut details when the workspace changes - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [personalDetails, session?.accountID, quickActionReport, quickActionPolicy, policyChatForActivePolicy, isReportArchived, isValidReport]); - - const quickActionTitle = useMemo(() => { - if (isEmptyObject(quickActionReport)) { - return ''; - } + let quickActionAvatars: ReturnType = []; + if (isValidReport) { + const avatars = getIcons(quickActionReport, formatPhoneNumber, personalDetails, null, undefined, undefined, undefined, undefined, isReportArchived); + quickActionAvatars = avatars.length <= 1 || isPolicyExpenseChat(quickActionReport) ? avatars : avatars.filter((avatar) => avatar.id !== session?.accountID); + } else if (!isEmptyObject(policyChatForActivePolicy)) { + quickActionAvatars = getIcons(policyChatForActivePolicy, formatPhoneNumber, personalDetails, null, undefined, undefined, undefined, undefined, isReportArchived); + } + + let quickActionTitle = ''; + if (!isEmptyObject(quickActionReport)) { if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && quickActionAvatars.length > 0) { const accountID = quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID; const name = getDisplayNameForParticipant({accountID: Number(accountID), shouldUseShortForm: true, formatPhoneNumber}) ?? ''; - return translate('quickAction.paySomeone', name); + quickActionTitle = translate('quickAction.paySomeone', name); + } else { + const titleKey = getQuickActionTitle(quickAction?.action ?? ('' as QuickActionName)); + quickActionTitle = titleKey ? translate(titleKey) : ''; } - const titleKey = getQuickActionTitle(quickAction?.action ?? ('' as QuickActionName)); - return titleKey ? translate(titleKey) : ''; - }, [quickAction?.action, translate, quickActionAvatars, quickActionReport, formatPhoneNumber]); + } - const hideQABSubtitle = useMemo(() => { - if (!isValidReport) { - return true; - } + let hideQABSubtitle = true; + if (isValidReport) { if (quickActionAvatars.length === 0) { - return false; + hideQABSubtitle = false; + } else { + const displayName = personalDetails?.[quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID]?.firstName ?? ''; + hideQABSubtitle = quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && displayName.length === 0; } - const displayName = personalDetails?.[quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID]?.firstName ?? ''; - return quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && displayName.length === 0; - }, [isValidReport, quickActionAvatars, personalDetails, quickAction?.action]); - - const quickActionSubtitle = useMemo(() => { - // eslint-disable-next-line @typescript-eslint/no-deprecated - return !hideQABSubtitle ? (getReportName(quickActionReport, quickActionPolicy, undefined, personalDetails) ?? translate('quickAction.updateDestination')) : ''; - // Intentionally using property accessors (quickAction?.action, quickActionPolicy?.name) instead of the - // full objects to prevent recomputation when unrelated properties on those objects change - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hideQABSubtitle, personalDetails, quickAction?.action, quickActionPolicy?.name, quickActionReport, translate]); - - return useMemo(() => { - const baseQuickAction = { - label: translate('quickAction.header'), - labelStyle: [styles.pt3, styles.pb2], - isLabelHoverable: false, - numberOfLinesDescription: 1, - tooltipAnchorAlignment: { - vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, - horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, - }, - shouldTeleportPortalToModalLayer: true, - }; + } - if (quickAction?.action && quickActionReport) { - if (!isQuickActionAllowed(quickAction, quickActionReport, quickActionPolicy, isReportArchived, allBetas, isRestrictedToPreferredPolicy)) { - return []; - } - const onSelected = () => { - interceptAnonymousUser(() => { - if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && isDelegateAccessRestricted) { - showDelegateNoAccessModal(); - return; - } - const targetAccountPersonalDetails = { - ...personalDetails?.[quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID], - accountID: quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID, - }; - - navigateToQuickAction({ - isValidReport, - quickAction, - selectOption, - lastDistanceExpenseType, - targetAccountPersonalDetails, - currentUserAccountID: currentUserPersonalDetails.accountID, - isFromFloatingActionButton: true, - }); - }); - }; - return [ - { - ...baseQuickAction, - icon: getQuickActionIcon(icons as Parameters[0], quickAction?.action), - text: quickActionTitle, - rightIconAccountID: quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID, - description: quickActionSubtitle, - onSelected, - shouldCallAfterModalHide: shouldUseNarrowLayout, - rightIconReportID: quickActionReport?.reportID, - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.QUICK_ACTION, - }, - ]; + // eslint-disable-next-line @typescript-eslint/no-deprecated + const quickActionSubtitle = !hideQABSubtitle ? (getReportName(quickActionReport, quickActionPolicy, undefined, personalDetails) ?? translate('quickAction.updateDestination')) : ''; + + const baseQuickAction = { + label: translate('quickAction.header'), + labelStyle: [styles.pt3, styles.pb2], + isLabelHoverable: false, + numberOfLinesDescription: 1, + tooltipAnchorAlignment: { + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, + }, + shouldTeleportPortalToModalLayer: true, + }; + + if (quickAction?.action && quickActionReport) { + if (!isQuickActionAllowed(quickAction, quickActionReport, quickActionPolicy, isReportArchived, allBetas, isRestrictedToPreferredPolicy)) { + return []; } - if (!isEmptyObject(policyChatForActivePolicy)) { - const onSelected = () => { - interceptAnonymousUser(() => { - if (policyChatForActivePolicy?.policyID && shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatForActivePolicy.policyID)); - return; - } - - const quickActionReportID = policyChatForActivePolicy?.reportID || reportID; - startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, allTransactionDrafts, true); + const onSelected = () => { + interceptAnonymousUser(() => { + if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + const targetAccountPersonalDetails = { + ...personalDetails?.[quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID], + accountID: quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID, + }; + + navigateToQuickAction({ + isValidReport, + quickAction, + selectOption, + lastDistanceExpenseType, + targetAccountPersonalDetails, + currentUserAccountID: currentUserPersonalDetails.accountID, + isFromFloatingActionButton: true, }); - }; - - return [ - { - ...baseQuickAction, - icon: icons.ReceiptScan, - text: translate('quickAction.scanReceipt'), - // eslint-disable-next-line @typescript-eslint/no-deprecated - description: getReportName(policyChatForActivePolicy), - shouldCallAfterModalHide: shouldUseNarrowLayout, - onSelected, - rightIconReportID: policyChatForActivePolicy?.reportID, - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.QUICK_ACTION, - }, - ]; - } + }); + }; + return [ + { + ...baseQuickAction, + icon: getQuickActionIcon(icons as Parameters[0], quickAction?.action), + text: quickActionTitle, + rightIconAccountID: quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID, + description: quickActionSubtitle, + onSelected, + shouldCallAfterModalHide: shouldUseNarrowLayout, + rightIconReportID: quickActionReport?.reportID, + sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.QUICK_ACTION, + }, + ]; + } + if (!isEmptyObject(policyChatForActivePolicy)) { + const onSelected = () => { + interceptAnonymousUser(() => { + if (policyChatForActivePolicy?.policyID && shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatForActivePolicy.policyID)); + return; + } + + const quickActionReportID = policyChatForActivePolicy?.reportID || reportID; + startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, allTransactionDrafts, true); + }); + }; + + return [ + { + ...baseQuickAction, + icon: icons.ReceiptScan, + text: translate('quickAction.scanReceipt'), + // eslint-disable-next-line @typescript-eslint/no-deprecated + description: getReportName(policyChatForActivePolicy), + shouldCallAfterModalHide: shouldUseNarrowLayout, + onSelected, + rightIconReportID: policyChatForActivePolicy?.reportID, + sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.QUICK_ACTION, + }, + ]; + } - return []; - }, [ - icons, - translate, - styles.pt3, - styles.pb2, - quickAction, - policyChatForActivePolicy, - quickActionReport, - quickActionPolicy, - isReportArchived, - isRestrictedToPreferredPolicy, - quickActionTitle, - quickActionAvatars, - quickActionSubtitle, - shouldUseNarrowLayout, - isDelegateAccessRestricted, - isValidReport, - selectOption, - lastDistanceExpenseType, - personalDetails, - currentUserPersonalDetails.accountID, - showDelegateNoAccessModal, - reportID, - allTransactionDrafts, - allBetas, - ]); + return []; } export default useQuickActionMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts index 37a7eaa090700..83be9d26fb2b3 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts @@ -1,4 +1,4 @@ -import {useCallback, useMemo} from 'react'; +import {useCallback, useRef} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import useOnyx from '@hooks/useOnyx'; import {startMoneyRequest} from '@libs/actions/IOU'; @@ -29,14 +29,10 @@ function useScanActions() { const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); - const reportID = useMemo(() => generateReportID(), []); + const reportID = useRef(generateReportID()).current; - const policyChatForActivePolicy = useMemo(() => { - if (isEmptyObject(activePolicy) || !activePolicy?.isPolicyExpenseChatEnabled) { - return {} as OnyxTypes.Report; - } - return policyChats.length > 0 ? policyChats.at(0) : ({} as OnyxTypes.Report); - }, [activePolicy, policyChats]); + const policyChatForActivePolicy: OnyxTypes.Report = + !isEmptyObject(activePolicy) && activePolicy?.isPolicyExpenseChatEnabled && policyChats.length > 0 ? (policyChats.at(0) ?? ({} as OnyxTypes.Report)) : ({} as OnyxTypes.Report); const startScan = useCallback(() => { interceptAnonymousUser(() => { From 538804d6e32775356c6e6347bec45065b7b08855 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 23 Feb 2026 13:57:46 +0100 Subject: [PATCH 07/54] convert FAB menu hooks to JSX components with registry pattern --- .../sidebar/FABPopoverContent/FABMenuItem.tsx | 30 +++ .../FABPopoverContent/FABMenuRegistry.tsx | 37 +++ .../FABMenuRegistryContext.tsx | 18 ++ .../FABPopoverContentInner.tsx | 75 ++++-- .../menuItems/CreateReportMenuItem.tsx | 147 ++++++++++++ .../menuItems/ExpenseMenuItem.tsx | 45 ++++ .../menuItems/InvoiceMenuItem.tsx | 53 +++++ .../menuItems/NewChatMenuItem.tsx | 29 +++ .../menuItems/NewWorkspaceMenuItem.tsx | 64 ++++++ .../menuItems/QuickActionMenuItem.tsx | 215 ++++++++++++++++++ .../menuItems/TestDriveMenuItem.tsx | 45 ++++ .../menuItems/TrackDistanceMenuItem.tsx | 43 ++++ .../menuItems/TravelMenuItem.tsx | 67 ++++++ 13 files changed, 846 insertions(+), 22 deletions(-) create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/FABMenuItem.tsx create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/FABMenuRegistry.tsx create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/FABMenuRegistryContext.tsx create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuItem.tsx new file mode 100644 index 0000000000000..e6b702cce0583 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuItem.tsx @@ -0,0 +1,30 @@ +import {useLayoutEffect, useRef} from 'react'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; +import {useFABMenuRegistryContext} from './FABMenuRegistryContext'; + +type FABMenuItemProps = PopoverMenuItem & { + /** Unique stable ID for this item in the registry - use sentryLabel */ + registryId: string; +}; + +function FABMenuItem({registryId, ...item}: FABMenuItemProps) { + const {registerItem, unregisterItem} = useFABMenuRegistryContext(); + const itemRef = useRef(item); + itemRef.current = item; + + // Re-register on every render (overwrites with latest props) + useLayoutEffect(() => { + registerItem(registryId, itemRef.current); + }); + + // Unregister only on unmount + useLayoutEffect( + () => () => unregisterItem(registryId), + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); + + return null; +} + +export default FABMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuRegistry.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuRegistry.tsx new file mode 100644 index 0000000000000..3e54ee642b0b1 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuRegistry.tsx @@ -0,0 +1,37 @@ +import React, {useCallback, useRef} from 'react'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; +import {FABMenuRegistryContext} from './FABMenuRegistryContext'; + +type FABMenuRegistryProps = { + children: React.ReactNode; + onItemsChange: (items: PopoverMenuItem[]) => void; +}; + +function FABMenuRegistry({children, onItemsChange}: FABMenuRegistryProps) { + const orderedIdsRef = useRef([]); + const itemsMapRef = useRef>(new Map()); + + const registerItem = useCallback( + (id: string, item: PopoverMenuItem) => { + if (!orderedIdsRef.current.includes(id)) { + orderedIdsRef.current = [...orderedIdsRef.current, id]; + } + itemsMapRef.current.set(id, item); + onItemsChange(orderedIdsRef.current.map((i) => itemsMapRef.current.get(i)).filter(Boolean) as PopoverMenuItem[]); + }, + [onItemsChange], + ); + + const unregisterItem = useCallback( + (id: string) => { + orderedIdsRef.current = orderedIdsRef.current.filter((i) => i !== id); + itemsMapRef.current.delete(id); + onItemsChange(orderedIdsRef.current.map((i) => itemsMapRef.current.get(i)).filter(Boolean) as PopoverMenuItem[]); + }, + [onItemsChange], + ); + + return {children}; +} + +export default FABMenuRegistry; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuRegistryContext.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuRegistryContext.tsx new file mode 100644 index 0000000000000..ef9ca6ab71501 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuRegistryContext.tsx @@ -0,0 +1,18 @@ +import {createContext, useContext} from 'react'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; + +type FABMenuRegistryContextType = { + registerItem: (id: string, item: PopoverMenuItem) => void; + unregisterItem: (id: string) => void; +}; + +const FABMenuRegistryContext = createContext({ + registerItem: () => {}, + unregisterItem: () => {}, +}); + +function useFABMenuRegistryContext() { + return useContext(FABMenuRegistryContext); +} + +export {FABMenuRegistryContext, useFABMenuRegistryContext}; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx index e11ff8357fd77..fd05edd21870e 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx @@ -1,17 +1,19 @@ -import React from 'react'; +import React, {useState} from 'react'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; import PopoverMenu from '@components/PopoverMenu'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction'; import CONST from '@src/CONST'; -import useCreateReportMenuItem from './menuItems/useCreateReportMenuItem'; -import useExpenseMenuItem from './menuItems/useExpenseMenuItem'; -import useInvoiceMenuItem from './menuItems/useInvoiceMenuItem'; -import useNewChatMenuItem from './menuItems/useNewChatMenuItem'; -import useNewWorkspaceMenuItem from './menuItems/useNewWorkspaceMenuItem'; -import useQuickActionMenuItem from './menuItems/useQuickActionMenuItem'; -import useTestDriveMenuItem from './menuItems/useTestDriveMenuItem'; -import useTrackDistanceMenuItem from './menuItems/useTrackDistanceMenuItem'; -import useTravelMenuItem from './menuItems/useTravelMenuItem'; +import FABMenuRegistry from './FABMenuRegistry'; +import CreateReportMenuItem from './menuItems/CreateReportMenuItem'; +import ExpenseMenuItem from './menuItems/ExpenseMenuItem'; +import InvoiceMenuItem from './menuItems/InvoiceMenuItem'; +import NewChatMenuItem from './menuItems/NewChatMenuItem'; +import NewWorkspaceMenuItem from './menuItems/NewWorkspaceMenuItem'; +import QuickActionMenuItem from './menuItems/QuickActionMenuItem'; +import TestDriveMenuItem from './menuItems/TestDriveMenuItem'; +import TrackDistanceMenuItem from './menuItems/TrackDistanceMenuItem'; +import TravelMenuItem from './menuItems/TravelMenuItem'; import type {FABPopoverContentInnerProps} from './types'; type FABPopoverContentInnerExtraProps = FABPopoverContentInnerProps & { @@ -51,21 +53,50 @@ function FABPopoverContentInner({ 'Clock', ] as const); - const expenseItem = useExpenseMenuItem({shouldUseNarrowLayout, icons, reportID}); - const trackDistanceItem = useTrackDistanceMenuItem({shouldUseNarrowLayout, icons, reportID}); - const {menuItem: createReportItem, confirmationModal} = useCreateReportMenuItem({shouldUseNarrowLayout, icons, activePolicyID}); - const newChatItem = useNewChatMenuItem({shouldUseNarrowLayout, icons}); - const invoiceItem = useInvoiceMenuItem({shouldUseNarrowLayout, icons, reportID}); - const travelItem = useTravelMenuItem({icons, activePolicyID}); - const testDriveItem = useTestDriveMenuItem({icons}); - const newWorkspaceItem = useNewWorkspaceMenuItem({shouldUseNarrowLayout, icons}); - const quickActionItem = useQuickActionMenuItem({shouldUseNarrowLayout, icons, reportID}); - - const menuItems = [...expenseItem, ...trackDistanceItem, ...createReportItem, ...newChatItem, ...invoiceItem, ...travelItem, ...testDriveItem, ...newWorkspaceItem, ...quickActionItem]; + const [menuItems, setMenuItems] = useState([]); return ( <> - {confirmationModal} + + + + + + + + + + + ) => ({email: session?.email, accountID: session?.accountID}); + +function CreateReportMenuItem({shouldUseNarrowLayout, icons, activePolicyID}: CreateReportMenuItemProps) { + const {translate} = useLocalize(); + const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); + const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); + const [hasDismissedEmptyReportsConfirmation] = useOnyx(ONYXKEYS.NVP_EMPTY_REPORTS_CONFIRMATION_DISMISSED, {canBeMissing: true}); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const {isBetaEnabled} = usePermissions(); + const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); + const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); + + const groupPaidPoliciesWithChatEnabled = useCallback( + (policies: Parameters[0]) => groupPaidPoliciesWithExpenseChatEnabledSelector(policies, session?.email), + [session?.email], + ); + + const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPaidPoliciesWithChatEnabled, canBeMissing: true}, [session?.email]); + + const shouldShowCreateReportOption = shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; + + const defaultChatEnabledPolicy = useMemo( + () => getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array>, activePolicy), + [activePolicy, groupPoliciesWithChatEnabled], + ); + + const defaultChatEnabledPolicyID = defaultChatEnabledPolicy?.id; + const hasEmptyReport = useHasEmptyReportsForPolicy(defaultChatEnabledPolicyID); + const shouldShowEmptyReportConfirmation = hasEmptyReport && hasDismissedEmptyReportsConfirmation !== true; + + const isReportInSearch = isOnSearchMoneyRequestReportPage(); + + const handleCreateWorkspaceReport = useCallback( + (shouldDismissEmptyReportsConfirmation?: boolean) => { + if (!defaultChatEnabledPolicy?.id) { + return; + } + + if (isReportInSearch) { + clearLastSearchParams(); + } + + const {reportID: createdReportID} = createNewReport( + currentUserPersonalDetails, + hasViolations, + isASAPSubmitBetaEnabled, + defaultChatEnabledPolicy, + allBetas, + false, + shouldDismissEmptyReportsConfirmation, + ); + Navigation.setNavigationActionToMicrotaskQueue(() => { + Navigation.navigate( + isSearchTopmostFullScreenRoute() + ? ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: createdReportID, backTo: Navigation.getActiveRoute()}) + : ROUTES.REPORT_WITH_ID.getRoute(createdReportID, undefined, undefined, Navigation.getActiveRoute()), + {forceReplace: isReportInSearch}, + ); + }); + }, + [currentUserPersonalDetails, hasViolations, defaultChatEnabledPolicy, isASAPSubmitBetaEnabled, isReportInSearch, allBetas], + ); + + const {openCreateReportConfirmation, CreateReportConfirmationModal} = useCreateEmptyReportConfirmation({ + policyID: defaultChatEnabledPolicyID, + policyName: defaultChatEnabledPolicy?.name ?? '', + onConfirm: handleCreateWorkspaceReport, + }); + + return ( + <> + {shouldShowCreateReportOption && ( + { + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + + const workspaceIDForReportCreation = defaultChatEnabledPolicyID; + + if (!workspaceIDForReportCreation || (shouldRestrictUserBillableActions(workspaceIDForReportCreation) && groupPoliciesWithChatEnabled.length > 1)) { + Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute()); + return; + } + + if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { + if (shouldShowEmptyReportConfirmation) { + openCreateReportConfirmation(); + } else { + handleCreateWorkspaceReport(false); + } + return; + } + + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(workspaceIDForReportCreation)); + }); + }} + sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.CREATE_REPORT} + /> + )} + {CreateReportConfirmationModal} + + ); +} + +export default CreateReportMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx new file mode 100644 index 0000000000000..8e8accc423c13 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import {startMoneyRequest} from '@libs/actions/IOU'; +import getIconForAction from '@libs/getIconForAction'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type ExpenseMenuItemProps = { + shouldUseNarrowLayout: boolean; + icons: MenuItemIcons; + reportID: string; +}; + +function ExpenseMenuItem({shouldUseNarrowLayout, icons, reportID}: ExpenseMenuItemProps) { + const {translate} = useLocalize(); + const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); + const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); + + return ( + [1])} + text={translate('iou.createExpense')} + testID="create-expense" + shouldCallAfterModalHide={shouldRedirectToExpensifyClassic || shouldUseNarrowLayout} + onSelected={() => + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); + }) + } + sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.CREATE_EXPENSE} + /> + ); +} + +export default ExpenseMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx new file mode 100644 index 0000000000000..483a4f3254b54 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import type {OnyxCollection} from 'react-native-onyx'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import {startMoneyRequest} from '@libs/actions/IOU'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import {canSendInvoice as canSendInvoicePolicyUtils} from '@libs/PolicyUtils'; +import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; + +type InvoiceMenuItemProps = { + shouldUseNarrowLayout: boolean; + icons: MenuItemIcons; + reportID: string; +}; + +function InvoiceMenuItem({shouldUseNarrowLayout, icons, reportID}: InvoiceMenuItemProps) { + const {translate} = useLocalize(); + const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal, allPolicies} = useRedirectToExpensifyClassic(); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); + const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); + + const canSendInvoice = canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email); + + if (!canSendInvoice) { + return null; + } + + return ( + + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + startMoneyRequest(CONST.IOU.TYPE.INVOICE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); + }) + } + sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.SEND_INVOICE} + /> + ); +} + +export default InvoiceMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx new file mode 100644 index 0000000000000..0d4a8ce0444ce --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import useLocalize from '@hooks/useLocalize'; +import {startNewChat} from '@libs/actions/Report'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import CONST from '@src/CONST'; + +type NewChatMenuItemProps = { + shouldUseNarrowLayout: boolean; + icons: MenuItemIcons; +}; + +function NewChatMenuItem({shouldUseNarrowLayout, icons}: NewChatMenuItemProps) { + const {translate} = useLocalize(); + + return ( + interceptAnonymousUser(startNewChat)} + sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.START_CHAT} + /> + ); +} + +export default NewChatMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx new file mode 100644 index 0000000000000..7d894e5c75fe2 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx @@ -0,0 +1,64 @@ +import type {ImageContentFit} from 'expo-image'; +import React, {useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import useLocalize from '@hooks/useLocalize'; +import useMappedPolicies from '@hooks/useMappedPolicies'; +import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; +import usePreferredPolicy from '@hooks/usePreferredPolicy'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import Navigation from '@libs/Navigation/Navigation'; +import {shouldShowPolicy} from '@libs/PolicyUtils'; +import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import {policyMapper} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; + +type NewWorkspaceMenuItemProps = { + shouldUseNarrowLayout: boolean; + icons: MenuItemIcons; +}; + +function NewWorkspaceMenuItem({shouldUseNarrowLayout, icons}: NewWorkspaceMenuItemProps) { + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); + const [isLoading = false] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); + const [allPolicies] = useMappedPolicies(policyMapper); + const {isRestrictedPolicyCreation} = usePreferredPolicy(); + + const shouldShowNewWorkspaceButton = useMemo(() => { + if (isRestrictedPolicyCreation) { + return false; + } + const isOfflineBool = !!isOffline; + const email = session?.email; + return Object.values(allPolicies ?? {}).every((policy) => !shouldShowPolicy(policy as OnyxEntry, isOfflineBool, email)); + }, [isRestrictedPolicyCreation, allPolicies, isOffline, session?.email]); + + if (isLoading || !shouldShowNewWorkspaceButton) { + return null; + } + + return ( + interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(Navigation.getActiveRoute())))} + sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.NEW_WORKSPACE} + /> + ); +} + +export default NewWorkspaceMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx new file mode 100644 index 0000000000000..849828f3278d5 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -0,0 +1,215 @@ +import React, {useCallback} from 'react'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import usePreferredPolicy from '@hooks/usePreferredPolicy'; +import useReportIsArchived from '@hooks/useReportIsArchived'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {startMoneyRequest} from '@libs/actions/IOU'; +import {navigateToQuickAction} from '@libs/actions/QuickActionNavigation'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import Navigation from '@libs/Navigation/Navigation'; +import {getQuickActionIcon, getQuickActionTitle, isQuickActionAllowed} from '@libs/QuickActionUtils'; +import { + getDisplayNameForParticipant, + getIcons, + // Will be fixed in https://github.com/Expensify/App/issues/76852 + // eslint-disable-next-line @typescript-eslint/no-deprecated + getReportName, + getWorkspaceChats, + isPolicyExpenseChat, +} from '@libs/ReportUtils'; +import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; +import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {QuickActionName} from '@src/types/onyx/QuickAction'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import getEmptyArray from '@src/types/utils/getEmptyArray'; + +const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); + +type QuickActionMenuItemProps = { + shouldUseNarrowLayout: boolean; + icons: MenuItemIcons; + reportID: string; +}; + +function QuickActionMenuItem({shouldUseNarrowLayout, icons, reportID}: QuickActionMenuItemProps) { + const styles = useThemeStyles(); + const {translate, formatPhoneNumber} = useLocalize(); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); + const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); + const workspaceChatsSelector = useCallback( + (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports), + [activePolicyID, session?.accountID], + ); + const [policyChats = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector, canBeMissing: true}); + const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, {canBeMissing: true}); + const [quickActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`, {canBeMissing: true}); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); + const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); + const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const {isDelegateAccessRestricted} = useDelegateNoAccessState(); + const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); + const isReportArchived = useReportIsArchived(quickActionReport?.reportID); + const {isRestrictedToPreferredPolicy} = usePreferredPolicy(); + + const quickActionPolicyID = quickAction?.action === CONST.QUICK_ACTIONS.TRACK_PER_DIEM && quickAction?.perDiemPolicyID ? quickAction?.perDiemPolicyID : quickActionReport?.policyID; + const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionPolicyID}`, {canBeMissing: true}); + + const isValidReport = !(isEmptyObject(quickActionReport) || isReportArchived); + + const policyChatForActivePolicy: OnyxTypes.Report = + !isEmptyObject(activePolicy) && activePolicy?.isPolicyExpenseChatEnabled && policyChats.length > 0 ? (policyChats.at(0) ?? ({} as OnyxTypes.Report)) : ({} as OnyxTypes.Report); + + const quickActionReportPolicyID = quickActionReport?.policyID; + const selectOption = useCallback( + (onSelected: () => void, shouldRestrictAction: boolean) => { + if (shouldRestrictAction && quickActionReportPolicyID && shouldRestrictUserBillableActions(quickActionReportPolicyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(quickActionReportPolicyID)); + return; + } + onSelected(); + }, + [quickActionReportPolicyID], + ); + + let quickActionAvatars: ReturnType = []; + if (isValidReport) { + const avatars = getIcons(quickActionReport, formatPhoneNumber, personalDetails, null, undefined, undefined, undefined, undefined, isReportArchived); + quickActionAvatars = avatars.length <= 1 || isPolicyExpenseChat(quickActionReport) ? avatars : avatars.filter((avatar) => avatar.id !== session?.accountID); + } else if (!isEmptyObject(policyChatForActivePolicy)) { + quickActionAvatars = getIcons(policyChatForActivePolicy, formatPhoneNumber, personalDetails, null, undefined, undefined, undefined, undefined, isReportArchived); + } + + let quickActionTitle = ''; + if (!isEmptyObject(quickActionReport)) { + if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && quickActionAvatars.length > 0) { + const accountID = quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID; + const name = getDisplayNameForParticipant({accountID: Number(accountID), shouldUseShortForm: true, formatPhoneNumber}) ?? ''; + quickActionTitle = translate('quickAction.paySomeone', name); + } else { + const titleKey = getQuickActionTitle(quickAction?.action ?? ('' as QuickActionName)); + quickActionTitle = titleKey ? translate(titleKey) : ''; + } + } + + let hideQABSubtitle = true; + if (isValidReport) { + if (quickActionAvatars.length === 0) { + hideQABSubtitle = false; + } else { + const displayName = personalDetails?.[quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID]?.firstName ?? ''; + hideQABSubtitle = quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && displayName.length === 0; + } + } + + // eslint-disable-next-line @typescript-eslint/no-deprecated + const quickActionSubtitle = !hideQABSubtitle ? (getReportName(quickActionReport, quickActionPolicy, undefined, personalDetails) ?? translate('quickAction.updateDestination')) : ''; + + const baseQuickAction = { + label: translate('quickAction.header'), + labelStyle: [styles.pt3, styles.pb2], + isLabelHoverable: false, + numberOfLinesDescription: 1, + tooltipAnchorAlignment: { + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, + }, + shouldTeleportPortalToModalLayer: true, + }; + + if (quickAction?.action && quickActionReport) { + if (!isQuickActionAllowed(quickAction, quickActionReport, quickActionPolicy, isReportArchived, allBetas, isRestrictedToPreferredPolicy)) { + return null; + } + const onSelected = () => { + interceptAnonymousUser(() => { + if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + const targetAccountPersonalDetails = { + ...personalDetails?.[quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID], + accountID: quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID, + }; + + navigateToQuickAction({ + isValidReport, + quickAction, + selectOption, + lastDistanceExpenseType, + targetAccountPersonalDetails, + currentUserAccountID: currentUserPersonalDetails.accountID, + isFromFloatingActionButton: true, + }); + }); + }; + return ( + [0], quickAction?.action)} + text={quickActionTitle} + rightIconAccountID={quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID} + description={quickActionSubtitle} + onSelected={onSelected} + shouldCallAfterModalHide={shouldUseNarrowLayout} + rightIconReportID={quickActionReport?.reportID} + sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.QUICK_ACTION} + /> + ); + } + + if (!isEmptyObject(policyChatForActivePolicy)) { + const onSelected = () => { + interceptAnonymousUser(() => { + if (policyChatForActivePolicy?.policyID && shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatForActivePolicy.policyID)); + return; + } + + const quickActionReportID = policyChatForActivePolicy?.reportID || reportID; + startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, allTransactionDrafts, true); + }); + }; + + return ( + + ); + } + + return null; +} + +export default QuickActionMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx new file mode 100644 index 0000000000000..9f17197673e79 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx @@ -0,0 +1,45 @@ +import {hasSeenTourSelector, tryNewDotOnyxSelector} from '@selectors/Onboarding'; +import React from 'react'; +import useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {startTestDrive} from '@libs/actions/Tour'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type TestDriveMenuItemProps = { + icons: MenuItemIcons; +}; + +function TestDriveMenuItem({icons}: TestDriveMenuItemProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const theme = useTheme(); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); + const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector, canBeMissing: true}); + const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true}); + const isUserPaidPolicyMember = useIsPaidPolicyAdmin(); + + if (hasSeenTour) { + return null; + } + + return ( + interceptAnonymousUser(() => startTestDrive(introSelected, tryNewDot?.hasBeenAddedToNudgeMigration ?? false, isUserPaidPolicyMember))} + sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.TEST_DRIVE} + /> + ); +} + +export default TestDriveMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx new file mode 100644 index 0000000000000..cccdf8bddeff2 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import {startDistanceRequest} from '@libs/actions/IOU'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type TrackDistanceMenuItemProps = { + shouldUseNarrowLayout: boolean; + icons: MenuItemIcons; + reportID: string; +}; + +function TrackDistanceMenuItem({shouldUseNarrowLayout, icons, reportID}: TrackDistanceMenuItemProps) { + const {translate} = useLocalize(); + const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); + const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); + + return ( + { + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + startDistanceRequest(CONST.IOU.TYPE.CREATE, reportID, lastDistanceExpenseType, undefined, undefined, true); + }); + }} + sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.TRACK_DISTANCE} + /> + ); +} + +export default TrackDistanceMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx new file mode 100644 index 0000000000000..2d56ead145fbd --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx @@ -0,0 +1,67 @@ +import {Str} from 'expensify-common'; +import React, {useCallback, useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import Navigation from '@libs/Navigation/Navigation'; +import {openTravelDotLink, shouldOpenTravelDotLinkWeb} from '@libs/openTravelDotLink'; +import Permissions from '@libs/Permissions'; +import {isPaidGroupPolicy} from '@libs/PolicyUtils'; +import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; + +type TravelMenuItemProps = { + icons: MenuItemIcons; + activePolicyID: string | undefined; +}; + +const accountPrimaryLoginSelector = (account: OnyxEntry) => account?.primaryLogin; + +function TravelMenuItem({icons, activePolicyID}: TravelMenuItemProps) { + const {translate} = useLocalize(); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); + const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS, {canBeMissing: true}); + const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: accountPrimaryLoginSelector, canBeMissing: true}); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); + const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const isBlockedFromSpotnanaTravel = Permissions.isBetaEnabled(CONST.BETAS.PREVENT_SPOTNANA_TRAVEL, allBetas); + const primaryContactMethod = primaryLogin ?? session?.email ?? ''; + + const isTravelEnabled = useMemo(() => { + if (!!isBlockedFromSpotnanaTravel || !primaryContactMethod || Str.isSMSLogin(primaryContactMethod) || !isPaidGroupPolicy(activePolicy)) { + return false; + } + const isPolicyProvisioned = activePolicy?.travelSettings?.spotnanaCompanyID ?? activePolicy?.travelSettings?.associatedTravelDomainAccountID; + return activePolicy?.travelSettings?.hasAcceptedTerms ?? (travelSettings?.hasAcceptedTerms && isPolicyProvisioned); + }, [activePolicy, isBlockedFromSpotnanaTravel, primaryContactMethod, travelSettings?.hasAcceptedTerms]); + + const openTravel = useCallback(() => { + if (isTravelEnabled) { + openTravelDotLink(activePolicy?.id); + return; + } + Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS.getRoute(activePolicy?.id)); + }, [activePolicy?.id, isTravelEnabled]); + + if (!activePolicy?.isTravelEnabled) { + return null; + } + + return ( + interceptAnonymousUser(() => openTravel())} + sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.BOOK_TRAVEL} + /> + ); +} + +export default TravelMenuItem; From 6b5705ceaf8166dc9a79c59723c14522cce176b2 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 23 Feb 2026 14:02:25 +0100 Subject: [PATCH 08/54] delete old use*MenuItem hooks replaced by JSX components --- .../menuItems/useCreateReportMenuItem.ts | 166 --------------- .../menuItems/useExpenseMenuItem.ts | 46 ---- .../menuItems/useInvoiceMenuItem.ts | 49 ----- .../menuItems/useNewChatMenuItem.ts | 31 --- .../menuItems/useNewWorkspaceMenuItem.ts | 64 ------ .../menuItems/useQuickActionMenuItem.ts | 200 ------------------ .../menuItems/useTestDriveMenuItem.ts | 45 ---- .../menuItems/useTrackDistanceMenuItem.ts | 45 ---- .../menuItems/useTravelMenuItem.ts | 67 ------ 9 files changed, 713 deletions(-) delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useCreateReportMenuItem.ts delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useExpenseMenuItem.ts delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useInvoiceMenuItem.ts delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useNewChatMenuItem.ts delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useNewWorkspaceMenuItem.ts delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTestDriveMenuItem.ts delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTrackDistanceMenuItem.ts delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTravelMenuItem.ts diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useCreateReportMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useCreateReportMenuItem.ts deleted file mode 100644 index ce8972e998b00..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useCreateReportMenuItem.ts +++ /dev/null @@ -1,166 +0,0 @@ -import type {ReactNode} from 'react'; -import {useCallback, useMemo} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; -import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useHasEmptyReportsForPolicy from '@hooks/useHasEmptyReportsForPolicy'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import usePermissions from '@hooks/usePermissions'; -import {createNewReport} from '@libs/actions/Report'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; -import Navigation from '@libs/Navigation/Navigation'; -import {getDefaultChatEnabledPolicy} from '@libs/PolicyUtils'; -import {hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; -import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; -import {groupPaidPoliciesWithExpenseChatEnabledSelector} from '@selectors/Policy'; -import isOnSearchMoneyRequestReportPage from '@navigation/helpers/isOnSearchMoneyRequestReportPage'; -import {clearLastSearchParams} from '@userActions/ReportNavigation'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type * as OnyxTypes from '@src/types/onyx'; -import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; - -type UseCreateReportMenuItemParams = { - shouldUseNarrowLayout: boolean; - icons: MenuItemIcons; - activePolicyID: string | undefined; -}; - -type UseCreateReportMenuItemResult = { - menuItem: PopoverMenuItem[]; - confirmationModal: ReactNode; -}; - -const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); - -function useCreateReportMenuItem({shouldUseNarrowLayout, icons, activePolicyID}: UseCreateReportMenuItemParams): UseCreateReportMenuItemResult { - const {translate} = useLocalize(); - const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); - const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); - const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); - const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); - const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); - const [hasDismissedEmptyReportsConfirmation] = useOnyx(ONYXKEYS.NVP_EMPTY_REPORTS_CONFIRMATION_DISMISSED, {canBeMissing: true}); - const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {isBetaEnabled} = usePermissions(); - const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); - const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); - - const groupPaidPoliciesWithChatEnabled = useCallback( - (policies: Parameters[0]) => groupPaidPoliciesWithExpenseChatEnabledSelector(policies, session?.email), - [session?.email], - ); - - const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPaidPoliciesWithChatEnabled, canBeMissing: true}, [session?.email]); - - const shouldShowCreateReportOption = shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; - - const defaultChatEnabledPolicy = useMemo( - () => getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array>, activePolicy), - [activePolicy, groupPoliciesWithChatEnabled], - ); - - const defaultChatEnabledPolicyID = defaultChatEnabledPolicy?.id; - const hasEmptyReport = useHasEmptyReportsForPolicy(defaultChatEnabledPolicyID); - const shouldShowEmptyReportConfirmation = hasEmptyReport && hasDismissedEmptyReportsConfirmation !== true; - - const isReportInSearch = isOnSearchMoneyRequestReportPage(); - - const handleCreateWorkspaceReport = useCallback( - (shouldDismissEmptyReportsConfirmation?: boolean) => { - if (!defaultChatEnabledPolicy?.id) { - return; - } - - if (isReportInSearch) { - clearLastSearchParams(); - } - - const {reportID: createdReportID} = createNewReport( - currentUserPersonalDetails, - hasViolations, - isASAPSubmitBetaEnabled, - defaultChatEnabledPolicy, - allBetas, - false, - shouldDismissEmptyReportsConfirmation, - ); - Navigation.setNavigationActionToMicrotaskQueue(() => { - Navigation.navigate( - isSearchTopmostFullScreenRoute() - ? ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: createdReportID, backTo: Navigation.getActiveRoute()}) - : ROUTES.REPORT_WITH_ID.getRoute(createdReportID, undefined, undefined, Navigation.getActiveRoute()), - {forceReplace: isReportInSearch}, - ); - }); - }, - [currentUserPersonalDetails, hasViolations, defaultChatEnabledPolicy, isASAPSubmitBetaEnabled, isReportInSearch, allBetas], - ); - - const {openCreateReportConfirmation, CreateReportConfirmationModal} = useCreateEmptyReportConfirmation({ - policyID: defaultChatEnabledPolicyID, - policyName: defaultChatEnabledPolicy?.name ?? '', - onConfirm: handleCreateWorkspaceReport, - }); - - const menuItem = useMemo(() => { - if (!shouldShowCreateReportOption) { - return []; - } - return [ - { - icon: icons.Document, - text: translate('report.newReport.createReport'), - shouldCallAfterModalHide: shouldUseNarrowLayout, - onSelected: () => { - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - - const workspaceIDForReportCreation = defaultChatEnabledPolicyID; - - if (!workspaceIDForReportCreation || (shouldRestrictUserBillableActions(workspaceIDForReportCreation) && groupPoliciesWithChatEnabled.length > 1)) { - Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute()); - return; - } - - if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { - if (shouldShowEmptyReportConfirmation) { - openCreateReportConfirmation(); - } else { - handleCreateWorkspaceReport(false); - } - return; - } - - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(workspaceIDForReportCreation)); - }); - }, - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.CREATE_REPORT, - }, - ]; - }, [ - shouldShowCreateReportOption, - icons.Document, - translate, - shouldUseNarrowLayout, - shouldRedirectToExpensifyClassic, - showRedirectToExpensifyClassicModal, - defaultChatEnabledPolicyID, - groupPoliciesWithChatEnabled.length, - shouldShowEmptyReportConfirmation, - openCreateReportConfirmation, - handleCreateWorkspaceReport, - ]); - - return {menuItem, confirmationModal: CreateReportConfirmationModal}; -} - -export default useCreateReportMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useExpenseMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useExpenseMenuItem.ts deleted file mode 100644 index e98a15c3bd110..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useExpenseMenuItem.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {useMemo} from 'react'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import {startMoneyRequest} from '@libs/actions/IOU'; -import getIconForAction from '@libs/getIconForAction'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; - -type UseExpenseMenuItemParams = { - shouldUseNarrowLayout: boolean; - icons: MenuItemIcons; - reportID: string; -}; - -function useExpenseMenuItem({shouldUseNarrowLayout, icons, reportID}: UseExpenseMenuItemParams): PopoverMenuItem[] { - const {translate} = useLocalize(); - const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); - const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); - - return useMemo( - () => [ - { - icon: getIconForAction(CONST.IOU.TYPE.CREATE, icons as Parameters[1]), - text: translate('iou.createExpense'), - testID: 'create-expense', - shouldCallAfterModalHide: shouldRedirectToExpensifyClassic || shouldUseNarrowLayout, - onSelected: () => - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); - }), - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.CREATE_EXPENSE, - }, - ], - [translate, shouldRedirectToExpensifyClassic, shouldUseNarrowLayout, allTransactionDrafts, reportID, icons, showRedirectToExpensifyClassicModal], - ); -} - -export default useExpenseMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useInvoiceMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useInvoiceMenuItem.ts deleted file mode 100644 index 41a8d2a1af2ae..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useInvoiceMenuItem.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type {OnyxCollection} from 'react-native-onyx'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import {startMoneyRequest} from '@libs/actions/IOU'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {canSendInvoice as canSendInvoicePolicyUtils} from '@libs/PolicyUtils'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; -import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type * as OnyxTypes from '@src/types/onyx'; - -type UseInvoiceMenuItemParams = { - shouldUseNarrowLayout: boolean; - icons: MenuItemIcons; - reportID: string; -}; - -function useInvoiceMenuItem({shouldUseNarrowLayout, icons, reportID}: UseInvoiceMenuItemParams): PopoverMenuItem[] { - const {translate} = useLocalize(); - const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal, allPolicies} = useRedirectToExpensifyClassic(); - const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); - const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); - - const canSendInvoice = canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email); - - if (!canSendInvoice) { - return []; - } - return [ - { - icon: icons.InvoiceGeneric, - text: translate('workspace.invoices.sendInvoice'), - shouldCallAfterModalHide: shouldRedirectToExpensifyClassic || shouldUseNarrowLayout, - onSelected: () => - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - startMoneyRequest(CONST.IOU.TYPE.INVOICE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); - }), - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.SEND_INVOICE, - }, - ]; -} - -export default useInvoiceMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useNewChatMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useNewChatMenuItem.ts deleted file mode 100644 index 3adfd632341c5..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useNewChatMenuItem.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {useMemo} from 'react'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; -import useLocalize from '@hooks/useLocalize'; -import {startNewChat} from '@libs/actions/Report'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import CONST from '@src/CONST'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; - -type UseNewChatMenuItemParams = { - shouldUseNarrowLayout: boolean; - icons: MenuItemIcons; -}; - -function useNewChatMenuItem({shouldUseNarrowLayout, icons}: UseNewChatMenuItemParams): PopoverMenuItem[] { - const {translate} = useLocalize(); - - return useMemo( - () => [ - { - icon: icons.ChatBubble, - text: translate('sidebarScreen.fabNewChat'), - shouldCallAfterModalHide: shouldUseNarrowLayout, - onSelected: () => interceptAnonymousUser(startNewChat), - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.START_CHAT, - }, - ], - [icons.ChatBubble, translate, shouldUseNarrowLayout], - ); -} - -export default useNewChatMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useNewWorkspaceMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useNewWorkspaceMenuItem.ts deleted file mode 100644 index e2bea5b4f0082..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useNewWorkspaceMenuItem.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {useMemo} from 'react'; -import type {ImageContentFit} from 'expo-image'; -import type {OnyxEntry} from 'react-native-onyx'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; -import useLocalize from '@hooks/useLocalize'; -import useMappedPolicies from '@hooks/useMappedPolicies'; -import useNetwork from '@hooks/useNetwork'; -import useOnyx from '@hooks/useOnyx'; -import usePreferredPolicy from '@hooks/usePreferredPolicy'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import Navigation from '@libs/Navigation/Navigation'; -import {shouldShowPolicy} from '@libs/PolicyUtils'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; -import {policyMapper} from '@pages/inbox/sidebar/FABPopoverContent/types'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type * as OnyxTypes from '@src/types/onyx'; - -type UseNewWorkspaceMenuItemParams = { - shouldUseNarrowLayout: boolean; - icons: MenuItemIcons; -}; - -function useNewWorkspaceMenuItem({shouldUseNarrowLayout, icons}: UseNewWorkspaceMenuItemParams): PopoverMenuItem[] { - const {translate} = useLocalize(); - const {isOffline} = useNetwork(); - const [isLoading = false] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); - const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); - const [allPolicies] = useMappedPolicies(policyMapper); - const {isRestrictedPolicyCreation} = usePreferredPolicy(); - - const shouldShowNewWorkspaceButton = useMemo(() => { - if (isRestrictedPolicyCreation) { - return false; - } - const isOfflineBool = !!isOffline; - const email = session?.email; - return Object.values(allPolicies ?? {}).every((policy) => !shouldShowPolicy(policy as OnyxEntry, isOfflineBool, email)); - }, [isRestrictedPolicyCreation, allPolicies, isOffline, session?.email]); - - return useMemo(() => { - if (isLoading || !shouldShowNewWorkspaceButton) { - return []; - } - return [ - { - displayInDefaultIconColor: true, - contentFit: 'contain' as ImageContentFit, - icon: icons.NewWorkspace, - iconWidth: variables.w46, - iconHeight: variables.h40, - text: translate('workspace.new.newWorkspace'), - description: translate('workspace.new.getTheExpensifyCardAndMore'), - shouldCallAfterModalHide: shouldUseNarrowLayout, - onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(Navigation.getActiveRoute()))), - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.NEW_WORKSPACE, - }, - ]; - }, [isLoading, shouldShowNewWorkspaceButton, icons.NewWorkspace, translate, shouldUseNarrowLayout]); -} - -export default useNewWorkspaceMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts deleted file mode 100644 index 79c3081642b73..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useQuickActionMenuItem.ts +++ /dev/null @@ -1,200 +0,0 @@ -import {useCallback} from 'react'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import usePreferredPolicy from '@hooks/usePreferredPolicy'; -import useReportIsArchived from '@hooks/useReportIsArchived'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {startMoneyRequest} from '@libs/actions/IOU'; -import {navigateToQuickAction} from '@libs/actions/QuickActionNavigation'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import Navigation from '@libs/Navigation/Navigation'; -import {getQuickActionIcon, getQuickActionTitle, isQuickActionAllowed} from '@libs/QuickActionUtils'; -import { - getDisplayNameForParticipant, - getIcons, - // Will be fixed in https://github.com/Expensify/App/issues/76852 - // eslint-disable-next-line @typescript-eslint/no-deprecated - getReportName, - getWorkspaceChats, - isPolicyExpenseChat, -} from '@libs/ReportUtils'; -import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type * as OnyxTypes from '@src/types/onyx'; -import type {QuickActionName} from '@src/types/onyx/QuickAction'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; - -const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); - -type UseQuickActionMenuItemParams = { - shouldUseNarrowLayout: boolean; - icons: MenuItemIcons; - reportID: string; -}; - -function useQuickActionMenuItem({shouldUseNarrowLayout, icons, reportID}: UseQuickActionMenuItemParams): PopoverMenuItem[] { - const styles = useThemeStyles(); - const {translate, formatPhoneNumber} = useLocalize(); - const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); - const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); - const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); - const workspaceChatsSelector = useCallback( - (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports), - [activePolicyID, session?.accountID], - ); - const [policyChats = []] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector, canBeMissing: true}); - const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, {canBeMissing: true}); - const [quickActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`, {canBeMissing: true}); - const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); - const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); - const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); - const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {isDelegateAccessRestricted} = useDelegateNoAccessState(); - const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); - const isReportArchived = useReportIsArchived(quickActionReport?.reportID); - const {isRestrictedToPreferredPolicy} = usePreferredPolicy(); - - const quickActionPolicyID = quickAction?.action === CONST.QUICK_ACTIONS.TRACK_PER_DIEM && quickAction?.perDiemPolicyID ? quickAction?.perDiemPolicyID : quickActionReport?.policyID; - const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionPolicyID}`, {canBeMissing: true}); - - const isValidReport = !(isEmptyObject(quickActionReport) || isReportArchived); - - const policyChatForActivePolicy: OnyxTypes.Report = - !isEmptyObject(activePolicy) && activePolicy?.isPolicyExpenseChatEnabled && policyChats.length > 0 ? (policyChats.at(0) ?? ({} as OnyxTypes.Report)) : ({} as OnyxTypes.Report); - - const selectOption = useCallback( - (onSelected: () => void, shouldRestrictAction: boolean) => { - if (shouldRestrictAction && quickActionReport?.policyID && shouldRestrictUserBillableActions(quickActionReport.policyID)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(quickActionReport.policyID)); - return; - } - onSelected(); - }, - [quickActionReport?.policyID], - ); - - let quickActionAvatars: ReturnType = []; - if (isValidReport) { - const avatars = getIcons(quickActionReport, formatPhoneNumber, personalDetails, null, undefined, undefined, undefined, undefined, isReportArchived); - quickActionAvatars = avatars.length <= 1 || isPolicyExpenseChat(quickActionReport) ? avatars : avatars.filter((avatar) => avatar.id !== session?.accountID); - } else if (!isEmptyObject(policyChatForActivePolicy)) { - quickActionAvatars = getIcons(policyChatForActivePolicy, formatPhoneNumber, personalDetails, null, undefined, undefined, undefined, undefined, isReportArchived); - } - - let quickActionTitle = ''; - if (!isEmptyObject(quickActionReport)) { - if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && quickActionAvatars.length > 0) { - const accountID = quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID; - const name = getDisplayNameForParticipant({accountID: Number(accountID), shouldUseShortForm: true, formatPhoneNumber}) ?? ''; - quickActionTitle = translate('quickAction.paySomeone', name); - } else { - const titleKey = getQuickActionTitle(quickAction?.action ?? ('' as QuickActionName)); - quickActionTitle = titleKey ? translate(titleKey) : ''; - } - } - - let hideQABSubtitle = true; - if (isValidReport) { - if (quickActionAvatars.length === 0) { - hideQABSubtitle = false; - } else { - const displayName = personalDetails?.[quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID]?.firstName ?? ''; - hideQABSubtitle = quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && displayName.length === 0; - } - } - - // eslint-disable-next-line @typescript-eslint/no-deprecated - const quickActionSubtitle = !hideQABSubtitle ? (getReportName(quickActionReport, quickActionPolicy, undefined, personalDetails) ?? translate('quickAction.updateDestination')) : ''; - - const baseQuickAction = { - label: translate('quickAction.header'), - labelStyle: [styles.pt3, styles.pb2], - isLabelHoverable: false, - numberOfLinesDescription: 1, - tooltipAnchorAlignment: { - vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, - horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, - }, - shouldTeleportPortalToModalLayer: true, - }; - - if (quickAction?.action && quickActionReport) { - if (!isQuickActionAllowed(quickAction, quickActionReport, quickActionPolicy, isReportArchived, allBetas, isRestrictedToPreferredPolicy)) { - return []; - } - const onSelected = () => { - interceptAnonymousUser(() => { - if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && isDelegateAccessRestricted) { - showDelegateNoAccessModal(); - return; - } - const targetAccountPersonalDetails = { - ...personalDetails?.[quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID], - accountID: quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID, - }; - - navigateToQuickAction({ - isValidReport, - quickAction, - selectOption, - lastDistanceExpenseType, - targetAccountPersonalDetails, - currentUserAccountID: currentUserPersonalDetails.accountID, - isFromFloatingActionButton: true, - }); - }); - }; - return [ - { - ...baseQuickAction, - icon: getQuickActionIcon(icons as Parameters[0], quickAction?.action), - text: quickActionTitle, - rightIconAccountID: quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID, - description: quickActionSubtitle, - onSelected, - shouldCallAfterModalHide: shouldUseNarrowLayout, - rightIconReportID: quickActionReport?.reportID, - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.QUICK_ACTION, - }, - ]; - } - if (!isEmptyObject(policyChatForActivePolicy)) { - const onSelected = () => { - interceptAnonymousUser(() => { - if (policyChatForActivePolicy?.policyID && shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatForActivePolicy.policyID)); - return; - } - - const quickActionReportID = policyChatForActivePolicy?.reportID || reportID; - startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, allTransactionDrafts, true); - }); - }; - - return [ - { - ...baseQuickAction, - icon: icons.ReceiptScan, - text: translate('quickAction.scanReceipt'), - // eslint-disable-next-line @typescript-eslint/no-deprecated - description: getReportName(policyChatForActivePolicy), - shouldCallAfterModalHide: shouldUseNarrowLayout, - onSelected, - rightIconReportID: policyChatForActivePolicy?.reportID, - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.QUICK_ACTION, - }, - ]; - } - - return []; -} - -export default useQuickActionMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTestDriveMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTestDriveMenuItem.ts deleted file mode 100644 index 0d2ff4eb3d423..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTestDriveMenuItem.ts +++ /dev/null @@ -1,45 +0,0 @@ -import {useMemo} from 'react'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; -import useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {startTestDrive} from '@libs/actions/Tour'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {hasSeenTourSelector, tryNewDotOnyxSelector} from '@selectors/Onboarding'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; - -type UseTestDriveMenuItemParams = { - icons: MenuItemIcons; -}; - -function useTestDriveMenuItem({icons}: UseTestDriveMenuItemParams): PopoverMenuItem[] { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const theme = useTheme(); - const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); - const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector, canBeMissing: true}); - const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true}); - const isUserPaidPolicyMember = useIsPaidPolicyAdmin(); - - return useMemo(() => { - if (hasSeenTour) { - return []; - } - return [ - { - icon: icons.Binoculars, - iconStyles: styles.popoverIconCircle, - iconFill: theme.icon, - text: translate('testDrive.quickAction.takeATwoMinuteTestDrive'), - onSelected: () => interceptAnonymousUser(() => startTestDrive(introSelected, tryNewDot?.hasBeenAddedToNudgeMigration ?? false, isUserPaidPolicyMember)), - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.TEST_DRIVE, - }, - ]; - }, [hasSeenTour, icons.Binoculars, styles.popoverIconCircle, theme.icon, translate, introSelected, tryNewDot?.hasBeenAddedToNudgeMigration, isUserPaidPolicyMember]); -} - -export default useTestDriveMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTrackDistanceMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTrackDistanceMenuItem.ts deleted file mode 100644 index f8810c98de5dc..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTrackDistanceMenuItem.ts +++ /dev/null @@ -1,45 +0,0 @@ -import {useMemo} from 'react'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import {startDistanceRequest} from '@libs/actions/IOU'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; - -type UseTrackDistanceMenuItemParams = { - shouldUseNarrowLayout: boolean; - icons: MenuItemIcons; - reportID: string; -}; - -function useTrackDistanceMenuItem({shouldUseNarrowLayout, icons, reportID}: UseTrackDistanceMenuItemParams): PopoverMenuItem[] { - const {translate} = useLocalize(); - const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); - const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); - - return useMemo( - () => [ - { - icon: icons.Location, - text: translate('iou.trackDistance'), - shouldCallAfterModalHide: shouldUseNarrowLayout, - onSelected: () => { - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - startDistanceRequest(CONST.IOU.TYPE.CREATE, reportID, lastDistanceExpenseType, undefined, undefined, true); - }); - }, - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.TRACK_DISTANCE, - }, - ], - [icons.Location, translate, shouldUseNarrowLayout, shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal, reportID, lastDistanceExpenseType], - ); -} - -export default useTrackDistanceMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTravelMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTravelMenuItem.ts deleted file mode 100644 index ecbdecc3c7a89..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/useTravelMenuItem.ts +++ /dev/null @@ -1,67 +0,0 @@ -import {Str} from 'expensify-common'; -import {useCallback, useMemo} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import Navigation from '@libs/Navigation/Navigation'; -import {openTravelDotLink, shouldOpenTravelDotLinkWeb} from '@libs/openTravelDotLink'; -import Permissions from '@libs/Permissions'; -import {isPaidGroupPolicy} from '@libs/PolicyUtils'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type * as OnyxTypes from '@src/types/onyx'; - -type UseTravelMenuItemParams = { - icons: MenuItemIcons; - activePolicyID: string | undefined; -}; - -const accountPrimaryLoginSelector = (account: OnyxEntry) => account?.primaryLogin; - -function useTravelMenuItem({icons, activePolicyID}: UseTravelMenuItemParams): PopoverMenuItem[] { - const {translate} = useLocalize(); - const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); - const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS, {canBeMissing: true}); - const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: accountPrimaryLoginSelector, canBeMissing: true}); - const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); - const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); - const isBlockedFromSpotnanaTravel = Permissions.isBetaEnabled(CONST.BETAS.PREVENT_SPOTNANA_TRAVEL, allBetas); - const primaryContactMethod = primaryLogin ?? session?.email ?? ''; - - const isTravelEnabled = useMemo(() => { - if (!!isBlockedFromSpotnanaTravel || !primaryContactMethod || Str.isSMSLogin(primaryContactMethod) || !isPaidGroupPolicy(activePolicy)) { - return false; - } - const isPolicyProvisioned = activePolicy?.travelSettings?.spotnanaCompanyID ?? activePolicy?.travelSettings?.associatedTravelDomainAccountID; - return activePolicy?.travelSettings?.hasAcceptedTerms ?? (travelSettings?.hasAcceptedTerms && isPolicyProvisioned); - }, [activePolicy, isBlockedFromSpotnanaTravel, primaryContactMethod, travelSettings?.hasAcceptedTerms]); - - const openTravel = useCallback(() => { - if (isTravelEnabled) { - openTravelDotLink(activePolicy?.id); - return; - } - Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS.getRoute(activePolicy?.id)); - }, [activePolicy?.id, isTravelEnabled]); - - return useMemo(() => { - if (!activePolicy?.isTravelEnabled) { - return []; - } - return [ - { - icon: icons.Suitcase, - text: translate('travel.bookTravel'), - rightIcon: isTravelEnabled && shouldOpenTravelDotLinkWeb() ? icons.NewWindow : undefined, - onSelected: () => interceptAnonymousUser(() => openTravel()), - sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.BOOK_TRAVEL, - }, - ]; - }, [activePolicy?.isTravelEnabled, icons.Suitcase, icons.NewWindow, translate, isTravelEnabled, openTravel]); -} - -export default useTravelMenuItem; From eeeb56d9f2087aaac4def9c96b2aca81343d77e5 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 23 Feb 2026 14:43:58 +0100 Subject: [PATCH 09/54] refactor FAB menu to RC-compliant JSX architecture with FABPopoverMenu --- .../FABPopoverContent/FABMenuContext.tsx | 22 ++ .../sidebar/FABPopoverContent/FABMenuItem.tsx | 30 --- .../FABPopoverContent/FABMenuRegistry.tsx | 37 ---- .../FABMenuRegistryContext.tsx | 18 -- .../FABPopoverContentInner.tsx | 105 +++++----- .../FABPopoverContent/FABPopoverMenu.tsx | 104 ++++++++++ .../menuItems/CreateReportMenuItem.tsx | 102 ++++++---- .../menuItems/ExpenseMenuItem.tsx | 46 +++-- .../menuItems/InvoiceMenuItem.tsx | 52 +++-- .../menuItems/NewChatMenuItem.tsx | 27 ++- .../menuItems/NewWorkspaceMenuItem.tsx | 46 ++++- .../menuItems/QuickActionMenuItem.tsx | 191 +++++++++++------- .../menuItems/TestDriveMenuItem.tsx | 35 ++-- .../menuItems/TrackDistanceMenuItem.tsx | 47 +++-- .../menuItems/TravelMenuItem.tsx | 39 ++-- 15 files changed, 567 insertions(+), 334 deletions(-) create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/FABMenuContext.tsx delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/FABMenuItem.tsx delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/FABMenuRegistry.tsx delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/FABMenuRegistryContext.tsx create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuContext.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuContext.tsx new file mode 100644 index 0000000000000..451810edbb22b --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuContext.tsx @@ -0,0 +1,22 @@ +import {createContext, useContext} from 'react'; + +type FABMenuContextType = { + focusedIndex: number; + setFocusedIndex: (index: number) => void; + onItemPress: (onSelected: () => void, options?: {shouldCallAfterModalHide?: boolean}) => void; + isVisible: boolean; +}; + +const FABMenuContext = createContext({ + focusedIndex: -1, + setFocusedIndex: () => {}, + onItemPress: () => {}, + isVisible: false, +}); + +function useFABMenuContext() { + return useContext(FABMenuContext); +} + +export {FABMenuContext, useFABMenuContext}; +export type {FABMenuContextType}; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuItem.tsx deleted file mode 100644 index e6b702cce0583..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuItem.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import {useLayoutEffect, useRef} from 'react'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; -import {useFABMenuRegistryContext} from './FABMenuRegistryContext'; - -type FABMenuItemProps = PopoverMenuItem & { - /** Unique stable ID for this item in the registry - use sentryLabel */ - registryId: string; -}; - -function FABMenuItem({registryId, ...item}: FABMenuItemProps) { - const {registerItem, unregisterItem} = useFABMenuRegistryContext(); - const itemRef = useRef(item); - itemRef.current = item; - - // Re-register on every render (overwrites with latest props) - useLayoutEffect(() => { - registerItem(registryId, itemRef.current); - }); - - // Unregister only on unmount - useLayoutEffect( - () => () => unregisterItem(registryId), - // eslint-disable-next-line react-hooks/exhaustive-deps - [], - ); - - return null; -} - -export default FABMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuRegistry.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuRegistry.tsx deleted file mode 100644 index 3e54ee642b0b1..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuRegistry.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, {useCallback, useRef} from 'react'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; -import {FABMenuRegistryContext} from './FABMenuRegistryContext'; - -type FABMenuRegistryProps = { - children: React.ReactNode; - onItemsChange: (items: PopoverMenuItem[]) => void; -}; - -function FABMenuRegistry({children, onItemsChange}: FABMenuRegistryProps) { - const orderedIdsRef = useRef([]); - const itemsMapRef = useRef>(new Map()); - - const registerItem = useCallback( - (id: string, item: PopoverMenuItem) => { - if (!orderedIdsRef.current.includes(id)) { - orderedIdsRef.current = [...orderedIdsRef.current, id]; - } - itemsMapRef.current.set(id, item); - onItemsChange(orderedIdsRef.current.map((i) => itemsMapRef.current.get(i)).filter(Boolean) as PopoverMenuItem[]); - }, - [onItemsChange], - ); - - const unregisterItem = useCallback( - (id: string) => { - orderedIdsRef.current = orderedIdsRef.current.filter((i) => i !== id); - itemsMapRef.current.delete(id); - onItemsChange(orderedIdsRef.current.map((i) => itemsMapRef.current.get(i)).filter(Boolean) as PopoverMenuItem[]); - }, - [onItemsChange], - ); - - return {children}; -} - -export default FABMenuRegistry; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuRegistryContext.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuRegistryContext.tsx deleted file mode 100644 index ef9ca6ab71501..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuRegistryContext.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import {createContext, useContext} from 'react'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; - -type FABMenuRegistryContextType = { - registerItem: (id: string, item: PopoverMenuItem) => void; - unregisterItem: (id: string) => void; -}; - -const FABMenuRegistryContext = createContext({ - registerItem: () => {}, - unregisterItem: () => {}, -}); - -function useFABMenuRegistryContext() { - return useContext(FABMenuRegistryContext); -} - -export {FABMenuRegistryContext, useFABMenuRegistryContext}; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx index fd05edd21870e..24fbcb775c4ab 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx @@ -1,19 +1,16 @@ -import React, {useState} from 'react'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; -import PopoverMenu from '@components/PopoverMenu'; +import React from 'react'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction'; import CONST from '@src/CONST'; -import FABMenuRegistry from './FABMenuRegistry'; -import CreateReportMenuItem from './menuItems/CreateReportMenuItem'; +import FABPopoverMenu from './FABPopoverMenu'; +import CreateReportMenuItem, {useCreateReportMenuItemVisible} from './menuItems/CreateReportMenuItem'; import ExpenseMenuItem from './menuItems/ExpenseMenuItem'; -import InvoiceMenuItem from './menuItems/InvoiceMenuItem'; +import InvoiceMenuItem, {useInvoiceMenuItemVisible} from './menuItems/InvoiceMenuItem'; import NewChatMenuItem from './menuItems/NewChatMenuItem'; -import NewWorkspaceMenuItem from './menuItems/NewWorkspaceMenuItem'; -import QuickActionMenuItem from './menuItems/QuickActionMenuItem'; -import TestDriveMenuItem from './menuItems/TestDriveMenuItem'; +import NewWorkspaceMenuItem, {useNewWorkspaceMenuItemVisible} from './menuItems/NewWorkspaceMenuItem'; +import QuickActionMenuItem, {useQuickActionMenuItemVisible} from './menuItems/QuickActionMenuItem'; +import TestDriveMenuItem, {useTestDriveMenuItemVisible} from './menuItems/TestDriveMenuItem'; import TrackDistanceMenuItem from './menuItems/TrackDistanceMenuItem'; -import TravelMenuItem from './menuItems/TravelMenuItem'; +import TravelMenuItem, {useTravelMenuItemVisible} from './menuItems/TravelMenuItem'; import type {FABPopoverContentInnerProps} from './types'; type FABPopoverContentInnerExtraProps = FABPopoverContentInnerProps & { @@ -53,72 +50,74 @@ function FABPopoverContentInner({ 'Clock', ] as const); - const [menuItems, setMenuItems] = useState([]); + const showQuickAction = useQuickActionMenuItemVisible(); + const showInvoice = useInvoiceMenuItemVisible(); + const showTravel = useTravelMenuItemVisible(activePolicyID); + const showTestDrive = useTestDriveMenuItemVisible(); + const showNewWorkspace = useNewWorkspaceMenuItemVisible(); + const showCreateReport = useCreateReportMenuItemVisible(); return ( - <> - - - + {showQuickAction && ( + + )} + + + {showCreateReport && ( - + )} + + {showInvoice && ( + )} + {showTravel && ( - + )} + {showTestDrive && } + {showNewWorkspace && ( - - - ({ - ...item, - onSelected: () => { - if (!item.onSelected) { - return; - } - navigateAfterInteraction(item.onSelected); - }, - }))} - anchorRef={anchorRef} - /> - + )} + ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx new file mode 100644 index 0000000000000..36442651ee94d --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx @@ -0,0 +1,104 @@ +import React, {useCallback} from 'react'; +import type {RefObject} from 'react'; +import {View} from 'react-native'; +import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; +import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; +import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {close} from '@libs/actions/Modal'; +import {isSafari} from '@libs/Browser'; +import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction'; +import CONST from '@src/CONST'; +import type {AnchorPosition} from '@src/styles'; +import {FABMenuContext} from './FABMenuContext'; + +type FABMenuItemElement = React.ReactElement<{itemIndex?: number}>; + +type FABPopoverMenuProps = { + isVisible: boolean; + onClose: () => void; + onItemSelected: () => void; + onModalHide: () => void; + anchorPosition: AnchorPosition; + anchorRef: RefObject; + fromSidebarMediumScreen?: boolean; + animationInTiming?: number; + animationOutTiming?: number; + children: React.ReactNode; +}; + +function FABPopoverMenu({ + isVisible, + onClose, + onItemSelected, + onModalHide, + anchorPosition, + anchorRef, + fromSidebarMediumScreen, + animationInTiming, + animationOutTiming, + children, +}: FABPopoverMenuProps) { + const styles = useThemeStyles(); + + // React.Children.toArray filters out null/false/undefined produced by {cond && }, + // giving us an accurate count of actually-rendered items for arrow-key focus management. + const childrenArray = React.Children.toArray(children) as FABMenuItemElement[]; + const itemCount = childrenArray.length; + + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ + initialFocusedIndex: -1, + maxIndex: itemCount - 1, + isActive: isVisible, + }); + + const onItemPress = useCallback( + (onSelected: () => void, options?: {shouldCallAfterModalHide?: boolean}) => { + onItemSelected(); + if (options?.shouldCallAfterModalHide && !isSafari()) { + close(() => { + navigateAfterInteraction(onSelected); + }); + } else { + navigateAfterInteraction(onSelected); + } + setFocusedIndex(-1); + }, + [onItemSelected, setFocusedIndex], + ); + + // Inject itemIndex into each child so it can interact with focus management via context + const childrenWithIndex = childrenArray.map((child, index) => React.cloneElement(child, {itemIndex: index})); + + return ( + + + + {childrenWithIndex} + + + + ); +} + +export default FABPopoverMenu; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index 7684abe94174e..333c399e49332 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -1,12 +1,15 @@ import {groupPaidPoliciesWithExpenseChatEnabledSelector} from '@selectors/Policy'; import React, {useCallback, useMemo} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useHasEmptyReportsForPolicy from '@hooks/useHasEmptyReportsForPolicy'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import {createNewReport} from '@libs/actions/Report'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; @@ -15,7 +18,7 @@ import {getDefaultChatEnabledPolicy} from '@libs/PolicyUtils'; import {hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import isOnSearchMoneyRequestReportPage from '@navigation/helpers/isOnSearchMoneyRequestReportPage'; -import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import {clearLastSearchParams} from '@userActions/ReportNavigation'; @@ -28,11 +31,26 @@ type CreateReportMenuItemProps = { shouldUseNarrowLayout: boolean; icons: MenuItemIcons; activePolicyID: string | undefined; + /** Injected by FABPopoverMenu via React.cloneElement */ + itemIndex?: number; }; const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); -function CreateReportMenuItem({shouldUseNarrowLayout, icons, activePolicyID}: CreateReportMenuItemProps) { +function useCreateReportMenuItemVisible(): boolean { + const {shouldRedirectToExpensifyClassic} = useRedirectToExpensifyClassic(); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); + + const groupPaidPoliciesWithChatEnabled = useCallback( + (policies: Parameters[0]) => groupPaidPoliciesWithExpenseChatEnabledSelector(policies, session?.email), + [session?.email], + ); + const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPaidPoliciesWithChatEnabled, canBeMissing: true}, [session?.email]); + + return shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; +} + +function CreateReportMenuItem({shouldUseNarrowLayout, icons, activePolicyID, itemIndex = -1}: CreateReportMenuItemProps) { const {translate} = useLocalize(); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); @@ -44,6 +62,9 @@ function CreateReportMenuItem({shouldUseNarrowLayout, icons, activePolicyID}: Cr const {isBetaEnabled} = usePermissions(); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); + const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); + const StyleUtils = useStyleUtils(); + const theme = useTheme(); const groupPaidPoliciesWithChatEnabled = useCallback( (policies: Parameters[0]) => groupPaidPoliciesWithExpenseChatEnabledSelector(policies, session?.email), @@ -52,8 +73,6 @@ function CreateReportMenuItem({shouldUseNarrowLayout, icons, activePolicyID}: Cr const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPaidPoliciesWithChatEnabled, canBeMissing: true}, [session?.email]); - const shouldShowCreateReportOption = shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; - const defaultChatEnabledPolicy = useMemo( () => getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array>, activePolicy), [activePolicy, groupPoliciesWithChatEnabled], @@ -104,44 +123,51 @@ function CreateReportMenuItem({shouldUseNarrowLayout, icons, activePolicyID}: Cr return ( <> - {shouldShowCreateReportOption && ( - { - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - - const workspaceIDForReportCreation = defaultChatEnabledPolicyID; - - if (!workspaceIDForReportCreation || (shouldRestrictUserBillableActions(workspaceIDForReportCreation) && groupPoliciesWithChatEnabled.length > 1)) { - Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute()); - return; - } - - if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { - if (shouldShowEmptyReportConfirmation) { - openCreateReportConfirmation(); - } else { - handleCreateWorkspaceReport(false); + setFocusedIndex(itemIndex)} + onPress={() => + onItemPress( + () => { + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + + const workspaceIDForReportCreation = defaultChatEnabledPolicyID; + + if (!workspaceIDForReportCreation || (shouldRestrictUserBillableActions(workspaceIDForReportCreation) && groupPoliciesWithChatEnabled.length > 1)) { + Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute()); + return; + } + + if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { + if (shouldShowEmptyReportConfirmation) { + openCreateReportConfirmation(); + } else { + handleCreateWorkspaceReport(false); + } + return; } - return; - } - - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(workspaceIDForReportCreation)); - }); - }} - sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.CREATE_REPORT} - /> - )} + + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(workspaceIDForReportCreation)); + }); + }, + {shouldCallAfterModalHide: shouldUseNarrowLayout}, + ) + } + shouldCheckActionAllowedOnPress={false} + role={CONST.ROLE.BUTTON} + wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === itemIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} + /> {CreateReportConfirmationModal} ); } +export {useCreateReportMenuItemVisible}; export default CreateReportMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx index 8e8accc423c13..15af8465bc720 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx @@ -1,10 +1,13 @@ import React from 'react'; +import FocusableMenuItem from '@components/FocusableMenuItem'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import {startMoneyRequest} from '@libs/actions/IOU'; import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; @@ -14,30 +17,41 @@ type ExpenseMenuItemProps = { shouldUseNarrowLayout: boolean; icons: MenuItemIcons; reportID: string; + /** Injected by FABPopoverMenu via React.cloneElement */ + itemIndex?: number; }; -function ExpenseMenuItem({shouldUseNarrowLayout, icons, reportID}: ExpenseMenuItemProps) { +function ExpenseMenuItem({shouldUseNarrowLayout, icons, reportID, itemIndex = -1}: ExpenseMenuItemProps) { const {translate} = useLocalize(); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); + const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); + const StyleUtils = useStyleUtils(); + const theme = useTheme(); return ( - [1])} - text={translate('iou.createExpense')} - testID="create-expense" - shouldCallAfterModalHide={shouldRedirectToExpensifyClassic || shouldUseNarrowLayout} - onSelected={() => - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); - }) + title={translate('iou.createExpense')} + focused={focusedIndex === itemIndex} + onFocus={() => setFocusedIndex(itemIndex)} + onPress={() => + onItemPress( + () => + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); + }), + {shouldCallAfterModalHide: shouldRedirectToExpensifyClassic || shouldUseNarrowLayout}, + ) } - sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.CREATE_EXPENSE} + shouldCheckActionAllowedOnPress={false} + role={CONST.ROLE.BUTTON} + wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === itemIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx index 483a4f3254b54..ce51cf35bbe3e 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx @@ -1,11 +1,14 @@ import React from 'react'; import type {OnyxCollection} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import {startMoneyRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {canSendInvoice as canSendInvoicePolicyUtils} from '@libs/PolicyUtils'; -import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; @@ -16,13 +19,24 @@ type InvoiceMenuItemProps = { shouldUseNarrowLayout: boolean; icons: MenuItemIcons; reportID: string; + /** Injected by FABPopoverMenu via React.cloneElement */ + itemIndex?: number; }; -function InvoiceMenuItem({shouldUseNarrowLayout, icons, reportID}: InvoiceMenuItemProps) { +function useInvoiceMenuItemVisible(): boolean { + const {allPolicies} = useRedirectToExpensifyClassic(); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); + return canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email); +} + +function InvoiceMenuItem({shouldUseNarrowLayout, icons, reportID, itemIndex = -1}: InvoiceMenuItemProps) { const {translate} = useLocalize(); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal, allPolicies} = useRedirectToExpensifyClassic(); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); + const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); + const StyleUtils = useStyleUtils(); + const theme = useTheme(); const canSendInvoice = canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email); @@ -31,23 +45,31 @@ function InvoiceMenuItem({shouldUseNarrowLayout, icons, reportID}: InvoiceMenuIt } return ( - - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - startMoneyRequest(CONST.IOU.TYPE.INVOICE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); - }) + title={translate('workspace.invoices.sendInvoice')} + focused={focusedIndex === itemIndex} + onFocus={() => setFocusedIndex(itemIndex)} + onPress={() => + onItemPress( + () => + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + startMoneyRequest(CONST.IOU.TYPE.INVOICE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); + }), + {shouldCallAfterModalHide: shouldRedirectToExpensifyClassic || shouldUseNarrowLayout}, + ) } - sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.SEND_INVOICE} + shouldCheckActionAllowedOnPress={false} + role={CONST.ROLE.BUTTON} + wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === itemIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} /> ); } +export {useInvoiceMenuItemVisible}; export default InvoiceMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx index 0d4a8ce0444ce..3567905557a68 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx @@ -1,27 +1,38 @@ import React from 'react'; +import FocusableMenuItem from '@components/FocusableMenuItem'; import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import {startNewChat} from '@libs/actions/Report'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import CONST from '@src/CONST'; type NewChatMenuItemProps = { shouldUseNarrowLayout: boolean; icons: MenuItemIcons; + /** Injected by FABPopoverMenu via React.cloneElement */ + itemIndex?: number; }; -function NewChatMenuItem({shouldUseNarrowLayout, icons}: NewChatMenuItemProps) { +function NewChatMenuItem({shouldUseNarrowLayout, icons, itemIndex = -1}: NewChatMenuItemProps) { const {translate} = useLocalize(); + const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); + const StyleUtils = useStyleUtils(); + const theme = useTheme(); return ( - interceptAnonymousUser(startNewChat)} - sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.START_CHAT} + title={translate('sidebarScreen.fabNewChat')} + focused={focusedIndex === itemIndex} + onFocus={() => setFocusedIndex(itemIndex)} + onPress={() => onItemPress(() => interceptAnonymousUser(startNewChat), {shouldCallAfterModalHide: shouldUseNarrowLayout})} + shouldCheckActionAllowedOnPress={false} + role={CONST.ROLE.BUTTON} + wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === itemIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx index 7d894e5c75fe2..4a6b83362a92c 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx @@ -1,15 +1,18 @@ import type {ImageContentFit} from 'expo-image'; import React, {useMemo} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; import useLocalize from '@hooks/useLocalize'; import useMappedPolicies from '@hooks/useMappedPolicies'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePreferredPolicy from '@hooks/usePreferredPolicy'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {shouldShowPolicy} from '@libs/PolicyUtils'; -import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import {policyMapper} from '@pages/inbox/sidebar/FABPopoverContent/types'; import variables from '@styles/variables'; @@ -21,15 +24,34 @@ import type * as OnyxTypes from '@src/types/onyx'; type NewWorkspaceMenuItemProps = { shouldUseNarrowLayout: boolean; icons: MenuItemIcons; + /** Injected by FABPopoverMenu via React.cloneElement */ + itemIndex?: number; }; -function NewWorkspaceMenuItem({shouldUseNarrowLayout, icons}: NewWorkspaceMenuItemProps) { +function useNewWorkspaceMenuItemVisible(): boolean { + const {isOffline} = useNetwork(); + const [isLoading = false] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); + const [allPolicies] = useMappedPolicies(policyMapper); + const {isRestrictedPolicyCreation} = usePreferredPolicy(); + + if (isLoading || isRestrictedPolicyCreation) { + return false; + } + + return Object.values(allPolicies ?? {}).every((policy) => !shouldShowPolicy(policy as OnyxEntry, !!isOffline, session?.email)); +} + +function NewWorkspaceMenuItem({shouldUseNarrowLayout, icons, itemIndex = -1}: NewWorkspaceMenuItemProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const [isLoading = false] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const [allPolicies] = useMappedPolicies(policyMapper); const {isRestrictedPolicyCreation} = usePreferredPolicy(); + const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); + const StyleUtils = useStyleUtils(); + const theme = useTheme(); const shouldShowNewWorkspaceButton = useMemo(() => { if (isRestrictedPolicyCreation) { @@ -45,20 +67,28 @@ function NewWorkspaceMenuItem({shouldUseNarrowLayout, icons}: NewWorkspaceMenuIt } return ( - interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(Navigation.getActiveRoute())))} - sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.NEW_WORKSPACE} + focused={focusedIndex === itemIndex} + onFocus={() => setFocusedIndex(itemIndex)} + onPress={() => + onItemPress(() => interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(Navigation.getActiveRoute()))), { + shouldCallAfterModalHide: shouldUseNarrowLayout, + }) + } + shouldCheckActionAllowedOnPress={false} + role={CONST.ROLE.BUTTON} + wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === itemIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} /> ); } +export {useNewWorkspaceMenuItemVisible}; export default NewWorkspaceMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index 849828f3278d5..342b6ab022832 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -1,11 +1,14 @@ import React, {useCallback} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; +import FocusableMenuItem from '@components/FocusableMenuItem'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePreferredPolicy from '@hooks/usePreferredPolicy'; import useReportIsArchived from '@hooks/useReportIsArchived'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {startMoneyRequest} from '@libs/actions/IOU'; import {navigateToQuickAction} from '@libs/actions/QuickActionNavigation'; @@ -22,7 +25,7 @@ import { isPolicyExpenseChat, } from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; -import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -38,9 +41,42 @@ type QuickActionMenuItemProps = { shouldUseNarrowLayout: boolean; icons: MenuItemIcons; reportID: string; + /** Injected by FABPopoverMenu via React.cloneElement */ + itemIndex?: number; }; -function QuickActionMenuItem({shouldUseNarrowLayout, icons, reportID}: QuickActionMenuItemProps) { +function useQuickActionMenuItemVisible(): boolean { + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); + const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, {canBeMissing: true}); + const [quickActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`, {canBeMissing: true}); + const quickActionPolicyID = quickAction?.action === CONST.QUICK_ACTIONS.TRACK_PER_DIEM && quickAction?.perDiemPolicyID ? quickAction?.perDiemPolicyID : quickActionReport?.policyID; + const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionPolicyID}`, {canBeMissing: true}); + const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const isReportArchived = useReportIsArchived(quickActionReport?.reportID); + const {isRestrictedToPreferredPolicy} = usePreferredPolicy(); + + const workspaceChatsSelector = useCallback( + (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports), + [activePolicyID, session?.accountID], + ); + const [policyChats = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector, canBeMissing: true}); + + const policyChatForActivePolicy: OnyxTypes.Report = + !isEmptyObject(activePolicy) && activePolicy?.isPolicyExpenseChatEnabled && policyChats.length > 0 ? (policyChats.at(0) ?? ({} as OnyxTypes.Report)) : ({} as OnyxTypes.Report); + + if (quickAction?.action && quickActionReport) { + if (!isQuickActionAllowed(quickAction, quickActionReport, quickActionPolicy, isReportArchived, allBetas, isRestrictedToPreferredPolicy)) { + return false; + } + return true; + } + + return !isEmptyObject(policyChatForActivePolicy); +} + +function QuickActionMenuItem({shouldUseNarrowLayout, icons, reportID, itemIndex = -1}: QuickActionMenuItemProps) { const styles = useThemeStyles(); const {translate, formatPhoneNumber} = useLocalize(); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); @@ -62,6 +98,9 @@ function QuickActionMenuItem({shouldUseNarrowLayout, icons, reportID}: QuickActi const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const isReportArchived = useReportIsArchived(quickActionReport?.reportID); const {isRestrictedToPreferredPolicy} = usePreferredPolicy(); + const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); + const StyleUtils = useStyleUtils(); + const theme = useTheme(); const quickActionPolicyID = quickAction?.action === CONST.QUICK_ACTIONS.TRACK_PER_DIEM && quickAction?.perDiemPolicyID ? quickAction?.perDiemPolicyID : quickActionReport?.policyID; const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionPolicyID}`, {canBeMissing: true}); @@ -116,95 +155,104 @@ function QuickActionMenuItem({shouldUseNarrowLayout, icons, reportID}: QuickActi // eslint-disable-next-line @typescript-eslint/no-deprecated const quickActionSubtitle = !hideQABSubtitle ? (getReportName(quickActionReport, quickActionPolicy, undefined, personalDetails) ?? translate('quickAction.updateDestination')) : ''; - const baseQuickAction = { - label: translate('quickAction.header'), - labelStyle: [styles.pt3, styles.pb2], - isLabelHoverable: false, - numberOfLinesDescription: 1, - tooltipAnchorAlignment: { - vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, - horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, - }, - shouldTeleportPortalToModalLayer: true, - }; + const isFocused = focusedIndex === itemIndex; + const focusWrapperStyle = StyleUtils.getItemBackgroundColorStyle(false, isFocused, false, theme.activeComponentBG, theme.hoverComponentBG); if (quickAction?.action && quickActionReport) { if (!isQuickActionAllowed(quickAction, quickActionReport, quickActionPolicy, isReportArchived, allBetas, isRestrictedToPreferredPolicy)) { return null; } - const onSelected = () => { - interceptAnonymousUser(() => { - if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && isDelegateAccessRestricted) { - showDelegateNoAccessModal(); - return; - } - const targetAccountPersonalDetails = { - ...personalDetails?.[quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID], - accountID: quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID, - }; - - navigateToQuickAction({ - isValidReport, - quickAction, - selectOption, - lastDistanceExpenseType, - targetAccountPersonalDetails, - currentUserAccountID: currentUserPersonalDetails.accountID, - isFromFloatingActionButton: true, - }); - }); - }; + return ( - setFocusedIndex(itemIndex)} + shouldCheckActionAllowedOnPress={false} + role={CONST.ROLE.BUTTON} + wrapperStyle={focusWrapperStyle} icon={getQuickActionIcon(icons as Parameters[0], quickAction?.action)} - text={quickActionTitle} + title={quickActionTitle} rightIconAccountID={quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID} description={quickActionSubtitle} - onSelected={onSelected} - shouldCallAfterModalHide={shouldUseNarrowLayout} rightIconReportID={quickActionReport?.reportID} - sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.QUICK_ACTION} + onPress={() => + onItemPress( + () => + interceptAnonymousUser(() => { + if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + const targetAccountPersonalDetails = { + ...personalDetails?.[quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID], + accountID: quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID, + }; + + navigateToQuickAction({ + isValidReport, + quickAction, + selectOption, + lastDistanceExpenseType, + targetAccountPersonalDetails, + currentUserAccountID: currentUserPersonalDetails.accountID, + isFromFloatingActionButton: true, + }); + }), + {shouldCallAfterModalHide: shouldUseNarrowLayout}, + ) + } /> ); } if (!isEmptyObject(policyChatForActivePolicy)) { - const onSelected = () => { - interceptAnonymousUser(() => { - if (policyChatForActivePolicy?.policyID && shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatForActivePolicy.policyID)); - return; - } - - const quickActionReportID = policyChatForActivePolicy?.reportID || reportID; - startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, allTransactionDrafts, true); - }); - }; - return ( - setFocusedIndex(itemIndex)} + shouldCheckActionAllowedOnPress={false} + role={CONST.ROLE.BUTTON} + wrapperStyle={focusWrapperStyle} icon={icons.ReceiptScan} - text={translate('quickAction.scanReceipt')} + title={translate('quickAction.scanReceipt')} // eslint-disable-next-line @typescript-eslint/no-deprecated description={getReportName(policyChatForActivePolicy)} - shouldCallAfterModalHide={shouldUseNarrowLayout} - onSelected={onSelected} rightIconReportID={policyChatForActivePolicy?.reportID} - sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.QUICK_ACTION} + onPress={() => + onItemPress( + () => + interceptAnonymousUser(() => { + if (policyChatForActivePolicy?.policyID && shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatForActivePolicy.policyID)); + return; + } + + const quickActionReportID = policyChatForActivePolicy?.reportID || reportID; + startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, allTransactionDrafts, true); + }), + {shouldCallAfterModalHide: shouldUseNarrowLayout}, + ) + } /> ); } @@ -212,4 +260,5 @@ function QuickActionMenuItem({shouldUseNarrowLayout, icons, reportID}: QuickActi return null; } +export {useQuickActionMenuItemVisible}; export default QuickActionMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx index 9f17197673e79..12871f190b259 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx @@ -1,45 +1,56 @@ import {hasSeenTourSelector, tryNewDotOnyxSelector} from '@selectors/Onboarding'; import React from 'react'; +import FocusableMenuItem from '@components/FocusableMenuItem'; import useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {startTestDrive} from '@libs/actions/Tour'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; type TestDriveMenuItemProps = { icons: MenuItemIcons; + /** Injected by FABPopoverMenu via React.cloneElement */ + itemIndex?: number; }; -function TestDriveMenuItem({icons}: TestDriveMenuItemProps) { +function useTestDriveMenuItemVisible(): boolean { + const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector, canBeMissing: true}); + return !hasSeenTour; +} + +function TestDriveMenuItem({icons, itemIndex = -1}: TestDriveMenuItemProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); + const StyleUtils = useStyleUtils(); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); - const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector, canBeMissing: true}); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true}); const isUserPaidPolicyMember = useIsPaidPolicyAdmin(); - - if (hasSeenTour) { - return null; - } + const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); return ( - interceptAnonymousUser(() => startTestDrive(introSelected, tryNewDot?.hasBeenAddedToNudgeMigration ?? false, isUserPaidPolicyMember))} - sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.TEST_DRIVE} + title={translate('testDrive.quickAction.takeATwoMinuteTestDrive')} + focused={focusedIndex === itemIndex} + onFocus={() => setFocusedIndex(itemIndex)} + onPress={() => onItemPress(() => interceptAnonymousUser(() => startTestDrive(introSelected, tryNewDot?.hasBeenAddedToNudgeMigration ?? false, isUserPaidPolicyMember)))} + shouldCheckActionAllowedOnPress={false} + role={CONST.ROLE.BUTTON} + wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === itemIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} /> ); } +export {useTestDriveMenuItemVisible}; export default TestDriveMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx index cccdf8bddeff2..c704c45c16a29 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx @@ -1,9 +1,12 @@ import React from 'react'; +import FocusableMenuItem from '@components/FocusableMenuItem'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import {startDistanceRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; @@ -13,29 +16,41 @@ type TrackDistanceMenuItemProps = { shouldUseNarrowLayout: boolean; icons: MenuItemIcons; reportID: string; + /** Injected by FABPopoverMenu via React.cloneElement */ + itemIndex?: number; }; -function TrackDistanceMenuItem({shouldUseNarrowLayout, icons, reportID}: TrackDistanceMenuItemProps) { +function TrackDistanceMenuItem({shouldUseNarrowLayout, icons, reportID, itemIndex = -1}: TrackDistanceMenuItemProps) { const {translate} = useLocalize(); const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); + const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); + const StyleUtils = useStyleUtils(); + const theme = useTheme(); return ( - { - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - startDistanceRequest(CONST.IOU.TYPE.CREATE, reportID, lastDistanceExpenseType, undefined, undefined, true); - }); - }} - sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.TRACK_DISTANCE} + title={translate('iou.trackDistance')} + focused={focusedIndex === itemIndex} + onFocus={() => setFocusedIndex(itemIndex)} + onPress={() => + onItemPress( + () => + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + startDistanceRequest(CONST.IOU.TYPE.CREATE, reportID, lastDistanceExpenseType, undefined, undefined, true); + }), + {shouldCallAfterModalHide: shouldUseNarrowLayout}, + ) + } + shouldCheckActionAllowedOnPress={false} + role={CONST.ROLE.BUTTON} + wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === itemIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx index 2d56ead145fbd..7e342d9a5d588 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx @@ -1,14 +1,17 @@ import {Str} from 'expensify-common'; import React, {useCallback, useMemo} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {openTravelDotLink, shouldOpenTravelDotLinkWeb} from '@libs/openTravelDotLink'; import Permissions from '@libs/Permissions'; import {isPaidGroupPolicy} from '@libs/PolicyUtils'; -import FABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItem'; +import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -18,11 +21,18 @@ import type * as OnyxTypes from '@src/types/onyx'; type TravelMenuItemProps = { icons: MenuItemIcons; activePolicyID: string | undefined; + /** Injected by FABPopoverMenu via React.cloneElement */ + itemIndex?: number; }; const accountPrimaryLoginSelector = (account: OnyxEntry) => account?.primaryLogin; -function TravelMenuItem({icons, activePolicyID}: TravelMenuItemProps) { +function useTravelMenuItemVisible(activePolicyID: string | undefined): boolean { + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); + return !!activePolicy?.isTravelEnabled; +} + +function TravelMenuItem({icons, activePolicyID, itemIndex = -1}: TravelMenuItemProps) { const {translate} = useLocalize(); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS, {canBeMissing: true}); @@ -31,6 +41,9 @@ function TravelMenuItem({icons, activePolicyID}: TravelMenuItemProps) { const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); const isBlockedFromSpotnanaTravel = Permissions.isBetaEnabled(CONST.BETAS.PREVENT_SPOTNANA_TRAVEL, allBetas); const primaryContactMethod = primaryLogin ?? session?.email ?? ''; + const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); + const StyleUtils = useStyleUtils(); + const theme = useTheme(); const isTravelEnabled = useMemo(() => { if (!!isBlockedFromSpotnanaTravel || !primaryContactMethod || Str.isSMSLogin(primaryContactMethod) || !isPaidGroupPolicy(activePolicy)) { @@ -48,20 +61,22 @@ function TravelMenuItem({icons, activePolicyID}: TravelMenuItemProps) { Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS.getRoute(activePolicy?.id)); }, [activePolicy?.id, isTravelEnabled]); - if (!activePolicy?.isTravelEnabled) { - return null; - } - return ( - interceptAnonymousUser(() => openTravel())} - sentryLabel={CONST.SENTRY_LABEL.FAB_MENU.BOOK_TRAVEL} + title={translate('travel.bookTravel')} + iconRight={isTravelEnabled && shouldOpenTravelDotLinkWeb() ? icons.NewWindow : undefined} + shouldShowRightIcon={!!(isTravelEnabled && shouldOpenTravelDotLinkWeb())} + focused={focusedIndex === itemIndex} + onFocus={() => setFocusedIndex(itemIndex)} + onPress={() => onItemPress(() => interceptAnonymousUser(() => openTravel()))} + shouldCheckActionAllowedOnPress={false} + role={CONST.ROLE.BUTTON} + wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === itemIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} /> ); } +export {useTravelMenuItemVisible}; export default TravelMenuItem; From 88ef1b22820e2f4ec4196c103741b33a8ae87959 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 23 Feb 2026 14:50:11 +0100 Subject: [PATCH 10/54] fix FABPopoverMenu mobile layout - use flexGrow1 on small screens --- .../sidebar/FABPopoverContent/FABPopoverMenu.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx index 36442651ee94d..fcabc11718236 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx @@ -4,6 +4,7 @@ import {View} from 'react-native'; import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {close} from '@libs/actions/Modal'; import {isSafari} from '@libs/Browser'; @@ -40,6 +41,8 @@ function FABPopoverMenu({ children, }: FABPopoverMenuProps) { const styles = useThemeStyles(); + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {isSmallScreenWidth} = useResponsiveLayout(); // React.Children.toArray filters out null/false/undefined produced by {cond && }, // giving us an accurate count of actually-rendered items for arrow-key focus management. @@ -94,7 +97,14 @@ function FABPopoverMenu({ active={isVisible} shouldReturnFocus > - {childrenWithIndex} + {/* + * Replicates PopoverMenu's layout: + * - mobile: flexGrow1 outer (no fixed width), pv4 inner for item padding + * - web: createMenuContainer (fixed sidebar width) + flex1 outer, pv4 inner + */} + + {childrenWithIndex} + From c250d479893a07f903a21c6438ba4199473be05f Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 23 Feb 2026 14:54:27 +0100 Subject: [PATCH 11/54] fix RC violations in useScanActions - drop useCallback/useRef, use useState for reportID --- .../FABPopoverContent/useScanActions.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts index 83be9d26fb2b3..29a10c45ac225 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts @@ -1,4 +1,4 @@ -import {useCallback, useRef} from 'react'; +import {useState} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import useOnyx from '@hooks/useOnyx'; import {startMoneyRequest} from '@libs/actions/IOU'; @@ -12,6 +12,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import getEmptyArray from '@src/types/utils/getEmptyArray'; import useRedirectToExpensifyClassic from './useRedirectToExpensifyClassic'; const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); @@ -21,20 +22,18 @@ function useScanActions() { const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); - const workspaceChatsSelector = useCallback( - (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports), - [activePolicyID, session?.accountID], - ); - const [policyChats = []] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector, canBeMissing: true}); + const workspaceChatsSelector = (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports); + const [policyChats = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector, canBeMissing: true}); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); - const reportID = useRef(generateReportID()).current; + // useState lazy initializer generates the ID once on mount and keeps it stable across renders + const [reportID] = useState(() => generateReportID()); const policyChatForActivePolicy: OnyxTypes.Report = !isEmptyObject(activePolicy) && activePolicy?.isPolicyExpenseChatEnabled && policyChats.length > 0 ? (policyChats.at(0) ?? ({} as OnyxTypes.Report)) : ({} as OnyxTypes.Report); - const startScan = useCallback(() => { + const startScan = () => { interceptAnonymousUser(() => { if (shouldRedirectToExpensifyClassic) { showRedirectToExpensifyClassicModal(); @@ -42,12 +41,12 @@ function useScanActions() { } startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, CONST.IOU.REQUEST_TYPE.SCAN, false, undefined, allTransactionDrafts, true); }); - }, [shouldRedirectToExpensifyClassic, allTransactionDrafts, reportID, showRedirectToExpensifyClassicModal]); + }; const policyChatPolicyID = policyChatForActivePolicy?.policyID; const policyChatReportID = policyChatForActivePolicy?.reportID; - const startQuickScan = useCallback(() => { + const startQuickScan = () => { interceptAnonymousUser(() => { if (policyChatPolicyID && shouldRestrictUserBillableActions(policyChatPolicyID)) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatPolicyID)); @@ -58,7 +57,7 @@ function useScanActions() { Tab.setSelectedTab(CONST.TAB.IOU_REQUEST_TYPE, CONST.IOU.REQUEST_TYPE.SCAN); startMoneyRequest(CONST.IOU.TYPE.CREATE, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, !!policyChatReportID, undefined, allTransactionDrafts, true); }); - }, [policyChatPolicyID, policyChatReportID, reportID, allTransactionDrafts]); + }; return {startScan, startQuickScan, reportID, activePolicyID}; } From b551cedf76fe7e6413d642d726986996ed6470ee Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 23 Feb 2026 15:20:37 +0100 Subject: [PATCH 12/54] drop useCallback/useMemo from FAB menu items - let RC handle memoization --- .../menuItems/CreateReportMenuItem.tsx | 75 ++++++++----------- .../menuItems/NewWorkspaceMenuItem.tsx | 6 +- .../menuItems/QuickActionMenuItem.tsx | 30 +++----- .../menuItems/TravelMenuItem.tsx | 10 +-- 4 files changed, 52 insertions(+), 69 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index 333c399e49332..3e183bd202ed0 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -1,5 +1,5 @@ import {groupPaidPoliciesWithExpenseChatEnabledSelector} from '@selectors/Policy'; -import React, {useCallback, useMemo} from 'react'; +import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation'; @@ -41,10 +41,8 @@ function useCreateReportMenuItemVisible(): boolean { const {shouldRedirectToExpensifyClassic} = useRedirectToExpensifyClassic(); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); - const groupPaidPoliciesWithChatEnabled = useCallback( - (policies: Parameters[0]) => groupPaidPoliciesWithExpenseChatEnabledSelector(policies, session?.email), - [session?.email], - ); + const groupPaidPoliciesWithChatEnabled = (policies: Parameters[0]) => + groupPaidPoliciesWithExpenseChatEnabledSelector(policies, session?.email); const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPaidPoliciesWithChatEnabled, canBeMissing: true}, [session?.email]); return shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; @@ -66,17 +64,13 @@ function CreateReportMenuItem({shouldUseNarrowLayout, icons, activePolicyID, ite const StyleUtils = useStyleUtils(); const theme = useTheme(); - const groupPaidPoliciesWithChatEnabled = useCallback( - (policies: Parameters[0]) => groupPaidPoliciesWithExpenseChatEnabledSelector(policies, session?.email), - [session?.email], - ); + const groupPaidPoliciesWithChatEnabled = (policies: Parameters[0]) => + groupPaidPoliciesWithExpenseChatEnabledSelector(policies, session?.email); + // eslint-disable-next-line rulesdir/no-inline-useOnyx-selector const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPaidPoliciesWithChatEnabled, canBeMissing: true}, [session?.email]); - const defaultChatEnabledPolicy = useMemo( - () => getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array>, activePolicy), - [activePolicy, groupPoliciesWithChatEnabled], - ); + const defaultChatEnabledPolicy = getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array>, activePolicy); const defaultChatEnabledPolicyID = defaultChatEnabledPolicy?.id; const hasEmptyReport = useHasEmptyReportsForPolicy(defaultChatEnabledPolicyID); @@ -84,36 +78,33 @@ function CreateReportMenuItem({shouldUseNarrowLayout, icons, activePolicyID, ite const isReportInSearch = isOnSearchMoneyRequestReportPage(); - const handleCreateWorkspaceReport = useCallback( - (shouldDismissEmptyReportsConfirmation?: boolean) => { - if (!defaultChatEnabledPolicy?.id) { - return; - } - - if (isReportInSearch) { - clearLastSearchParams(); - } - - const {reportID: createdReportID} = createNewReport( - currentUserPersonalDetails, - hasViolations, - isASAPSubmitBetaEnabled, - defaultChatEnabledPolicy, - allBetas, - false, - shouldDismissEmptyReportsConfirmation, + const handleCreateWorkspaceReport = (shouldDismissEmptyReportsConfirmation?: boolean) => { + if (!defaultChatEnabledPolicy?.id) { + return; + } + + if (isReportInSearch) { + clearLastSearchParams(); + } + + const {reportID: createdReportID} = createNewReport( + currentUserPersonalDetails, + hasViolations, + isASAPSubmitBetaEnabled, + defaultChatEnabledPolicy, + allBetas, + false, + shouldDismissEmptyReportsConfirmation, + ); + Navigation.setNavigationActionToMicrotaskQueue(() => { + Navigation.navigate( + isSearchTopmostFullScreenRoute() + ? ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: createdReportID, backTo: Navigation.getActiveRoute()}) + : ROUTES.REPORT_WITH_ID.getRoute(createdReportID, undefined, undefined, Navigation.getActiveRoute()), + {forceReplace: isReportInSearch}, ); - Navigation.setNavigationActionToMicrotaskQueue(() => { - Navigation.navigate( - isSearchTopmostFullScreenRoute() - ? ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: createdReportID, backTo: Navigation.getActiveRoute()}) - : ROUTES.REPORT_WITH_ID.getRoute(createdReportID, undefined, undefined, Navigation.getActiveRoute()), - {forceReplace: isReportInSearch}, - ); - }); - }, - [currentUserPersonalDetails, hasViolations, defaultChatEnabledPolicy, isASAPSubmitBetaEnabled, isReportInSearch, allBetas], - ); + }); + }; const {openCreateReportConfirmation, CreateReportConfirmationModal} = useCreateEmptyReportConfirmation({ policyID: defaultChatEnabledPolicyID, diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx index 4a6b83362a92c..9dda3dd6b9e2a 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx @@ -1,5 +1,5 @@ import type {ImageContentFit} from 'expo-image'; -import React, {useMemo} from 'react'; +import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; import useLocalize from '@hooks/useLocalize'; @@ -53,14 +53,14 @@ function NewWorkspaceMenuItem({shouldUseNarrowLayout, icons, itemIndex = -1}: Ne const StyleUtils = useStyleUtils(); const theme = useTheme(); - const shouldShowNewWorkspaceButton = useMemo(() => { + const shouldShowNewWorkspaceButton = (() => { if (isRestrictedPolicyCreation) { return false; } const isOfflineBool = !!isOffline; const email = session?.email; return Object.values(allPolicies ?? {}).every((policy) => !shouldShowPolicy(policy as OnyxEntry, isOfflineBool, email)); - }, [isRestrictedPolicyCreation, allPolicies, isOffline, session?.email]); + })(); if (isLoading || !shouldShowNewWorkspaceButton) { return null; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index 342b6ab022832..0335b56ada01d 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react'; +import React from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import FocusableMenuItem from '@components/FocusableMenuItem'; @@ -57,10 +57,7 @@ function useQuickActionMenuItemVisible(): boolean { const isReportArchived = useReportIsArchived(quickActionReport?.reportID); const {isRestrictedToPreferredPolicy} = usePreferredPolicy(); - const workspaceChatsSelector = useCallback( - (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports), - [activePolicyID, session?.accountID], - ); + const workspaceChatsSelector = (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports); const [policyChats = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector, canBeMissing: true}); const policyChatForActivePolicy: OnyxTypes.Report = @@ -83,10 +80,8 @@ function QuickActionMenuItem({shouldUseNarrowLayout, icons, reportID, itemIndex const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); - const workspaceChatsSelector = useCallback( - (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports), - [activePolicyID, session?.accountID], - ); + const workspaceChatsSelector = (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports); + // eslint-disable-next-line rulesdir/no-inline-useOnyx-selector const [policyChats = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector, canBeMissing: true}); const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, {canBeMissing: true}); const [quickActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`, {canBeMissing: true}); @@ -111,16 +106,13 @@ function QuickActionMenuItem({shouldUseNarrowLayout, icons, reportID, itemIndex !isEmptyObject(activePolicy) && activePolicy?.isPolicyExpenseChatEnabled && policyChats.length > 0 ? (policyChats.at(0) ?? ({} as OnyxTypes.Report)) : ({} as OnyxTypes.Report); const quickActionReportPolicyID = quickActionReport?.policyID; - const selectOption = useCallback( - (onSelected: () => void, shouldRestrictAction: boolean) => { - if (shouldRestrictAction && quickActionReportPolicyID && shouldRestrictUserBillableActions(quickActionReportPolicyID)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(quickActionReportPolicyID)); - return; - } - onSelected(); - }, - [quickActionReportPolicyID], - ); + const selectOption = (onSelected: () => void, shouldRestrictAction: boolean) => { + if (shouldRestrictAction && quickActionReportPolicyID && shouldRestrictUserBillableActions(quickActionReportPolicyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(quickActionReportPolicyID)); + return; + } + onSelected(); + }; let quickActionAvatars: ReturnType = []; if (isValidReport) { diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx index 7e342d9a5d588..5b301badae28e 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx @@ -1,5 +1,5 @@ import {Str} from 'expensify-common'; -import React, {useCallback, useMemo} from 'react'; +import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; import useLocalize from '@hooks/useLocalize'; @@ -45,21 +45,21 @@ function TravelMenuItem({icons, activePolicyID, itemIndex = -1}: TravelMenuItemP const StyleUtils = useStyleUtils(); const theme = useTheme(); - const isTravelEnabled = useMemo(() => { + const isTravelEnabled = (() => { if (!!isBlockedFromSpotnanaTravel || !primaryContactMethod || Str.isSMSLogin(primaryContactMethod) || !isPaidGroupPolicy(activePolicy)) { return false; } const isPolicyProvisioned = activePolicy?.travelSettings?.spotnanaCompanyID ?? activePolicy?.travelSettings?.associatedTravelDomainAccountID; return activePolicy?.travelSettings?.hasAcceptedTerms ?? (travelSettings?.hasAcceptedTerms && isPolicyProvisioned); - }, [activePolicy, isBlockedFromSpotnanaTravel, primaryContactMethod, travelSettings?.hasAcceptedTerms]); + })(); - const openTravel = useCallback(() => { + const openTravel = () => { if (isTravelEnabled) { openTravelDotLink(activePolicy?.id); return; } Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS.getRoute(activePolicy?.id)); - }, [activePolicy?.id, isTravelEnabled]); + }; return ( Date: Mon, 23 Feb 2026 15:25:21 +0100 Subject: [PATCH 13/54] remove useCallback/useMemo from FABPopoverMenu and useRedirectToExpensifyClassic --- .../FABPopoverContent/FABPopoverMenu.tsx | 25 ++++++++----------- .../useRedirectToExpensifyClassic.ts | 9 +++---- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx index fcabc11718236..5a92e4497b969 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react'; +import React from 'react'; import type {RefObject} from 'react'; import {View} from 'react-native'; import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; @@ -55,20 +55,17 @@ function FABPopoverMenu({ isActive: isVisible, }); - const onItemPress = useCallback( - (onSelected: () => void, options?: {shouldCallAfterModalHide?: boolean}) => { - onItemSelected(); - if (options?.shouldCallAfterModalHide && !isSafari()) { - close(() => { - navigateAfterInteraction(onSelected); - }); - } else { + const onItemPress = (onSelected: () => void, options?: {shouldCallAfterModalHide?: boolean}) => { + onItemSelected(); + if (options?.shouldCallAfterModalHide && !isSafari()) { + close(() => { navigateAfterInteraction(onSelected); - } - setFocusedIndex(-1); - }, - [onItemSelected, setFocusedIndex], - ); + }); + } else { + navigateAfterInteraction(onSelected); + } + setFocusedIndex(-1); + }; // Inject itemIndex into each child so it can interact with focus management via context const childrenWithIndex = childrenArray.map((child, index) => React.cloneElement(child, {itemIndex: index})); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts b/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts index abc7634d227e0..ec5c6217224c9 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts @@ -1,4 +1,3 @@ -import {useCallback, useMemo} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import useConfirmModal from '@hooks/useConfirmModal'; @@ -21,11 +20,9 @@ function useRedirectToExpensifyClassic() { const [isTrackingGPS = false] = useOnyx(ONYXKEYS.GPS_DRAFT_DETAILS, {canBeMissing: true, selector: isTrackingSelector}); const [allPolicies] = useMappedPolicies(policyMapper); - const shouldRedirectToExpensifyClassic = useMemo(() => { - return areAllGroupPoliciesExpenseChatDisabled((allPolicies as OnyxCollection) ?? {}); - }, [allPolicies]); + const shouldRedirectToExpensifyClassic = areAllGroupPoliciesExpenseChatDisabled((allPolicies as OnyxCollection) ?? {}); - const showRedirectToExpensifyClassicModal = useCallback(async () => { + const showRedirectToExpensifyClassicModal = async () => { const {action} = await showConfirmModal({ title: translate('sidebarScreen.redirectToExpensifyClassicModal.title'), prompt: translate('sidebarScreen.redirectToExpensifyClassicModal.description'), @@ -40,7 +37,7 @@ function useRedirectToExpensifyClassic() { return; } openOldDotLink(CONST.OLDDOT_URLS.INBOX); - }, [showConfirmModal, translate, isTrackingGPS]); + }; return {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal, allPolicies}; } From a7a0179776e50a2ff092795139ef58da2cf98488 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 08:38:44 +0100 Subject: [PATCH 14/54] remove shouldUseNarrowLayout prop drilling from FAB popover components --- .../FABPopoverContent/FABPopoverContent.tsx | 14 +-------- .../FABPopoverContentInner.tsx | 31 +++---------------- .../menuItems/CreateReportMenuItem.tsx | 5 +-- .../menuItems/ExpenseMenuItem.tsx | 5 +-- .../menuItems/InvoiceMenuItem.tsx | 5 +-- .../menuItems/NewChatMenuItem.tsx | 5 +-- .../menuItems/NewWorkspaceMenuItem.tsx | 5 +-- .../menuItems/QuickActionMenuItem.tsx | 5 +-- .../menuItems/TrackDistanceMenuItem.tsx | 5 +-- .../inbox/sidebar/FABPopoverContent/types.ts | 1 - .../FloatingActionButtonAndPopover.tsx | 1 - 11 files changed, 27 insertions(+), 55 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx index 4563de096ecb0..31ff45b8057e9 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx @@ -7,18 +7,7 @@ type FABPopoverContentExtraProps = FABPopoverContentProps & { activePolicyID: string | undefined; }; -function FABPopoverContent({ - isMenuMounted, - isVisible, - onClose, - onItemSelected, - onModalHide, - anchorPosition, - anchorRef, - shouldUseNarrowLayout, - reportID, - activePolicyID, -}: FABPopoverContentExtraProps) { +function FABPopoverContent({isMenuMounted, isVisible, onClose, onItemSelected, onModalHide, anchorPosition, anchorRef, reportID, activePolicyID}: FABPopoverContentExtraProps) { if (!isMenuMounted) { return null; } @@ -31,7 +20,6 @@ function FABPopoverContent({ onModalHide={onModalHide} anchorPosition={anchorPosition} anchorRef={anchorRef} - shouldUseNarrowLayout={shouldUseNarrowLayout} reportID={reportID} activePolicyID={activePolicyID} /> diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx index 24fbcb775c4ab..201ef5b650bcd 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import CONST from '@src/CONST'; import FABPopoverMenu from './FABPopoverMenu'; import CreateReportMenuItem, {useCreateReportMenuItemVisible} from './menuItems/CreateReportMenuItem'; @@ -18,17 +19,8 @@ type FABPopoverContentInnerExtraProps = FABPopoverContentInnerProps & { activePolicyID: string | undefined; }; -function FABPopoverContentInner({ - isVisible, - onClose, - onItemSelected, - onModalHide, - anchorPosition, - anchorRef, - shouldUseNarrowLayout, - reportID, - activePolicyID, -}: FABPopoverContentInnerExtraProps) { +function FABPopoverContentInner({isVisible, onClose, onItemSelected, onModalHide, anchorPosition, anchorRef, reportID, activePolicyID}: FABPopoverContentInnerExtraProps) { + const {shouldUseNarrowLayout} = useResponsiveLayout(); const icons = useMemoizedLazyExpensifyIcons([ 'CalendarSolid', 'Document', @@ -71,35 +63,27 @@ function FABPopoverContentInner({ > {showQuickAction && ( )} {showCreateReport && ( )} - + {showInvoice && ( @@ -111,12 +95,7 @@ function FABPopoverContentInner({ /> )} {showTestDrive && } - {showNewWorkspace && ( - - )} + {showNewWorkspace && } ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index 3e183bd202ed0..150d0b5892641 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -8,6 +8,7 @@ import useHasEmptyReportsForPolicy from '@hooks/useHasEmptyReportsForPolicy'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import {createNewReport} from '@libs/actions/Report'; @@ -28,7 +29,6 @@ import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; type CreateReportMenuItemProps = { - shouldUseNarrowLayout: boolean; icons: MenuItemIcons; activePolicyID: string | undefined; /** Injected by FABPopoverMenu via React.cloneElement */ @@ -48,8 +48,9 @@ function useCreateReportMenuItemVisible(): boolean { return shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; } -function CreateReportMenuItem({shouldUseNarrowLayout, icons, activePolicyID, itemIndex = -1}: CreateReportMenuItemProps) { +function CreateReportMenuItem({icons, activePolicyID, itemIndex = -1}: CreateReportMenuItemProps) { const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx index 15af8465bc720..54fe68cbfe605 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx @@ -2,6 +2,7 @@ import React from 'react'; import FocusableMenuItem from '@components/FocusableMenuItem'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import {startMoneyRequest} from '@libs/actions/IOU'; @@ -14,15 +15,15 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; type ExpenseMenuItemProps = { - shouldUseNarrowLayout: boolean; icons: MenuItemIcons; reportID: string; /** Injected by FABPopoverMenu via React.cloneElement */ itemIndex?: number; }; -function ExpenseMenuItem({shouldUseNarrowLayout, icons, reportID, itemIndex = -1}: ExpenseMenuItemProps) { +function ExpenseMenuItem({icons, reportID, itemIndex = -1}: ExpenseMenuItemProps) { const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx index ce51cf35bbe3e..ed4cd40d34851 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx @@ -3,6 +3,7 @@ import type {OnyxCollection} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import {startMoneyRequest} from '@libs/actions/IOU'; @@ -16,7 +17,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; type InvoiceMenuItemProps = { - shouldUseNarrowLayout: boolean; icons: MenuItemIcons; reportID: string; /** Injected by FABPopoverMenu via React.cloneElement */ @@ -29,8 +29,9 @@ function useInvoiceMenuItemVisible(): boolean { return canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email); } -function InvoiceMenuItem({shouldUseNarrowLayout, icons, reportID, itemIndex = -1}: InvoiceMenuItemProps) { +function InvoiceMenuItem({icons, reportID, itemIndex = -1}: InvoiceMenuItemProps) { const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal, allPolicies} = useRedirectToExpensifyClassic(); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx index 3567905557a68..97308f0e70d0f 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx @@ -1,6 +1,7 @@ import React from 'react'; import FocusableMenuItem from '@components/FocusableMenuItem'; import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import {startNewChat} from '@libs/actions/Report'; @@ -10,14 +11,14 @@ import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import CONST from '@src/CONST'; type NewChatMenuItemProps = { - shouldUseNarrowLayout: boolean; icons: MenuItemIcons; /** Injected by FABPopoverMenu via React.cloneElement */ itemIndex?: number; }; -function NewChatMenuItem({shouldUseNarrowLayout, icons, itemIndex = -1}: NewChatMenuItemProps) { +function NewChatMenuItem({icons, itemIndex = -1}: NewChatMenuItemProps) { const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); const StyleUtils = useStyleUtils(); const theme = useTheme(); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx index 9dda3dd6b9e2a..bf24285fae8d1 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx @@ -7,6 +7,7 @@ import useMappedPolicies from '@hooks/useMappedPolicies'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePreferredPolicy from '@hooks/usePreferredPolicy'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; @@ -22,7 +23,6 @@ import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; type NewWorkspaceMenuItemProps = { - shouldUseNarrowLayout: boolean; icons: MenuItemIcons; /** Injected by FABPopoverMenu via React.cloneElement */ itemIndex?: number; @@ -42,8 +42,9 @@ function useNewWorkspaceMenuItemVisible(): boolean { return Object.values(allPolicies ?? {}).every((policy) => !shouldShowPolicy(policy as OnyxEntry, !!isOffline, session?.email)); } -function NewWorkspaceMenuItem({shouldUseNarrowLayout, icons, itemIndex = -1}: NewWorkspaceMenuItemProps) { +function NewWorkspaceMenuItem({icons, itemIndex = -1}: NewWorkspaceMenuItemProps) { const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const {isOffline} = useNetwork(); const [isLoading = false] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index 0335b56ada01d..4d6acc882ac51 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -7,6 +7,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePreferredPolicy from '@hooks/usePreferredPolicy'; import useReportIsArchived from '@hooks/useReportIsArchived'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -38,7 +39,6 @@ import getEmptyArray from '@src/types/utils/getEmptyArray'; const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); type QuickActionMenuItemProps = { - shouldUseNarrowLayout: boolean; icons: MenuItemIcons; reportID: string; /** Injected by FABPopoverMenu via React.cloneElement */ @@ -73,8 +73,9 @@ function useQuickActionMenuItemVisible(): boolean { return !isEmptyObject(policyChatForActivePolicy); } -function QuickActionMenuItem({shouldUseNarrowLayout, icons, reportID, itemIndex = -1}: QuickActionMenuItemProps) { +function QuickActionMenuItem({icons, reportID, itemIndex = -1}: QuickActionMenuItemProps) { const styles = useThemeStyles(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const {translate, formatPhoneNumber} = useLocalize(); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx index c704c45c16a29..a3d6c20ed55d8 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx @@ -2,6 +2,7 @@ import React from 'react'; import FocusableMenuItem from '@components/FocusableMenuItem'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import {startDistanceRequest} from '@libs/actions/IOU'; @@ -13,15 +14,15 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; type TrackDistanceMenuItemProps = { - shouldUseNarrowLayout: boolean; icons: MenuItemIcons; reportID: string; /** Injected by FABPopoverMenu via React.cloneElement */ itemIndex?: number; }; -function TrackDistanceMenuItem({shouldUseNarrowLayout, icons, reportID, itemIndex = -1}: TrackDistanceMenuItemProps) { +function TrackDistanceMenuItem({icons, reportID, itemIndex = -1}: TrackDistanceMenuItemProps) { const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/types.ts b/src/pages/inbox/sidebar/FABPopoverContent/types.ts index 1b41450df3608..e731cb2adbcc9 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/types.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/types.ts @@ -26,7 +26,6 @@ type FABPopoverContentProps = { onModalHide: () => void; anchorPosition: AnchorPosition; anchorRef: RefObject; - shouldUseNarrowLayout: boolean; }; type FABPopoverContentInnerProps = Omit; diff --git a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx index adcabf7a32c74..228deb38efb1e 100644 --- a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx @@ -84,7 +84,6 @@ function FloatingActionButtonAndPopover() { onModalHide={handleMenuModalHide} anchorPosition={styles.createMenuPositionSidebar(windowHeight)} anchorRef={fabRef} - shouldUseNarrowLayout={shouldUseNarrowLayout} reportID={reportID} activePolicyID={activePolicyID} /> From d9718e0ecdf964c33a54c316d5ed8458549e53df Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 08:51:01 +0100 Subject: [PATCH 15/54] move visibility logic inside menu items, drop React.Children.toArray approach --- .../FABPopoverContent/FABMenuContext.tsx | 6 + .../FABPopoverContentInner.tsx | 63 +++---- .../FABPopoverContent/FABPopoverMenu.tsx | 46 +++-- .../menuItems/CreateReportMenuItem.tsx | 43 ++--- .../menuItems/ExpenseMenuItem.tsx | 18 +- .../menuItems/InvoiceMenuItem.tsx | 28 +-- .../menuItems/NewChatMenuItem.tsx | 18 +- .../menuItems/NewWorkspaceMenuItem.tsx | 40 +++-- .../menuItems/QuickActionMenuItem.tsx | 159 ++++++++---------- .../menuItems/TestDriveMenuItem.tsx | 34 ++-- .../menuItems/TrackDistanceMenuItem.tsx | 18 +- .../menuItems/TravelMenuItem.tsx | 37 ++-- 12 files changed, 281 insertions(+), 229 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuContext.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuContext.tsx index 451810edbb22b..320d9ce06c54e 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuContext.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuContext.tsx @@ -5,6 +5,9 @@ type FABMenuContextType = { setFocusedIndex: (index: number) => void; onItemPress: (onSelected: () => void, options?: {shouldCallAfterModalHide?: boolean}) => void; isVisible: boolean; + registeredItems: readonly string[]; + registerItem: (id: string) => void; + unregisterItem: (id: string) => void; }; const FABMenuContext = createContext({ @@ -12,6 +15,9 @@ const FABMenuContext = createContext({ setFocusedIndex: () => {}, onItemPress: () => {}, isVisible: false, + registeredItems: [], + registerItem: () => {}, + unregisterItem: () => {}, }); function useFABMenuContext() { diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx index 201ef5b650bcd..773b0e1688ece 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx @@ -3,15 +3,15 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import CONST from '@src/CONST'; import FABPopoverMenu from './FABPopoverMenu'; -import CreateReportMenuItem, {useCreateReportMenuItemVisible} from './menuItems/CreateReportMenuItem'; +import CreateReportMenuItem from './menuItems/CreateReportMenuItem'; import ExpenseMenuItem from './menuItems/ExpenseMenuItem'; -import InvoiceMenuItem, {useInvoiceMenuItemVisible} from './menuItems/InvoiceMenuItem'; +import InvoiceMenuItem from './menuItems/InvoiceMenuItem'; import NewChatMenuItem from './menuItems/NewChatMenuItem'; -import NewWorkspaceMenuItem, {useNewWorkspaceMenuItemVisible} from './menuItems/NewWorkspaceMenuItem'; -import QuickActionMenuItem, {useQuickActionMenuItemVisible} from './menuItems/QuickActionMenuItem'; -import TestDriveMenuItem, {useTestDriveMenuItemVisible} from './menuItems/TestDriveMenuItem'; +import NewWorkspaceMenuItem from './menuItems/NewWorkspaceMenuItem'; +import QuickActionMenuItem from './menuItems/QuickActionMenuItem'; +import TestDriveMenuItem from './menuItems/TestDriveMenuItem'; import TrackDistanceMenuItem from './menuItems/TrackDistanceMenuItem'; -import TravelMenuItem, {useTravelMenuItemVisible} from './menuItems/TravelMenuItem'; +import TravelMenuItem from './menuItems/TravelMenuItem'; import type {FABPopoverContentInnerProps} from './types'; type FABPopoverContentInnerExtraProps = FABPopoverContentInnerProps & { @@ -42,13 +42,6 @@ function FABPopoverContentInner({isVisible, onClose, onItemSelected, onModalHide 'Clock', ] as const); - const showQuickAction = useQuickActionMenuItemVisible(); - const showInvoice = useInvoiceMenuItemVisible(); - const showTravel = useTravelMenuItemVisible(activePolicyID); - const showTestDrive = useTestDriveMenuItemVisible(); - const showNewWorkspace = useNewWorkspaceMenuItemVisible(); - const showCreateReport = useCreateReportMenuItemVisible(); - return ( - {showQuickAction && ( - - )} + - {showCreateReport && ( - - )} + - {showInvoice && ( - - )} - {showTravel && ( - - )} - {showTestDrive && } - {showNewWorkspace && } + + + + ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx index 5a92e4497b969..fa0c9e72a6eb3 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useState} from 'react'; import type {RefObject} from 'react'; import {View} from 'react-native'; import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; @@ -13,7 +13,10 @@ import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; import {FABMenuContext} from './FABMenuContext'; -type FABMenuItemElement = React.ReactElement<{itemIndex?: number}>; +// Fixed display order for all possible menu items. +// Components self-register — this array ensures arrow-key indices always follow JSX order +// regardless of when each item becomes visible. +const FAB_ITEM_ORDER = ['quick-action', 'expense', 'track-distance', 'create-report', 'new-chat', 'invoice', 'travel', 'test-drive', 'new-workspace'] as const; type FABPopoverMenuProps = { isVisible: boolean; @@ -44,10 +47,34 @@ function FABPopoverMenu({ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); - // React.Children.toArray filters out null/false/undefined produced by {cond && }, - // giving us an accurate count of actually-rendered items for arrow-key focus management. - const childrenArray = React.Children.toArray(children) as FABMenuItemElement[]; - const itemCount = childrenArray.length; + const [registeredSet, setRegisteredSet] = useState>(new Set()); + + // Derive ordered list from the fixed order array so indices are stable + // regardless of registration order. + const registeredItems = FAB_ITEM_ORDER.filter((id) => registeredSet.has(id)); + const itemCount = registeredItems.length; + + const registerItem = (id: string) => { + setRegisteredSet((prev) => { + if (prev.has(id)) { + return prev; + } + const next = new Set(prev); + next.add(id); + return next; + }); + }; + + const unregisterItem = (id: string) => { + setRegisteredSet((prev) => { + if (!prev.has(id)) { + return prev; + } + const next = new Set(prev); + next.delete(id); + return next; + }); + }; const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ initialFocusedIndex: -1, @@ -67,11 +94,8 @@ function FABPopoverMenu({ setFocusedIndex(-1); }; - // Inject itemIndex into each child so it can interact with focus management via context - const childrenWithIndex = childrenArray.map((child, index) => React.cloneElement(child, {itemIndex: index})); - return ( - + - {childrenWithIndex} + {children} diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index 150d0b5892641..18eda399a31c1 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -1,5 +1,5 @@ import {groupPaidPoliciesWithExpenseChatEnabledSelector} from '@selectors/Policy'; -import React from 'react'; +import React, {useLayoutEffect} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation'; @@ -28,27 +28,16 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; +const ITEM_ID = 'create-report'; + +const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); + type CreateReportMenuItemProps = { icons: MenuItemIcons; activePolicyID: string | undefined; - /** Injected by FABPopoverMenu via React.cloneElement */ - itemIndex?: number; }; -const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); - -function useCreateReportMenuItemVisible(): boolean { - const {shouldRedirectToExpensifyClassic} = useRedirectToExpensifyClassic(); - const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); - - const groupPaidPoliciesWithChatEnabled = (policies: Parameters[0]) => - groupPaidPoliciesWithExpenseChatEnabledSelector(policies, session?.email); - const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPaidPoliciesWithChatEnabled, canBeMissing: true}, [session?.email]); - - return shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; -} - -function CreateReportMenuItem({icons, activePolicyID, itemIndex = -1}: CreateReportMenuItemProps) { +function CreateReportMenuItem({icons, activePolicyID}: CreateReportMenuItemProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); @@ -61,7 +50,7 @@ function CreateReportMenuItem({icons, activePolicyID, itemIndex = -1}: CreateRep const {isBetaEnabled} = usePermissions(); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); - const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); + const {focusedIndex, setFocusedIndex, onItemPress, registeredItems, registerItem, unregisterItem} = useFABMenuContext(); const StyleUtils = useStyleUtils(); const theme = useTheme(); @@ -71,6 +60,19 @@ function CreateReportMenuItem({icons, activePolicyID, itemIndex = -1}: CreateRep // eslint-disable-next-line rulesdir/no-inline-useOnyx-selector const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPaidPoliciesWithChatEnabled, canBeMissing: true}, [session?.email]); + const isVisible = shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; + + useLayoutEffect(() => { + if (!isVisible) { + return; + } + registerItem(ITEM_ID); + return () => unregisterItem(ITEM_ID); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isVisible]); + + const itemIndex = registeredItems.indexOf(ITEM_ID); + const defaultChatEnabledPolicy = getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array>, activePolicy); const defaultChatEnabledPolicyID = defaultChatEnabledPolicy?.id; @@ -113,6 +115,10 @@ function CreateReportMenuItem({icons, activePolicyID, itemIndex = -1}: CreateRep onConfirm: handleCreateWorkspaceReport, }); + if (!isVisible) { + return null; + } + return ( <> { + registerItem(ITEM_ID); + return () => unregisterItem(ITEM_ID); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const itemIndex = registeredItems.indexOf(ITEM_ID); + return ( , session?.email); -} - -function InvoiceMenuItem({icons, reportID, itemIndex = -1}: InvoiceMenuItemProps) { +function InvoiceMenuItem({icons, reportID}: InvoiceMenuItemProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal, allPolicies} = useRedirectToExpensifyClassic(); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); - const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); + const {focusedIndex, setFocusedIndex, onItemPress, registeredItems, registerItem, unregisterItem} = useFABMenuContext(); const StyleUtils = useStyleUtils(); const theme = useTheme(); const canSendInvoice = canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email); + useLayoutEffect(() => { + if (!canSendInvoice) { + return; + } + registerItem(ITEM_ID); + return () => unregisterItem(ITEM_ID); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [canSendInvoice]); + + const itemIndex = registeredItems.indexOf(ITEM_ID); + if (!canSendInvoice) { return null; } @@ -72,5 +77,4 @@ function InvoiceMenuItem({icons, reportID, itemIndex = -1}: InvoiceMenuItemProps ); } -export {useInvoiceMenuItemVisible}; export default InvoiceMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx index 97308f0e70d0f..09d64473c4e96 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useLayoutEffect} from 'react'; import FocusableMenuItem from '@components/FocusableMenuItem'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -10,19 +10,27 @@ import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuC import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import CONST from '@src/CONST'; +const ITEM_ID = 'new-chat'; + type NewChatMenuItemProps = { icons: MenuItemIcons; - /** Injected by FABPopoverMenu via React.cloneElement */ - itemIndex?: number; }; -function NewChatMenuItem({icons, itemIndex = -1}: NewChatMenuItemProps) { +function NewChatMenuItem({icons}: NewChatMenuItemProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); + const {focusedIndex, setFocusedIndex, onItemPress, registeredItems, registerItem, unregisterItem} = useFABMenuContext(); const StyleUtils = useStyleUtils(); const theme = useTheme(); + useLayoutEffect(() => { + registerItem(ITEM_ID); + return () => unregisterItem(ITEM_ID); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const itemIndex = registeredItems.indexOf(ITEM_ID); + return ( !shouldShowPolicy(policy as OnyxEntry, !!isOffline, session?.email)); -} - -function NewWorkspaceMenuItem({icons, itemIndex = -1}: NewWorkspaceMenuItemProps) { +function NewWorkspaceMenuItem({icons}: NewWorkspaceMenuItemProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {isOffline} = useNetwork(); @@ -50,7 +36,7 @@ function NewWorkspaceMenuItem({icons, itemIndex = -1}: NewWorkspaceMenuItemProps const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const [allPolicies] = useMappedPolicies(policyMapper); const {isRestrictedPolicyCreation} = usePreferredPolicy(); - const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); + const {focusedIndex, setFocusedIndex, onItemPress, registeredItems, registerItem, unregisterItem} = useFABMenuContext(); const StyleUtils = useStyleUtils(); const theme = useTheme(); @@ -63,7 +49,20 @@ function NewWorkspaceMenuItem({icons, itemIndex = -1}: NewWorkspaceMenuItemProps return Object.values(allPolicies ?? {}).every((policy) => !shouldShowPolicy(policy as OnyxEntry, isOfflineBool, email)); })(); - if (isLoading || !shouldShowNewWorkspaceButton) { + const isVisible = !isLoading && shouldShowNewWorkspaceButton; + + useLayoutEffect(() => { + if (!isVisible) { + return; + } + registerItem(ITEM_ID); + return () => unregisterItem(ITEM_ID); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isVisible]); + + const itemIndex = registeredItems.indexOf(ITEM_ID); + + if (!isVisible) { return null; } @@ -91,5 +90,4 @@ function NewWorkspaceMenuItem({icons, itemIndex = -1}: NewWorkspaceMenuItemProps ); } -export {useNewWorkspaceMenuItemVisible}; export default NewWorkspaceMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index 4d6acc882ac51..d795bcb54f9d6 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useLayoutEffect} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import FocusableMenuItem from '@components/FocusableMenuItem'; @@ -36,44 +36,16 @@ import type {QuickActionName} from '@src/types/onyx/QuickAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import getEmptyArray from '@src/types/utils/getEmptyArray'; +const ITEM_ID = 'quick-action'; + const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); type QuickActionMenuItemProps = { icons: MenuItemIcons; reportID: string; - /** Injected by FABPopoverMenu via React.cloneElement */ - itemIndex?: number; }; -function useQuickActionMenuItemVisible(): boolean { - const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); - const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); - const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, {canBeMissing: true}); - const [quickActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`, {canBeMissing: true}); - const quickActionPolicyID = quickAction?.action === CONST.QUICK_ACTIONS.TRACK_PER_DIEM && quickAction?.perDiemPolicyID ? quickAction?.perDiemPolicyID : quickActionReport?.policyID; - const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionPolicyID}`, {canBeMissing: true}); - const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); - const isReportArchived = useReportIsArchived(quickActionReport?.reportID); - const {isRestrictedToPreferredPolicy} = usePreferredPolicy(); - - const workspaceChatsSelector = (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports); - const [policyChats = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector, canBeMissing: true}); - - const policyChatForActivePolicy: OnyxTypes.Report = - !isEmptyObject(activePolicy) && activePolicy?.isPolicyExpenseChatEnabled && policyChats.length > 0 ? (policyChats.at(0) ?? ({} as OnyxTypes.Report)) : ({} as OnyxTypes.Report); - - if (quickAction?.action && quickActionReport) { - if (!isQuickActionAllowed(quickAction, quickActionReport, quickActionPolicy, isReportArchived, allBetas, isRestrictedToPreferredPolicy)) { - return false; - } - return true; - } - - return !isEmptyObject(policyChatForActivePolicy); -} - -function QuickActionMenuItem({icons, reportID, itemIndex = -1}: QuickActionMenuItemProps) { +function QuickActionMenuItem({icons, reportID}: QuickActionMenuItemProps) { const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {translate, formatPhoneNumber} = useLocalize(); @@ -94,7 +66,7 @@ function QuickActionMenuItem({icons, reportID, itemIndex = -1}: QuickActionMenuI const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const isReportArchived = useReportIsArchived(quickActionReport?.reportID); const {isRestrictedToPreferredPolicy} = usePreferredPolicy(); - const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); + const {focusedIndex, setFocusedIndex, onItemPress, registeredItems, registerItem, unregisterItem} = useFABMenuContext(); const StyleUtils = useStyleUtils(); const theme = useTheme(); @@ -106,14 +78,24 @@ function QuickActionMenuItem({icons, reportID, itemIndex = -1}: QuickActionMenuI const policyChatForActivePolicy: OnyxTypes.Report = !isEmptyObject(activePolicy) && activePolicy?.isPolicyExpenseChatEnabled && policyChats.length > 0 ? (policyChats.at(0) ?? ({} as OnyxTypes.Report)) : ({} as OnyxTypes.Report); - const quickActionReportPolicyID = quickActionReport?.policyID; - const selectOption = (onSelected: () => void, shouldRestrictAction: boolean) => { - if (shouldRestrictAction && quickActionReportPolicyID && shouldRestrictUserBillableActions(quickActionReportPolicyID)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(quickActionReportPolicyID)); + const isVisible = + (quickAction?.action && quickActionReport + ? isQuickActionAllowed(quickAction, quickActionReport, quickActionPolicy, isReportArchived, allBetas, isRestrictedToPreferredPolicy) + : false) || + (!quickAction?.action && !isEmptyObject(policyChatForActivePolicy)); + + useLayoutEffect(() => { + if (!isVisible) { return; } - onSelected(); - }; + registerItem(ITEM_ID); + return () => unregisterItem(ITEM_ID); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isVisible]); + + const itemIndex = registeredItems.indexOf(ITEM_ID); + const isFocused = focusedIndex === itemIndex; + const focusWrapperStyle = StyleUtils.getItemBackgroundColorStyle(false, isFocused, false, theme.activeComponentBG, theme.hoverComponentBG); let quickActionAvatars: ReturnType = []; if (isValidReport) { @@ -148,14 +130,20 @@ function QuickActionMenuItem({icons, reportID, itemIndex = -1}: QuickActionMenuI // eslint-disable-next-line @typescript-eslint/no-deprecated const quickActionSubtitle = !hideQABSubtitle ? (getReportName(quickActionReport, quickActionPolicy, undefined, personalDetails) ?? translate('quickAction.updateDestination')) : ''; - const isFocused = focusedIndex === itemIndex; - const focusWrapperStyle = StyleUtils.getItemBackgroundColorStyle(false, isFocused, false, theme.activeComponentBG, theme.hoverComponentBG); + if (!isVisible) { + return null; + } - if (quickAction?.action && quickActionReport) { - if (!isQuickActionAllowed(quickAction, quickActionReport, quickActionPolicy, isReportArchived, allBetas, isRestrictedToPreferredPolicy)) { - return null; + const quickActionReportPolicyID = quickActionReport?.policyID; + const selectOption = (onSelected: () => void, shouldRestrictAction: boolean) => { + if (shouldRestrictAction && quickActionReportPolicyID && shouldRestrictUserBillableActions(quickActionReportPolicyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(quickActionReportPolicyID)); + return; } + onSelected(); + }; + if (quickAction?.action && quickActionReport) { return ( setFocusedIndex(itemIndex)} - shouldCheckActionAllowedOnPress={false} - role={CONST.ROLE.BUTTON} - wrapperStyle={focusWrapperStyle} - icon={icons.ReceiptScan} - title={translate('quickAction.scanReceipt')} - // eslint-disable-next-line @typescript-eslint/no-deprecated - description={getReportName(policyChatForActivePolicy)} - rightIconReportID={policyChatForActivePolicy?.reportID} - onPress={() => - onItemPress( - () => - interceptAnonymousUser(() => { - if (policyChatForActivePolicy?.policyID && shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatForActivePolicy.policyID)); - return; - } - - const quickActionReportID = policyChatForActivePolicy?.reportID || reportID; - startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, allTransactionDrafts, true); - }), - {shouldCallAfterModalHide: shouldUseNarrowLayout}, - ) - } - /> - ); - } - - return null; + return ( + setFocusedIndex(itemIndex)} + shouldCheckActionAllowedOnPress={false} + role={CONST.ROLE.BUTTON} + wrapperStyle={focusWrapperStyle} + icon={icons.ReceiptScan} + title={translate('quickAction.scanReceipt')} + // eslint-disable-next-line @typescript-eslint/no-deprecated + description={getReportName(policyChatForActivePolicy)} + rightIconReportID={policyChatForActivePolicy?.reportID} + onPress={() => + onItemPress( + () => + interceptAnonymousUser(() => { + if (policyChatForActivePolicy?.policyID && shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatForActivePolicy.policyID)); + return; + } + + const quickActionReportID = policyChatForActivePolicy?.reportID || reportID; + startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, allTransactionDrafts, true); + }), + {shouldCallAfterModalHide: shouldUseNarrowLayout}, + ) + } + /> + ); } -export {useQuickActionMenuItemVisible}; export default QuickActionMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx index 12871f190b259..3c89ccbe8bed3 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx @@ -1,5 +1,5 @@ import {hasSeenTourSelector, tryNewDotOnyxSelector} from '@selectors/Onboarding'; -import React from 'react'; +import React, {useLayoutEffect} from 'react'; import FocusableMenuItem from '@components/FocusableMenuItem'; import useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin'; import useLocalize from '@hooks/useLocalize'; @@ -14,26 +14,39 @@ import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +const ITEM_ID = 'test-drive'; + type TestDriveMenuItemProps = { icons: MenuItemIcons; - /** Injected by FABPopoverMenu via React.cloneElement */ - itemIndex?: number; }; -function useTestDriveMenuItemVisible(): boolean { - const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector, canBeMissing: true}); - return !hasSeenTour; -} - -function TestDriveMenuItem({icons, itemIndex = -1}: TestDriveMenuItemProps) { +function TestDriveMenuItem({icons}: TestDriveMenuItemProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); + const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector, canBeMissing: true}); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true}); const isUserPaidPolicyMember = useIsPaidPolicyAdmin(); - const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); + const {focusedIndex, setFocusedIndex, onItemPress, registeredItems, registerItem, unregisterItem} = useFABMenuContext(); + + const isVisible = !hasSeenTour; + + useLayoutEffect(() => { + if (!isVisible) { + return; + } + registerItem(ITEM_ID); + return () => unregisterItem(ITEM_ID); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isVisible]); + + const itemIndex = registeredItems.indexOf(ITEM_ID); + + if (!isVisible) { + return null; + } return ( { + registerItem(ITEM_ID); + return () => unregisterItem(ITEM_ID); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const itemIndex = registeredItems.indexOf(ITEM_ID); + return ( ) => account?.primaryLogin; + type TravelMenuItemProps = { icons: MenuItemIcons; activePolicyID: string | undefined; - /** Injected by FABPopoverMenu via React.cloneElement */ - itemIndex?: number; }; -const accountPrimaryLoginSelector = (account: OnyxEntry) => account?.primaryLogin; - -function useTravelMenuItemVisible(activePolicyID: string | undefined): boolean { - const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); - return !!activePolicy?.isTravelEnabled; -} - -function TravelMenuItem({icons, activePolicyID, itemIndex = -1}: TravelMenuItemProps) { +function TravelMenuItem({icons, activePolicyID}: TravelMenuItemProps) { const {translate} = useLocalize(); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS, {canBeMissing: true}); @@ -41,10 +36,23 @@ function TravelMenuItem({icons, activePolicyID, itemIndex = -1}: TravelMenuItemP const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); const isBlockedFromSpotnanaTravel = Permissions.isBetaEnabled(CONST.BETAS.PREVENT_SPOTNANA_TRAVEL, allBetas); const primaryContactMethod = primaryLogin ?? session?.email ?? ''; - const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); + const {focusedIndex, setFocusedIndex, onItemPress, registeredItems, registerItem, unregisterItem} = useFABMenuContext(); const StyleUtils = useStyleUtils(); const theme = useTheme(); + const isVisible = !!activePolicy?.isTravelEnabled; + + useLayoutEffect(() => { + if (!isVisible) { + return; + } + registerItem(ITEM_ID); + return () => unregisterItem(ITEM_ID); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isVisible]); + + const itemIndex = registeredItems.indexOf(ITEM_ID); + const isTravelEnabled = (() => { if (!!isBlockedFromSpotnanaTravel || !primaryContactMethod || Str.isSMSLogin(primaryContactMethod) || !isPaidGroupPolicy(activePolicy)) { return false; @@ -61,6 +69,10 @@ function TravelMenuItem({icons, activePolicyID, itemIndex = -1}: TravelMenuItemP Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS.getRoute(activePolicy?.id)); }; + if (!isVisible) { + return null; + } + return ( Date: Tue, 24 Feb 2026 08:58:14 +0100 Subject: [PATCH 16/54] move icon loading inside each menu item, remove icons prop and MenuItemIcons type --- .../FABPopoverContentInner.tsx | 57 +++---------------- .../menuItems/CreateReportMenuItem.tsx | 6 +- .../menuItems/ExpenseMenuItem.tsx | 8 +-- .../menuItems/InvoiceMenuItem.tsx | 6 +- .../menuItems/NewChatMenuItem.tsx | 9 +-- .../menuItems/NewWorkspaceMenuItem.tsx | 9 +-- .../menuItems/QuickActionMenuItem.tsx | 8 +-- .../menuItems/TestDriveMenuItem.tsx | 9 +-- .../menuItems/TrackDistanceMenuItem.tsx | 6 +- .../menuItems/TravelMenuItem.tsx | 6 +- .../inbox/sidebar/FABPopoverContent/types.ts | 5 +- 11 files changed, 39 insertions(+), 90 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx index 773b0e1688ece..0c5999b5d68da 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import CONST from '@src/CONST'; import FABPopoverMenu from './FABPopoverMenu'; @@ -21,26 +20,6 @@ type FABPopoverContentInnerExtraProps = FABPopoverContentInnerProps & { function FABPopoverContentInner({isVisible, onClose, onItemSelected, onModalHide, anchorPosition, anchorRef, reportID, activePolicyID}: FABPopoverContentInnerExtraProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); - const icons = useMemoizedLazyExpensifyIcons([ - 'CalendarSolid', - 'Document', - 'NewWorkspace', - 'NewWindow', - 'Binoculars', - 'Car', - 'Location', - 'Suitcase', - 'Task', - 'InvoiceGeneric', - 'ReceiptScan', - 'ChatBubble', - 'Coins', - 'Receipt', - 'Cash', - 'Transfer', - 'MoneyCircle', - 'Clock', - ] as const); return ( - - - - - - - - - + + + + + + + + + ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index 18eda399a31c1..8efeefd4b4601 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -5,6 +5,7 @@ import FocusableMenuItem from '@components/FocusableMenuItem'; import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useHasEmptyReportsForPolicy from '@hooks/useHasEmptyReportsForPolicy'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; @@ -20,7 +21,6 @@ import {hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import isOnSearchMoneyRequestReportPage from '@navigation/helpers/isOnSearchMoneyRequestReportPage'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import {clearLastSearchParams} from '@userActions/ReportNavigation'; import CONST from '@src/CONST'; @@ -33,13 +33,13 @@ const ITEM_ID = 'create-report'; const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); type CreateReportMenuItemProps = { - icons: MenuItemIcons; activePolicyID: string | undefined; }; -function CreateReportMenuItem({icons, activePolicyID}: CreateReportMenuItemProps) { +function CreateReportMenuItem({activePolicyID}: CreateReportMenuItemProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const icons = useMemoizedLazyExpensifyIcons(['Document'] as const); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx index 4bfcbd4961952..1e9b1e0ab1c3e 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx @@ -1,5 +1,6 @@ import React, {useLayoutEffect} from 'react'; import FocusableMenuItem from '@components/FocusableMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -9,7 +10,6 @@ import {startMoneyRequest} from '@libs/actions/IOU'; import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -17,13 +17,13 @@ import ONYXKEYS from '@src/ONYXKEYS'; const ITEM_ID = 'expense'; type ExpenseMenuItemProps = { - icons: MenuItemIcons; reportID: string; }; -function ExpenseMenuItem({icons, reportID}: ExpenseMenuItemProps) { +function ExpenseMenuItem({reportID}: ExpenseMenuItemProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const icons = useMemoizedLazyExpensifyIcons(['Coins', 'Receipt', 'Cash', 'Transfer', 'MoneyCircle'] as const); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); const {focusedIndex, setFocusedIndex, onItemPress, registeredItems, registerItem, unregisterItem} = useFABMenuContext(); @@ -41,7 +41,7 @@ function ExpenseMenuItem({icons, reportID}: ExpenseMenuItemProps) { return ( [1])} + icon={getIconForAction(CONST.IOU.TYPE.CREATE, icons)} title={translate('iou.createExpense')} focused={focusedIndex === itemIndex} onFocus={() => setFocusedIndex(itemIndex)} diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx index fb66c28b0817d..05658ab9ca87c 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx @@ -1,6 +1,7 @@ import React, {useLayoutEffect} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -10,7 +11,6 @@ import {startMoneyRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {canSendInvoice as canSendInvoicePolicyUtils} from '@libs/PolicyUtils'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -19,13 +19,13 @@ import type * as OnyxTypes from '@src/types/onyx'; const ITEM_ID = 'invoice'; type InvoiceMenuItemProps = { - icons: MenuItemIcons; reportID: string; }; -function InvoiceMenuItem({icons, reportID}: InvoiceMenuItemProps) { +function InvoiceMenuItem({reportID}: InvoiceMenuItemProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const icons = useMemoizedLazyExpensifyIcons(['InvoiceGeneric'] as const); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal, allPolicies} = useRedirectToExpensifyClassic(); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx index 09d64473c4e96..35dc7e7e245fd 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx @@ -1,5 +1,6 @@ import React, {useLayoutEffect} from 'react'; import FocusableMenuItem from '@components/FocusableMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -7,18 +8,14 @@ import useTheme from '@hooks/useTheme'; import {startNewChat} from '@libs/actions/Report'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import CONST from '@src/CONST'; const ITEM_ID = 'new-chat'; -type NewChatMenuItemProps = { - icons: MenuItemIcons; -}; - -function NewChatMenuItem({icons}: NewChatMenuItemProps) { +function NewChatMenuItem() { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const icons = useMemoizedLazyExpensifyIcons(['ChatBubble'] as const); const {focusedIndex, setFocusedIndex, onItemPress, registeredItems, registerItem, unregisterItem} = useFABMenuContext(); const StyleUtils = useStyleUtils(); const theme = useTheme(); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx index d26ea78c68d68..e0822cfc001cb 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx @@ -2,6 +2,7 @@ import type {ImageContentFit} from 'expo-image'; import React, {useLayoutEffect} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useMappedPolicies from '@hooks/useMappedPolicies'; import useNetwork from '@hooks/useNetwork'; @@ -14,7 +15,6 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {shouldShowPolicy} from '@libs/PolicyUtils'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import {policyMapper} from '@pages/inbox/sidebar/FABPopoverContent/types'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -24,13 +24,10 @@ import type * as OnyxTypes from '@src/types/onyx'; const ITEM_ID = 'new-workspace'; -type NewWorkspaceMenuItemProps = { - icons: MenuItemIcons; -}; - -function NewWorkspaceMenuItem({icons}: NewWorkspaceMenuItemProps) { +function NewWorkspaceMenuItem() { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const icons = useMemoizedLazyExpensifyIcons(['NewWorkspace'] as const); const {isOffline} = useNetwork(); const [isLoading = false] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index d795bcb54f9d6..320687e1e376a 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -3,6 +3,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import FocusableMenuItem from '@components/FocusableMenuItem'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePreferredPolicy from '@hooks/usePreferredPolicy'; @@ -27,7 +28,6 @@ import { } from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -41,14 +41,14 @@ const ITEM_ID = 'quick-action'; const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); type QuickActionMenuItemProps = { - icons: MenuItemIcons; reportID: string; }; -function QuickActionMenuItem({icons, reportID}: QuickActionMenuItemProps) { +function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {translate, formatPhoneNumber} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['CalendarSolid', 'ReceiptScan', 'Car', 'Task', 'Clock', 'MoneyCircle', 'Coins', 'Receipt', 'Cash', 'Transfer'] as const); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); @@ -161,7 +161,7 @@ function QuickActionMenuItem({icons, reportID}: QuickActionMenuItemProps) { shouldCheckActionAllowedOnPress={false} role={CONST.ROLE.BUTTON} wrapperStyle={focusWrapperStyle} - icon={getQuickActionIcon(icons as Parameters[0], quickAction?.action)} + icon={getQuickActionIcon(icons, quickAction?.action)} title={quickActionTitle} rightIconAccountID={quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID} description={quickActionSubtitle} diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx index 3c89ccbe8bed3..449bfb909f6fb 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx @@ -2,6 +2,7 @@ import {hasSeenTourSelector, tryNewDotOnyxSelector} from '@selectors/Onboarding' import React, {useLayoutEffect} from 'react'; import FocusableMenuItem from '@components/FocusableMenuItem'; import useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -10,21 +11,17 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {startTestDrive} from '@libs/actions/Tour'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; const ITEM_ID = 'test-drive'; -type TestDriveMenuItemProps = { - icons: MenuItemIcons; -}; - -function TestDriveMenuItem({icons}: TestDriveMenuItemProps) { +function TestDriveMenuItem() { const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); + const icons = useMemoizedLazyExpensifyIcons(['Binoculars'] as const); const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector, canBeMissing: true}); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true}); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx index d66eeb2da87e5..35f55f23b67f1 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx @@ -1,5 +1,6 @@ import React, {useLayoutEffect} from 'react'; import FocusableMenuItem from '@components/FocusableMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -8,7 +9,6 @@ import useTheme from '@hooks/useTheme'; import {startDistanceRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -16,13 +16,13 @@ import ONYXKEYS from '@src/ONYXKEYS'; const ITEM_ID = 'track-distance'; type TrackDistanceMenuItemProps = { - icons: MenuItemIcons; reportID: string; }; -function TrackDistanceMenuItem({icons, reportID}: TrackDistanceMenuItemProps) { +function TrackDistanceMenuItem({reportID}: TrackDistanceMenuItemProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const icons = useMemoizedLazyExpensifyIcons(['Location'] as const); const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); const {focusedIndex, setFocusedIndex, onItemPress, registeredItems, registerItem, unregisterItem} = useFABMenuContext(); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx index 5d96804a9d58b..8b1c5a8d1ff38 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx @@ -2,6 +2,7 @@ import {Str} from 'expensify-common'; import React, {useLayoutEffect} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -12,7 +13,6 @@ import {openTravelDotLink, shouldOpenTravelDotLinkWeb} from '@libs/openTravelDot import Permissions from '@libs/Permissions'; import {isPaidGroupPolicy} from '@libs/PolicyUtils'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import type {MenuItemIcons} from '@pages/inbox/sidebar/FABPopoverContent/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -23,12 +23,12 @@ const ITEM_ID = 'travel'; const accountPrimaryLoginSelector = (account: OnyxEntry) => account?.primaryLogin; type TravelMenuItemProps = { - icons: MenuItemIcons; activePolicyID: string | undefined; }; -function TravelMenuItem({icons, activePolicyID}: TravelMenuItemProps) { +function TravelMenuItem({activePolicyID}: TravelMenuItemProps) { const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Suitcase', 'NewWindow'] as const); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS, {canBeMissing: true}); const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: accountPrimaryLoginSelector, canBeMissing: true}); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/types.ts b/src/pages/inbox/sidebar/FABPopoverContent/types.ts index e731cb2adbcc9..588d7bde108c8 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/types.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/types.ts @@ -2,7 +2,6 @@ import type {RefObject} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import type {AnchorPosition} from '@src/styles'; import type * as OnyxTypes from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; type PolicySelector = Pick; @@ -30,7 +29,5 @@ type FABPopoverContentProps = { type FABPopoverContentInnerProps = Omit; -type MenuItemIcons = Record; - -export type {PolicySelector, FABPopoverContentProps, FABPopoverContentInnerProps, MenuItemIcons}; +export type {PolicySelector, FABPopoverContentProps, FABPopoverContentInnerProps}; export {policyMapper}; From 9ff0839b091cec743e1bc3467476463ac8e893cb Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 09:04:06 +0100 Subject: [PATCH 17/54] internalize anchorPosition inside FABPopoverMenu --- .../FABPopoverContent/FABPopoverContent.tsx | 3 +-- .../FABPopoverContentInner.tsx | 3 +-- .../FABPopoverContent/FABPopoverMenu.tsx | 18 ++++-------------- .../inbox/sidebar/FABPopoverContent/types.ts | 2 -- .../sidebar/FloatingActionButtonAndPopover.tsx | 3 --- 5 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx index 31ff45b8057e9..462d7381a6aca 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx @@ -7,7 +7,7 @@ type FABPopoverContentExtraProps = FABPopoverContentProps & { activePolicyID: string | undefined; }; -function FABPopoverContent({isMenuMounted, isVisible, onClose, onItemSelected, onModalHide, anchorPosition, anchorRef, reportID, activePolicyID}: FABPopoverContentExtraProps) { +function FABPopoverContent({isMenuMounted, isVisible, onClose, onItemSelected, onModalHide, anchorRef, reportID, activePolicyID}: FABPopoverContentExtraProps) { if (!isMenuMounted) { return null; } @@ -18,7 +18,6 @@ function FABPopoverContent({isMenuMounted, isVisible, onClose, onItemSelected, o onClose={onClose} onItemSelected={onItemSelected} onModalHide={onModalHide} - anchorPosition={anchorPosition} anchorRef={anchorRef} reportID={reportID} activePolicyID={activePolicyID} diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx index 0c5999b5d68da..220795bd21092 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx @@ -18,7 +18,7 @@ type FABPopoverContentInnerExtraProps = FABPopoverContentInnerProps & { activePolicyID: string | undefined; }; -function FABPopoverContentInner({isVisible, onClose, onItemSelected, onModalHide, anchorPosition, anchorRef, reportID, activePolicyID}: FABPopoverContentInnerExtraProps) { +function FABPopoverContentInner({isVisible, onClose, onItemSelected, onModalHide, anchorRef, reportID, activePolicyID}: FABPopoverContentInnerExtraProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); return ( @@ -27,7 +27,6 @@ function FABPopoverContentInner({isVisible, onClose, onItemSelected, onModalHide onClose={onClose} onItemSelected={onItemSelected} onModalHide={onModalHide} - anchorPosition={anchorPosition} anchorRef={anchorRef} fromSidebarMediumScreen={!shouldUseNarrowLayout} animationInTiming={CONST.MODAL.ANIMATION_TIMING.FAB_IN} diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx index fa0c9e72a6eb3..73335cc3bba6a 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx @@ -6,11 +6,11 @@ import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import {close} from '@libs/actions/Modal'; import {isSafari} from '@libs/Browser'; import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction'; import CONST from '@src/CONST'; -import type {AnchorPosition} from '@src/styles'; import {FABMenuContext} from './FABMenuContext'; // Fixed display order for all possible menu items. @@ -23,7 +23,6 @@ type FABPopoverMenuProps = { onClose: () => void; onItemSelected: () => void; onModalHide: () => void; - anchorPosition: AnchorPosition; anchorRef: RefObject; fromSidebarMediumScreen?: boolean; animationInTiming?: number; @@ -31,21 +30,12 @@ type FABPopoverMenuProps = { children: React.ReactNode; }; -function FABPopoverMenu({ - isVisible, - onClose, - onItemSelected, - onModalHide, - anchorPosition, - anchorRef, - fromSidebarMediumScreen, - animationInTiming, - animationOutTiming, - children, -}: FABPopoverMenuProps) { +function FABPopoverMenu({isVisible, onClose, onItemSelected, onModalHide, anchorRef, fromSidebarMediumScreen, animationInTiming, animationOutTiming, children}: FABPopoverMenuProps) { const styles = useThemeStyles(); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); + const {windowHeight} = useWindowDimensions(); + const anchorPosition = styles.createMenuPositionSidebar(windowHeight); const [registeredSet, setRegisteredSet] = useState>(new Set()); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/types.ts b/src/pages/inbox/sidebar/FABPopoverContent/types.ts index 588d7bde108c8..12fe10848e01a 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/types.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/types.ts @@ -1,6 +1,5 @@ import type {RefObject} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import type {AnchorPosition} from '@src/styles'; import type * as OnyxTypes from '@src/types/onyx'; type PolicySelector = Pick; @@ -23,7 +22,6 @@ type FABPopoverContentProps = { onClose: () => void; onItemSelected: () => void; onModalHide: () => void; - anchorPosition: AnchorPosition; anchorRef: RefObject; }; diff --git a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx index 228deb38efb1e..58a81c323dab5 100644 --- a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx @@ -8,7 +8,6 @@ import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; import FABPopoverContent from './FABPopoverContent'; import useScanActions from './FABPopoverContent/useScanActions'; @@ -20,7 +19,6 @@ import useScanActions from './FABPopoverContent/useScanActions'; function FloatingActionButtonAndPopover() { const styles = useThemeStyles(); const {translate} = useLocalize(); - const {windowHeight} = useWindowDimensions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const isFocused = useIsFocused(); const prevIsFocused = usePrevious(isFocused); @@ -82,7 +80,6 @@ function FloatingActionButtonAndPopover() { onClose={hideCreateMenu} onItemSelected={hideCreateMenu} onModalHide={handleMenuModalHide} - anchorPosition={styles.createMenuPositionSidebar(windowHeight)} anchorRef={fabRef} reportID={reportID} activePolicyID={activePolicyID} From 6a8806eed484e0469b72aa347202b0a5cccb85aa Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 09:12:46 +0100 Subject: [PATCH 18/54] internalize fromSidebarMediumScreen in FABPopoverMenu --- .../sidebar/FABPopoverContent/FABPopoverContentInner.tsx | 4 ---- .../inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx | 7 +++---- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx index 220795bd21092..1b3c143201572 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; import CONST from '@src/CONST'; import FABPopoverMenu from './FABPopoverMenu'; import CreateReportMenuItem from './menuItems/CreateReportMenuItem'; @@ -19,8 +18,6 @@ type FABPopoverContentInnerExtraProps = FABPopoverContentInnerProps & { }; function FABPopoverContentInner({isVisible, onClose, onItemSelected, onModalHide, anchorRef, reportID, activePolicyID}: FABPopoverContentInnerExtraProps) { - const {shouldUseNarrowLayout} = useResponsiveLayout(); - return ( diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx index 73335cc3bba6a..a6655c53a2e1d 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx @@ -24,16 +24,15 @@ type FABPopoverMenuProps = { onItemSelected: () => void; onModalHide: () => void; anchorRef: RefObject; - fromSidebarMediumScreen?: boolean; animationInTiming?: number; animationOutTiming?: number; children: React.ReactNode; }; -function FABPopoverMenu({isVisible, onClose, onItemSelected, onModalHide, anchorRef, fromSidebarMediumScreen, animationInTiming, animationOutTiming, children}: FABPopoverMenuProps) { +function FABPopoverMenu({isVisible, onClose, onItemSelected, onModalHide, anchorRef, animationInTiming, animationOutTiming, children}: FABPopoverMenuProps) { const styles = useThemeStyles(); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {isSmallScreenWidth} = useResponsiveLayout(); + const {isSmallScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const {windowHeight} = useWindowDimensions(); const anchorPosition = styles.createMenuPositionSidebar(windowHeight); @@ -96,7 +95,7 @@ function FABPopoverMenu({isVisible, onClose, onItemSelected, onModalHide, anchor onClose={onClose} isVisible={isVisible} onModalHide={onModalHide} - fromSidebarMediumScreen={fromSidebarMediumScreen} + fromSidebarMediumScreen={!shouldUseNarrowLayout} animationIn="fadeIn" animationOut="fadeOut" animationInTiming={animationInTiming} From afc0d0b299485f7b63538bb65bac4ae29771b39f Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 09:15:14 +0100 Subject: [PATCH 19/54] extract FAB_MENU_ITEM_IDS constants and replace hardcoded strings --- .../sidebar/FABPopoverContent/FABMenuItemIDs.ts | 13 +++++++++++++ .../sidebar/FABPopoverContent/FABPopoverMenu.tsx | 13 ++++++++++++- .../menuItems/CreateReportMenuItem.tsx | 3 ++- .../FABPopoverContent/menuItems/ExpenseMenuItem.tsx | 3 ++- .../FABPopoverContent/menuItems/InvoiceMenuItem.tsx | 3 ++- .../FABPopoverContent/menuItems/NewChatMenuItem.tsx | 3 ++- .../menuItems/NewWorkspaceMenuItem.tsx | 3 ++- .../menuItems/QuickActionMenuItem.tsx | 3 ++- .../menuItems/TestDriveMenuItem.tsx | 3 ++- .../menuItems/TrackDistanceMenuItem.tsx | 3 ++- .../FABPopoverContent/menuItems/TravelMenuItem.tsx | 3 ++- 11 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs.ts diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs.ts b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs.ts new file mode 100644 index 0000000000000..3a08cf79604b1 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs.ts @@ -0,0 +1,13 @@ +const FAB_MENU_ITEM_IDS = { + QUICK_ACTION: 'quick-action', + EXPENSE: 'expense', + TRACK_DISTANCE: 'track-distance', + CREATE_REPORT: 'create-report', + NEW_CHAT: 'new-chat', + INVOICE: 'invoice', + TRAVEL: 'travel', + TEST_DRIVE: 'test-drive', + NEW_WORKSPACE: 'new-workspace', +} as const; + +export default FAB_MENU_ITEM_IDS; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx index a6655c53a2e1d..fe944c4c837a8 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx @@ -12,11 +12,22 @@ import {isSafari} from '@libs/Browser'; import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction'; import CONST from '@src/CONST'; import {FABMenuContext} from './FABMenuContext'; +import FAB_MENU_ITEM_IDS from './FABMenuItemIDs'; // Fixed display order for all possible menu items. // Components self-register — this array ensures arrow-key indices always follow JSX order // regardless of when each item becomes visible. -const FAB_ITEM_ORDER = ['quick-action', 'expense', 'track-distance', 'create-report', 'new-chat', 'invoice', 'travel', 'test-drive', 'new-workspace'] as const; +const FAB_ITEM_ORDER = [ + FAB_MENU_ITEM_IDS.QUICK_ACTION, + FAB_MENU_ITEM_IDS.EXPENSE, + FAB_MENU_ITEM_IDS.TRACK_DISTANCE, + FAB_MENU_ITEM_IDS.CREATE_REPORT, + FAB_MENU_ITEM_IDS.NEW_CHAT, + FAB_MENU_ITEM_IDS.INVOICE, + FAB_MENU_ITEM_IDS.TRAVEL, + FAB_MENU_ITEM_IDS.TEST_DRIVE, + FAB_MENU_ITEM_IDS.NEW_WORKSPACE, +] as const; type FABPopoverMenuProps = { isVisible: boolean; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index 8efeefd4b4601..b87e3661ef3af 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -21,6 +21,7 @@ import {hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import isOnSearchMoneyRequestReportPage from '@navigation/helpers/isOnSearchMoneyRequestReportPage'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import {clearLastSearchParams} from '@userActions/ReportNavigation'; import CONST from '@src/CONST'; @@ -28,7 +29,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -const ITEM_ID = 'create-report'; +const ITEM_ID = FAB_MENU_ITEM_IDS.CREATE_REPORT; const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx index 1e9b1e0ab1c3e..a75d288948890 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx @@ -10,11 +10,12 @@ import {startMoneyRequest} from '@libs/actions/IOU'; import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -const ITEM_ID = 'expense'; +const ITEM_ID = FAB_MENU_ITEM_IDS.EXPENSE; type ExpenseMenuItemProps = { reportID: string; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx index 05658ab9ca87c..14f9ff8c1657b 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx @@ -11,12 +11,13 @@ import {startMoneyRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {canSendInvoice as canSendInvoicePolicyUtils} from '@libs/PolicyUtils'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -const ITEM_ID = 'invoice'; +const ITEM_ID = FAB_MENU_ITEM_IDS.INVOICE; type InvoiceMenuItemProps = { reportID: string; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx index 35dc7e7e245fd..9a65aeede416d 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx @@ -8,9 +8,10 @@ import useTheme from '@hooks/useTheme'; import {startNewChat} from '@libs/actions/Report'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import CONST from '@src/CONST'; -const ITEM_ID = 'new-chat'; +const ITEM_ID = FAB_MENU_ITEM_IDS.NEW_CHAT; function NewChatMenuItem() { const {translate} = useLocalize(); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx index e0822cfc001cb..5e992024f91f6 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx @@ -15,6 +15,7 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {shouldShowPolicy} from '@libs/PolicyUtils'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import {policyMapper} from '@pages/inbox/sidebar/FABPopoverContent/types'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -22,7 +23,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -const ITEM_ID = 'new-workspace'; +const ITEM_ID = FAB_MENU_ITEM_IDS.NEW_WORKSPACE; function NewWorkspaceMenuItem() { const {translate} = useLocalize(); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index 320687e1e376a..69175f4078742 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -28,6 +28,7 @@ import { } from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -36,7 +37,7 @@ import type {QuickActionName} from '@src/types/onyx/QuickAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import getEmptyArray from '@src/types/utils/getEmptyArray'; -const ITEM_ID = 'quick-action'; +const ITEM_ID = FAB_MENU_ITEM_IDS.QUICK_ACTION; const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx index 449bfb909f6fb..2b963cfbcbe16 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx @@ -11,10 +11,11 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {startTestDrive} from '@libs/actions/Tour'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -const ITEM_ID = 'test-drive'; +const ITEM_ID = FAB_MENU_ITEM_IDS.TEST_DRIVE; function TestDriveMenuItem() { const {translate} = useLocalize(); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx index 35f55f23b67f1..7737e140f3734 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx @@ -9,11 +9,12 @@ import useTheme from '@hooks/useTheme'; import {startDistanceRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -const ITEM_ID = 'track-distance'; +const ITEM_ID = FAB_MENU_ITEM_IDS.TRACK_DISTANCE; type TrackDistanceMenuItemProps = { reportID: string; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx index 8b1c5a8d1ff38..090ee317f006c 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx @@ -13,12 +13,13 @@ import {openTravelDotLink, shouldOpenTravelDotLinkWeb} from '@libs/openTravelDot import Permissions from '@libs/Permissions'; import {isPaidGroupPolicy} from '@libs/PolicyUtils'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -const ITEM_ID = 'travel'; +const ITEM_ID = FAB_MENU_ITEM_IDS.TRAVEL; const accountPrimaryLoginSelector = (account: OnyxEntry) => account?.primaryLogin; From f7fceb0a2148100ef7ae4556fc878c3cc5cd1646 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 09:17:02 +0100 Subject: [PATCH 20/54] move FAB_MENU_ITEM_IDS into CONST/index.ts --- src/CONST/index.ts | 11 +++++++++++ .../FABPopoverContent/FABMenuItemIDs.ts | 13 ------------- .../FABPopoverContent/FABPopoverMenu.tsx | 19 +++++++++---------- .../menuItems/CreateReportMenuItem.tsx | 3 +-- .../menuItems/ExpenseMenuItem.tsx | 3 +-- .../menuItems/InvoiceMenuItem.tsx | 3 +-- .../menuItems/NewChatMenuItem.tsx | 3 +-- .../menuItems/NewWorkspaceMenuItem.tsx | 3 +-- .../menuItems/QuickActionMenuItem.tsx | 3 +-- .../menuItems/TestDriveMenuItem.tsx | 3 +-- .../menuItems/TrackDistanceMenuItem.tsx | 3 +-- .../menuItems/TravelMenuItem.tsx | 3 +-- 12 files changed, 29 insertions(+), 41 deletions(-) delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs.ts diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 3f115cf69442e..8971e98a4e0fc 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1719,6 +1719,17 @@ const CONST = { FAB_OUT: 200, }, }, + FAB_MENU_ITEM_IDS: { + QUICK_ACTION: 'quick-action', + EXPENSE: 'expense', + TRACK_DISTANCE: 'track-distance', + CREATE_REPORT: 'create-report', + NEW_CHAT: 'new-chat', + INVOICE: 'invoice', + TRAVEL: 'travel', + TEST_DRIVE: 'test-drive', + NEW_WORKSPACE: 'new-workspace', + }, TIMING: { GET_ORDERED_REPORT_IDS: 'get_ordered_report_ids', CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action', diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs.ts b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs.ts deleted file mode 100644 index 3a08cf79604b1..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs.ts +++ /dev/null @@ -1,13 +0,0 @@ -const FAB_MENU_ITEM_IDS = { - QUICK_ACTION: 'quick-action', - EXPENSE: 'expense', - TRACK_DISTANCE: 'track-distance', - CREATE_REPORT: 'create-report', - NEW_CHAT: 'new-chat', - INVOICE: 'invoice', - TRAVEL: 'travel', - TEST_DRIVE: 'test-drive', - NEW_WORKSPACE: 'new-workspace', -} as const; - -export default FAB_MENU_ITEM_IDS; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx index fe944c4c837a8..9e1aba147d899 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx @@ -12,21 +12,20 @@ import {isSafari} from '@libs/Browser'; import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction'; import CONST from '@src/CONST'; import {FABMenuContext} from './FABMenuContext'; -import FAB_MENU_ITEM_IDS from './FABMenuItemIDs'; // Fixed display order for all possible menu items. // Components self-register — this array ensures arrow-key indices always follow JSX order // regardless of when each item becomes visible. const FAB_ITEM_ORDER = [ - FAB_MENU_ITEM_IDS.QUICK_ACTION, - FAB_MENU_ITEM_IDS.EXPENSE, - FAB_MENU_ITEM_IDS.TRACK_DISTANCE, - FAB_MENU_ITEM_IDS.CREATE_REPORT, - FAB_MENU_ITEM_IDS.NEW_CHAT, - FAB_MENU_ITEM_IDS.INVOICE, - FAB_MENU_ITEM_IDS.TRAVEL, - FAB_MENU_ITEM_IDS.TEST_DRIVE, - FAB_MENU_ITEM_IDS.NEW_WORKSPACE, + CONST.FAB_MENU_ITEM_IDS.QUICK_ACTION, + CONST.FAB_MENU_ITEM_IDS.EXPENSE, + CONST.FAB_MENU_ITEM_IDS.TRACK_DISTANCE, + CONST.FAB_MENU_ITEM_IDS.CREATE_REPORT, + CONST.FAB_MENU_ITEM_IDS.NEW_CHAT, + CONST.FAB_MENU_ITEM_IDS.INVOICE, + CONST.FAB_MENU_ITEM_IDS.TRAVEL, + CONST.FAB_MENU_ITEM_IDS.TEST_DRIVE, + CONST.FAB_MENU_ITEM_IDS.NEW_WORKSPACE, ] as const; type FABPopoverMenuProps = { diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index b87e3661ef3af..ffab98dd0ee63 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -21,7 +21,6 @@ import {hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import isOnSearchMoneyRequestReportPage from '@navigation/helpers/isOnSearchMoneyRequestReportPage'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import {clearLastSearchParams} from '@userActions/ReportNavigation'; import CONST from '@src/CONST'; @@ -29,7 +28,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -const ITEM_ID = FAB_MENU_ITEM_IDS.CREATE_REPORT; +const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.CREATE_REPORT; const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx index a75d288948890..71eb326d5eb92 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx @@ -10,12 +10,11 @@ import {startMoneyRequest} from '@libs/actions/IOU'; import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -const ITEM_ID = FAB_MENU_ITEM_IDS.EXPENSE; +const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.EXPENSE; type ExpenseMenuItemProps = { reportID: string; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx index 14f9ff8c1657b..49d490a54782d 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx @@ -11,13 +11,12 @@ import {startMoneyRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {canSendInvoice as canSendInvoicePolicyUtils} from '@libs/PolicyUtils'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -const ITEM_ID = FAB_MENU_ITEM_IDS.INVOICE; +const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.INVOICE; type InvoiceMenuItemProps = { reportID: string; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx index 9a65aeede416d..0cc29cfcfb569 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx @@ -8,10 +8,9 @@ import useTheme from '@hooks/useTheme'; import {startNewChat} from '@libs/actions/Report'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import CONST from '@src/CONST'; -const ITEM_ID = FAB_MENU_ITEM_IDS.NEW_CHAT; +const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.NEW_CHAT; function NewChatMenuItem() { const {translate} = useLocalize(); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx index 5e992024f91f6..81c1964099c1d 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx @@ -15,7 +15,6 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {shouldShowPolicy} from '@libs/PolicyUtils'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import {policyMapper} from '@pages/inbox/sidebar/FABPopoverContent/types'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -23,7 +22,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -const ITEM_ID = FAB_MENU_ITEM_IDS.NEW_WORKSPACE; +const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.NEW_WORKSPACE; function NewWorkspaceMenuItem() { const {translate} = useLocalize(); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index 69175f4078742..714641116f7cb 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -28,7 +28,6 @@ import { } from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -37,7 +36,7 @@ import type {QuickActionName} from '@src/types/onyx/QuickAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import getEmptyArray from '@src/types/utils/getEmptyArray'; -const ITEM_ID = FAB_MENU_ITEM_IDS.QUICK_ACTION; +const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.QUICK_ACTION; const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx index 2b963cfbcbe16..9ffb2025ab269 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx @@ -11,11 +11,10 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {startTestDrive} from '@libs/actions/Tour'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -const ITEM_ID = FAB_MENU_ITEM_IDS.TEST_DRIVE; +const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.TEST_DRIVE; function TestDriveMenuItem() { const {translate} = useLocalize(); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx index 7737e140f3734..4c116794397d1 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx @@ -9,12 +9,11 @@ import useTheme from '@hooks/useTheme'; import {startDistanceRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -const ITEM_ID = FAB_MENU_ITEM_IDS.TRACK_DISTANCE; +const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.TRACK_DISTANCE; type TrackDistanceMenuItemProps = { reportID: string; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx index 090ee317f006c..f2d0a8d9ebfc3 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx @@ -13,13 +13,12 @@ import {openTravelDotLink, shouldOpenTravelDotLinkWeb} from '@libs/openTravelDot import Permissions from '@libs/Permissions'; import {isPaidGroupPolicy} from '@libs/PolicyUtils'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import FAB_MENU_ITEM_IDS from '@pages/inbox/sidebar/FABPopoverContent/FABMenuItemIDs'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -const ITEM_ID = FAB_MENU_ITEM_IDS.TRAVEL; +const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.TRAVEL; const accountPrimaryLoginSelector = (account: OnyxEntry) => account?.primaryLogin; From feca2a3386cc6891dd3e989a5b6c516297e0bbe2 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 09:26:11 +0100 Subject: [PATCH 21/54] extract useFABMenuItem hook from menu item registration boilerplate --- .../menuItems/CreateReportMenuItem.tsx | 23 +++++++----------- .../menuItems/ExpenseMenuItem.tsx | 13 ++++------ .../menuItems/InvoiceMenuItem.tsx | 16 ++++--------- .../menuItems/NewChatMenuItem.tsx | 13 ++++------ .../menuItems/NewWorkspaceMenuItem.tsx | 16 ++++--------- .../menuItems/QuickActionMenuItem.tsx | 16 ++++--------- .../menuItems/TestDriveMenuItem.tsx | 16 ++++--------- .../menuItems/TrackDistanceMenuItem.tsx | 13 ++++------ .../menuItems/TravelMenuItem.tsx | 16 ++++--------- .../FABPopoverContent/useFABMenuItem.ts | 24 +++++++++++++++++++ 10 files changed, 64 insertions(+), 102 deletions(-) create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/useFABMenuItem.ts diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index ffab98dd0ee63..7d3d220b7bdbd 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -1,5 +1,5 @@ import {groupPaidPoliciesWithExpenseChatEnabledSelector} from '@selectors/Policy'; -import React, {useLayoutEffect} from 'react'; +import React, {useCallback} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation'; @@ -21,6 +21,7 @@ import {hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import isOnSearchMoneyRequestReportPage from '@navigation/helpers/isOnSearchMoneyRequestReportPage'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import {clearLastSearchParams} from '@userActions/ReportNavigation'; import CONST from '@src/CONST'; @@ -50,28 +51,20 @@ function CreateReportMenuItem({activePolicyID}: CreateReportMenuItemProps) { const {isBetaEnabled} = usePermissions(); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); - const {focusedIndex, setFocusedIndex, onItemPress, registeredItems, registerItem, unregisterItem} = useFABMenuContext(); + const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); const StyleUtils = useStyleUtils(); const theme = useTheme(); - const groupPaidPoliciesWithChatEnabled = (policies: Parameters[0]) => - groupPaidPoliciesWithExpenseChatEnabledSelector(policies, session?.email); + const groupPaidPoliciesWithChatEnabled = useCallback( + (policies: Parameters[0]) => groupPaidPoliciesWithExpenseChatEnabledSelector(policies, session?.email), + [session?.email], + ); - // eslint-disable-next-line rulesdir/no-inline-useOnyx-selector const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPaidPoliciesWithChatEnabled, canBeMissing: true}, [session?.email]); const isVisible = shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; - useLayoutEffect(() => { - if (!isVisible) { - return; - } - registerItem(ITEM_ID); - return () => unregisterItem(ITEM_ID); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isVisible]); - - const itemIndex = registeredItems.indexOf(ITEM_ID); + const itemIndex = useFABMenuItem(ITEM_ID, isVisible); const defaultChatEnabledPolicy = getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array>, activePolicy); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx index 71eb326d5eb92..ea3839f65fd06 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx @@ -1,4 +1,4 @@ -import React, {useLayoutEffect} from 'react'; +import React from 'react'; import FocusableMenuItem from '@components/FocusableMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -10,6 +10,7 @@ import {startMoneyRequest} from '@libs/actions/IOU'; import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -26,17 +27,11 @@ function ExpenseMenuItem({reportID}: ExpenseMenuItemProps) { const icons = useMemoizedLazyExpensifyIcons(['Coins', 'Receipt', 'Cash', 'Transfer', 'MoneyCircle'] as const); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); - const {focusedIndex, setFocusedIndex, onItemPress, registeredItems, registerItem, unregisterItem} = useFABMenuContext(); + const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); const StyleUtils = useStyleUtils(); const theme = useTheme(); - useLayoutEffect(() => { - registerItem(ITEM_ID); - return () => unregisterItem(ITEM_ID); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const itemIndex = registeredItems.indexOf(ITEM_ID); + const itemIndex = useFABMenuItem(ITEM_ID); return ( , session?.email); - useLayoutEffect(() => { - if (!canSendInvoice) { - return; - } - registerItem(ITEM_ID); - return () => unregisterItem(ITEM_ID); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [canSendInvoice]); - - const itemIndex = registeredItems.indexOf(ITEM_ID); + const itemIndex = useFABMenuItem(ITEM_ID, canSendInvoice); if (!canSendInvoice) { return null; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx index 0cc29cfcfb569..8042f20a89f9d 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx @@ -1,4 +1,4 @@ -import React, {useLayoutEffect} from 'react'; +import React from 'react'; import FocusableMenuItem from '@components/FocusableMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -8,6 +8,7 @@ import useTheme from '@hooks/useTheme'; import {startNewChat} from '@libs/actions/Report'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; import CONST from '@src/CONST'; const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.NEW_CHAT; @@ -16,17 +17,11 @@ function NewChatMenuItem() { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const icons = useMemoizedLazyExpensifyIcons(['ChatBubble'] as const); - const {focusedIndex, setFocusedIndex, onItemPress, registeredItems, registerItem, unregisterItem} = useFABMenuContext(); + const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); const StyleUtils = useStyleUtils(); const theme = useTheme(); - useLayoutEffect(() => { - registerItem(ITEM_ID); - return () => unregisterItem(ITEM_ID); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const itemIndex = registeredItems.indexOf(ITEM_ID); + const itemIndex = useFABMenuItem(ITEM_ID); return ( { - if (!isVisible) { - return; - } - registerItem(ITEM_ID); - return () => unregisterItem(ITEM_ID); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isVisible]); - - const itemIndex = registeredItems.indexOf(ITEM_ID); + const itemIndex = useFABMenuItem(ITEM_ID, isVisible); if (!isVisible) { return null; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index 714641116f7cb..55bd63eb40529 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -1,4 +1,4 @@ -import React, {useLayoutEffect} from 'react'; +import React from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import FocusableMenuItem from '@components/FocusableMenuItem'; @@ -28,6 +28,7 @@ import { } from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -66,7 +67,7 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const isReportArchived = useReportIsArchived(quickActionReport?.reportID); const {isRestrictedToPreferredPolicy} = usePreferredPolicy(); - const {focusedIndex, setFocusedIndex, onItemPress, registeredItems, registerItem, unregisterItem} = useFABMenuContext(); + const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); const StyleUtils = useStyleUtils(); const theme = useTheme(); @@ -84,16 +85,7 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { : false) || (!quickAction?.action && !isEmptyObject(policyChatForActivePolicy)); - useLayoutEffect(() => { - if (!isVisible) { - return; - } - registerItem(ITEM_ID); - return () => unregisterItem(ITEM_ID); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isVisible]); - - const itemIndex = registeredItems.indexOf(ITEM_ID); + const itemIndex = useFABMenuItem(ITEM_ID, isVisible); const isFocused = focusedIndex === itemIndex; const focusWrapperStyle = StyleUtils.getItemBackgroundColorStyle(false, isFocused, false, theme.activeComponentBG, theme.hoverComponentBG); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx index 9ffb2025ab269..9e3ebc3ed4517 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx @@ -1,5 +1,5 @@ import {hasSeenTourSelector, tryNewDotOnyxSelector} from '@selectors/Onboarding'; -import React, {useLayoutEffect} from 'react'; +import React from 'react'; import FocusableMenuItem from '@components/FocusableMenuItem'; import useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -11,6 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {startTestDrive} from '@libs/actions/Tour'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -26,20 +27,11 @@ function TestDriveMenuItem() { const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true}); const isUserPaidPolicyMember = useIsPaidPolicyAdmin(); - const {focusedIndex, setFocusedIndex, onItemPress, registeredItems, registerItem, unregisterItem} = useFABMenuContext(); + const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); const isVisible = !hasSeenTour; - useLayoutEffect(() => { - if (!isVisible) { - return; - } - registerItem(ITEM_ID); - return () => unregisterItem(ITEM_ID); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isVisible]); - - const itemIndex = registeredItems.indexOf(ITEM_ID); + const itemIndex = useFABMenuItem(ITEM_ID, isVisible); if (!isVisible) { return null; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx index 4c116794397d1..627b446d0b06a 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx @@ -1,4 +1,4 @@ -import React, {useLayoutEffect} from 'react'; +import React from 'react'; import FocusableMenuItem from '@components/FocusableMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -9,6 +9,7 @@ import useTheme from '@hooks/useTheme'; import {startDistanceRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -25,17 +26,11 @@ function TrackDistanceMenuItem({reportID}: TrackDistanceMenuItemProps) { const icons = useMemoizedLazyExpensifyIcons(['Location'] as const); const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); - const {focusedIndex, setFocusedIndex, onItemPress, registeredItems, registerItem, unregisterItem} = useFABMenuContext(); + const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); const StyleUtils = useStyleUtils(); const theme = useTheme(); - useLayoutEffect(() => { - registerItem(ITEM_ID); - return () => unregisterItem(ITEM_ID); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const itemIndex = registeredItems.indexOf(ITEM_ID); + const itemIndex = useFABMenuItem(ITEM_ID); return ( { - if (!isVisible) { - return; - } - registerItem(ITEM_ID); - return () => unregisterItem(ITEM_ID); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isVisible]); - - const itemIndex = registeredItems.indexOf(ITEM_ID); + const itemIndex = useFABMenuItem(ITEM_ID, isVisible); const isTravelEnabled = (() => { if (!!isBlockedFromSpotnanaTravel || !primaryContactMethod || Str.isSMSLogin(primaryContactMethod) || !isPaidGroupPolicy(activePolicy)) { diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useFABMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/useFABMenuItem.ts new file mode 100644 index 0000000000000..f80c661367bbd --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/useFABMenuItem.ts @@ -0,0 +1,24 @@ +import {useLayoutEffect} from 'react'; +import {useFABMenuContext} from './FABMenuContext'; + +/** + * Handles registration of a FAB menu item for arrow-key focus management. + * Pass `isVisible` for items that conditionally render — registration mirrors visibility. + * Returns the item's current index in the registered list (used for focus tracking). + */ +function useFABMenuItem(itemId: string, isVisible = true): number { + const {registerItem, unregisterItem, registeredItems} = useFABMenuContext(); + + useLayoutEffect(() => { + if (!isVisible) { + return; + } + registerItem(itemId); + return () => unregisterItem(itemId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isVisible]); + + return registeredItems.indexOf(itemId); +} + +export default useFABMenuItem; From 62a0f3d02ff53e79e5fff2aa8825cdddfd46a9ac Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 09:26:50 +0100 Subject: [PATCH 22/54] cleanup FABPopoverMenu --- .../sidebar/FABPopoverContent/FABPopoverMenu.tsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx index 9e1aba147d899..c54d8271e7b5a 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx @@ -13,9 +13,6 @@ import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction' import CONST from '@src/CONST'; import {FABMenuContext} from './FABMenuContext'; -// Fixed display order for all possible menu items. -// Components self-register — this array ensures arrow-key indices always follow JSX order -// regardless of when each item becomes visible. const FAB_ITEM_ORDER = [ CONST.FAB_MENU_ITEM_IDS.QUICK_ACTION, CONST.FAB_MENU_ITEM_IDS.EXPENSE, @@ -41,15 +38,12 @@ type FABPopoverMenuProps = { function FABPopoverMenu({isVisible, onClose, onItemSelected, onModalHide, anchorRef, animationInTiming, animationOutTiming, children}: FABPopoverMenuProps) { const styles = useThemeStyles(); - // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {isSmallScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const {windowHeight} = useWindowDimensions(); const anchorPosition = styles.createMenuPositionSidebar(windowHeight); const [registeredSet, setRegisteredSet] = useState>(new Set()); - // Derive ordered list from the fixed order array so indices are stable - // regardless of registration order. const registeredItems = FAB_ITEM_ORDER.filter((id) => registeredSet.has(id)); const itemCount = registeredItems.length; @@ -117,12 +111,7 @@ function FABPopoverMenu({isVisible, onClose, onItemSelected, onModalHide, anchor active={isVisible} shouldReturnFocus > - {/* - * Replicates PopoverMenu's layout: - * - mobile: flexGrow1 outer (no fixed width), pv4 inner for item padding - * - web: createMenuContainer (fixed sidebar width) + flex1 outer, pv4 inner - */} - + {children} From f7308942ff29883de294b3316781e54528188463 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 09:39:20 +0100 Subject: [PATCH 23/54] extend useFABMenuItem to return isFocused and wrapperStyle --- .../menuItems/CreateReportMenuItem.tsx | 13 ++++------- .../menuItems/ExpenseMenuItem.tsx | 13 ++++------- .../menuItems/InvoiceMenuItem.tsx | 13 ++++------- .../menuItems/NewChatMenuItem.tsx | 13 ++++------- .../menuItems/NewWorkspaceMenuItem.tsx | 13 ++++------- .../menuItems/QuickActionMenuItem.tsx | 15 ++++-------- .../menuItems/TestDriveMenuItem.tsx | 10 ++++---- .../menuItems/TrackDistanceMenuItem.tsx | 13 ++++------- .../menuItems/TravelMenuItem.tsx | 13 ++++------- .../FABPopoverContent/useFABMenuItem.ts | 23 +++++++++++++++---- 10 files changed, 55 insertions(+), 84 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index 7d3d220b7bdbd..631dd849bcf20 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -10,8 +10,6 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import {createNewReport} from '@libs/actions/Report'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; @@ -51,10 +49,7 @@ function CreateReportMenuItem({activePolicyID}: CreateReportMenuItemProps) { const {isBetaEnabled} = usePermissions(); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); - const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); - const StyleUtils = useStyleUtils(); - const theme = useTheme(); - + const {setFocusedIndex, onItemPress} = useFABMenuContext(); const groupPaidPoliciesWithChatEnabled = useCallback( (policies: Parameters[0]) => groupPaidPoliciesWithExpenseChatEnabledSelector(policies, session?.email), [session?.email], @@ -64,7 +59,7 @@ function CreateReportMenuItem({activePolicyID}: CreateReportMenuItemProps) { const isVisible = shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; - const itemIndex = useFABMenuItem(ITEM_ID, isVisible); + const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID, isVisible); const defaultChatEnabledPolicy = getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array>, activePolicy); @@ -118,7 +113,7 @@ function CreateReportMenuItem({activePolicyID}: CreateReportMenuItemProps) { pressableTestID={CONST.SENTRY_LABEL.FAB_MENU.CREATE_REPORT} icon={icons.Document} title={translate('report.newReport.createReport')} - focused={focusedIndex === itemIndex} + focused={isFocused} onFocus={() => setFocusedIndex(itemIndex)} onPress={() => onItemPress( @@ -153,7 +148,7 @@ function CreateReportMenuItem({activePolicyID}: CreateReportMenuItemProps) { } shouldCheckActionAllowedOnPress={false} role={CONST.ROLE.BUTTON} - wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === itemIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} + wrapperStyle={wrapperStyle} /> {CreateReportConfirmationModal} diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx index ea3839f65fd06..8b80c01964214 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx @@ -4,8 +4,6 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import {startMoneyRequest} from '@libs/actions/IOU'; import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; @@ -27,18 +25,15 @@ function ExpenseMenuItem({reportID}: ExpenseMenuItemProps) { const icons = useMemoizedLazyExpensifyIcons(['Coins', 'Receipt', 'Cash', 'Transfer', 'MoneyCircle'] as const); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); - const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); - const StyleUtils = useStyleUtils(); - const theme = useTheme(); - - const itemIndex = useFABMenuItem(ITEM_ID); + const {setFocusedIndex, onItemPress} = useFABMenuContext(); + const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID); return ( setFocusedIndex(itemIndex)} onPress={() => onItemPress( @@ -55,7 +50,7 @@ function ExpenseMenuItem({reportID}: ExpenseMenuItemProps) { } shouldCheckActionAllowedOnPress={false} role={CONST.ROLE.BUTTON} - wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === itemIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} + wrapperStyle={wrapperStyle} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx index fa7139d87bb93..0158a9de4d7f0 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx @@ -5,8 +5,6 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import {startMoneyRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {canSendInvoice as canSendInvoicePolicyUtils} from '@libs/PolicyUtils'; @@ -30,13 +28,10 @@ function InvoiceMenuItem({reportID}: InvoiceMenuItemProps) { const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal, allPolicies} = useRedirectToExpensifyClassic(); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); - const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); - const StyleUtils = useStyleUtils(); - const theme = useTheme(); - + const {setFocusedIndex, onItemPress} = useFABMenuContext(); const canSendInvoice = canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email); - const itemIndex = useFABMenuItem(ITEM_ID, canSendInvoice); + const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID, canSendInvoice); if (!canSendInvoice) { return null; @@ -47,7 +42,7 @@ function InvoiceMenuItem({reportID}: InvoiceMenuItemProps) { pressableTestID={CONST.SENTRY_LABEL.FAB_MENU.SEND_INVOICE} icon={icons.InvoiceGeneric} title={translate('workspace.invoices.sendInvoice')} - focused={focusedIndex === itemIndex} + focused={isFocused} onFocus={() => setFocusedIndex(itemIndex)} onPress={() => onItemPress( @@ -64,7 +59,7 @@ function InvoiceMenuItem({reportID}: InvoiceMenuItemProps) { } shouldCheckActionAllowedOnPress={false} role={CONST.ROLE.BUTTON} - wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === itemIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} + wrapperStyle={wrapperStyle} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx index 8042f20a89f9d..7f01170b47316 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx @@ -3,8 +3,6 @@ import FocusableMenuItem from '@components/FocusableMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import {startNewChat} from '@libs/actions/Report'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; @@ -17,23 +15,20 @@ function NewChatMenuItem() { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const icons = useMemoizedLazyExpensifyIcons(['ChatBubble'] as const); - const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); - const StyleUtils = useStyleUtils(); - const theme = useTheme(); - - const itemIndex = useFABMenuItem(ITEM_ID); + const {setFocusedIndex, onItemPress} = useFABMenuContext(); + const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID); return ( setFocusedIndex(itemIndex)} onPress={() => onItemPress(() => interceptAnonymousUser(startNewChat), {shouldCallAfterModalHide: shouldUseNarrowLayout})} shouldCheckActionAllowedOnPress={false} role={CONST.ROLE.BUTTON} - wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === itemIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} + wrapperStyle={wrapperStyle} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx index 8c4869db3e24d..40e3e5a956d37 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx @@ -9,8 +9,6 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePreferredPolicy from '@hooks/usePreferredPolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {shouldShowPolicy} from '@libs/PolicyUtils'; @@ -34,10 +32,7 @@ function NewWorkspaceMenuItem() { const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const [allPolicies] = useMappedPolicies(policyMapper); const {isRestrictedPolicyCreation} = usePreferredPolicy(); - const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); - const StyleUtils = useStyleUtils(); - const theme = useTheme(); - + const {setFocusedIndex, onItemPress} = useFABMenuContext(); const shouldShowNewWorkspaceButton = (() => { if (isRestrictedPolicyCreation) { return false; @@ -49,7 +44,7 @@ function NewWorkspaceMenuItem() { const isVisible = !isLoading && shouldShowNewWorkspaceButton; - const itemIndex = useFABMenuItem(ITEM_ID, isVisible); + const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID, isVisible); if (!isVisible) { return null; @@ -65,7 +60,7 @@ function NewWorkspaceMenuItem() { iconHeight={variables.h40} title={translate('workspace.new.newWorkspace')} description={translate('workspace.new.getTheExpensifyCardAndMore')} - focused={focusedIndex === itemIndex} + focused={isFocused} onFocus={() => setFocusedIndex(itemIndex)} onPress={() => onItemPress(() => interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(Navigation.getActiveRoute()))), { @@ -74,7 +69,7 @@ function NewWorkspaceMenuItem() { } shouldCheckActionAllowedOnPress={false} role={CONST.ROLE.BUTTON} - wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === itemIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} + wrapperStyle={wrapperStyle} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index 55bd63eb40529..8583e91b32473 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -9,8 +9,6 @@ import useOnyx from '@hooks/useOnyx'; import usePreferredPolicy from '@hooks/usePreferredPolicy'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {startMoneyRequest} from '@libs/actions/IOU'; import {navigateToQuickAction} from '@libs/actions/QuickActionNavigation'; @@ -67,10 +65,7 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const isReportArchived = useReportIsArchived(quickActionReport?.reportID); const {isRestrictedToPreferredPolicy} = usePreferredPolicy(); - const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); - const StyleUtils = useStyleUtils(); - const theme = useTheme(); - + const {setFocusedIndex, onItemPress} = useFABMenuContext(); const quickActionPolicyID = quickAction?.action === CONST.QUICK_ACTIONS.TRACK_PER_DIEM && quickAction?.perDiemPolicyID ? quickAction?.perDiemPolicyID : quickActionReport?.policyID; const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionPolicyID}`, {canBeMissing: true}); @@ -85,9 +80,7 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { : false) || (!quickAction?.action && !isEmptyObject(policyChatForActivePolicy)); - const itemIndex = useFABMenuItem(ITEM_ID, isVisible); - const isFocused = focusedIndex === itemIndex; - const focusWrapperStyle = StyleUtils.getItemBackgroundColorStyle(false, isFocused, false, theme.activeComponentBG, theme.hoverComponentBG); + const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID, isVisible); let quickActionAvatars: ReturnType = []; if (isValidReport) { @@ -152,7 +145,7 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { onFocus={() => setFocusedIndex(itemIndex)} shouldCheckActionAllowedOnPress={false} role={CONST.ROLE.BUTTON} - wrapperStyle={focusWrapperStyle} + wrapperStyle={wrapperStyle} icon={getQuickActionIcon(icons, quickAction?.action)} title={quickActionTitle} rightIconAccountID={quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID} @@ -204,7 +197,7 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { onFocus={() => setFocusedIndex(itemIndex)} shouldCheckActionAllowedOnPress={false} role={CONST.ROLE.BUTTON} - wrapperStyle={focusWrapperStyle} + wrapperStyle={wrapperStyle} icon={icons.ReceiptScan} title={translate('quickAction.scanReceipt')} // eslint-disable-next-line @typescript-eslint/no-deprecated diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx index 9e3ebc3ed4517..ce6cb6ab94c80 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx @@ -5,7 +5,6 @@ import useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {startTestDrive} from '@libs/actions/Tour'; @@ -21,17 +20,16 @@ function TestDriveMenuItem() { const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); - const StyleUtils = useStyleUtils(); const icons = useMemoizedLazyExpensifyIcons(['Binoculars'] as const); const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector, canBeMissing: true}); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true}); const isUserPaidPolicyMember = useIsPaidPolicyAdmin(); - const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); + const {setFocusedIndex, onItemPress} = useFABMenuContext(); const isVisible = !hasSeenTour; - const itemIndex = useFABMenuItem(ITEM_ID, isVisible); + const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID, isVisible); if (!isVisible) { return null; @@ -44,12 +42,12 @@ function TestDriveMenuItem() { iconStyles={styles.popoverIconCircle} iconFill={theme.icon} title={translate('testDrive.quickAction.takeATwoMinuteTestDrive')} - focused={focusedIndex === itemIndex} + focused={isFocused} onFocus={() => setFocusedIndex(itemIndex)} onPress={() => onItemPress(() => interceptAnonymousUser(() => startTestDrive(introSelected, tryNewDot?.hasBeenAddedToNudgeMigration ?? false, isUserPaidPolicyMember)))} shouldCheckActionAllowedOnPress={false} role={CONST.ROLE.BUTTON} - wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === itemIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} + wrapperStyle={wrapperStyle} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx index 627b446d0b06a..e649233dea399 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx @@ -4,8 +4,6 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import {startDistanceRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; @@ -26,18 +24,15 @@ function TrackDistanceMenuItem({reportID}: TrackDistanceMenuItemProps) { const icons = useMemoizedLazyExpensifyIcons(['Location'] as const); const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); - const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); - const StyleUtils = useStyleUtils(); - const theme = useTheme(); - - const itemIndex = useFABMenuItem(ITEM_ID); + const {setFocusedIndex, onItemPress} = useFABMenuContext(); + const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID); return ( setFocusedIndex(itemIndex)} onPress={() => onItemPress( @@ -54,7 +49,7 @@ function TrackDistanceMenuItem({reportID}: TrackDistanceMenuItemProps) { } shouldCheckActionAllowedOnPress={false} role={CONST.ROLE.BUTTON} - wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === itemIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} + wrapperStyle={wrapperStyle} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx index 3efec5c88d653..ff2c0fff4fb46 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx @@ -5,8 +5,6 @@ import FocusableMenuItem from '@components/FocusableMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {openTravelDotLink, shouldOpenTravelDotLinkWeb} from '@libs/openTravelDotLink'; @@ -37,13 +35,10 @@ function TravelMenuItem({activePolicyID}: TravelMenuItemProps) { const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); const isBlockedFromSpotnanaTravel = Permissions.isBetaEnabled(CONST.BETAS.PREVENT_SPOTNANA_TRAVEL, allBetas); const primaryContactMethod = primaryLogin ?? session?.email ?? ''; - const {focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); - const StyleUtils = useStyleUtils(); - const theme = useTheme(); - + const {setFocusedIndex, onItemPress} = useFABMenuContext(); const isVisible = !!activePolicy?.isTravelEnabled; - const itemIndex = useFABMenuItem(ITEM_ID, isVisible); + const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID, isVisible); const isTravelEnabled = (() => { if (!!isBlockedFromSpotnanaTravel || !primaryContactMethod || Str.isSMSLogin(primaryContactMethod) || !isPaidGroupPolicy(activePolicy)) { @@ -72,12 +67,12 @@ function TravelMenuItem({activePolicyID}: TravelMenuItemProps) { title={translate('travel.bookTravel')} iconRight={isTravelEnabled && shouldOpenTravelDotLinkWeb() ? icons.NewWindow : undefined} shouldShowRightIcon={!!(isTravelEnabled && shouldOpenTravelDotLinkWeb())} - focused={focusedIndex === itemIndex} + focused={isFocused} onFocus={() => setFocusedIndex(itemIndex)} onPress={() => onItemPress(() => interceptAnonymousUser(() => openTravel()))} shouldCheckActionAllowedOnPress={false} role={CONST.ROLE.BUTTON} - wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === itemIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} + wrapperStyle={wrapperStyle} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useFABMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/useFABMenuItem.ts index f80c661367bbd..f163b41e29504 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useFABMenuItem.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useFABMenuItem.ts @@ -1,13 +1,24 @@ import {useLayoutEffect} from 'react'; +import type {ViewStyle} from 'react-native'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import {useFABMenuContext} from './FABMenuContext'; +type FABMenuItemResult = { + itemIndex: number; + isFocused: boolean; + wrapperStyle: ViewStyle; +}; + /** * Handles registration of a FAB menu item for arrow-key focus management. * Pass `isVisible` for items that conditionally render — registration mirrors visibility. - * Returns the item's current index in the registered list (used for focus tracking). + * Returns itemIndex, isFocused, and the pre-computed wrapperStyle for FocusableMenuItem. */ -function useFABMenuItem(itemId: string, isVisible = true): number { - const {registerItem, unregisterItem, registeredItems} = useFABMenuContext(); +function useFABMenuItem(itemId: string, isVisible = true): FABMenuItemResult { + const {registerItem, unregisterItem, registeredItems, focusedIndex} = useFABMenuContext(); + const StyleUtils = useStyleUtils(); + const theme = useTheme(); useLayoutEffect(() => { if (!isVisible) { @@ -18,7 +29,11 @@ function useFABMenuItem(itemId: string, isVisible = true): number { // eslint-disable-next-line react-hooks/exhaustive-deps }, [isVisible]); - return registeredItems.indexOf(itemId); + const itemIndex = registeredItems.indexOf(itemId); + const isFocused = focusedIndex === itemIndex; + const wrapperStyle = StyleUtils.getItemBackgroundColorStyle(false, isFocused, false, theme.activeComponentBG, theme.hoverComponentBG); + + return {itemIndex, isFocused, wrapperStyle}; } export default useFABMenuItem; From 485d3d1d70652fb325993f0b6b0c4c45b88b4111 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 09:43:13 +0100 Subject: [PATCH 24/54] remove canBeMissing --- .../menuItems/CreateReportMenuItem.tsx | 12 +++++----- .../menuItems/ExpenseMenuItem.tsx | 2 +- .../menuItems/InvoiceMenuItem.tsx | 4 ++-- .../menuItems/NewWorkspaceMenuItem.tsx | 4 ++-- .../menuItems/QuickActionMenuItem.tsx | 22 +++++++++---------- .../menuItems/TestDriveMenuItem.tsx | 6 ++--- .../menuItems/TrackDistanceMenuItem.tsx | 2 +- .../menuItems/TravelMenuItem.tsx | 10 ++++----- .../useRedirectToExpensifyClassic.ts | 2 +- .../FABPopoverContent/useScanActions.ts | 10 ++++----- 10 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index 631dd849bcf20..e00293ee461f2 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -40,11 +40,11 @@ function CreateReportMenuItem({activePolicyID}: CreateReportMenuItemProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const icons = useMemoizedLazyExpensifyIcons(['Document'] as const); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); - const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); - const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); - const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); - const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); - const [hasDismissedEmptyReportsConfirmation] = useOnyx(ONYXKEYS.NVP_EMPTY_REPORTS_CONFIRMATION_DISMISSED, {canBeMissing: true}); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); + const [session] = useOnyx(ONYXKEYS.SESSION, {selector: sessionSelector}); + const [allBetas] = useOnyx(ONYXKEYS.BETAS); + const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const [hasDismissedEmptyReportsConfirmation] = useOnyx(ONYXKEYS.NVP_EMPTY_REPORTS_CONFIRMATION_DISMISSED); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {isBetaEnabled} = usePermissions(); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); @@ -55,7 +55,7 @@ function CreateReportMenuItem({activePolicyID}: CreateReportMenuItemProps) { [session?.email], ); - const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPaidPoliciesWithChatEnabled, canBeMissing: true}, [session?.email]); + const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPaidPoliciesWithChatEnabled}, [session?.email]); const isVisible = shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx index 8b80c01964214..031f2d28428fe 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx @@ -23,7 +23,7 @@ function ExpenseMenuItem({reportID}: ExpenseMenuItemProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const icons = useMemoizedLazyExpensifyIcons(['Coins', 'Receipt', 'Cash', 'Transfer', 'MoneyCircle'] as const); - const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); + const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); const {setFocusedIndex, onItemPress} = useFABMenuContext(); const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx index 0158a9de4d7f0..c52f0a7b9ff06 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx @@ -26,8 +26,8 @@ function InvoiceMenuItem({reportID}: InvoiceMenuItemProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const icons = useMemoizedLazyExpensifyIcons(['InvoiceGeneric'] as const); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal, allPolicies} = useRedirectToExpensifyClassic(); - const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); - const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); + const [session] = useOnyx(ONYXKEYS.SESSION); + const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); const {setFocusedIndex, onItemPress} = useFABMenuContext(); const canSendInvoice = canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx index 40e3e5a956d37..003b0ca3b17b9 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx @@ -28,8 +28,8 @@ function NewWorkspaceMenuItem() { const {shouldUseNarrowLayout} = useResponsiveLayout(); const icons = useMemoizedLazyExpensifyIcons(['NewWorkspace'] as const); const {isOffline} = useNetwork(); - const [isLoading = false] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); - const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); + const [isLoading = false] = useOnyx(ONYXKEYS.IS_LOADING_APP); + const [session] = useOnyx(ONYXKEYS.SESSION); const [allPolicies] = useMappedPolicies(policyMapper); const {isRestrictedPolicyCreation} = usePreferredPolicy(); const {setFocusedIndex, onItemPress} = useFABMenuContext(); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index 8583e91b32473..df9cf4c32562e 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -48,18 +48,18 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const {translate, formatPhoneNumber} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['CalendarSolid', 'ReceiptScan', 'Car', 'Task', 'Clock', 'MoneyCircle', 'Coins', 'Receipt', 'Cash', 'Transfer'] as const); - const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); - const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); - const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); + const [session] = useOnyx(ONYXKEYS.SESSION, {selector: sessionSelector}); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); + const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); const workspaceChatsSelector = (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports); // eslint-disable-next-line rulesdir/no-inline-useOnyx-selector - const [policyChats = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector, canBeMissing: true}); - const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, {canBeMissing: true}); - const [quickActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`, {canBeMissing: true}); - const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); - const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); - const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const [policyChats = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector}); + const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE); + const [quickActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); + const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE); + const [allBetas] = useOnyx(ONYXKEYS.BETAS); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {isDelegateAccessRestricted} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); @@ -67,7 +67,7 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { const {isRestrictedToPreferredPolicy} = usePreferredPolicy(); const {setFocusedIndex, onItemPress} = useFABMenuContext(); const quickActionPolicyID = quickAction?.action === CONST.QUICK_ACTIONS.TRACK_PER_DIEM && quickAction?.perDiemPolicyID ? quickAction?.perDiemPolicyID : quickActionReport?.policyID; - const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionPolicyID}`, {canBeMissing: true}); + const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionPolicyID}`); const isValidReport = !(isEmptyObject(quickActionReport) || isReportArchived); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx index ce6cb6ab94c80..93bc98d774b5b 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx @@ -21,9 +21,9 @@ function TestDriveMenuItem() { const styles = useThemeStyles(); const theme = useTheme(); const icons = useMemoizedLazyExpensifyIcons(['Binoculars'] as const); - const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector, canBeMissing: true}); - const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); - const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true}); + const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); + const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector}); const isUserPaidPolicyMember = useIsPaidPolicyAdmin(); const {setFocusedIndex, onItemPress} = useFABMenuContext(); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx index e649233dea399..54b8aab8de3fc 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx @@ -22,7 +22,7 @@ function TrackDistanceMenuItem({reportID}: TrackDistanceMenuItemProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const icons = useMemoizedLazyExpensifyIcons(['Location'] as const); - const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); + const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); const {setFocusedIndex, onItemPress} = useFABMenuContext(); const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx index ff2c0fff4fb46..3ae152bd1e2a6 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx @@ -28,11 +28,11 @@ type TravelMenuItemProps = { function TravelMenuItem({activePolicyID}: TravelMenuItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Suitcase', 'NewWindow'] as const); - const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); - const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS, {canBeMissing: true}); - const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: accountPrimaryLoginSelector, canBeMissing: true}); - const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); - const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); + const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS); + const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: accountPrimaryLoginSelector}); + const [session] = useOnyx(ONYXKEYS.SESSION); + const [allBetas] = useOnyx(ONYXKEYS.BETAS); const isBlockedFromSpotnanaTravel = Permissions.isBetaEnabled(CONST.BETAS.PREVENT_SPOTNANA_TRAVEL, allBetas); const primaryContactMethod = primaryLogin ?? session?.email ?? ''; const {setFocusedIndex, onItemPress} = useFABMenuContext(); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts b/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts index ec5c6217224c9..d20d74afa9b07 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts @@ -17,7 +17,7 @@ import {policyMapper} from './types'; function useRedirectToExpensifyClassic() { const {translate} = useLocalize(); const {showConfirmModal} = useConfirmModal(); - const [isTrackingGPS = false] = useOnyx(ONYXKEYS.GPS_DRAFT_DETAILS, {canBeMissing: true, selector: isTrackingSelector}); + const [isTrackingGPS = false] = useOnyx(ONYXKEYS.GPS_DRAFT_DETAILS, {selector: isTrackingSelector}); const [allPolicies] = useMappedPolicies(policyMapper); const shouldRedirectToExpensifyClassic = areAllGroupPoliciesExpenseChatDisabled((allPolicies as OnyxCollection) ?? {}); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts index 29a10c45ac225..9738f770f6997 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts @@ -18,12 +18,12 @@ import useRedirectToExpensifyClassic from './useRedirectToExpensifyClassic'; const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); function useScanActions() { - const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector}); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); - const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); - const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); + const [session] = useOnyx(ONYXKEYS.SESSION, {selector: sessionSelector}); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); + const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); const workspaceChatsSelector = (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports); - const [policyChats = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector, canBeMissing: true}); + const [policyChats = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector}); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); From 92f924732083a39fa31afe903f24b9685fd7cf7d Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 09:51:39 +0100 Subject: [PATCH 25/54] extend useFABMenuItem to expose setFocusedIndex and onItemPress, eliminating direct FABMenuContext usage from all menu items --- .../menuItems/CreateReportMenuItem.tsx | 4 +--- .../FABPopoverContent/menuItems/ExpenseMenuItem.tsx | 4 +--- .../FABPopoverContent/menuItems/InvoiceMenuItem.tsx | 4 +--- .../FABPopoverContent/menuItems/NewChatMenuItem.tsx | 4 +--- .../menuItems/NewWorkspaceMenuItem.tsx | 4 +--- .../menuItems/QuickActionMenuItem.tsx | 4 +--- .../menuItems/TestDriveMenuItem.tsx | 5 +---- .../menuItems/TrackDistanceMenuItem.tsx | 4 +--- .../FABPopoverContent/menuItems/TravelMenuItem.tsx | 4 +--- .../sidebar/FABPopoverContent/useFABMenuItem.ts | 12 +++++++----- 10 files changed, 16 insertions(+), 33 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index e00293ee461f2..85c7e07eed44e 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -18,7 +18,6 @@ import {getDefaultChatEnabledPolicy} from '@libs/PolicyUtils'; import {hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import isOnSearchMoneyRequestReportPage from '@navigation/helpers/isOnSearchMoneyRequestReportPage'; -import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import {clearLastSearchParams} from '@userActions/ReportNavigation'; @@ -49,7 +48,6 @@ function CreateReportMenuItem({activePolicyID}: CreateReportMenuItemProps) { const {isBetaEnabled} = usePermissions(); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); - const {setFocusedIndex, onItemPress} = useFABMenuContext(); const groupPaidPoliciesWithChatEnabled = useCallback( (policies: Parameters[0]) => groupPaidPoliciesWithExpenseChatEnabledSelector(policies, session?.email), [session?.email], @@ -59,7 +57,7 @@ function CreateReportMenuItem({activePolicyID}: CreateReportMenuItemProps) { const isVisible = shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; - const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID, isVisible); + const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID, isVisible); const defaultChatEnabledPolicy = getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array>, activePolicy); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx index 031f2d28428fe..f56b5b7fc1eef 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx @@ -7,7 +7,6 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {startMoneyRequest} from '@libs/actions/IOU'; import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; @@ -25,8 +24,7 @@ function ExpenseMenuItem({reportID}: ExpenseMenuItemProps) { const icons = useMemoizedLazyExpensifyIcons(['Coins', 'Receipt', 'Cash', 'Transfer', 'MoneyCircle'] as const); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); - const {setFocusedIndex, onItemPress} = useFABMenuContext(); - const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID); + const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID); return ( , session?.email); - const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID, canSendInvoice); + const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID, canSendInvoice); if (!canSendInvoice) { return null; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx index 7f01170b47316..afe7b52e056bc 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx @@ -5,7 +5,6 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {startNewChat} from '@libs/actions/Report'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; import CONST from '@src/CONST'; @@ -15,8 +14,7 @@ function NewChatMenuItem() { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const icons = useMemoizedLazyExpensifyIcons(['ChatBubble'] as const); - const {setFocusedIndex, onItemPress} = useFABMenuContext(); - const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID); + const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID); return ( { if (isRestrictedPolicyCreation) { return false; @@ -44,7 +42,7 @@ function NewWorkspaceMenuItem() { const isVisible = !isLoading && shouldShowNewWorkspaceButton; - const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID, isVisible); + const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID, isVisible); if (!isVisible) { return null; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index df9cf4c32562e..49af8cb3b66d1 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -25,7 +25,6 @@ import { isPolicyExpenseChat, } from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; -import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -65,7 +64,6 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const isReportArchived = useReportIsArchived(quickActionReport?.reportID); const {isRestrictedToPreferredPolicy} = usePreferredPolicy(); - const {setFocusedIndex, onItemPress} = useFABMenuContext(); const quickActionPolicyID = quickAction?.action === CONST.QUICK_ACTIONS.TRACK_PER_DIEM && quickAction?.perDiemPolicyID ? quickAction?.perDiemPolicyID : quickActionReport?.policyID; const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionPolicyID}`); @@ -80,7 +78,7 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { : false) || (!quickAction?.action && !isEmptyObject(policyChatForActivePolicy)); - const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID, isVisible); + const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID, isVisible); let quickActionAvatars: ReturnType = []; if (isValidReport) { diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx index 93bc98d774b5b..be68eb31713cf 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx @@ -9,7 +9,6 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {startTestDrive} from '@libs/actions/Tour'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -25,11 +24,9 @@ function TestDriveMenuItem() { const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector}); const isUserPaidPolicyMember = useIsPaidPolicyAdmin(); - const {setFocusedIndex, onItemPress} = useFABMenuContext(); - const isVisible = !hasSeenTour; - const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID, isVisible); + const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID, isVisible); if (!isVisible) { return null; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx index 54b8aab8de3fc..ff9ffcbb5f05f 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx @@ -6,7 +6,6 @@ import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {startDistanceRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; @@ -24,8 +23,7 @@ function TrackDistanceMenuItem({reportID}: TrackDistanceMenuItemProps) { const icons = useMemoizedLazyExpensifyIcons(['Location'] as const); const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); - const {setFocusedIndex, onItemPress} = useFABMenuContext(); - const {itemIndex, isFocused, wrapperStyle} = useFABMenuItem(ITEM_ID); + const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID); return ( { if (!!isBlockedFromSpotnanaTravel || !primaryContactMethod || Str.isSMSLogin(primaryContactMethod) || !isPaidGroupPolicy(activePolicy)) { diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useFABMenuItem.ts b/src/pages/inbox/sidebar/FABPopoverContent/useFABMenuItem.ts index f163b41e29504..f69474052d6fa 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useFABMenuItem.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useFABMenuItem.ts @@ -8,15 +8,18 @@ type FABMenuItemResult = { itemIndex: number; isFocused: boolean; wrapperStyle: ViewStyle; + setFocusedIndex: (index: number) => void; + onItemPress: (onSelected: () => void, options?: {shouldCallAfterModalHide?: boolean}) => void; }; /** * Handles registration of a FAB menu item for arrow-key focus management. * Pass `isVisible` for items that conditionally render — registration mirrors visibility. - * Returns itemIndex, isFocused, and the pre-computed wrapperStyle for FocusableMenuItem. + * Returns itemIndex, isFocused, wrapperStyle, setFocusedIndex, and onItemPress — everything + * a menu item needs to render and interact, with no direct context access required. */ function useFABMenuItem(itemId: string, isVisible = true): FABMenuItemResult { - const {registerItem, unregisterItem, registeredItems, focusedIndex} = useFABMenuContext(); + const {registerItem, unregisterItem, registeredItems, focusedIndex, setFocusedIndex, onItemPress} = useFABMenuContext(); const StyleUtils = useStyleUtils(); const theme = useTheme(); @@ -26,14 +29,13 @@ function useFABMenuItem(itemId: string, isVisible = true): FABMenuItemResult { } registerItem(itemId); return () => unregisterItem(itemId); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isVisible]); + }, [isVisible, itemId, registerItem, unregisterItem]); const itemIndex = registeredItems.indexOf(itemId); const isFocused = focusedIndex === itemIndex; const wrapperStyle = StyleUtils.getItemBackgroundColorStyle(false, isFocused, false, theme.activeComponentBG, theme.hoverComponentBG); - return {itemIndex, isFocused, wrapperStyle}; + return {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress}; } export default useFABMenuItem; From 3ff2e560f939f58dfc707303691f985fcb294311 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 10:08:37 +0100 Subject: [PATCH 26/54] extract FABFocusableMenuItem wrapper, eliminating 5 repeated props and useFABMenuItem hook calls from all menu items --- .../FABFocusableMenuItem.tsx | 35 +++++++ .../menuItems/CreateReportMenuItem.tsx | 76 ++++++--------- .../menuItems/ExpenseMenuItem.tsx | 31 +++--- .../menuItems/InvoiceMenuItem.tsx | 37 +++----- .../menuItems/NewChatMenuItem.tsx | 15 +-- .../menuItems/NewWorkspaceMenuItem.tsx | 25 ++--- .../menuItems/QuickActionMenuItem.tsx | 95 ++++++++----------- .../menuItems/TestDriveMenuItem.tsx | 20 +--- .../menuItems/TrackDistanceMenuItem.tsx | 31 +++--- .../menuItems/TravelMenuItem.tsx | 20 +--- 10 files changed, 159 insertions(+), 226 deletions(-) create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem.tsx diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem.tsx new file mode 100644 index 0000000000000..ff9125a9f9982 --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import type {MenuItemProps} from '@components/MenuItem'; +import CONST from '@src/CONST'; +import useFABMenuItem from './useFABMenuItem'; + +type FABFocusableMenuItemProps = Omit & { + itemId: string; + isVisible?: boolean; + onPress?: () => void; + shouldCallAfterModalHide?: boolean; +}; + +function FABFocusableMenuItem({itemId, isVisible = true, onPress, shouldCallAfterModalHide, ...props}: FABFocusableMenuItemProps) { + const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(itemId, isVisible); + + if (!isVisible) { + return null; + } + + return ( + setFocusedIndex(itemIndex)} + wrapperStyle={wrapperStyle} + shouldCheckActionAllowedOnPress={false} + role={CONST.ROLE.BUTTON} + onPress={onPress ? () => onItemPress(onPress, {shouldCallAfterModalHide}) : undefined} + /> + ); +} + +export default FABFocusableMenuItem; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index 85c7e07eed44e..6a91345aac8e6 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -1,7 +1,6 @@ import {groupPaidPoliciesWithExpenseChatEnabledSelector} from '@selectors/Policy'; import React, {useCallback} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import FocusableMenuItem from '@components/FocusableMenuItem'; import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useHasEmptyReportsForPolicy from '@hooks/useHasEmptyReportsForPolicy'; @@ -18,7 +17,7 @@ import {getDefaultChatEnabledPolicy} from '@libs/PolicyUtils'; import {hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import isOnSearchMoneyRequestReportPage from '@navigation/helpers/isOnSearchMoneyRequestReportPage'; -import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; +import FABFocusableMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import {clearLastSearchParams} from '@userActions/ReportNavigation'; import CONST from '@src/CONST'; @@ -57,8 +56,6 @@ function CreateReportMenuItem({activePolicyID}: CreateReportMenuItemProps) { const isVisible = shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; - const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID, isVisible); - const defaultChatEnabledPolicy = getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array>, activePolicy); const defaultChatEnabledPolicyID = defaultChatEnabledPolicy?.id; @@ -101,52 +98,41 @@ function CreateReportMenuItem({activePolicyID}: CreateReportMenuItemProps) { onConfirm: handleCreateWorkspaceReport, }); - if (!isVisible) { - return null; - } - return ( <> - setFocusedIndex(itemIndex)} - onPress={() => - onItemPress( - () => { - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - - const workspaceIDForReportCreation = defaultChatEnabledPolicyID; - - if (!workspaceIDForReportCreation || (shouldRestrictUserBillableActions(workspaceIDForReportCreation) && groupPoliciesWithChatEnabled.length > 1)) { - Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute()); - return; - } - - if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { - if (shouldShowEmptyReportConfirmation) { - openCreateReportConfirmation(); - } else { - handleCreateWorkspaceReport(false); - } - return; - } - - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(workspaceIDForReportCreation)); - }); - }, - {shouldCallAfterModalHide: shouldUseNarrowLayout}, - ) - } - shouldCheckActionAllowedOnPress={false} - role={CONST.ROLE.BUTTON} - wrapperStyle={wrapperStyle} + onPress={() => { + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + + const workspaceIDForReportCreation = defaultChatEnabledPolicyID; + + if (!workspaceIDForReportCreation || (shouldRestrictUserBillableActions(workspaceIDForReportCreation) && groupPoliciesWithChatEnabled.length > 1)) { + Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute()); + return; + } + + if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { + if (shouldShowEmptyReportConfirmation) { + openCreateReportConfirmation(); + } else { + handleCreateWorkspaceReport(false); + } + return; + } + + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(workspaceIDForReportCreation)); + }); + }} + shouldCallAfterModalHide={shouldUseNarrowLayout} /> {CreateReportConfirmationModal} diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx index f56b5b7fc1eef..7c31f7a5fc857 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/ExpenseMenuItem.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import FocusableMenuItem from '@components/FocusableMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -7,7 +6,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {startMoneyRequest} from '@libs/actions/IOU'; import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; +import FABFocusableMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -24,31 +23,23 @@ function ExpenseMenuItem({reportID}: ExpenseMenuItemProps) { const icons = useMemoizedLazyExpensifyIcons(['Coins', 'Receipt', 'Cash', 'Transfer', 'MoneyCircle'] as const); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); - const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID); return ( - setFocusedIndex(itemIndex)} onPress={() => - onItemPress( - () => - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); - }), - {shouldCallAfterModalHide: shouldRedirectToExpensifyClassic || shouldUseNarrowLayout}, - ) + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); + }) } - shouldCheckActionAllowedOnPress={false} - role={CONST.ROLE.BUTTON} - wrapperStyle={wrapperStyle} + shouldCallAfterModalHide={shouldRedirectToExpensifyClassic || shouldUseNarrowLayout} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx index 45dc9e2079c79..5813df716f610 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx @@ -1,6 +1,5 @@ import React from 'react'; import type {OnyxCollection} from 'react-native-onyx'; -import FocusableMenuItem from '@components/FocusableMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -8,7 +7,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {startMoneyRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {canSendInvoice as canSendInvoicePolicyUtils} from '@libs/PolicyUtils'; -import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; +import FABFocusableMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -29,35 +28,23 @@ function InvoiceMenuItem({reportID}: InvoiceMenuItemProps) { const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); const canSendInvoice = canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email); - const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID, canSendInvoice); - - if (!canSendInvoice) { - return null; - } - return ( - setFocusedIndex(itemIndex)} onPress={() => - onItemPress( - () => - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - startMoneyRequest(CONST.IOU.TYPE.INVOICE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); - }), - {shouldCallAfterModalHide: shouldRedirectToExpensifyClassic || shouldUseNarrowLayout}, - ) + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + startMoneyRequest(CONST.IOU.TYPE.INVOICE, reportID, undefined, undefined, undefined, allTransactionDrafts, true); + }) } - shouldCheckActionAllowedOnPress={false} - role={CONST.ROLE.BUTTON} - wrapperStyle={wrapperStyle} + shouldCallAfterModalHide={shouldRedirectToExpensifyClassic || shouldUseNarrowLayout} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx index afe7b52e056bc..720627e5c44e2 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewChatMenuItem.tsx @@ -1,11 +1,10 @@ import React from 'react'; -import FocusableMenuItem from '@components/FocusableMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {startNewChat} from '@libs/actions/Report'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; +import FABFocusableMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem'; import CONST from '@src/CONST'; const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.NEW_CHAT; @@ -14,19 +13,15 @@ function NewChatMenuItem() { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const icons = useMemoizedLazyExpensifyIcons(['ChatBubble'] as const); - const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID); return ( - setFocusedIndex(itemIndex)} - onPress={() => onItemPress(() => interceptAnonymousUser(startNewChat), {shouldCallAfterModalHide: shouldUseNarrowLayout})} - shouldCheckActionAllowedOnPress={false} - role={CONST.ROLE.BUTTON} - wrapperStyle={wrapperStyle} + onPress={() => interceptAnonymousUser(startNewChat)} + shouldCallAfterModalHide={shouldUseNarrowLayout} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx index 018abefd050ba..c21ffac09771c 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx @@ -1,7 +1,6 @@ import type {ImageContentFit} from 'expo-image'; import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import FocusableMenuItem from '@components/FocusableMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useMappedPolicies from '@hooks/useMappedPolicies'; @@ -12,8 +11,8 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {shouldShowPolicy} from '@libs/PolicyUtils'; +import FABFocusableMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem'; import {policyMapper} from '@pages/inbox/sidebar/FABPopoverContent/types'; -import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -42,14 +41,10 @@ function NewWorkspaceMenuItem() { const isVisible = !isLoading && shouldShowNewWorkspaceButton; - const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID, isVisible); - - if (!isVisible) { - return null; - } - return ( - setFocusedIndex(itemIndex)} - onPress={() => - onItemPress(() => interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(Navigation.getActiveRoute()))), { - shouldCallAfterModalHide: shouldUseNarrowLayout, - }) - } - shouldCheckActionAllowedOnPress={false} - role={CONST.ROLE.BUTTON} - wrapperStyle={wrapperStyle} + onPress={() => interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(Navigation.getActiveRoute())))} + shouldCallAfterModalHide={shouldUseNarrowLayout} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index 49af8cb3b66d1..374d2fe6af059 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -1,7 +1,6 @@ import React from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; -import FocusableMenuItem from '@components/FocusableMenuItem'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -25,7 +24,7 @@ import { isPolicyExpenseChat, } from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; -import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; +import FABFocusableMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -78,8 +77,6 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { : false) || (!quickAction?.action && !isEmptyObject(policyChatForActivePolicy)); - const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID, isVisible); - let quickActionAvatars: ReturnType = []; if (isValidReport) { const avatars = getIcons(quickActionReport, formatPhoneNumber, personalDetails, null, undefined, undefined, undefined, undefined, isReportArchived); @@ -113,10 +110,6 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { // eslint-disable-next-line @typescript-eslint/no-deprecated const quickActionSubtitle = !hideQABSubtitle ? (getReportName(quickActionReport, quickActionPolicy, undefined, personalDetails) ?? translate('quickAction.updateDestination')) : ''; - if (!isVisible) { - return null; - } - const quickActionReportPolicyID = quickActionReport?.policyID; const selectOption = (onSelected: () => void, shouldRestrictAction: boolean) => { if (shouldRestrictAction && quickActionReportPolicyID && shouldRestrictUserBillableActions(quickActionReportPolicyID)) { @@ -128,7 +121,9 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { if (quickAction?.action && quickActionReport) { return ( - setFocusedIndex(itemIndex)} - shouldCheckActionAllowedOnPress={false} - role={CONST.ROLE.BUTTON} - wrapperStyle={wrapperStyle} icon={getQuickActionIcon(icons, quickAction?.action)} title={quickActionTitle} rightIconAccountID={quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID} description={quickActionSubtitle} rightIconReportID={quickActionReport?.reportID} onPress={() => - onItemPress( - () => - interceptAnonymousUser(() => { - if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && isDelegateAccessRestricted) { - showDelegateNoAccessModal(); - return; - } - const targetAccountPersonalDetails = { - ...personalDetails?.[quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID], - accountID: quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID, - }; - - navigateToQuickAction({ - isValidReport, - quickAction, - selectOption, - lastDistanceExpenseType, - targetAccountPersonalDetails, - currentUserAccountID: currentUserPersonalDetails.accountID, - isFromFloatingActionButton: true, - }); - }), - {shouldCallAfterModalHide: shouldUseNarrowLayout}, - ) + interceptAnonymousUser(() => { + if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + const targetAccountPersonalDetails = { + ...personalDetails?.[quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID], + accountID: quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID, + }; + + navigateToQuickAction({ + isValidReport, + quickAction, + selectOption, + lastDistanceExpenseType, + targetAccountPersonalDetails, + currentUserAccountID: currentUserPersonalDetails.accountID, + isFromFloatingActionButton: true, + }); + }) } + shouldCallAfterModalHide={shouldUseNarrowLayout} /> ); } return ( - setFocusedIndex(itemIndex)} - shouldCheckActionAllowedOnPress={false} - role={CONST.ROLE.BUTTON} - wrapperStyle={wrapperStyle} icon={icons.ReceiptScan} title={translate('quickAction.scanReceipt')} // eslint-disable-next-line @typescript-eslint/no-deprecated description={getReportName(policyChatForActivePolicy)} rightIconReportID={policyChatForActivePolicy?.reportID} onPress={() => - onItemPress( - () => - interceptAnonymousUser(() => { - if (policyChatForActivePolicy?.policyID && shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatForActivePolicy.policyID)); - return; - } - - const quickActionReportID = policyChatForActivePolicy?.reportID || reportID; - startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, allTransactionDrafts, true); - }), - {shouldCallAfterModalHide: shouldUseNarrowLayout}, - ) + interceptAnonymousUser(() => { + if (policyChatForActivePolicy?.policyID && shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatForActivePolicy.policyID)); + return; + } + + const quickActionReportID = policyChatForActivePolicy?.reportID || reportID; + startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, allTransactionDrafts, true); + }) } + shouldCallAfterModalHide={shouldUseNarrowLayout} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx index be68eb31713cf..bfaa7514f9967 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TestDriveMenuItem.tsx @@ -1,6 +1,5 @@ import {hasSeenTourSelector, tryNewDotOnyxSelector} from '@selectors/Onboarding'; import React from 'react'; -import FocusableMenuItem from '@components/FocusableMenuItem'; import useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -9,7 +8,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {startTestDrive} from '@libs/actions/Tour'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; +import FABFocusableMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -26,25 +25,16 @@ function TestDriveMenuItem() { const isUserPaidPolicyMember = useIsPaidPolicyAdmin(); const isVisible = !hasSeenTour; - const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID, isVisible); - - if (!isVisible) { - return null; - } - return ( - setFocusedIndex(itemIndex)} - onPress={() => onItemPress(() => interceptAnonymousUser(() => startTestDrive(introSelected, tryNewDot?.hasBeenAddedToNudgeMigration ?? false, isUserPaidPolicyMember)))} - shouldCheckActionAllowedOnPress={false} - role={CONST.ROLE.BUTTON} - wrapperStyle={wrapperStyle} + onPress={() => interceptAnonymousUser(() => startTestDrive(introSelected, tryNewDot?.hasBeenAddedToNudgeMigration ?? false, isUserPaidPolicyMember))} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx index ff9ffcbb5f05f..6cb1b4c4ee0ca 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import FocusableMenuItem from '@components/FocusableMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {startDistanceRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; +import FABFocusableMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -23,31 +22,23 @@ function TrackDistanceMenuItem({reportID}: TrackDistanceMenuItemProps) { const icons = useMemoizedLazyExpensifyIcons(['Location'] as const); const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); - const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID); return ( - setFocusedIndex(itemIndex)} onPress={() => - onItemPress( - () => - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - startDistanceRequest(CONST.IOU.TYPE.CREATE, reportID, lastDistanceExpenseType, undefined, undefined, true); - }), - {shouldCallAfterModalHide: shouldUseNarrowLayout}, - ) + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + startDistanceRequest(CONST.IOU.TYPE.CREATE, reportID, lastDistanceExpenseType, undefined, undefined, true); + }) } - shouldCheckActionAllowedOnPress={false} - role={CONST.ROLE.BUTTON} - wrapperStyle={wrapperStyle} + shouldCallAfterModalHide={shouldUseNarrowLayout} /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx index aca54ed06b9f4..9730ee22cc28a 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx @@ -1,7 +1,6 @@ import {Str} from 'expensify-common'; import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import FocusableMenuItem from '@components/FocusableMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -10,7 +9,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {openTravelDotLink, shouldOpenTravelDotLinkWeb} from '@libs/openTravelDotLink'; import Permissions from '@libs/Permissions'; import {isPaidGroupPolicy} from '@libs/PolicyUtils'; -import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; +import FABFocusableMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -36,8 +35,6 @@ function TravelMenuItem({activePolicyID}: TravelMenuItemProps) { const primaryContactMethod = primaryLogin ?? session?.email ?? ''; const isVisible = !!activePolicy?.isTravelEnabled; - const {itemIndex, isFocused, wrapperStyle, setFocusedIndex, onItemPress} = useFABMenuItem(ITEM_ID, isVisible); - const isTravelEnabled = (() => { if (!!isBlockedFromSpotnanaTravel || !primaryContactMethod || Str.isSMSLogin(primaryContactMethod) || !isPaidGroupPolicy(activePolicy)) { return false; @@ -54,23 +51,16 @@ function TravelMenuItem({activePolicyID}: TravelMenuItemProps) { Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS.getRoute(activePolicy?.id)); }; - if (!isVisible) { - return null; - } - return ( - setFocusedIndex(itemIndex)} - onPress={() => onItemPress(() => interceptAnonymousUser(() => openTravel()))} - shouldCheckActionAllowedOnPress={false} - role={CONST.ROLE.BUTTON} - wrapperStyle={wrapperStyle} + onPress={() => interceptAnonymousUser(() => openTravel())} /> ); } From c98df63f4ce4353f77aa161f366e865dbb2b66a5 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 10:21:23 +0100 Subject: [PATCH 27/54] remove isMenuMounted lazy-mount guard and onModalHide chain --- .../inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx | 7 +------ .../sidebar/FABPopoverContent/FABPopoverContentInner.tsx | 7 +++---- .../inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx | 4 +--- src/pages/inbox/sidebar/FABPopoverContent/types.ts | 6 +----- .../inbox/sidebar/FloatingActionButtonAndPopover.tsx | 8 -------- 5 files changed, 6 insertions(+), 26 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx index 462d7381a6aca..6f9b910c7301d 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx @@ -7,17 +7,12 @@ type FABPopoverContentExtraProps = FABPopoverContentProps & { activePolicyID: string | undefined; }; -function FABPopoverContent({isMenuMounted, isVisible, onClose, onItemSelected, onModalHide, anchorRef, reportID, activePolicyID}: FABPopoverContentExtraProps) { - if (!isMenuMounted) { - return null; - } - +function FABPopoverContent({isVisible, onClose, onItemSelected, anchorRef, reportID, activePolicyID}: FABPopoverContentExtraProps) { return ( void; onItemSelected: () => void; - onModalHide: () => void; anchorRef: RefObject; animationInTiming?: number; animationOutTiming?: number; children: React.ReactNode; }; -function FABPopoverMenu({isVisible, onClose, onItemSelected, onModalHide, anchorRef, animationInTiming, animationOutTiming, children}: FABPopoverMenuProps) { +function FABPopoverMenu({isVisible, onClose, onItemSelected, anchorRef, animationInTiming, animationOutTiming, children}: FABPopoverMenuProps) { const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {windowHeight} = useWindowDimensions(); @@ -98,7 +97,6 @@ function FABPopoverMenu({isVisible, onClose, onItemSelected, onModalHide, anchor }} onClose={onClose} isVisible={isVisible} - onModalHide={onModalHide} fromSidebarMediumScreen={!shouldUseNarrowLayout} animationIn="fadeIn" animationOut="fadeOut" diff --git a/src/pages/inbox/sidebar/FABPopoverContent/types.ts b/src/pages/inbox/sidebar/FABPopoverContent/types.ts index 12fe10848e01a..60ed7b303a435 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/types.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/types.ts @@ -17,15 +17,11 @@ const policyMapper = (policy: OnyxEntry): PolicySelector => }) as PolicySelector; type FABPopoverContentProps = { - isMenuMounted: boolean; isVisible: boolean; onClose: () => void; onItemSelected: () => void; - onModalHide: () => void; anchorRef: RefObject; }; -type FABPopoverContentInnerProps = Omit; - -export type {PolicySelector, FABPopoverContentProps, FABPopoverContentInnerProps}; +export type {PolicySelector, FABPopoverContentProps}; export {policyMapper}; diff --git a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx index 58a81c323dab5..12140977bc101 100644 --- a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx @@ -24,7 +24,6 @@ function FloatingActionButtonAndPopover() { const prevIsFocused = usePrevious(isFocused); const [isCreateMenuActive, setIsCreateMenuActive] = useState(false); - const [isMenuMounted, setIsMenuMounted] = useState(false); const fabRef = useRef(null); const {startScan, startQuickScan, reportID, activePolicyID} = useScanActions(); @@ -33,7 +32,6 @@ function FloatingActionButtonAndPopover() { if (!isFocused && shouldUseNarrowLayout) { return; } - setIsMenuMounted(true); setIsCreateMenuActive(true); }, [isFocused, shouldUseNarrowLayout]); @@ -44,10 +42,6 @@ function FloatingActionButtonAndPopover() { setIsCreateMenuActive(false); }, [isCreateMenuActive]); - const handleMenuModalHide = useCallback(() => { - setIsMenuMounted(false); - }, []); - const didScreenBecomeInactive = useCallback((): boolean => !isFocused && prevIsFocused, [isFocused, prevIsFocused]); useEffect(() => { @@ -75,11 +69,9 @@ function FloatingActionButtonAndPopover() { return ( Date: Tue, 24 Feb 2026 10:22:56 +0100 Subject: [PATCH 28/54] collapse FABPopoverContent and FABPopoverContentInner pass-through layers, render FABPopoverMenu directly --- .../FABPopoverContent/FABPopoverContent.tsx | 25 ----------- .../FABPopoverContentInner.tsx | 45 ------------------- .../inbox/sidebar/FABPopoverContent/index.ts | 1 - .../inbox/sidebar/FABPopoverContent/types.ts | 10 +---- .../FloatingActionButtonAndPopover.tsx | 29 +++++++++--- 5 files changed, 25 insertions(+), 85 deletions(-) delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/index.ts diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx deleted file mode 100644 index 6f9b910c7301d..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContent.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import FABPopoverContentInner from './FABPopoverContentInner'; -import type {FABPopoverContentProps} from './types'; - -type FABPopoverContentExtraProps = FABPopoverContentProps & { - reportID: string; - activePolicyID: string | undefined; -}; - -function FABPopoverContent({isVisible, onClose, onItemSelected, anchorRef, reportID, activePolicyID}: FABPopoverContentExtraProps) { - return ( - - ); -} - -FABPopoverContent.displayName = 'FABPopoverContent'; - -export default FABPopoverContent; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx deleted file mode 100644 index 676b2f5144d95..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverContentInner.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import CONST from '@src/CONST'; -import FABPopoverMenu from './FABPopoverMenu'; -import CreateReportMenuItem from './menuItems/CreateReportMenuItem'; -import ExpenseMenuItem from './menuItems/ExpenseMenuItem'; -import InvoiceMenuItem from './menuItems/InvoiceMenuItem'; -import NewChatMenuItem from './menuItems/NewChatMenuItem'; -import NewWorkspaceMenuItem from './menuItems/NewWorkspaceMenuItem'; -import QuickActionMenuItem from './menuItems/QuickActionMenuItem'; -import TestDriveMenuItem from './menuItems/TestDriveMenuItem'; -import TrackDistanceMenuItem from './menuItems/TrackDistanceMenuItem'; -import TravelMenuItem from './menuItems/TravelMenuItem'; -import type {FABPopoverContentProps} from './types'; - -type FABPopoverContentInnerProps = FABPopoverContentProps & { - reportID: string; - activePolicyID: string | undefined; -}; - -function FABPopoverContentInner({isVisible, onClose, onItemSelected, anchorRef, reportID, activePolicyID}: FABPopoverContentInnerProps) { - return ( - - - - - - - - - - - - ); -} - -FABPopoverContentInner.displayName = 'FABPopoverContentInner'; - -export default FABPopoverContentInner; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/index.ts b/src/pages/inbox/sidebar/FABPopoverContent/index.ts deleted file mode 100644 index b11f5764e3e09..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default} from './FABPopoverContent'; // eslint-disable-line no-restricted-exports diff --git a/src/pages/inbox/sidebar/FABPopoverContent/types.ts b/src/pages/inbox/sidebar/FABPopoverContent/types.ts index 60ed7b303a435..83ec6b9c1de88 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/types.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/types.ts @@ -1,4 +1,3 @@ -import type {RefObject} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import type * as OnyxTypes from '@src/types/onyx'; @@ -16,12 +15,5 @@ const policyMapper = (policy: OnyxEntry): PolicySelector => areInvoicesEnabled: policy.areInvoicesEnabled, }) as PolicySelector; -type FABPopoverContentProps = { - isVisible: boolean; - onClose: () => void; - onItemSelected: () => void; - anchorRef: RefObject; -}; - -export type {PolicySelector, FABPopoverContentProps}; +export type {PolicySelector}; export {policyMapper}; diff --git a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx index 12140977bc101..49b09601019b8 100644 --- a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx @@ -9,7 +9,16 @@ import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; -import FABPopoverContent from './FABPopoverContent'; +import FABPopoverMenu from './FABPopoverContent/FABPopoverMenu'; +import CreateReportMenuItem from './FABPopoverContent/menuItems/CreateReportMenuItem'; +import ExpenseMenuItem from './FABPopoverContent/menuItems/ExpenseMenuItem'; +import InvoiceMenuItem from './FABPopoverContent/menuItems/InvoiceMenuItem'; +import NewChatMenuItem from './FABPopoverContent/menuItems/NewChatMenuItem'; +import NewWorkspaceMenuItem from './FABPopoverContent/menuItems/NewWorkspaceMenuItem'; +import QuickActionMenuItem from './FABPopoverContent/menuItems/QuickActionMenuItem'; +import TestDriveMenuItem from './FABPopoverContent/menuItems/TestDriveMenuItem'; +import TrackDistanceMenuItem from './FABPopoverContent/menuItems/TrackDistanceMenuItem'; +import TravelMenuItem from './FABPopoverContent/menuItems/TravelMenuItem'; import useScanActions from './FABPopoverContent/useScanActions'; /** @@ -68,14 +77,24 @@ function FloatingActionButtonAndPopover() { return ( - + animationInTiming={CONST.MODAL.ANIMATION_TIMING.FAB_IN} + animationOutTiming={CONST.MODAL.ANIMATION_TIMING.FAB_OUT} + > + + + + + + + + + + {!shouldUseNarrowLayout && ( Date: Tue, 24 Feb 2026 10:48:12 +0100 Subject: [PATCH 29/54] cleanup useScanActions --- .../FABPopoverContent/menuItems/CreateReportMenuItem.tsx | 7 ++----- .../FABPopoverContent/menuItems/TravelMenuItem.tsx | 7 ++----- .../inbox/sidebar/FABPopoverContent/useScanActions.ts | 2 +- .../inbox/sidebar/FloatingActionButtonAndPopover.tsx | 8 +++++--- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index 6a91345aac8e6..099eabb143e7f 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -29,11 +29,8 @@ const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.CREATE_REPORT; const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); -type CreateReportMenuItemProps = { - activePolicyID: string | undefined; -}; - -function CreateReportMenuItem({activePolicyID}: CreateReportMenuItemProps) { +function CreateReportMenuItem() { + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const icons = useMemoizedLazyExpensifyIcons(['Document'] as const); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx index 9730ee22cc28a..409a90dfe74c9 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx @@ -19,11 +19,8 @@ const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.TRAVEL; const accountPrimaryLoginSelector = (account: OnyxEntry) => account?.primaryLogin; -type TravelMenuItemProps = { - activePolicyID: string | undefined; -}; - -function TravelMenuItem({activePolicyID}: TravelMenuItemProps) { +function TravelMenuItem() { + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Suitcase', 'NewWindow'] as const); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts index 9738f770f6997..1926068659ae3 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts @@ -59,7 +59,7 @@ function useScanActions() { }); }; - return {startScan, startQuickScan, reportID, activePolicyID}; + return {startScan, startQuickScan}; } export default useScanActions; diff --git a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx index 49b09601019b8..272a306bec8b1 100644 --- a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx @@ -8,6 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import {generateReportID} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import FABPopoverMenu from './FABPopoverContent/FABPopoverMenu'; import CreateReportMenuItem from './FABPopoverContent/menuItems/CreateReportMenuItem'; @@ -35,7 +36,8 @@ function FloatingActionButtonAndPopover() { const [isCreateMenuActive, setIsCreateMenuActive] = useState(false); const fabRef = useRef(null); - const {startScan, startQuickScan, reportID, activePolicyID} = useScanActions(); + const {startScan, startQuickScan} = useScanActions(); + const [reportID] = useState(() => generateReportID()); const showCreateMenu = useCallback(() => { if (!isFocused && shouldUseNarrowLayout) { @@ -88,10 +90,10 @@ function FloatingActionButtonAndPopover() { - + - + From 83a45c6374fda2fbf8ad043d4baea5bb7408b186 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 10:51:17 +0100 Subject: [PATCH 30/54] remove allPolicies leak from useRedirectToExpensifyClassic --- .../sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx | 5 ++++- .../FABPopoverContent/useRedirectToExpensifyClassic.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx index 5813df716f610..e8757cce7018d 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx @@ -2,12 +2,14 @@ import React from 'react'; import type {OnyxCollection} from 'react-native-onyx'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useMappedPolicies from '@hooks/useMappedPolicies'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {startMoneyRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {canSendInvoice as canSendInvoicePolicyUtils} from '@libs/PolicyUtils'; import FABFocusableMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem'; +import {policyMapper} from '@pages/inbox/sidebar/FABPopoverContent/types'; import useRedirectToExpensifyClassic from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -23,7 +25,8 @@ function InvoiceMenuItem({reportID}: InvoiceMenuItemProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const icons = useMemoizedLazyExpensifyIcons(['InvoiceGeneric'] as const); - const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal, allPolicies} = useRedirectToExpensifyClassic(); + const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); + const [allPolicies] = useMappedPolicies(policyMapper); const [session] = useOnyx(ONYXKEYS.SESSION); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); const canSendInvoice = canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts b/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts index d20d74afa9b07..c09774cc17311 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts @@ -39,7 +39,7 @@ function useRedirectToExpensifyClassic() { openOldDotLink(CONST.OLDDOT_URLS.INBOX); }; - return {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal, allPolicies}; + return {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal}; } export default useRedirectToExpensifyClassic; From dd240d721db79ca625d03036b8170a35c70830d0 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 13:12:21 +0100 Subject: [PATCH 31/54] simplify closing the menu on losing focus --- .../FloatingActionButtonAndPopover.tsx | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx index 272a306bec8b1..48b044f7213fc 100644 --- a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx @@ -1,11 +1,10 @@ -import {useIsFocused} from '@react-navigation/native'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import {useFocusEffect, useIsFocused} from '@react-navigation/native'; +import React, {useCallback, useRef, useState} from 'react'; import {View} from 'react-native'; import FloatingActionButton from '@components/FloatingActionButton'; import FloatingReceiptButton from '@components/FloatingReceiptButton'; import useDragoverDismiss from '@hooks/useDragoverDismiss'; import useLocalize from '@hooks/useLocalize'; -import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {generateReportID} from '@libs/ReportUtils'; @@ -31,7 +30,6 @@ function FloatingActionButtonAndPopover() { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const isFocused = useIsFocused(); - const prevIsFocused = usePrevious(isFocused); const [isCreateMenuActive, setIsCreateMenuActive] = useState(false); const fabRef = useRef(null); @@ -53,18 +51,12 @@ function FloatingActionButtonAndPopover() { setIsCreateMenuActive(false); }, [isCreateMenuActive]); - const didScreenBecomeInactive = useCallback((): boolean => !isFocused && prevIsFocused, [isFocused, prevIsFocused]); - - useEffect(() => { - if (!didScreenBecomeInactive()) { - return; - } - - // Intentionally calling setState inside useEffect — we need to imperatively respond to - // navigation focus changes (an external event) which can't be expressed as an event handler - // eslint-disable-next-line react-hooks/set-state-in-effect - hideCreateMenu(); - }, [didScreenBecomeInactive, hideCreateMenu]); + // Close the menu when the screen loses focus (e.g. navigating away) + useFocusEffect( + useCallback(() => { + return () => hideCreateMenu(); + }, [hideCreateMenu]), + ); // Close menu on dragover — prevents popover from staying open during file drag useDragoverDismiss(isCreateMenuActive, hideCreateMenu); From b3cc3b1e813b3e5ffd37cdc78459846c04b78ea8 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 13:16:19 +0100 Subject: [PATCH 32/54] improve QuickActionMenuItem --- src/libs/ReportUtils.ts | 2 +- .../FABPopoverContent/menuItems/QuickActionMenuItem.tsx | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0de973a4bc60d..886d40673349d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -10101,7 +10101,7 @@ function canCreateRequest( function getWorkspaceChats(policyID: string | undefined, accountIDs: number[], reports: OnyxCollection = allReports): Array> { return Object.values(reports ?? {}).filter( - (report) => isPolicyExpenseChat(report) && !!policyID && report?.policyID === policyID && report?.ownerAccountID && accountIDs.includes(report?.ownerAccountID), + (report) => !!policyID && report?.policyID === policyID && report?.ownerAccountID && accountIDs.includes(report?.ownerAccountID) &&isPolicyExpenseChat(report), ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index 374d2fe6af059..c6b85a087995c 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -50,8 +50,7 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); - const workspaceChatsSelector = (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports); - // eslint-disable-next-line rulesdir/no-inline-useOnyx-selector + const workspaceChatsSelector = useCallback((reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports), [activePolicyID, session?.accountID] ); const [policyChats = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector}); const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE); const [quickActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`); From ec55ac9fa6754cf16c63a2b43843afe6e82c3dcd Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 13:31:02 +0100 Subject: [PATCH 33/54] reuse redirect to expensify classic --- .../FABPopoverContent/FABMenuContext.tsx | 4 ++++ .../FABPopoverContent/FABPopoverMenu.tsx | 17 ++++++++++++++++- .../menuItems/CreateReportMenuItem.tsx | 4 ++-- .../menuItems/ExpenseMenuItem.tsx | 4 ++-- .../menuItems/InvoiceMenuItem.tsx | 4 ++-- .../menuItems/TrackDistanceMenuItem.tsx | 4 ++-- 6 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuContext.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuContext.tsx index 320d9ce06c54e..7deca27d3ae6f 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuContext.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuContext.tsx @@ -8,6 +8,8 @@ type FABMenuContextType = { registeredItems: readonly string[]; registerItem: (id: string) => void; unregisterItem: (id: string) => void; + shouldRedirectToExpensifyClassic: boolean; + showRedirectToExpensifyClassicModal: () => Promise; }; const FABMenuContext = createContext({ @@ -18,6 +20,8 @@ const FABMenuContext = createContext({ registeredItems: [], registerItem: () => {}, unregisterItem: () => {}, + shouldRedirectToExpensifyClassic: false, + showRedirectToExpensifyClassicModal: () => Promise.resolve(), }); function useFABMenuContext() { diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx index 11ce5c75c7eef..15ef70e854e7d 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx @@ -12,6 +12,7 @@ import {isSafari} from '@libs/Browser'; import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction'; import CONST from '@src/CONST'; import {FABMenuContext} from './FABMenuContext'; +import useRedirectToExpensifyClassic from './useRedirectToExpensifyClassic'; const FAB_ITEM_ORDER = [ CONST.FAB_MENU_ITEM_IDS.QUICK_ACTION, @@ -68,6 +69,8 @@ function FABPopoverMenu({isVisible, onClose, onItemSelected, anchorRef, animatio }); }; + const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ initialFocusedIndex: -1, maxIndex: itemCount - 1, @@ -87,7 +90,19 @@ function FABPopoverMenu({isVisible, onClose, onItemSelected, anchorRef, animatio }; return ( - + Date: Tue, 24 Feb 2026 14:33:15 +0100 Subject: [PATCH 34/54] address PR comments --- .../FABFocusableMenuItem.tsx | 1 + .../FABPopoverContent/FABPopoverMenu.tsx | 2 +- .../menuItems/CreateReportMenuItem.tsx | 36 ++++++++++++++----- .../menuItems/InvoiceMenuItem.tsx | 2 +- .../menuItems/NewWorkspaceMenuItem.tsx | 2 +- .../menuItems/QuickActionMenuItem.tsx | 31 +++++++--------- .../inbox/sidebar/FABPopoverContent/types.ts | 19 ---------- .../useRedirectToExpensifyClassic.ts | 19 ++++++++-- .../FABPopoverContent/useScanActions.ts | 7 ++-- src/selectors/Session.ts | 4 ++- 10 files changed, 68 insertions(+), 55 deletions(-) delete mode 100644 src/pages/inbox/sidebar/FABPopoverContent/types.ts diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem.tsx index ff9125a9f9982..1e5515bc43146 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem.tsx @@ -20,6 +20,7 @@ function FABFocusableMenuItem({itemId, isVisible = true, onPress, shouldCallAfte return ( ) => ({email: session?.email, accountID: session?.accountID}); +// Returns up to 2 matching policies — we only ever check length > 0, length === 1, and length > 1. +const chatEnabledPaidGroupPoliciesSelector = (policies: OnyxCollection, currentUserLogin: string | undefined) => { + if (isEmptyObject(policies)) { + return CONST.EMPTY_ARRAY; + } + const result: OnyxTypes.Policy[] = []; + for (const policy of Object.values(policies)) { + if (!policy?.isPolicyExpenseChatEnabled || policy?.isJoinRequestPending || !isPaidGroupPolicy(policy) || !shouldShowPolicy(policy, false, currentUserLogin)) { + continue; + } + + result.push(policy); + + if (result.length === 2) { + break; + } + } + + return result; +}; function CreateReportMenuItem() { const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); @@ -36,7 +56,7 @@ function CreateReportMenuItem() { const icons = useMemoizedLazyExpensifyIcons(['Document'] as const); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useFABMenuContext(); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); - const [session] = useOnyx(ONYXKEYS.SESSION, {selector: sessionSelector}); + const [session] = useOnyx(ONYXKEYS.SESSION, {selector: sessionEmailAndAccountIDSelector}); const [allBetas] = useOnyx(ONYXKEYS.BETAS); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [hasDismissedEmptyReportsConfirmation] = useOnyx(ONYXKEYS.NVP_EMPTY_REPORTS_CONFIRMATION_DISMISSED); @@ -44,12 +64,12 @@ function CreateReportMenuItem() { const {isBetaEnabled} = usePermissions(); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); - const groupPaidPoliciesWithChatEnabled = useCallback( - (policies: Parameters[0]) => groupPaidPoliciesWithExpenseChatEnabledSelector(policies, session?.email), + const chatEnabledPaidGroupPolicies = useCallback( + (policies: Parameters[0]) => chatEnabledPaidGroupPoliciesSelector(policies, session?.email), [session?.email], ); - const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPaidPoliciesWithChatEnabled}, [session?.email]); + const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: chatEnabledPaidGroupPolicies}, [session?.email]); const isVisible = shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx index dec052c92aff8..6697b740376c9 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx @@ -10,7 +10,7 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {canSendInvoice as canSendInvoicePolicyUtils} from '@libs/PolicyUtils'; import FABFocusableMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem'; import {useFABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; -import {policyMapper} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import {policyMapper} from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx index c21ffac09771c..3bbe1131096f8 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx @@ -12,7 +12,7 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {shouldShowPolicy} from '@libs/PolicyUtils'; import FABFocusableMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem'; -import {policyMapper} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import {policyMapper} from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index c6b85a087995c..65e5a420c241b 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, {useCallback} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -14,20 +14,14 @@ import {navigateToQuickAction} from '@libs/actions/QuickActionNavigation'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {getQuickActionIcon, getQuickActionTitle, isQuickActionAllowed} from '@libs/QuickActionUtils'; -import { - getDisplayNameForParticipant, - getIcons, - // Will be fixed in https://github.com/Expensify/App/issues/76852 - // eslint-disable-next-line @typescript-eslint/no-deprecated - getReportName, - getWorkspaceChats, - isPolicyExpenseChat, -} from '@libs/ReportUtils'; +import {getReportName} from '@libs/ReportNameUtils'; +import {getDisplayNameForParticipant, getIcons, getWorkspaceChats, isPolicyExpenseChat} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import FABFocusableMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {sessionEmailAndAccountIDSelector} from '@src/selectors/Session'; import type * as OnyxTypes from '@src/types/onyx'; import type {QuickActionName} from '@src/types/onyx/QuickAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -35,8 +29,6 @@ import getEmptyArray from '@src/types/utils/getEmptyArray'; const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.QUICK_ACTION; -const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); - type QuickActionMenuItemProps = { reportID: string; }; @@ -46,11 +38,14 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const {translate, formatPhoneNumber} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['CalendarSolid', 'ReceiptScan', 'Car', 'Task', 'Clock', 'MoneyCircle', 'Coins', 'Receipt', 'Cash', 'Transfer'] as const); - const [session] = useOnyx(ONYXKEYS.SESSION, {selector: sessionSelector}); + const [session] = useOnyx(ONYXKEYS.SESSION, {selector: sessionEmailAndAccountIDSelector}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); - const workspaceChatsSelector = useCallback((reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports), [activePolicyID, session?.accountID] ); + const workspaceChatsSelector = useCallback( + (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports), + [activePolicyID, session?.accountID], + ); const [policyChats = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector}); const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE); const [quickActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`); @@ -64,6 +59,8 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { const {isRestrictedToPreferredPolicy} = usePreferredPolicy(); const quickActionPolicyID = quickAction?.action === CONST.QUICK_ACTIONS.TRACK_PER_DIEM && quickAction?.perDiemPolicyID ? quickAction?.perDiemPolicyID : quickActionReport?.policyID; const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionPolicyID}`); + const reportAttributesSelector = useCallback((attributes: OnyxEntry) => attributes?.reports, []); + const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: reportAttributesSelector}); const isValidReport = !(isEmptyObject(quickActionReport) || isReportArchived); @@ -106,8 +103,7 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { } } - // eslint-disable-next-line @typescript-eslint/no-deprecated - const quickActionSubtitle = !hideQABSubtitle ? (getReportName(quickActionReport, quickActionPolicy, undefined, personalDetails) ?? translate('quickAction.updateDestination')) : ''; + const quickActionSubtitle = !hideQABSubtitle ? getReportName(quickActionReport, reportAttributes) || translate('quickAction.updateDestination') : ''; const quickActionReportPolicyID = quickActionReport?.policyID; const selectOption = (onSelected: () => void, shouldRestrictAction: boolean) => { @@ -181,8 +177,7 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { shouldTeleportPortalToModalLayer icon={icons.ReceiptScan} title={translate('quickAction.scanReceipt')} - // eslint-disable-next-line @typescript-eslint/no-deprecated - description={getReportName(policyChatForActivePolicy)} + description={getReportName(policyChatForActivePolicy, reportAttributes)} rightIconReportID={policyChatForActivePolicy?.reportID} onPress={() => interceptAnonymousUser(() => { diff --git a/src/pages/inbox/sidebar/FABPopoverContent/types.ts b/src/pages/inbox/sidebar/FABPopoverContent/types.ts deleted file mode 100644 index 83ec6b9c1de88..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import type * as OnyxTypes from '@src/types/onyx'; - -type PolicySelector = Pick; - -const policyMapper = (policy: OnyxEntry): PolicySelector => - (policy && { - type: policy.type, - role: policy.role, - id: policy.id, - isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, - pendingAction: policy.pendingAction, - avatarURL: policy.avatarURL, - name: policy.name, - areInvoicesEnabled: policy.areInvoicesEnabled, - }) as PolicySelector; - -export type {PolicySelector}; -export {policyMapper}; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts b/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts index c09774cc17311..0436ed0785ea5 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts @@ -1,4 +1,4 @@ -import type {OnyxCollection} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import useConfirmModal from '@hooks/useConfirmModal'; import useLocalize from '@hooks/useLocalize'; @@ -12,7 +12,20 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {isTrackingSelector} from '@src/selectors/GPSDraftDetails'; import type * as OnyxTypes from '@src/types/onyx'; -import {policyMapper} from './types'; + +type PolicySelector = Pick; + +const policyMapper = (policy: OnyxEntry): PolicySelector => + (policy && { + type: policy.type, + role: policy.role, + id: policy.id, + isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, + pendingAction: policy.pendingAction, + avatarURL: policy.avatarURL, + name: policy.name, + areInvoicesEnabled: policy.areInvoicesEnabled, + }) as PolicySelector; function useRedirectToExpensifyClassic() { const {translate} = useLocalize(); @@ -42,4 +55,6 @@ function useRedirectToExpensifyClassic() { return {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal}; } +export type {PolicySelector}; +export {policyMapper}; export default useRedirectToExpensifyClassic; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts index 1926068659ae3..25355a7bd5761 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts @@ -1,5 +1,5 @@ import {useState} from 'react'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; import useOnyx from '@hooks/useOnyx'; import {startMoneyRequest} from '@libs/actions/IOU'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; @@ -10,15 +10,14 @@ import Tab from '@userActions/Tab'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {sessionEmailAndAccountIDSelector} from '@src/selectors/Session'; import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import getEmptyArray from '@src/types/utils/getEmptyArray'; import useRedirectToExpensifyClassic from './useRedirectToExpensifyClassic'; -const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); - function useScanActions() { - const [session] = useOnyx(ONYXKEYS.SESSION, {selector: sessionSelector}); + const [session] = useOnyx(ONYXKEYS.SESSION, {selector: sessionEmailAndAccountIDSelector}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); diff --git a/src/selectors/Session.ts b/src/selectors/Session.ts index ec457a7fd5b69..161c3e0c4c331 100644 --- a/src/selectors/Session.ts +++ b/src/selectors/Session.ts @@ -5,4 +5,6 @@ const emailSelector = (session: OnyxEntry) => session?.email; const accountIDSelector = (session: OnyxEntry) => session?.accountID; -export {emailSelector, accountIDSelector}; +const sessionEmailAndAccountIDSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); + +export {emailSelector, accountIDSelector, sessionEmailAndAccountIDSelector}; From f28760b3da965121d72078bd541e3b9f333e2316 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 14:47:46 +0100 Subject: [PATCH 35/54] prettier --- src/libs/ReportUtils.ts | 2 +- .../FABPopoverContent/menuItems/CreateReportMenuItem.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 886d40673349d..c97005856a879 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -10101,7 +10101,7 @@ function canCreateRequest( function getWorkspaceChats(policyID: string | undefined, accountIDs: number[], reports: OnyxCollection = allReports): Array> { return Object.values(reports ?? {}).filter( - (report) => !!policyID && report?.policyID === policyID && report?.ownerAccountID && accountIDs.includes(report?.ownerAccountID) &&isPolicyExpenseChat(report), + (report) => !!policyID && report?.policyID === policyID && report?.ownerAccountID && accountIDs.includes(report?.ownerAccountID) && isPolicyExpenseChat(report), ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index 914480ce3910d..80b5b8f539943 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -45,7 +45,7 @@ const chatEnabledPaidGroupPoliciesSelector = (policies: OnyxCollection Date: Tue, 24 Feb 2026 15:25:53 +0100 Subject: [PATCH 36/54] rewrite useRedirectToExpensifyClassic --- .../inbox/sidebar/FABPopoverContent/FABMenuContext.tsx | 4 ---- .../inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx | 7 +------ .../FABPopoverContent/menuItems/CreateReportMenuItem.tsx | 4 ++-- .../FABPopoverContent/menuItems/ExpenseMenuItem.tsx | 4 ++-- .../FABPopoverContent/menuItems/InvoiceMenuItem.tsx | 5 ++--- .../FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx | 4 ++-- .../FABPopoverContent/useRedirectToExpensifyClassic.ts | 7 +++---- 7 files changed, 12 insertions(+), 23 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuContext.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuContext.tsx index 7deca27d3ae6f..320d9ce06c54e 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABMenuContext.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABMenuContext.tsx @@ -8,8 +8,6 @@ type FABMenuContextType = { registeredItems: readonly string[]; registerItem: (id: string) => void; unregisterItem: (id: string) => void; - shouldRedirectToExpensifyClassic: boolean; - showRedirectToExpensifyClassicModal: () => Promise; }; const FABMenuContext = createContext({ @@ -20,8 +18,6 @@ const FABMenuContext = createContext({ registeredItems: [], registerItem: () => {}, unregisterItem: () => {}, - shouldRedirectToExpensifyClassic: false, - showRedirectToExpensifyClassicModal: () => Promise.resolve(), }); function useFABMenuContext() { diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx index 7585ef5ae0168..3fc4197bae81a 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useState} from 'react'; +import React, {useState} from 'react'; import type {RefObject} from 'react'; import {View} from 'react-native'; import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; @@ -12,7 +12,6 @@ import {isSafari} from '@libs/Browser'; import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction'; import CONST from '@src/CONST'; import {FABMenuContext} from './FABMenuContext'; -import useRedirectToExpensifyClassic from './useRedirectToExpensifyClassic'; const FAB_ITEM_ORDER = [ CONST.FAB_MENU_ITEM_IDS.QUICK_ACTION, @@ -69,8 +68,6 @@ function FABPopoverMenu({isVisible, onClose, onItemSelected, anchorRef, animatio }); }; - const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); - const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ initialFocusedIndex: -1, maxIndex: itemCount - 1, @@ -99,8 +96,6 @@ function FABPopoverMenu({isVisible, onClose, onItemSelected, anchorRef, animatio registeredItems, registerItem, unregisterItem, - shouldRedirectToExpensifyClassic, - showRedirectToExpensifyClassicModal, }} > ): PolicySelector => areInvoicesEnabled: policy.areInvoicesEnabled, }) as PolicySelector; +const shouldRedirectSelector = (policies: OnyxCollection) => areAllGroupPoliciesExpenseChatDisabled(policies ?? {}); + function useRedirectToExpensifyClassic() { const {translate} = useLocalize(); const {showConfirmModal} = useConfirmModal(); const [isTrackingGPS = false] = useOnyx(ONYXKEYS.GPS_DRAFT_DETAILS, {selector: isTrackingSelector}); - const [allPolicies] = useMappedPolicies(policyMapper); - - const shouldRedirectToExpensifyClassic = areAllGroupPoliciesExpenseChatDisabled((allPolicies as OnyxCollection) ?? {}); + const [shouldRedirectToExpensifyClassic = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: shouldRedirectSelector}); const showRedirectToExpensifyClassicModal = async () => { const {action} = await showConfirmModal({ From ab102fe1fba37b55b74254fe98f3ac407340835e Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Feb 2026 15:39:04 +0100 Subject: [PATCH 37/54] fix ts --- src/libs/actions/Policy/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 3ebe7c38bc8fd..119e5634d8962 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -93,7 +93,7 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import {getCustomUnitsForDuplication, getMemberAccountIDsForWorkspace, goBackWhenEnableFeature, isControlPolicy, navigateToExpensifyCardPage} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import {hasValidModifiedAmount} from '@libs/TransactionUtils'; -import type {PolicySelector} from '@pages/inbox/sidebar/FABPopoverContent/types'; +import type {PolicySelector} from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import type {Feature} from '@pages/OnboardingInterestedFeatures/types'; import * as PaymentMethods from '@userActions/PaymentMethods'; import * as PersistedRequests from '@userActions/PersistedRequests'; From 76d91b39bdfcc55bcd75924e0fa545808b2c7562 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Fri, 6 Mar 2026 16:42:14 +0100 Subject: [PATCH 38/54] Review fixes for FAB popover decomposition Remove manual memoization (useCallback/useMemo) in favor of React Compiler, narrow SESSION selectors to emailSelector where only email is needed, fix stale JSDoc reference, and remove ref-during-render pattern in AttachmentPickerWithMenuItems. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../BaseFloatingCameraButton.tsx | 30 +-- .../SearchFiltersBarCreateButton.tsx | 3 +- ...onditionalCreateEmptyReportConfirmation.ts | 6 +- .../useCreateEmptyReportConfirmation.tsx | 145 +++++----- src/hooks/useSearchTypeMenuSections.ts | 3 +- src/pages/NewReportWorkspaceSelectionPage.tsx | 3 +- src/pages/Search/EmptySearchView.tsx | 44 ++- .../Search/SearchTransactionsChangeReport.tsx | 29 +- src/pages/Search/SearchTypeMenu.tsx | 95 ++++--- .../AttachmentPickerWithMenuItems.tsx | 14 +- .../menuItems/CreateReportMenuItem.tsx | 77 +++--- .../menuItems/InvoiceMenuItem.tsx | 5 +- .../menuItems/NewWorkspaceMenuItem.tsx | 13 +- .../menuItems/QuickActionMenuItem.tsx | 8 +- .../menuItems/TravelMenuItem.tsx | 5 +- .../FloatingActionButtonAndPopover.tsx | 23 +- .../iou/request/step/IOURequestEditReport.tsx | 25 +- .../iou/request/step/IOURequestStepReport.tsx | 35 ++- .../SearchFiltersBarCreateButtonTest.tsx | 1 - .../useCreateEmptyReportConfirmationTest.tsx | 254 ++++-------------- tests/unit/useSearchTypeMenuSectionsTest.ts | 2 +- 21 files changed, 292 insertions(+), 528 deletions(-) diff --git a/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx b/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx index 61aec9e812fb3..4c9cac9be76b6 100644 --- a/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx +++ b/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx @@ -1,6 +1,6 @@ -import React, {useCallback, useMemo} from 'react'; +import {useState} from 'react'; import {View} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; import Icon from '@components/Icon'; import {PressableWithoutFeedback} from '@components/Pressable'; import useLocalize from '@hooks/useLocalize'; @@ -18,12 +18,11 @@ import Tab from '@userActions/Tab'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {sessionEmailAndAccountIDSelector} from '@src/selectors/Session'; import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; -const sessionSelector = (session: OnyxEntry) => ({email: session?.email, accountID: session?.accountID}); - type BaseFloatingCameraButtonProps = { icon: IconAsset; }; @@ -35,22 +34,19 @@ function BaseFloatingCameraButton({icon}: BaseFloatingCameraButtonProps) { const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); - const [session] = useOnyx(ONYXKEYS.SESSION, {selector: sessionSelector}); + const [session] = useOnyx(ONYXKEYS.SESSION, {selector: sessionEmailAndAccountIDSelector}); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); const [userBillingGraceEndPeriods] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END); - const reportID = useMemo(() => generateReportID(), []); + const [reportID] = useState(() => generateReportID()); - const policyChatForActivePolicySelector = useCallback( - (reports: OnyxCollection) => { - if (isEmptyObject(activePolicy) || !activePolicy?.isPolicyExpenseChatEnabled) { - return undefined; - } - const policyChatsForActivePolicy = getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports); - return policyChatsForActivePolicy.at(0); - }, - [activePolicy, activePolicyID, session?.accountID], - ); - const [policyChatForActivePolicy] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: policyChatForActivePolicySelector}, [policyChatForActivePolicySelector]); + const policyChatForActivePolicySelector = (reports: OnyxCollection) => { + if (isEmptyObject(activePolicy) || !activePolicy?.isPolicyExpenseChatEnabled) { + return undefined; + } + const policyChatsForActivePolicy = getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports); + return policyChatsForActivePolicy.at(0); + }; + const [policyChatForActivePolicy] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: policyChatForActivePolicySelector}); const onPress = () => { interceptAnonymousUser(() => { diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBarCreateButton.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBarCreateButton.tsx index e908cdd70fae0..5120e4fc3fb83 100644 --- a/src/components/Search/SearchPageHeader/SearchFiltersBarCreateButton.tsx +++ b/src/components/Search/SearchPageHeader/SearchFiltersBarCreateButton.tsx @@ -112,7 +112,7 @@ function SearchFiltersBarCreateButton() { [currentUserPersonalDetails, hasViolations, defaultChatEnabledPolicy, isASAPSubmitBetaEnabled, allBetas], ); - const {openCreateReportConfirmation, CreateReportConfirmationModal} = useCreateEmptyReportConfirmation({ + const {openCreateReportConfirmation} = useCreateEmptyReportConfirmation({ policyID: defaultChatEnabledPolicyID, policyName: defaultChatEnabledPolicy?.name ?? '', onConfirm: handleCreateWorkspaceReport, @@ -215,7 +215,6 @@ function SearchFiltersBarCreateButton() { return ( - {CreateReportConfirmationModal} void; /** Whether an empty report already exists for the provided policy */ hasEmptyReport: boolean; - /** The confirmation modal React component to render */ - CreateReportConfirmationModal: ReactNode; }; /** @@ -49,7 +46,7 @@ export default function useConditionalCreateEmptyReportConfirmation({ [onCreateReport], ); - const {openCreateReportConfirmation, CreateReportConfirmationModal} = useCreateEmptyReportConfirmation({ + const {openCreateReportConfirmation} = useCreateEmptyReportConfirmation({ policyID, policyName, onConfirm: handleReportCreationConfirmed, @@ -68,6 +65,5 @@ export default function useConditionalCreateEmptyReportConfirmation({ return { handleCreateReport, hasEmptyReport, - CreateReportConfirmationModal, }; } diff --git a/src/hooks/useCreateEmptyReportConfirmation.tsx b/src/hooks/useCreateEmptyReportConfirmation.tsx index ec278d87d435d..a4759fc8c3904 100644 --- a/src/hooks/useCreateEmptyReportConfirmation.tsx +++ b/src/hooks/useCreateEmptyReportConfirmation.tsx @@ -1,14 +1,14 @@ -import type {ReactNode} from 'react'; -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; -import ConfirmModal from '@components/ConfirmModal'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import Navigation from '@libs/Navigation/Navigation'; import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import useConfirmModal from './useConfirmModal'; import useLocalize from './useLocalize'; import useThemeStyles from './useThemeStyles'; @@ -26,100 +26,75 @@ type UseCreateEmptyReportConfirmationParams = { type UseCreateEmptyReportConfirmationResult = { /** Function to open the confirmation modal */ openCreateReportConfirmation: () => void; - /** The confirmation modal React component to render */ - CreateReportConfirmationModal: ReactNode; }; -/** - * A React hook that provides a confirmation modal for creating empty reports. - * When a user attempts to create a new report in a workspace where they already have an empty report, - * this hook displays a confirmation modal to prevent accidental duplicate empty reports. - * - * @param params - Configuration object for the hook - * @param params.policyName - The display name of the policy/workspace - * @param params.onConfirm - Callback function to execute when user confirms report creation - * @returns An object containing: - * - openCreateReportConfirmation: Function to open the confirmation modal - * - CreateReportConfirmationModal: The confirmation modal React component to render - * - * @example - * const {openCreateReportConfirmation, CreateReportConfirmationModal} = useCreateEmptyReportConfirmation({ - * policyID: 'policy123', - * policyName: 'Engineering Team', - * onConfirm: handleCreateReport, - * }); - * - */ -export default function useCreateEmptyReportConfirmation({policyName, onConfirm, onCancel}: UseCreateEmptyReportConfirmationParams): UseCreateEmptyReportConfirmationResult { +function ConfirmationPrompt({workspaceName, checkboxRef, onLinkPress}: {workspaceName: string; checkboxRef: React.MutableRefObject; onLinkPress: () => void}) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const workspaceDisplayName = useMemo(() => (policyName?.trim().length ? policyName : translate('report.newReport.genericWorkspaceName')), [policyName, translate]); - const [isVisible, setIsVisible] = useState(false); - const [modalWorkspaceName, setModalWorkspaceName] = useState(workspaceDisplayName); - const [shouldDismissEmptyReportsConfirmation, setShouldDismissEmptyReportsConfirmation] = useState(false); + const [isChecked, setIsChecked] = useState(false); + + return ( + + + {translate('report.newReport.emptyReportConfirmationPrompt', {workspaceName})}{' '} + {translate('report.newReport.emptyReportConfirmationPromptLink')}. + + { + const checked = !!value; + setIsChecked(checked); + // eslint-disable-next-line no-param-reassign + checkboxRef.current = checked; + }} + /> + + ); +} - const handleConfirm = useCallback(() => { - onConfirm(shouldDismissEmptyReportsConfirmation); - setShouldDismissEmptyReportsConfirmation(false); - setIsVisible(false); - }, [onConfirm, shouldDismissEmptyReportsConfirmation]); +export default function useCreateEmptyReportConfirmation({policyName, onConfirm, onCancel}: UseCreateEmptyReportConfirmationParams): UseCreateEmptyReportConfirmationResult { + const {translate} = useLocalize(); + const {showConfirmModal} = useConfirmModal(); + const workspaceDisplayName = policyName?.trim().length ? policyName : translate('report.newReport.genericWorkspaceName'); - const handleCancel = useCallback(() => { - onCancel?.(); - setShouldDismissEmptyReportsConfirmation(false); - setIsVisible(false); - }, [onCancel]); + const onConfirmRef = useRef(onConfirm); + const onCancelRef = useRef(onCancel); + useEffect(() => { + onConfirmRef.current = onConfirm; + onCancelRef.current = onCancel; + }, [onConfirm, onCancel]); - const handleReportsLinkPress = useCallback(() => { - onCancel?.(); - setShouldDismissEmptyReportsConfirmation(false); - setIsVisible(false); - Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery({type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT})})); - }, [onCancel]); + const openCreateReportConfirmation = () => { + const checkboxRef = {current: false}; - const openCreateReportConfirmation = useCallback(() => { - // The caller is responsible for determining if empty report confirmation - // should be shown. We simply open the modal when called. - setModalWorkspaceName(workspaceDisplayName); - setShouldDismissEmptyReportsConfirmation(false); - setIsVisible(true); - }, [workspaceDisplayName]); + const handleLinkPress = () => { + onCancelRef.current?.(); + Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery({type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT})})); + }; - const prompt = useMemo( - () => ( - - - {translate('report.newReport.emptyReportConfirmationPrompt', {workspaceName: modalWorkspaceName})}{' '} - {translate('report.newReport.emptyReportConfirmationPromptLink')}. - - setShouldDismissEmptyReportsConfirmation(!!value)} + showConfirmModal({ + title: `${translate('report.newReport.emptyReportConfirmationTitle')} `, + confirmText: translate('report.newReport.createReport'), + cancelText: translate('common.cancel'), + prompt: ( + - - ), - [handleReportsLinkPress, modalWorkspaceName, shouldDismissEmptyReportsConfirmation, styles.gap4, translate], - ); - - const CreateReportConfirmationModal = useMemo( - () => ( - - ), - [handleCancel, handleConfirm, isVisible, prompt, translate], - ); + ), + }).then((result) => { + if (result.action === ModalActions.CONFIRM) { + onConfirmRef.current(checkboxRef.current); + } else { + onCancelRef.current?.(); + } + }); + }; return { openCreateReportConfirmation, - CreateReportConfirmationModal, }; } diff --git a/src/hooks/useSearchTypeMenuSections.ts b/src/hooks/useSearchTypeMenuSections.ts index 9816f48c9163e..66cc2ba68a511 100644 --- a/src/hooks/useSearchTypeMenuSections.ts +++ b/src/hooks/useSearchTypeMenuSections.ts @@ -82,7 +82,7 @@ const useSearchTypeMenuSections = (queryParams?: UseSearchTypeMenuSectionsParams setPendingReportCreation(null); }, [setPendingReportCreation]); - const {openCreateReportConfirmation, CreateReportConfirmationModal} = useCreateEmptyReportConfirmation({ + const {openCreateReportConfirmation} = useCreateEmptyReportConfirmation({ policyID: pendingReportCreation?.policyID, policyName: pendingReportCreation?.policyName ?? '', onConfirm: handlePendingConfirm, @@ -148,7 +148,6 @@ const useSearchTypeMenuSections = (queryParams?: UseSearchTypeMenuSectionsParams return { typeMenuSections, - CreateReportConfirmationModal, shouldShowSuggestedSearchSkeleton: !isSuggestedSearchDataReady && !isOffline, activeItemIndex, }; diff --git a/src/pages/NewReportWorkspaceSelectionPage.tsx b/src/pages/NewReportWorkspaceSelectionPage.tsx index e260890e553d8..25e7daf6dcb2f 100644 --- a/src/pages/NewReportWorkspaceSelectionPage.tsx +++ b/src/pages/NewReportWorkspaceSelectionPage.tsx @@ -150,7 +150,7 @@ function NewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelectionPag navigateToNewReport(optimisticReport.reportID); }; - const {openCreateReportConfirmation, CreateReportConfirmationModal} = useCreateEmptyReportConfirmation({ + const {openCreateReportConfirmation} = useCreateEmptyReportConfirmation({ policyID: pendingPolicySelection?.policy.policyID, policyName: pendingPolicySelection?.policy.text ?? '', onConfirm: (shouldDismissEmptyReportsConfirmation: boolean) => { @@ -264,7 +264,6 @@ function NewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelectionPag title={translate('report.newReport.createReport')} onBackButtonPress={Navigation.goBack} /> - {CreateReportConfirmationModal} {shouldShowLoadingIndicator ? ( ) : ( diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index 428d44265cf3d..5c4337ecc91ac 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -1,7 +1,6 @@ import {hasSeenTourSelector, tryNewDotOnyxSelector} from '@selectors/Onboarding'; import {accountIDSelector} from '@selectors/Session'; import React from 'react'; -import type {ReactNode} from 'react'; // eslint-disable-next-line no-restricted-imports import type {ImageStyle, TextStyle, ViewStyle} from 'react-native'; import {Linking, View} from 'react-native'; @@ -60,7 +59,6 @@ type EmptySearchViewContentProps = EmptySearchViewProps & { groupPoliciesWithChatEnabled: readonly never[] | Array>; introSelected: OnyxEntry; hasSeenTour: boolean; - searchMenuCreateReportConfirmationModal: ReactNode; }; type EmptySearchViewItem = { @@ -76,7 +74,7 @@ type EmptySearchViewItem = { function EmptySearchView({similarSearchHash, type, hasResults, queryJSON}: EmptySearchViewProps) { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {typeMenuSections, CreateReportConfirmationModal: SearchMenuCreateReportConfirmationModal} = useSearchTypeMenuSections(); + const {typeMenuSections} = useSearchTypeMenuSections(); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); @@ -106,7 +104,6 @@ function EmptySearchView({similarSearchHash, type, hasResults, queryJSON}: Empty groupPoliciesWithChatEnabled={groupPoliciesWithChatEnabled} introSelected={introSelected} hasSeenTour={hasSeenTour} - searchMenuCreateReportConfirmationModal={SearchMenuCreateReportConfirmationModal} queryJSON={queryJSON} /> @@ -131,7 +128,6 @@ function EmptySearchViewContent({ groupPoliciesWithChatEnabled, introSelected, hasSeenTour, - searchMenuCreateReportConfirmationModal, queryJSON, }: EmptySearchViewContentProps) { const {translate} = useLocalize(); @@ -201,7 +197,7 @@ function EmptySearchViewContent({ }); }; - const {openCreateReportConfirmation: openCreateReportFromSearch, CreateReportConfirmationModal} = useCreateEmptyReportConfirmation({ + const {openCreateReportConfirmation: openCreateReportFromSearch} = useCreateEmptyReportConfirmation({ policyID: defaultChatEnabledPolicyID, policyName: defaultChatEnabledPolicy?.name ?? '', onConfirm: handleCreateWorkspaceReport, @@ -447,27 +443,23 @@ function EmptySearchViewContent({ } return ( - <> - {searchMenuCreateReportConfirmationModal} - + } > - } - > - {content.children} - - - {CreateReportConfirmationModal} - + {content.children} + + ); } diff --git a/src/pages/Search/SearchTransactionsChangeReport.tsx b/src/pages/Search/SearchTransactionsChangeReport.tsx index eb523797adf6c..454bb7e6742ed 100644 --- a/src/pages/Search/SearchTransactionsChangeReport.tsx +++ b/src/pages/Search/SearchTransactionsChangeReport.tsx @@ -123,7 +123,7 @@ function SearchTransactionsChangeReport() { Navigation.goBack(); }; - const {handleCreateReport, CreateReportConfirmationModal} = useConditionalCreateEmptyReportConfirmation({ + const {handleCreateReport} = useConditionalCreateEmptyReportConfirmation({ policyID: policyForMovingExpensesID, policyName: policyForMovingExpenses?.name ?? '', onCreateReport: createReportForPolicy, @@ -189,21 +189,18 @@ function SearchTransactionsChangeReport() { }; return ( - <> - {CreateReportConfirmationModal} - - + ); } diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 4d6f70788adc6..1646efb6d64fc 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -35,7 +35,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { const styles = useThemeStyles(); const {singleExecution} = useSingleExecution(); const {translate} = useLocalize(); - const {typeMenuSections, CreateReportConfirmationModal, shouldShowSuggestedSearchSkeleton, activeItemIndex} = useSearchTypeMenuSections({hash, similarSearchHash}); + const {typeMenuSections, shouldShowSuggestedSearchSkeleton, activeItemIndex} = useSearchTypeMenuSections({hash, similarSearchHash}); const expensifyIcons = useMemoizedLazyExpensifyIcons([ 'Basket', 'CalendarSolid', @@ -88,56 +88,53 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { }); return ( - <> - {CreateReportConfirmationModal} - - {shouldShowSuggestedSearchSkeleton ? ( - - - - ) : ( - - {typeMenuSections.map((section, sectionIndex) => ( - - - {translate(section.translationPath)} - + + {shouldShowSuggestedSearchSkeleton ? ( + + + + ) : ( + + {typeMenuSections.map((section, sectionIndex) => ( + + + {translate(section.translationPath)} + - {section.translationPath === 'search.savedSearchesMenuItemTitle' ? ( - - ) : ( - <> - {section.menuItems.map((item, itemIndex) => { - const flattenedIndex = (sectionStartIndices?.at(sectionIndex) ?? 0) + itemIndex; - const focused = activeItemIndex === flattenedIndex; - const icon = typeof item.icon === 'string' ? expensifyIcons[item.icon] : item.icon; + {section.translationPath === 'search.savedSearchesMenuItemTitle' ? ( + + ) : ( + <> + {section.menuItems.map((item, itemIndex) => { + const flattenedIndex = (sectionStartIndices?.at(sectionIndex) ?? 0) + itemIndex; + const focused = activeItemIndex === flattenedIndex; + const icon = typeof item.icon === 'string' ? expensifyIcons[item.icon] : item.icon; - return ( - handleTypeMenuItemPress(item.searchQuery)} - /> - ); - })} - - )} - - ))} - - )} - - + return ( + handleTypeMenuItemPress(item.searchQuery)} + /> + ); + })} + + )} + + ))} + + )} + ); } diff --git a/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 1d1c9f603d0d0..cf4531e740d08 100644 --- a/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -1,6 +1,6 @@ import {useIsFocused} from '@react-navigation/native'; import {accountIDSelector} from '@selectors/Session'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import AttachmentPicker from '@components/AttachmentPicker'; @@ -186,23 +186,20 @@ function AttachmentPickerWithMenuItems({ [policy], ); - const {openCreateReportConfirmation, CreateReportConfirmationModal} = useCreateEmptyReportConfirmation({ + const {openCreateReportConfirmation} = useCreateEmptyReportConfirmation({ policyID: report?.policyID, policyName: policy?.name ?? '', onConfirm: (shouldDismissEmptyReportsConfirmation) => selectOption(() => createNewReport(currentUserPersonalDetails, isASAPSubmitBetaEnabled, hasViolations, policy, betas, true, shouldDismissEmptyReportsConfirmation), true), }); - const openCreateReportConfirmationRef = useRef(openCreateReportConfirmation); - openCreateReportConfirmationRef.current = openCreateReportConfirmation; - - const handleCreateReport = useCallback(() => { + const handleCreateReport = () => { if (shouldShowEmptyReportConfirmation) { - openCreateReportConfirmationRef.current(); + openCreateReportConfirmation(); } else { createNewReport(currentUserPersonalDetails, isASAPSubmitBetaEnabled, hasViolations, policy, betas, true, false); } - }, [currentUserPersonalDetails, isASAPSubmitBetaEnabled, hasViolations, policy, shouldShowEmptyReportConfirmation, betas]); + }; const teacherUnitePolicyID = isProduction ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; const isTeachersUniteReport = report?.policyID === teacherUnitePolicyID; @@ -429,7 +426,6 @@ function AttachmentPickerWithMenuItems({ ]; return ( <> - {CreateReportConfirmationModal} diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index 9251611e79410..86d1b7011df98 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -1,4 +1,3 @@ -import React, {useCallback} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -64,10 +63,7 @@ function CreateReportMenuItem() { const {isBetaEnabled} = usePermissions(); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); - const chatEnabledPaidGroupPolicies = useCallback( - (policies: Parameters[0]) => chatEnabledPaidGroupPoliciesSelector(policies, session?.email), - [session?.email], - ); + const chatEnabledPaidGroupPolicies = (policies: Parameters[0]) => chatEnabledPaidGroupPoliciesSelector(policies, session?.email); const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: chatEnabledPaidGroupPolicies}, [session?.email]); @@ -109,50 +105,47 @@ function CreateReportMenuItem() { }); }; - const {openCreateReportConfirmation, CreateReportConfirmationModal} = useCreateEmptyReportConfirmation({ + const {openCreateReportConfirmation} = useCreateEmptyReportConfirmation({ policyID: defaultChatEnabledPolicyID, policyName: defaultChatEnabledPolicy?.name ?? '', onConfirm: handleCreateWorkspaceReport, }); return ( - <> - { - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - - const workspaceIDForReportCreation = defaultChatEnabledPolicyID; - - if (!workspaceIDForReportCreation || (shouldRestrictUserBillableActions(workspaceIDForReportCreation) && groupPoliciesWithChatEnabled.length > 1)) { - Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute()); - return; + { + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + + const workspaceIDForReportCreation = defaultChatEnabledPolicyID; + + if (!workspaceIDForReportCreation || (shouldRestrictUserBillableActions(workspaceIDForReportCreation) && groupPoliciesWithChatEnabled.length > 1)) { + Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute()); + return; + } + + if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { + if (shouldShowEmptyReportConfirmation) { + openCreateReportConfirmation(); + } else { + handleCreateWorkspaceReport(false); } - - if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { - if (shouldShowEmptyReportConfirmation) { - openCreateReportConfirmation(); - } else { - handleCreateWorkspaceReport(false); - } - return; - } - - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(workspaceIDForReportCreation)); - }); - }} - shouldCallAfterModalHide={shouldUseNarrowLayout} - /> - {CreateReportConfirmationModal} - + return; + } + + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(workspaceIDForReportCreation)); + }); + }} + shouldCallAfterModalHide={shouldUseNarrowLayout} + /> ); } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx index c897938255bc0..32e74aaf2d11a 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx @@ -12,6 +12,7 @@ import FABFocusableMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABFocu import useRedirectToExpensifyClassic, {policyMapper} from '@pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {emailSelector} from '@src/selectors/Session'; import type * as OnyxTypes from '@src/types/onyx'; const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.INVOICE; @@ -26,9 +27,9 @@ function InvoiceMenuItem({reportID}: InvoiceMenuItemProps) { const icons = useMemoizedLazyExpensifyIcons(['InvoiceGeneric'] as const); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); const [allPolicies] = useMappedPolicies(policyMapper); - const [session] = useOnyx(ONYXKEYS.SESSION); + const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector}); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); - const canSendInvoice = canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email); + const canSendInvoice = canSendInvoicePolicyUtils(allPolicies as OnyxCollection, sessionEmail); return ( { - if (isRestrictedPolicyCreation) { - return false; - } - const isOfflineBool = !!isOffline; - const email = session?.email; - return Object.values(allPolicies ?? {}).every((policy) => !shouldShowPolicy(policy as OnyxEntry, isOfflineBool, email)); - })(); + const shouldShowNewWorkspaceButton = + !isRestrictedPolicyCreation && Object.values(allPolicies ?? {}).every((policy) => !shouldShowPolicy(policy as OnyxEntry, !!isOffline, sessionEmail)); const isVisible = !isLoading && shouldShowNewWorkspaceButton; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index 65e5a420c241b..89c18b5c3dcbb 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -1,4 +1,3 @@ -import React, {useCallback} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -42,10 +41,7 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); - const workspaceChatsSelector = useCallback( - (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports), - [activePolicyID, session?.accountID], - ); + const workspaceChatsSelector = (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports); const [policyChats = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector}); const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE); const [quickActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`); @@ -59,7 +55,7 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { const {isRestrictedToPreferredPolicy} = usePreferredPolicy(); const quickActionPolicyID = quickAction?.action === CONST.QUICK_ACTIONS.TRACK_PER_DIEM && quickAction?.perDiemPolicyID ? quickAction?.perDiemPolicyID : quickActionReport?.policyID; const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionPolicyID}`); - const reportAttributesSelector = useCallback((attributes: OnyxEntry) => attributes?.reports, []); + const reportAttributesSelector = (attributes: OnyxEntry) => attributes?.reports; const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: reportAttributesSelector}); const isValidReport = !(isEmptyObject(quickActionReport) || isReportArchived); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx index 409a90dfe74c9..76632615bf289 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx @@ -13,6 +13,7 @@ import FABFocusableMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABFocu import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {emailSelector} from '@src/selectors/Session'; import type * as OnyxTypes from '@src/types/onyx'; const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.TRAVEL; @@ -26,10 +27,10 @@ function TravelMenuItem() { const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS); const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: accountPrimaryLoginSelector}); - const [session] = useOnyx(ONYXKEYS.SESSION); + const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector}); const [allBetas] = useOnyx(ONYXKEYS.BETAS); const isBlockedFromSpotnanaTravel = Permissions.isBetaEnabled(CONST.BETAS.PREVENT_SPOTNANA_TRAVEL, allBetas); - const primaryContactMethod = primaryLogin ?? session?.email ?? ''; + const primaryContactMethod = primaryLogin ?? sessionEmail ?? ''; const isVisible = !!activePolicy?.isTravelEnabled; const isTravelEnabled = (() => { diff --git a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx index 48b044f7213fc..bc4a9b0121055 100644 --- a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx @@ -1,5 +1,5 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; -import React, {useCallback, useRef, useState} from 'react'; +import {useRef, useState} from 'react'; import {View} from 'react-native'; import FloatingActionButton from '@components/FloatingActionButton'; import FloatingReceiptButton from '@components/FloatingReceiptButton'; @@ -22,7 +22,7 @@ import TravelMenuItem from './FABPopoverContent/menuItems/TravelMenuItem'; import useScanActions from './FABPopoverContent/useScanActions'; /** - * Responsible for rendering the {@link FABPopoverContent}, and the accompanying + * Responsible for rendering the {@link FABPopoverMenu}, and the accompanying * FAB that can open or close the menu. */ function FloatingActionButtonAndPopover() { @@ -37,26 +37,21 @@ function FloatingActionButtonAndPopover() { const {startScan, startQuickScan} = useScanActions(); const [reportID] = useState(() => generateReportID()); - const showCreateMenu = useCallback(() => { + const showCreateMenu = () => { if (!isFocused && shouldUseNarrowLayout) { return; } setIsCreateMenuActive(true); - }, [isFocused, shouldUseNarrowLayout]); + }; - const hideCreateMenu = useCallback(() => { - if (!isCreateMenuActive) { - return; - } + const hideCreateMenu = () => { setIsCreateMenuActive(false); - }, [isCreateMenuActive]); + }; // Close the menu when the screen loses focus (e.g. navigating away) - useFocusEffect( - useCallback(() => { - return () => hideCreateMenu(); - }, [hideCreateMenu]), - ); + useFocusEffect(() => { + return () => hideCreateMenu(); + }); // Close menu on dragover — prevents popover from staying open during file drag useDragoverDismiss(isCreateMenuActive, hideCreateMenu); diff --git a/src/pages/iou/request/step/IOURequestEditReport.tsx b/src/pages/iou/request/step/IOURequestEditReport.tsx index 84b1c3358fda8..30b67a3cc7625 100644 --- a/src/pages/iou/request/step/IOURequestEditReport.tsx +++ b/src/pages/iou/request/step/IOURequestEditReport.tsx @@ -121,7 +121,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { selectReport({value: optimisticReport.reportID}, optimisticReport); }; - const {handleCreateReport, CreateReportConfirmationModal} = useConditionalCreateEmptyReportConfirmation({ + const {handleCreateReport} = useConditionalCreateEmptyReportConfirmation({ policyID: policyForMovingExpensesID, policyName: policyForMovingExpenses?.name ?? '', onCreateReport: createReportForPolicy, @@ -148,19 +148,16 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { }; return ( - <> - {CreateReportConfirmationModal} - - + ); } diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index bbe4467cd4696..076771415f5ca 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -239,7 +239,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { handleRegularReportSelection({value: optimisticReport.reportID}, optimisticReport); }; - const {handleCreateReport, CreateReportConfirmationModal} = useConditionalCreateEmptyReportConfirmation({ + const {handleCreateReport} = useConditionalCreateEmptyReportConfirmation({ policyID: policyForMovingExpensesID, policyName: policyForMovingExpenses?.name ?? '', onCreateReport: createReportForPolicy, @@ -274,24 +274,21 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { } return ( - <> - {CreateReportConfirmationModal} - - + ); } diff --git a/tests/ui/components/SearchFiltersBarCreateButtonTest.tsx b/tests/ui/components/SearchFiltersBarCreateButtonTest.tsx index d99ec6148a723..09928070985c7 100644 --- a/tests/ui/components/SearchFiltersBarCreateButtonTest.tsx +++ b/tests/ui/components/SearchFiltersBarCreateButtonTest.tsx @@ -47,7 +47,6 @@ jest.mock('@hooks/useHasEmptyReportsForPolicy', () => () => false); jest.mock('@hooks/useCreateEmptyReportConfirmation', () => () => ({ openCreateReportConfirmation: jest.fn(), - CreateReportConfirmationModal: null, })); jest.mock('@libs/Navigation/helpers/isSearchTopmostFullScreenRoute', () => () => true); diff --git a/tests/unit/useCreateEmptyReportConfirmationTest.tsx b/tests/unit/useCreateEmptyReportConfirmationTest.tsx index b94d4c836e11d..13589ce44a02d 100644 --- a/tests/unit/useCreateEmptyReportConfirmationTest.tsx +++ b/tests/unit/useCreateEmptyReportConfirmationTest.tsx @@ -1,43 +1,46 @@ -import {act, render, renderHook} from '@testing-library/react-native'; +import {act, renderHook} from '@testing-library/react-native'; import type {ReactElement, ReactNode} from 'react'; import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation'; -import Navigation from '@libs/Navigation/Navigation'; -import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; -import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; -type MockConfirmModalProps = { - prompt: ReactNode; +type ShowConfirmModalOptions = { + prompt?: ReactNode; confirmText?: string; cancelText?: string; - isVisible?: boolean; - onConfirm?: () => void | Promise; - onCancel?: () => void; title?: string; }; -type MockTextLinkProps = { - children?: ReactNode; - onPress?: () => void; - onLongPress?: (event: unknown) => void; -}; +type ModalResult = {action: string}; type MockReactModule = { createElement: (...args: unknown[]) => ReactElement; }; -const mockTranslate = jest.fn((key: string, params?: Record) => (params?.workspaceName ? `${key}:${params.workspaceName}` : key)); +let lastShowConfirmModalOptions: ShowConfirmModalOptions | undefined; +let resolveModalPromise: ((result: ModalResult) => void) | undefined; + +jest.mock('@hooks/useConfirmModal', () => () => ({ + showConfirmModal: jest.fn((options: ShowConfirmModalOptions) => { + lastShowConfirmModalOptions = options; + return new Promise((resolve) => { + resolveModalPromise = resolve; + }); + }), +})); -let mockTextLinkProps: MockTextLinkProps | undefined; +const mockTranslate = jest.fn((key: string, params?: Record) => (params?.workspaceName ? `${key}:${params.workspaceName}` : key)); jest.mock('@hooks/useLocalize', () => () => ({ translate: mockTranslate, })); -jest.mock('@components/ConfirmModal', () => { +jest.mock('@hooks/useThemeStyles', () => () => ({ + gap4: {}, +})); + +jest.mock('@components/CheckboxWithLabel', () => { const mockReact: MockReactModule = jest.requireActual('react'); - return ({prompt, confirmText, cancelText, isVisible, onConfirm, onCancel, title}: MockConfirmModalProps) => - mockReact.createElement('mock-confirm-modal', {prompt, confirmText, cancelText, isVisible, onConfirm, onCancel, title}, null); + return (props: {accessibilityLabel: string; label: string; isChecked: boolean; onInputChange: (value: boolean) => void}) => + mockReact.createElement('mock-checkbox', {accessibilityLabel: props.accessibilityLabel}); }); jest.mock('@components/Text', () => { @@ -47,53 +50,27 @@ jest.mock('@components/Text', () => { jest.mock('@components/TextLink', () => { const mockReact: MockReactModule = jest.requireActual('react'); - return (props: MockTextLinkProps) => { - mockTextLinkProps = props; - const {children, onPress, onLongPress} = props; - return mockReact.createElement('mock-text-link', {onPress, onLongPress}, children); - }; + return ({children}: {children?: ReactNode}) => mockReact.createElement('mock-text-link', null, children); }); jest.mock('@libs/Navigation/Navigation', () => ({ navigate: jest.fn(), })); -type HookValue = ReturnType; -type HookProps = { - policyName?: string; - onCancel?: () => void; -}; - -type MockConfirmModalElement = ReactElement; - -function getModal(hookValue: HookValue): MockConfirmModalElement { - return hookValue.CreateReportConfirmationModal as MockConfirmModalElement; -} - -function getRequiredHandler unknown>(handler: T | undefined, name: string): T { - if (!handler) { - throw new Error(`${name} handler was not provided`); - } - return handler; -} - const policyID = 'policy-123'; const policyName = 'Engineering Team'; -const expectedSearchRoute = ROUTES.SEARCH_ROOT.getRoute({ - query: buildCannedSearchQuery({type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT}), -}); - describe('useCreateEmptyReportConfirmation', () => { beforeEach(() => { jest.clearAllMocks(); mockTranslate.mockClear(); - mockTextLinkProps = undefined; + lastShowConfirmModalOptions = undefined; + resolveModalPromise = undefined; }); - it('modal is hidden by default and opens on demand', () => { + it('calls showConfirmModal when openCreateReportConfirmation is invoked', () => { const onConfirm = jest.fn(); - const {result} = renderHook(() => + const {result} = renderHook(() => useCreateEmptyReportConfirmation({ policyID, policyName, @@ -101,20 +78,18 @@ describe('useCreateEmptyReportConfirmation', () => { }), ); - let modal = getModal(result.current); - expect(modal.props.isVisible).toBe(false); - act(() => { result.current.openCreateReportConfirmation(); }); - modal = getModal(result.current); - expect(modal.props.isVisible).toBe(true); + expect(lastShowConfirmModalOptions).toBeDefined(); + expect(lastShowConfirmModalOptions?.confirmText).toBe('report.newReport.createReport'); + expect(lastShowConfirmModalOptions?.cancelText).toBe('common.cancel'); }); - it('invokes onConfirm and resets state after completion', () => { + it('invokes onConfirm when modal resolves with CONFIRM', async () => { const onConfirm = jest.fn(); - const {result} = renderHook(() => + const {result} = renderHook(() => useCreateEmptyReportConfirmation({ policyID, policyName, @@ -126,24 +101,19 @@ describe('useCreateEmptyReportConfirmation', () => { result.current.openCreateReportConfirmation(); }); - let modal = getModal(result.current); - const confirmHandler = getRequiredHandler(modal.props.onConfirm, 'onConfirm'); - - act(() => { - confirmHandler(); + await act(async () => { + resolveModalPromise?.({action: 'CONFIRM'}); }); expect(onConfirm).toHaveBeenCalledTimes(1); - - modal = getModal(result.current); - expect(modal.props.isVisible).toBe(false); + expect(onConfirm).toHaveBeenCalledWith(false); }); - it('calls onCancel when cancellation occurs', () => { + it('calls onCancel when modal resolves with CLOSE', async () => { const onConfirm = jest.fn(); const onCancel = jest.fn(); - const {result} = renderHook(() => + const {result} = renderHook(() => useCreateEmptyReportConfirmation({ policyID, policyName, @@ -156,54 +126,34 @@ describe('useCreateEmptyReportConfirmation', () => { result.current.openCreateReportConfirmation(); }); - const modal = getModal(result.current); - const cancelHandler = getRequiredHandler(modal.props.onCancel, 'onCancel'); - - act(() => { - cancelHandler(); + await act(async () => { + resolveModalPromise?.({action: 'CLOSE'}); }); expect(onConfirm).not.toHaveBeenCalled(); expect(onCancel).toHaveBeenCalledTimes(1); - - const updatedModal = getModal(result.current); - expect(updatedModal.props.isVisible).toBe(false); }); - it('navigates to reports search when link in prompt is pressed', () => { + it('falls back to generic workspace name in translations when necessary', () => { const onConfirm = jest.fn(); - const {result} = renderHook(() => + renderHook(() => useCreateEmptyReportConfirmation({ policyID, - policyName: '', + policyName: ' ', onConfirm, }), ); - const modal = getModal(result.current); - const {unmount} = render(modal.props.prompt as ReactElement); - const onPress = mockTextLinkProps?.onPress; - - expect(onPress).toBeDefined(); - - act(() => { - onPress?.(); - }); - - expect(Navigation.navigate).toHaveBeenCalledWith(expectedSearchRoute); - unmount(); + expect(mockTranslate).toHaveBeenCalledWith('report.newReport.genericWorkspaceName'); }); - it('calls onCancel when reports link in prompt is pressed', () => { + it('does not call onCancel when not provided and modal is closed', async () => { const onConfirm = jest.fn(); - const onCancel = jest.fn(); - - const {result} = renderHook(() => + const {result} = renderHook(() => useCreateEmptyReportConfirmation({ policyID, policyName, onConfirm, - onCancel, }), ); @@ -211,116 +161,10 @@ describe('useCreateEmptyReportConfirmation', () => { result.current.openCreateReportConfirmation(); }); - const modal = getModal(result.current); - const {unmount} = render(modal.props.prompt as ReactElement); - const onPress = mockTextLinkProps?.onPress; - - expect(onPress).toBeDefined(); - - act(() => { - onPress?.(); + await act(async () => { + resolveModalPromise?.({action: 'CLOSE'}); }); - expect(onCancel).toHaveBeenCalledTimes(1); - unmount(); - }); - - it('retains displayed workspace name while parent clears selection', () => { - const onConfirm = jest.fn(); - const onCancel = jest.fn(); - const initialPolicyName = policyName; - - const {result, rerender} = renderHook( - ({policyName: currentPolicyName, onCancel: currentOnCancel}: HookProps) => - useCreateEmptyReportConfirmation({ - policyID, - policyName: currentPolicyName, - onConfirm, - onCancel: currentOnCancel, - }), - { - initialProps: { - policyName: initialPolicyName, - onCancel, - }, - }, - ); - - act(() => { - result.current.openCreateReportConfirmation(); - }); - - const modal = getModal(result.current); - const renderedPrompt = render(modal.props.prompt as ReactElement); - expect(JSON.stringify(renderedPrompt.toJSON())).toContain(`report.newReport.emptyReportConfirmationPrompt:${initialPolicyName}`); - renderedPrompt.unmount(); - - rerender({policyName: '', onCancel}); - - const updatedModal = getModal(result.current); - const renderedPromptAfterClear = render(updatedModal.props.prompt as ReactElement); - expect(JSON.stringify(renderedPromptAfterClear.toJSON())).toContain(`report.newReport.emptyReportConfirmationPrompt:${initialPolicyName}`); - renderedPromptAfterClear.unmount(); - }); - - it('uses updated workspace name on subsequent opens', () => { - const onConfirm = jest.fn(); - const onCancel = jest.fn(); - const initialPolicyName = policyName; - const updatedPolicyName = 'Finance Team'; - - const {result, rerender} = renderHook( - ({policyName: currentPolicyName, onCancel: currentOnCancel}: HookProps) => - useCreateEmptyReportConfirmation({ - policyID, - policyName: currentPolicyName, - onConfirm, - onCancel: currentOnCancel, - }), - { - initialProps: { - policyName: initialPolicyName, - onCancel, - }, - }, - ); - - act(() => { - result.current.openCreateReportConfirmation(); - }); - - let modal = getModal(result.current); - const renderedPrompt = render(modal.props.prompt as ReactElement); - expect(JSON.stringify(renderedPrompt.toJSON())).toContain(`report.newReport.emptyReportConfirmationPrompt:${initialPolicyName}`); - renderedPrompt.unmount(); - - const cancelHandler = getRequiredHandler(modal.props.onCancel, 'onCancel'); - act(() => { - cancelHandler(); - }); - - rerender({policyName: updatedPolicyName, onCancel}); - - act(() => { - result.current.openCreateReportConfirmation(); - }); - - modal = getModal(result.current); - const renderedPromptAfterUpdate = render(modal.props.prompt as ReactElement); - expect(JSON.stringify(renderedPromptAfterUpdate.toJSON())).toContain(`report.newReport.emptyReportConfirmationPrompt:${updatedPolicyName}`); - renderedPromptAfterUpdate.unmount(); - }); - - it('falls back to generic workspace name in translations when necessary', () => { - const onConfirm = jest.fn(); - renderHook(() => - useCreateEmptyReportConfirmation({ - policyID, - policyName: ' ', - onConfirm, - }), - ); - - expect(mockTranslate).toHaveBeenCalledWith('report.newReport.genericWorkspaceName'); + expect(onConfirm).not.toHaveBeenCalled(); }); }); diff --git a/tests/unit/useSearchTypeMenuSectionsTest.ts b/tests/unit/useSearchTypeMenuSectionsTest.ts index 6ab0dca69bf9a..29ad606f70541 100644 --- a/tests/unit/useSearchTypeMenuSectionsTest.ts +++ b/tests/unit/useSearchTypeMenuSectionsTest.ts @@ -15,7 +15,7 @@ jest.mock('@userActions/Report', () => ({ })); jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}}))); -jest.mock('@hooks/useCreateEmptyReportConfirmation', () => jest.fn(() => ({openCreateReportConfirmation: jest.fn(), CreateReportConfirmationModal: null}))); +jest.mock('@hooks/useCreateEmptyReportConfirmation', () => jest.fn(() => ({openCreateReportConfirmation: jest.fn()}))); jest.mock('@hooks/useNetwork', () => jest.fn(() => ({isOffline: false}))); jest.mock('@hooks/usePermissions', () => jest.fn(() => ({isBetaEnabled: jest.fn(() => false)}))); From e4cb8500e634c923bf6c5085b2d5d8ec67151859 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Mon, 9 Mar 2026 16:21:09 +0100 Subject: [PATCH 39/54] fix: compilation errors --- .../FABPopoverContent/menuItems/CreateReportMenuItem.tsx | 1 + .../FABPopoverContent/menuItems/QuickActionMenuItem.tsx | 3 ++- src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index 86d1b7011df98..997e56a290a14 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index 89c18b5c3dcbb..febc389eccc85 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -1,4 +1,5 @@ -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import React from 'react'; +import type { OnyxCollection, OnyxEntry } from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; diff --git a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx index bc4a9b0121055..77ef228339625 100644 --- a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx @@ -1,5 +1,5 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; -import {useRef, useState} from 'react'; +import React, {useRef, useState} from 'react'; import {View} from 'react-native'; import FloatingActionButton from '@components/FloatingActionButton'; import FloatingReceiptButton from '@components/FloatingReceiptButton'; From 36c9e5b396b9cdd80ba17dac40928eb94d968fbf Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Mon, 9 Mar 2026 16:22:36 +0100 Subject: [PATCH 40/54] fix missing react import --- .../FloatingCameraButton/BaseFloatingCameraButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx b/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx index 4c9cac9be76b6..f72eea30d1b9d 100644 --- a/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx +++ b/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx @@ -1,4 +1,4 @@ -import {useState} from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import Icon from '@components/Icon'; From 2191d94659edbb22a6ff27e4acef2ed6b0c92a4c Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Mon, 9 Mar 2026 16:42:26 +0100 Subject: [PATCH 41/54] Replace inlined selectors with existing ones from src/selectors/ Co-Authored-By: Claude Opus 4.6 (1M context) --- .../FABPopoverContent/menuItems/TravelMenuItem.tsx | 7 ++----- .../FABPopoverContent/useRedirectToExpensifyClassic.ts | 8 +++----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx index 76632615bf289..a9a060db443a9 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx @@ -1,6 +1,5 @@ import {Str} from 'expensify-common'; import React from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -13,20 +12,18 @@ import FABFocusableMenuItem from '@pages/inbox/sidebar/FABPopoverContent/FABFocu import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {primaryLoginSelector} from '@src/selectors/Account'; import {emailSelector} from '@src/selectors/Session'; -import type * as OnyxTypes from '@src/types/onyx'; const ITEM_ID = CONST.FAB_MENU_ITEM_IDS.TRAVEL; -const accountPrimaryLoginSelector = (account: OnyxEntry) => account?.primaryLogin; - function TravelMenuItem() { const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Suitcase', 'NewWindow'] as const); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS); - const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: accountPrimaryLoginSelector}); + const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: primaryLoginSelector}); const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector}); const [allBetas] = useOnyx(ONYXKEYS.BETAS); const isBlockedFromSpotnanaTravel = Permissions.isBetaEnabled(CONST.BETAS.PREVENT_SPOTNANA_TRAVEL, allBetas); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts b/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts index eed71c48d7d6e..7d65107f32155 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts @@ -1,15 +1,15 @@ -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import useConfirmModal from '@hooks/useConfirmModal'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import {openOldDotLink} from '@libs/actions/Link'; -import {areAllGroupPoliciesExpenseChatDisabled} from '@libs/PolicyUtils'; import {closeReactNativeApp} from '@userActions/HybridApp'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {isTrackingSelector} from '@src/selectors/GPSDraftDetails'; +import {shouldRedirectToExpensifyClassicSelector} from '@src/selectors/Policy'; import type * as OnyxTypes from '@src/types/onyx'; type PolicySelector = Pick; @@ -26,13 +26,11 @@ const policyMapper = (policy: OnyxEntry): PolicySelector => areInvoicesEnabled: policy.areInvoicesEnabled, }) as PolicySelector; -const shouldRedirectSelector = (policies: OnyxCollection) => areAllGroupPoliciesExpenseChatDisabled(policies ?? {}); - function useRedirectToExpensifyClassic() { const {translate} = useLocalize(); const {showConfirmModal} = useConfirmModal(); const [isTrackingGPS = false] = useOnyx(ONYXKEYS.GPS_DRAFT_DETAILS, {selector: isTrackingSelector}); - const [shouldRedirectToExpensifyClassic = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: shouldRedirectSelector}); + const [shouldRedirectToExpensifyClassic = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: shouldRedirectToExpensifyClassicSelector}); const showRedirectToExpensifyClassicModal = async () => { const {action} = await showConfirmModal({ From 7d7150b64191156fb17716c45b73c9d2c88c60fa Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Mon, 9 Mar 2026 16:45:54 +0100 Subject: [PATCH 42/54] Replace IIFE with inline assignment for isTravelEnabled Co-Authored-By: Claude Opus 4.6 (1M context) --- .../FABPopoverContent/menuItems/TravelMenuItem.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx index a9a060db443a9..3548465b68303 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx @@ -30,13 +30,13 @@ function TravelMenuItem() { const primaryContactMethod = primaryLogin ?? sessionEmail ?? ''; const isVisible = !!activePolicy?.isTravelEnabled; - const isTravelEnabled = (() => { - if (!!isBlockedFromSpotnanaTravel || !primaryContactMethod || Str.isSMSLogin(primaryContactMethod) || !isPaidGroupPolicy(activePolicy)) { - return false; - } - const isPolicyProvisioned = activePolicy?.travelSettings?.spotnanaCompanyID ?? activePolicy?.travelSettings?.associatedTravelDomainAccountID; - return activePolicy?.travelSettings?.hasAcceptedTerms ?? (travelSettings?.hasAcceptedTerms && isPolicyProvisioned); - })(); + const isPolicyProvisioned = activePolicy?.travelSettings?.spotnanaCompanyID ?? activePolicy?.travelSettings?.associatedTravelDomainAccountID; + const isTravelEnabled = + !isBlockedFromSpotnanaTravel && + !!primaryContactMethod && + !Str.isSMSLogin(primaryContactMethod) && + isPaidGroupPolicy(activePolicy) && + (activePolicy?.travelSettings?.hasAcceptedTerms ?? (travelSettings?.hasAcceptedTerms && isPolicyProvisioned)); const openTravel = () => { if (isTravelEnabled) { From f17770b5ad25930ad81f32b2bd2dddbec0b473eb Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Mon, 9 Mar 2026 17:19:02 +0100 Subject: [PATCH 43/54] Extract FAB buttons into self-contained FABButtons wrapper Move useScanActions, useLocalize, and button rendering out of FloatingActionButtonAndPopover into a dedicated FABButtons component so scan-related Onyx changes no longer re-render the parent and its 9 menu item children. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../sidebar/FABPopoverContent/FABButtons.tsx | 44 +++++++++++++++++++ .../FloatingActionButtonAndPopover.tsx | 23 ++-------- 2 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 src/pages/inbox/sidebar/FABPopoverContent/FABButtons.tsx diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABButtons.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABButtons.tsx new file mode 100644 index 0000000000000..d5ee5153e96ba --- /dev/null +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABButtons.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import type {RefObject} from 'react'; +import FloatingActionButton from '@components/FloatingActionButton'; +import FloatingReceiptButton from '@components/FloatingReceiptButton'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import CONST from '@src/CONST'; +import useScanActions from './useScanActions'; + +type FABButtonsProps = { + isActive: boolean; + fabRef: RefObject; + onPress: () => void; +}; + +function FABButtons({isActive, fabRef, onPress}: FABButtonsProps) { + const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {startScan, startQuickScan} = useScanActions(); + + return ( + <> + {!shouldUseNarrowLayout && ( + + )} + + + ); +} + +export default FABButtons; diff --git a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx index 77ef228339625..3add1408039a3 100644 --- a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx @@ -1,14 +1,12 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import React, {useRef, useState} from 'react'; import {View} from 'react-native'; -import FloatingActionButton from '@components/FloatingActionButton'; -import FloatingReceiptButton from '@components/FloatingReceiptButton'; import useDragoverDismiss from '@hooks/useDragoverDismiss'; -import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {generateReportID} from '@libs/ReportUtils'; import CONST from '@src/CONST'; +import FABButtons from './FABPopoverContent/FABButtons'; import FABPopoverMenu from './FABPopoverContent/FABPopoverMenu'; import CreateReportMenuItem from './FABPopoverContent/menuItems/CreateReportMenuItem'; import ExpenseMenuItem from './FABPopoverContent/menuItems/ExpenseMenuItem'; @@ -19,7 +17,6 @@ import QuickActionMenuItem from './FABPopoverContent/menuItems/QuickActionMenuIt import TestDriveMenuItem from './FABPopoverContent/menuItems/TestDriveMenuItem'; import TrackDistanceMenuItem from './FABPopoverContent/menuItems/TrackDistanceMenuItem'; import TravelMenuItem from './FABPopoverContent/menuItems/TravelMenuItem'; -import useScanActions from './FABPopoverContent/useScanActions'; /** * Responsible for rendering the {@link FABPopoverMenu}, and the accompanying @@ -27,14 +24,12 @@ import useScanActions from './FABPopoverContent/useScanActions'; */ function FloatingActionButtonAndPopover() { const styles = useThemeStyles(); - const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const isFocused = useIsFocused(); const [isCreateMenuActive, setIsCreateMenuActive] = useState(false); const fabRef = useRef(null); - const {startScan, startQuickScan} = useScanActions(); const [reportID] = useState(() => generateReportID()); const showCreateMenu = () => { @@ -84,22 +79,10 @@ function FloatingActionButtonAndPopover() { - {!shouldUseNarrowLayout && ( - - )} - ); From 7a0674285fe0e20db107dd79c07bc9e5fdd2b5a9 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Mon, 9 Mar 2026 17:22:59 +0100 Subject: [PATCH 44/54] fix prettier --- .../sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index febc389eccc85..53f8b44d6a55a 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import type { OnyxCollection, OnyxEntry } from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; From abcb893d5998dc37da27abb648701876cd656f9f Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Tue, 10 Mar 2026 17:38:34 +0100 Subject: [PATCH 45/54] Fix deprecated MutableRefObject usage in useCreateEmptyReportConfirmation Replace React.MutableRefObject with React.RefObject to fix @typescript-eslint/no-deprecated lint error. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/hooks/useCreateEmptyReportConfirmation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useCreateEmptyReportConfirmation.tsx b/src/hooks/useCreateEmptyReportConfirmation.tsx index a4759fc8c3904..f57513a856ded 100644 --- a/src/hooks/useCreateEmptyReportConfirmation.tsx +++ b/src/hooks/useCreateEmptyReportConfirmation.tsx @@ -28,7 +28,7 @@ type UseCreateEmptyReportConfirmationResult = { openCreateReportConfirmation: () => void; }; -function ConfirmationPrompt({workspaceName, checkboxRef, onLinkPress}: {workspaceName: string; checkboxRef: React.MutableRefObject; onLinkPress: () => void}) { +function ConfirmationPrompt({workspaceName, checkboxRef, onLinkPress}: {workspaceName: string; checkboxRef: React.RefObject; onLinkPress: () => void}) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [isChecked, setIsChecked] = useState(false); From a1baa977479e93689822927ce8b87d796764d3b4 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Wed, 11 Mar 2026 11:55:24 +0100 Subject: [PATCH 46/54] Fix confirmation modal not dismissing when clicking reports link closeModal() properly dismisses the ConfirmModalWrapper and resolves the promise with CLOSE action, which triggers the onCancel callback. Previously the modal stayed visible overlaid on the destination screen. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/hooks/useCreateEmptyReportConfirmation.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useCreateEmptyReportConfirmation.tsx b/src/hooks/useCreateEmptyReportConfirmation.tsx index f57513a856ded..958439c36dd99 100644 --- a/src/hooks/useCreateEmptyReportConfirmation.tsx +++ b/src/hooks/useCreateEmptyReportConfirmation.tsx @@ -56,7 +56,7 @@ function ConfirmationPrompt({workspaceName, checkboxRef, onLinkPress}: {workspac export default function useCreateEmptyReportConfirmation({policyName, onConfirm, onCancel}: UseCreateEmptyReportConfirmationParams): UseCreateEmptyReportConfirmationResult { const {translate} = useLocalize(); - const {showConfirmModal} = useConfirmModal(); + const {showConfirmModal, closeModal} = useConfirmModal(); const workspaceDisplayName = policyName?.trim().length ? policyName : translate('report.newReport.genericWorkspaceName'); const onConfirmRef = useRef(onConfirm); @@ -70,7 +70,7 @@ export default function useCreateEmptyReportConfirmation({policyName, onConfirm, const checkboxRef = {current: false}; const handleLinkPress = () => { - onCancelRef.current?.(); + closeModal(); Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery({type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT})})); }; From c953885da3eb80c214159c0972f9ef371db755d1 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Wed, 11 Mar 2026 13:48:40 +0100 Subject: [PATCH 47/54] Add ScanShortcut telemetry span to startQuickScan Matches BaseFloatingCameraButton so both scan entry points are instrumented consistently. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts index 25355a7bd5761..9962a3024a9bd 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts @@ -6,6 +6,7 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {generateReportID, getWorkspaceChats} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; +import {startSpan} from '@libs/telemetry/activeSpans'; import Tab from '@userActions/Tab'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -54,6 +55,10 @@ function useScanActions() { const quickActionReportID = policyChatReportID ?? reportID; Tab.setSelectedTab(CONST.TAB.IOU_REQUEST_TYPE, CONST.IOU.REQUEST_TYPE.SCAN); + startSpan(CONST.TELEMETRY.SPAN_SCAN_SHORTCUT, { + name: CONST.TELEMETRY.SPAN_SCAN_SHORTCUT, + op: CONST.TELEMETRY.SPAN_SCAN_SHORTCUT, + }); startMoneyRequest(CONST.IOU.TYPE.CREATE, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, !!policyChatReportID, undefined, allTransactionDrafts, true); }); }; From efdba3690b8237c04794a81801ab64ac3ff278a5 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Thu, 12 Mar 2026 14:42:38 +0100 Subject: [PATCH 48/54] Address PR review comments - Move useDragoverDismiss to directory-based platform split - Add unit test for sessionEmailAndAccountIDSelector - Revert no-op filter reorder in getWorkspaceChats Co-Authored-By: Claude Opus 4.6 (1M context) --- .../index.native.ts} | 0 .../index.ts} | 0 src/libs/ReportUtils.ts | 2 +- tests/unit/selectors/SessionTest.ts | 29 +++++++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) rename src/hooks/{useDragoverDismiss.native.ts => useDragoverDismiss/index.native.ts} (100%) rename src/hooks/{useDragoverDismiss.ts => useDragoverDismiss/index.ts} (100%) create mode 100644 tests/unit/selectors/SessionTest.ts diff --git a/src/hooks/useDragoverDismiss.native.ts b/src/hooks/useDragoverDismiss/index.native.ts similarity index 100% rename from src/hooks/useDragoverDismiss.native.ts rename to src/hooks/useDragoverDismiss/index.native.ts diff --git a/src/hooks/useDragoverDismiss.ts b/src/hooks/useDragoverDismiss/index.ts similarity index 100% rename from src/hooks/useDragoverDismiss.ts rename to src/hooks/useDragoverDismiss/index.ts diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ce09ce5393edb..ce987540eb7af 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -10235,7 +10235,7 @@ function canCreateRequest( function getWorkspaceChats(policyID: string | undefined, accountIDs: number[], reports: OnyxCollection = allReports): Array> { return Object.values(reports ?? {}).filter( - (report) => !!policyID && report?.policyID === policyID && report?.ownerAccountID && accountIDs.includes(report?.ownerAccountID) && isPolicyExpenseChat(report), + (report) => isPolicyExpenseChat(report) && !!policyID && report?.policyID === policyID && report?.ownerAccountID && accountIDs.includes(report?.ownerAccountID), ); } diff --git a/tests/unit/selectors/SessionTest.ts b/tests/unit/selectors/SessionTest.ts new file mode 100644 index 0000000000000..41ce50422925f --- /dev/null +++ b/tests/unit/selectors/SessionTest.ts @@ -0,0 +1,29 @@ +import {sessionEmailAndAccountIDSelector} from '@selectors/Session'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {Session} from '@src/types/onyx'; + +describe('sessionEmailAndAccountIDSelector', () => { + it('returns email and accountID when both are present', () => { + const session: OnyxEntry = {email: 'test@expensify.com', accountID: 12345}; + expect(sessionEmailAndAccountIDSelector(session)).toEqual({email: 'test@expensify.com', accountID: 12345}); + }); + + it('returns undefined email when email is not set', () => { + const session: OnyxEntry = {accountID: 12345}; + expect(sessionEmailAndAccountIDSelector(session)).toEqual({email: undefined, accountID: 12345}); + }); + + it('returns undefined accountID when accountID is not set', () => { + const session: OnyxEntry = {email: 'test@expensify.com'}; + expect(sessionEmailAndAccountIDSelector(session)).toEqual({email: 'test@expensify.com', accountID: undefined}); + }); + + it('returns both undefined when session is empty', () => { + const session: OnyxEntry = {}; + expect(sessionEmailAndAccountIDSelector(session)).toEqual({email: undefined, accountID: undefined}); + }); + + it('returns both undefined when session is undefined', () => { + expect(sessionEmailAndAccountIDSelector(undefined)).toEqual({email: undefined, accountID: undefined}); + }); +}); From 1ebde666210f62b1b691632a60d88ea1ed42b22d Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Fri, 13 Mar 2026 17:54:11 +0100 Subject: [PATCH 49/54] Add unit tests for useFABMenuItem and useConditionalCreateEmptyReportConfirmation Co-Authored-By: Claude Opus 4.6 (1M context) --- ...ionalCreateEmptyReportConfirmationTest.tsx | 160 +++++++++++++++++ tests/unit/hooks/useFABMenuItemTest.tsx | 161 ++++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 tests/unit/hooks/useConditionalCreateEmptyReportConfirmationTest.tsx create mode 100644 tests/unit/hooks/useFABMenuItemTest.tsx diff --git a/tests/unit/hooks/useConditionalCreateEmptyReportConfirmationTest.tsx b/tests/unit/hooks/useConditionalCreateEmptyReportConfirmationTest.tsx new file mode 100644 index 0000000000000..509d2c6384cb5 --- /dev/null +++ b/tests/unit/hooks/useConditionalCreateEmptyReportConfirmationTest.tsx @@ -0,0 +1,160 @@ +import {act, renderHook} from '@testing-library/react-native'; +import useConditionalCreateEmptyReportConfirmation from '@hooks/useConditionalCreateEmptyReportConfirmation'; + +const mockOpenCreateReportConfirmation = jest.fn(); + +let mockOnConfirmFromModal: ((val?: boolean) => void) | undefined; + +jest.mock('@hooks/useCreateEmptyReportConfirmation', () => (params: {onConfirm: (val?: boolean) => void; onCancel?: () => void}) => { + // Store the onConfirm so tests can simulate the modal confirming + mockOnConfirmFromModal = params.onConfirm; + return {openCreateReportConfirmation: mockOpenCreateReportConfirmation}; +}); + +let mockHasEmptyReport = false; +jest.mock('@hooks/useHasEmptyReportsForPolicy', () => () => mockHasEmptyReport); + +let mockHasDismissedConfirmation: boolean | undefined; +jest.mock('@hooks/useOnyx', () => () => [mockHasDismissedConfirmation]); + +const policyID = 'policy-123'; +const policyName = 'Test Workspace'; + +describe('useConditionalCreateEmptyReportConfirmation', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockHasEmptyReport = false; + mockHasDismissedConfirmation = undefined; + mockOnConfirmFromModal = undefined; + }); + + it('calls onCreateReport directly when there is no empty report', () => { + mockHasEmptyReport = false; + const onCreateReport = jest.fn(); + + const {result} = renderHook(() => + useConditionalCreateEmptyReportConfirmation({ + policyID, + policyName, + onCreateReport, + }), + ); + + act(() => { + result.current.handleCreateReport(); + }); + + expect(onCreateReport).toHaveBeenCalledWith(false); + expect(mockOpenCreateReportConfirmation).not.toHaveBeenCalled(); + }); + + it('opens confirmation modal when there is an empty report and confirmation not dismissed', () => { + mockHasEmptyReport = true; + mockHasDismissedConfirmation = undefined; + const onCreateReport = jest.fn(); + + const {result} = renderHook(() => + useConditionalCreateEmptyReportConfirmation({ + policyID, + policyName, + onCreateReport, + }), + ); + + act(() => { + result.current.handleCreateReport(); + }); + + expect(mockOpenCreateReportConfirmation).toHaveBeenCalledTimes(1); + expect(onCreateReport).not.toHaveBeenCalled(); + }); + + it('skips confirmation when empty report exists but user previously dismissed it', () => { + mockHasEmptyReport = true; + mockHasDismissedConfirmation = true; + const onCreateReport = jest.fn(); + + const {result} = renderHook(() => + useConditionalCreateEmptyReportConfirmation({ + policyID, + policyName, + onCreateReport, + }), + ); + + act(() => { + result.current.handleCreateReport(); + }); + + expect(onCreateReport).toHaveBeenCalledWith(false); + expect(mockOpenCreateReportConfirmation).not.toHaveBeenCalled(); + }); + + it('skips confirmation when shouldBypassConfirmation is true even with empty report', () => { + mockHasEmptyReport = true; + mockHasDismissedConfirmation = undefined; + const onCreateReport = jest.fn(); + + const {result} = renderHook(() => + useConditionalCreateEmptyReportConfirmation({ + policyID, + policyName, + onCreateReport, + shouldBypassConfirmation: true, + }), + ); + + act(() => { + result.current.handleCreateReport(); + }); + + expect(onCreateReport).toHaveBeenCalledWith(false); + expect(mockOpenCreateReportConfirmation).not.toHaveBeenCalled(); + }); + + it('returns hasEmptyReport from the underlying hook', () => { + mockHasEmptyReport = true; + const {result} = renderHook(() => + useConditionalCreateEmptyReportConfirmation({ + policyID, + policyName, + onCreateReport: jest.fn(), + }), + ); + + expect(result.current.hasEmptyReport).toBe(true); + }); + + it('returns hasEmptyReport false when no empty report exists', () => { + mockHasEmptyReport = false; + const {result} = renderHook(() => + useConditionalCreateEmptyReportConfirmation({ + policyID, + policyName, + onCreateReport: jest.fn(), + }), + ); + + expect(result.current.hasEmptyReport).toBe(false); + }); + + it('passes onConfirm callback through to useCreateEmptyReportConfirmation', () => { + mockHasEmptyReport = true; + const onCreateReport = jest.fn(); + + renderHook(() => + useConditionalCreateEmptyReportConfirmation({ + policyID, + policyName, + onCreateReport, + }), + ); + + // Simulate the modal confirming with shouldDismissEmptyReportsConfirmation = true + act(() => { + mockOnConfirmFromModal?.(true); + }); + + expect(onCreateReport).toHaveBeenCalledWith(true); + }); +}); diff --git a/tests/unit/hooks/useFABMenuItemTest.tsx b/tests/unit/hooks/useFABMenuItemTest.tsx new file mode 100644 index 0000000000000..3fb3b08192c3e --- /dev/null +++ b/tests/unit/hooks/useFABMenuItemTest.tsx @@ -0,0 +1,161 @@ +import {act, renderHook} from '@testing-library/react-native'; +import React from 'react'; +import type {PropsWithChildren} from 'react'; +import {FABMenuContext} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import type {FABMenuContextType} from '@pages/inbox/sidebar/FABPopoverContent/FABMenuContext'; +import useFABMenuItem from '@pages/inbox/sidebar/FABPopoverContent/useFABMenuItem'; + +function createMockContext(overrides: Partial = {}): FABMenuContextType { + return { + focusedIndex: -1, + setFocusedIndex: jest.fn(), + onItemPress: jest.fn(), + isVisible: true, + registeredItems: [], + registerItem: jest.fn(), + unregisterItem: jest.fn(), + ...overrides, + }; +} + +function createWrapper(contextValue: FABMenuContextType) { + // eslint-disable-next-line react/function-component-definition + const Wrapper = ({children}: PropsWithChildren) => {children}; + return Wrapper; +} + +describe('useFABMenuItem', () => { + it('registers the item on mount when visible', () => { + const ctx = createMockContext(); + renderHook(() => useFABMenuItem('expense', true), {wrapper: createWrapper(ctx)}); + + expect(ctx.registerItem).toHaveBeenCalledWith('expense'); + }); + + it('does not register the item when not visible', () => { + const ctx = createMockContext(); + renderHook(() => useFABMenuItem('expense', false), {wrapper: createWrapper(ctx)}); + + expect(ctx.registerItem).not.toHaveBeenCalled(); + }); + + it('unregisters the item on unmount', () => { + const ctx = createMockContext(); + const {unmount} = renderHook(() => useFABMenuItem('expense', true), {wrapper: createWrapper(ctx)}); + + unmount(); + + expect(ctx.unregisterItem).toHaveBeenCalledWith('expense'); + }); + + it('does not unregister on unmount when the item was never visible', () => { + const ctx = createMockContext(); + const {unmount} = renderHook(() => useFABMenuItem('expense', false), {wrapper: createWrapper(ctx)}); + + unmount(); + + expect(ctx.unregisterItem).not.toHaveBeenCalled(); + }); + + it('registers when visibility changes from false to true', () => { + const ctx = createMockContext(); + const {rerender} = renderHook(({visible}: {visible: boolean}) => useFABMenuItem('expense', visible), { + wrapper: createWrapper(ctx), + initialProps: {visible: false}, + }); + + expect(ctx.registerItem).not.toHaveBeenCalled(); + + rerender({visible: true}); + + expect(ctx.registerItem).toHaveBeenCalledWith('expense'); + }); + + it('unregisters when visibility changes from true to false', () => { + const ctx = createMockContext(); + const {rerender} = renderHook(({visible}: {visible: boolean}) => useFABMenuItem('expense', visible), { + wrapper: createWrapper(ctx), + initialProps: {visible: true}, + }); + + rerender({visible: false}); + + expect(ctx.unregisterItem).toHaveBeenCalledWith('expense'); + }); + + it('returns correct itemIndex from registeredItems', () => { + const ctx = createMockContext({registeredItems: ['quick-action', 'expense', 'track-distance']}); + const {result} = renderHook(() => useFABMenuItem('expense', true), {wrapper: createWrapper(ctx)}); + + expect(result.current.itemIndex).toBe(1); + }); + + it('returns -1 when item is not in registeredItems', () => { + const ctx = createMockContext({registeredItems: ['quick-action', 'track-distance']}); + const {result} = renderHook(() => useFABMenuItem('expense', true), {wrapper: createWrapper(ctx)}); + + expect(result.current.itemIndex).toBe(-1); + }); + + it('returns isFocused true when focusedIndex matches itemIndex', () => { + const ctx = createMockContext({ + registeredItems: ['quick-action', 'expense', 'track-distance'], + focusedIndex: 1, + }); + const {result} = renderHook(() => useFABMenuItem('expense', true), {wrapper: createWrapper(ctx)}); + + expect(result.current.isFocused).toBe(true); + }); + + it('returns isFocused false when focusedIndex does not match', () => { + const ctx = createMockContext({ + registeredItems: ['quick-action', 'expense', 'track-distance'], + focusedIndex: 0, + }); + const {result} = renderHook(() => useFABMenuItem('expense', true), {wrapper: createWrapper(ctx)}); + + expect(result.current.isFocused).toBe(false); + }); + + it('returns isFocused false when focusedIndex is -1', () => { + const ctx = createMockContext({ + registeredItems: ['quick-action', 'expense'], + focusedIndex: -1, + }); + const {result} = renderHook(() => useFABMenuItem('expense', true), {wrapper: createWrapper(ctx)}); + + expect(result.current.isFocused).toBe(false); + }); + + it('passes through setFocusedIndex from context', () => { + const mockSetFocusedIndex = jest.fn(); + const ctx = createMockContext({setFocusedIndex: mockSetFocusedIndex}); + const {result} = renderHook(() => useFABMenuItem('expense', true), {wrapper: createWrapper(ctx)}); + + act(() => { + result.current.setFocusedIndex(2); + }); + + expect(mockSetFocusedIndex).toHaveBeenCalledWith(2); + }); + + it('passes through onItemPress from context', () => { + const mockOnItemPress = jest.fn(); + const ctx = createMockContext({onItemPress: mockOnItemPress}); + const {result} = renderHook(() => useFABMenuItem('expense', true), {wrapper: createWrapper(ctx)}); + + const callback = jest.fn(); + act(() => { + result.current.onItemPress(callback, {shouldCallAfterModalHide: true}); + }); + + expect(mockOnItemPress).toHaveBeenCalledWith(callback, {shouldCallAfterModalHide: true}); + }); + + it('defaults isVisible to true when not provided', () => { + const ctx = createMockContext(); + renderHook(() => useFABMenuItem('expense'), {wrapper: createWrapper(ctx)}); + + expect(ctx.registerItem).toHaveBeenCalledWith('expense'); + }); +}); From 3a98ff820d036b0da86f52351a6fd75fb7d4bb0e Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Mon, 16 Mar 2026 11:18:25 +0100 Subject: [PATCH 50/54] Make useDragoverDismiss one-shot to avoid redundant state updates The dragover event fires continuously while dragging. Remove the listener after the first fire so dismiss() is only called once. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/hooks/useDragoverDismiss/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/hooks/useDragoverDismiss/index.ts b/src/hooks/useDragoverDismiss/index.ts index 7557cb08fc29b..f4385a0ae3304 100644 --- a/src/hooks/useDragoverDismiss/index.ts +++ b/src/hooks/useDragoverDismiss/index.ts @@ -5,7 +5,10 @@ function useDragoverDismiss(isActive: boolean, dismiss: () => void) { if (!isActive) { return; } - const handler = () => dismiss(); + const handler = () => { + dismiss(); + document.removeEventListener('dragover', handler); + }; document.addEventListener('dragover', handler); return () => document.removeEventListener('dragover', handler); }, [isActive, dismiss]); From ce848a5735a63bb621e1da9925e254ee874f03c4 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Tue, 17 Mar 2026 18:23:42 +0100 Subject: [PATCH 51/54] Restore useful code comments removed during FAB decomposition Co-Authored-By: Claude Opus 4.6 (1M context) --- src/hooks/useCreateEmptyReportConfirmation.tsx | 1 + .../FABPopoverContent/menuItems/CreateReportMenuItem.tsx | 2 ++ .../FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx | 1 + .../FABPopoverContent/useRedirectToExpensifyClassic.ts | 5 +++++ 4 files changed, 9 insertions(+) diff --git a/src/hooks/useCreateEmptyReportConfirmation.tsx b/src/hooks/useCreateEmptyReportConfirmation.tsx index 958439c36dd99..5c71cf309c58c 100644 --- a/src/hooks/useCreateEmptyReportConfirmation.tsx +++ b/src/hooks/useCreateEmptyReportConfirmation.tsx @@ -75,6 +75,7 @@ export default function useCreateEmptyReportConfirmation({policyName, onConfirm, }; showConfirmModal({ + // Adding a space at the end because of this bug in react-native: https://github.com/facebook/react-native/issues/53286 title: `${translate('report.newReport.emptyReportConfirmationTitle')} `, confirmText: translate('report.newReport.createReport'), cancelText: translate('common.cancel'), diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index 997e56a290a14..d900d065697f7 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -128,12 +128,14 @@ function CreateReportMenuItem() { const workspaceIDForReportCreation = defaultChatEnabledPolicyID; + // If we couldn't guess the workspace to create the report, or a guessed workspace is past its grace period and we have other workspaces to choose from if (!workspaceIDForReportCreation || (shouldRestrictUserBillableActions(workspaceIDForReportCreation) && groupPoliciesWithChatEnabled.length > 1)) { Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute()); return; } if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { + // Check if empty report confirmation should be shown if (shouldShowEmptyReportConfirmation) { openCreateReportConfirmation(); } else { diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx index 32fe0c5d7ec07..3f5c54bc572e9 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/TrackDistanceMenuItem.tsx @@ -37,6 +37,7 @@ function TrackDistanceMenuItem({reportID}: TrackDistanceMenuItemProps) { showRedirectToExpensifyClassicModal(); return; } + // Start the flow to start tracking a distance request startDistanceRequest(CONST.IOU.TYPE.CREATE, reportID, draftTransactionIDs, lastDistanceExpenseType, undefined, undefined, true); }) } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts b/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts index 7d65107f32155..557a0fecfeb47 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useRedirectToExpensifyClassic.ts @@ -30,6 +30,11 @@ function useRedirectToExpensifyClassic() { const {translate} = useLocalize(); const {showConfirmModal} = useConfirmModal(); const [isTrackingGPS = false] = useOnyx(ONYXKEYS.GPS_DRAFT_DETAILS, {selector: isTrackingSelector}); + /** + * There are scenarios where users who have not yet had their group workspace-chats in NewDot (isPolicyExpenseChatEnabled). In those scenarios, things can get confusing if they try to submit/track expenses. To address this, we block them from Creating, Tracking, Submitting expenses from NewDot if they are: + * 1. on at least one group policy + * 2. none of the group policies they are a member of have isPolicyExpenseChatEnabled=true + */ const [shouldRedirectToExpensifyClassic = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: shouldRedirectToExpensifyClassicSelector}); const showRedirectToExpensifyClassicModal = async () => { From 274822042dd990a4486c44a741b955d9208ee894 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Tue, 17 Mar 2026 18:38:02 +0100 Subject: [PATCH 52/54] Fix double vertical padding on FAB popover menu (wide layout) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit createMenuContainer includes paddingVertical: 16, but the inner View also applies pv4 (16px), doubling the padding to 32px. Neutralize the container padding with pv0 so only the inner pv4 applies — matching the original PopoverMenu behavior. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx index 0af75dbd56ce6..f5cdc9c3a7757 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx @@ -120,7 +120,7 @@ function FABPopoverMenu({isVisible, onClose, onItemSelected, anchorRef, animatio active={isVisible} shouldReturnFocus > - + {children} From ce9f79d7db309a2a3f81d56df7d27d67118d60d4 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Tue, 17 Mar 2026 18:44:31 +0100 Subject: [PATCH 53/54] =?UTF-8?q?Fix=20Quick=20Action=20position=20?= =?UTF-8?q?=E2=80=94=20move=20to=20bottom=20of=20FAB=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original PopoverMenu places quickActionMenuItems last in the menuItems array. The decomposition incorrectly rendered QuickActionMenuItem first in the children order. Move it to the end and update FAB_ITEM_ORDER to match. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx | 2 +- src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx index f5cdc9c3a7757..d60334b041d8a 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABPopoverMenu.tsx @@ -14,7 +14,6 @@ import CONST from '@src/CONST'; import {FABMenuContext} from './FABMenuContext'; const FAB_ITEM_ORDER = [ - CONST.FAB_MENU_ITEM_IDS.QUICK_ACTION, CONST.FAB_MENU_ITEM_IDS.EXPENSE, CONST.FAB_MENU_ITEM_IDS.TRACK_DISTANCE, CONST.FAB_MENU_ITEM_IDS.CREATE_REPORT, @@ -23,6 +22,7 @@ const FAB_ITEM_ORDER = [ CONST.FAB_MENU_ITEM_IDS.TRAVEL, CONST.FAB_MENU_ITEM_IDS.TEST_DRIVE, CONST.FAB_MENU_ITEM_IDS.NEW_WORKSPACE, + CONST.FAB_MENU_ITEM_IDS.QUICK_ACTION, ] as const; type FABPopoverMenuProps = { diff --git a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx index 3add1408039a3..1353aeb54108d 100644 --- a/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/inbox/sidebar/FloatingActionButtonAndPopover.tsx @@ -69,7 +69,6 @@ function FloatingActionButtonAndPopover() { animationInTiming={CONST.MODAL.ANIMATION_TIMING.FAB_IN} animationOutTiming={CONST.MODAL.ANIMATION_TIMING.FAB_OUT} > - @@ -78,6 +77,7 @@ function FloatingActionButtonAndPopover() { + Date: Tue, 17 Mar 2026 19:13:38 +0100 Subject: [PATCH 54/54] Pass userBillingGraceEndPeriods from useOnyx in decomposed FAB components Adopt upstream change (cd28b2741c1) that passes userBillingGraceEndPeriods and ownerBillingGraceEndPeriod explicitly to shouldRestrictUserBillableActions instead of relying on deprecated module-scoped Onyx connections. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../menuItems/CreateReportMenuItem.tsx | 10 ++++++++-- .../menuItems/QuickActionMenuItem.tsx | 13 +++++++++++-- .../sidebar/FABPopoverContent/useScanActions.ts | 4 +++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx index d900d065697f7..d4a2c55a443fe 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/CreateReportMenuItem.tsx @@ -67,6 +67,8 @@ function CreateReportMenuItem() { const chatEnabledPaidGroupPolicies = (policies: Parameters[0]) => chatEnabledPaidGroupPoliciesSelector(policies, session?.email); const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: chatEnabledPaidGroupPolicies}, [session?.email]); + const [userBillingGraceEndPeriods] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END); + const [ownerBillingGraceEndPeriod] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END); const isVisible = shouldRedirectToExpensifyClassic || groupPoliciesWithChatEnabled.length > 0; @@ -129,12 +131,16 @@ function CreateReportMenuItem() { const workspaceIDForReportCreation = defaultChatEnabledPolicyID; // If we couldn't guess the workspace to create the report, or a guessed workspace is past its grace period and we have other workspaces to choose from - if (!workspaceIDForReportCreation || (shouldRestrictUserBillableActions(workspaceIDForReportCreation) && groupPoliciesWithChatEnabled.length > 1)) { + if ( + !workspaceIDForReportCreation || + (shouldRestrictUserBillableActions(workspaceIDForReportCreation, userBillingGraceEndPeriods, undefined, ownerBillingGraceEndPeriod) && + groupPoliciesWithChatEnabled.length > 1) + ) { Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute()); return; } - if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { + if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation, userBillingGraceEndPeriods, undefined, ownerBillingGraceEndPeriod)) { // Check if empty report confirmation should be shown if (shouldShowEmptyReportConfirmation) { openCreateReportConfirmation(); diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index 6599985f5006a..83a05b9924615 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -59,6 +59,8 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionPolicyID}`); const reportAttributesSelector = (attributes: OnyxEntry) => attributes?.reports; const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: reportAttributesSelector}); + const [userBillingGraceEndPeriods] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END); + const [ownerBillingGraceEndPeriod] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END); const isValidReport = !(isEmptyObject(quickActionReport) || isReportArchived); @@ -105,7 +107,11 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { const quickActionReportPolicyID = quickActionReport?.policyID; const selectOption = (onSelected: () => void, shouldRestrictAction: boolean) => { - if (shouldRestrictAction && quickActionReportPolicyID && shouldRestrictUserBillableActions(quickActionReportPolicyID)) { + if ( + shouldRestrictAction && + quickActionReportPolicyID && + shouldRestrictUserBillableActions(quickActionReportPolicyID, userBillingGraceEndPeriods, undefined, ownerBillingGraceEndPeriod) + ) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(quickActionReportPolicyID)); return; } @@ -180,7 +186,10 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { rightIconReportID={policyChatForActivePolicy?.reportID} onPress={() => interceptAnonymousUser(() => { - if (policyChatForActivePolicy?.policyID && shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID)) { + if ( + policyChatForActivePolicy?.policyID && + shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID, userBillingGraceEndPeriods, undefined, ownerBillingGraceEndPeriod) + ) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatForActivePolicy.policyID)); return; } diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts index 208580eaf69fa..17a667d4cdceb 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts +++ b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts @@ -25,6 +25,8 @@ function useScanActions() { const workspaceChatsSelector = (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports); const [policyChats = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector}); + const [userBillingGraceEndPeriods] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END); + const [ownerBillingGraceEndPeriod] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END); const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); // useState lazy initializer generates the ID once on mount and keeps it stable across renders @@ -48,7 +50,7 @@ function useScanActions() { const startQuickScan = () => { interceptAnonymousUser(() => { - if (policyChatPolicyID && shouldRestrictUserBillableActions(policyChatPolicyID)) { + if (policyChatPolicyID && shouldRestrictUserBillableActions(policyChatPolicyID, userBillingGraceEndPeriods, undefined, ownerBillingGraceEndPeriod)) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatPolicyID)); return; }