diff --git a/src/libs/actions/Scan.ts b/src/libs/actions/Scan.ts new file mode 100644 index 0000000000000..83bb6cc60c7a4 --- /dev/null +++ b/src/libs/actions/Scan.ts @@ -0,0 +1,125 @@ +import Onyx from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import Navigation from '@libs/Navigation/Navigation'; +import {generateReportID, getWorkspaceChats} from '@libs/ReportUtils'; +import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {BillingGraceEndPeriod, Policy, Report, Session, Transaction} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {startMoneyRequest} from './IOU'; +import Tab from './Tab'; + +// Module-level subscriptions — always current, zero cost to read at press time. +// These are not connected to any UI, so `Onyx.connectWithoutView` is appropriate. + +let session: OnyxEntry; +Onyx.connectWithoutView({ + key: ONYXKEYS.SESSION, + callback: (value) => { + session = value; + }, +}); + +let activePolicyID: OnyxEntry; +Onyx.connectWithoutView({ + key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, + callback: (value) => { + activePolicyID = value; + }, +}); + +let allPolicies: OnyxCollection; +Onyx.connectWithoutView({ + key: ONYXKEYS.COLLECTION.POLICY, + waitForCollectionCallback: true, + callback: (value) => { + allPolicies = value; + }, +}); + +let allTransactionDrafts: OnyxCollection; +Onyx.connectWithoutView({ + key: ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, + waitForCollectionCallback: true, + callback: (value) => { + allTransactionDrafts = value; + }, +}); + +let ownerBillingGracePeriodEnd: OnyxEntry; +Onyx.connectWithoutView({ + key: ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END, + callback: (value) => { + ownerBillingGracePeriodEnd = value; + }, +}); + +let userBillingGracePeriodEnds: OnyxCollection; +Onyx.connectWithoutView({ + key: ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END, + waitForCollectionCallback: true, + callback: (value) => { + userBillingGracePeriodEnds = value; + }, +}); + +function getDraftTransactionIDs(): string[] { + return Object.values(allTransactionDrafts ?? {}).reduce((acc, draft) => { + if (draft) { + acc.push(draft.transactionID); + } + return acc; + }, []); +} + +function getPolicyChatForActivePolicy(): OnyxEntry { + if (!activePolicyID || !session?.accountID) { + return undefined; + } + + const activePolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`]; + if (isEmptyObject(activePolicy) || !activePolicy?.isPolicyExpenseChatEnabled) { + return undefined; + } + + const policyChats = getWorkspaceChats(activePolicyID, [session.accountID]); + return policyChats.at(0) ?? undefined; +} + +/** + * Start a scan request (used by FAB long-press). + * Reads all necessary data from module-level Onyx subscriptions — no hooks needed in the component. + */ +function startScan() { + interceptAnonymousUser(() => { + const reportID = generateReportID(); + startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, getDraftTransactionIDs(), CONST.IOU.REQUEST_TYPE.SCAN, false, undefined, true); + }); +} + +/** + * Start a quick scan request (used by FloatingReceiptButton press). + * Reads all necessary data from module-level Onyx subscriptions — no hooks needed in the component. + */ +function startQuickScan() { + interceptAnonymousUser(() => { + const reportID = generateReportID(); + const policyChat = getPolicyChatForActivePolicy(); + const policyChatPolicyID = policyChat?.policyID; + const policyChatReportID = policyChat?.reportID; + + if (policyChatPolicyID && shouldRestrictUserBillableActions(policyChatPolicyID, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds)) { + 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, getDraftTransactionIDs(), CONST.IOU.REQUEST_TYPE.SCAN, !!policyChatReportID, undefined, true); + }); +} + +export {startScan, startQuickScan}; diff --git a/src/pages/inbox/sidebar/FABPopoverContent/FABButtons.tsx b/src/pages/inbox/sidebar/FABPopoverContent/FABButtons.tsx index d5ee5153e96ba..b61bc467c7841 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/FABButtons.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/FABButtons.tsx @@ -4,8 +4,9 @@ import FloatingActionButton from '@components/FloatingActionButton'; import FloatingReceiptButton from '@components/FloatingReceiptButton'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import {startQuickScan, startScan} from '@libs/actions/Scan'; import CONST from '@src/CONST'; -import useScanActions from './useScanActions'; +import useRedirectToExpensifyClassic from './useRedirectToExpensifyClassic'; type FABButtonsProps = { isActive: boolean; @@ -16,7 +17,23 @@ type FABButtonsProps = { function FABButtons({isActive, fabRef, onPress}: FABButtonsProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const {startScan, startQuickScan} = useScanActions(); + const {shouldRedirectToExpensifyClassic, showRedirectToExpensifyClassicModal} = useRedirectToExpensifyClassic(); + + const handleScan = () => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + startScan(); + }; + + const handleQuickScan = () => { + if (shouldRedirectToExpensifyClassic) { + showRedirectToExpensifyClassicModal(); + return; + } + startQuickScan(); + }; return ( <> @@ -24,7 +41,7 @@ function FABButtons({isActive, fabRef, onPress}: FABButtonsProps) { )} @@ -34,7 +51,7 @@ function FABButtons({isActive, fabRef, onPress}: FABButtonsProps) { isActive={isActive} ref={fabRef} onPress={onPress} - onLongPress={startScan} + onLongPress={handleScan} sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.FLOATING_ACTION_BUTTON} /> diff --git a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts b/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts deleted file mode 100644 index 3b9b3614be12f..0000000000000 --- a/src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts +++ /dev/null @@ -1,67 +0,0 @@ -import {useState} from 'react'; -import type {OnyxCollection} 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 {sessionEmailAndAccountIDSelector} from '@src/selectors/Session'; -import {validTransactionDraftIDsSelector} from '@src/selectors/TransactionDraft'; -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'; - -function useScanActions() { - const [session] = useOnyx(ONYXKEYS.SESSION, {selector: sessionEmailAndAccountIDSelector}); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); - const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); - const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); - const workspaceChatsSelector = (reports: OnyxCollection) => getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports); - const [policyChats = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: workspaceChatsSelector}); - - const [userBillingGracePeriodEnds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END); - const [ownerBillingGracePeriodEnd] = 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 - 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 = () => { - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - showRedirectToExpensifyClassicModal(); - return; - } - startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, draftTransactionIDs, CONST.IOU.REQUEST_TYPE.SCAN, false, undefined, true); - }); - }; - - const policyChatPolicyID = policyChatForActivePolicy?.policyID; - const policyChatReportID = policyChatForActivePolicy?.reportID; - - const startQuickScan = () => { - interceptAnonymousUser(() => { - if (policyChatPolicyID && shouldRestrictUserBillableActions(policyChatPolicyID, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds)) { - 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, draftTransactionIDs, CONST.IOU.REQUEST_TYPE.SCAN, !!policyChatReportID, undefined, true); - }); - }; - - return {startScan, startQuickScan}; -} - -export default useScanActions;