Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions src/libs/actions/Scan.ts
Original file line number Diff line number Diff line change
@@ -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<Session>;
Onyx.connectWithoutView({
key: ONYXKEYS.SESSION,
callback: (value) => {
session = value;
},
});

let activePolicyID: OnyxEntry<string>;
Onyx.connectWithoutView({
key: ONYXKEYS.NVP_ACTIVE_POLICY_ID,
callback: (value) => {
activePolicyID = value;
},
});

let allPolicies: OnyxCollection<Policy>;
Onyx.connectWithoutView({
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => {
allPolicies = value;
},
});

let allTransactionDrafts: OnyxCollection<Transaction>;
Onyx.connectWithoutView({
key: ONYXKEYS.COLLECTION.TRANSACTION_DRAFT,
waitForCollectionCallback: true,
callback: (value) => {
allTransactionDrafts = value;
},
});

let ownerBillingGracePeriodEnd: OnyxEntry<number>;
Onyx.connectWithoutView({
key: ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END,
callback: (value) => {
ownerBillingGracePeriodEnd = value;
},
});

let userBillingGracePeriodEnds: OnyxCollection<BillingGraceEndPeriod>;
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<string[]>((acc, draft) => {
if (draft) {
acc.push(draft.transactionID);
}
return acc;
}, []);
}

function getPolicyChatForActivePolicy(): OnyxEntry<Report> {
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};
25 changes: 21 additions & 4 deletions src/pages/inbox/sidebar/FABPopoverContent/FABButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,15 +17,31 @@ 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 (
<>
{!shouldUseNarrowLayout && (
<FloatingReceiptButton
accessibilityLabel={translate('sidebarScreen.fabScanReceiptExplained')}
role={CONST.ROLE.BUTTON}
onPress={startQuickScan}
onPress={handleQuickScan}
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.FLOATING_RECEIPT_BUTTON}
/>
)}
Expand All @@ -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}
/>
</>
Expand Down
67 changes: 0 additions & 67 deletions src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts

This file was deleted.

Loading