Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
edcc094
initial refactor of secondary actions
TMisiukiewicz Apr 7, 2026
caee100
Extract MoneyReportHeaderHoldMenu gate with early return
TMisiukiewicz Apr 7, 2026
30f290d
Deduplicate types, extract useTransactionThreadReport, wire up Action…
TMisiukiewicz Apr 8, 2026
790214c
ActionsBar renders sub-components directly, no ReactNode props
TMisiukiewicz Apr 8, 2026
1ad3d28
Merge remote-tracking branch 'origin/main' into perf/extract-money-re…
TMisiukiewicz Apr 8, 2026
07205df
Collapse ActionsBar into index, adopt upstream modals context
TMisiukiewicz Apr 8, 2026
ec34c1f
cleanup MoneyReportHeader
TMisiukiewicz Apr 8, 2026
3e9c1ec
Extract useReportPrimaryAction, remove animation knowledge from getRe…
TMisiukiewicz Apr 8, 2026
2361cab
Remove dead isSelectionModePaymentRef and unused selectedTransactionIDs
TMisiukiewicz Apr 8, 2026
e81ea4c
Restore clearSelectedTransactions and hold menu onConfirm callbacks
TMisiukiewicz Apr 8, 2026
2b49674
Pass isReportInSearch to useExpenseActions instead of hardcoding false
TMisiukiewicz Apr 8, 2026
926e9be
update MoneyReportHeaderActions
TMisiukiewicz Apr 8, 2026
e74d1a7
updateMoneyReportHeaderActions
TMisiukiewicz Apr 8, 2026
f20ccf4
create animation context
TMisiukiewicz Apr 8, 2026
0228a9e
Replace modal handler props with direct context consumption
TMisiukiewicz Apr 8, 2026
96b9eac
prettier
TMisiukiewicz Apr 8, 2026
d7841e4
Rename shouldShow booleans to descriptive hasX/isX naming
TMisiukiewicz Apr 8, 2026
20ffd04
Replace deprecated MutableRefObject with RefObject
TMisiukiewicz Apr 8, 2026
df29a4b
Fix isOnSearch, backTo route, and missing duplicate expense guards
TMisiukiewicz Apr 8, 2026
7275d98
Merge remote-tracking branch 'origin/main' into perf/extract-money-re…
TMisiukiewicz Apr 8, 2026
a24ee65
Merge remote-tracking branch 'origin/main' into perf/extract-money-re…
TMisiukiewicz Apr 9, 2026
4c8dcfc
update MoneyReportHeaderSecondaryActions
TMisiukiewicz Apr 9, 2026
87ab763
update MoneyReportHeaderSelectionDropdown
TMisiukiewicz Apr 9, 2026
58b5f43
update secondary actions
TMisiukiewicz Apr 9, 2026
cdabe00
update useExpenseActions
TMisiukiewicz Apr 9, 2026
2faecc9
update hold reject actions
TMisiukiewicz Apr 9, 2026
c614e6c
update useLifecycleActions
TMisiukiewicz Apr 9, 2026
a6b06bf
cleanup MoneyReportHeader
TMisiukiewicz Apr 9, 2026
efaaed1
Merge branch 'main' into perf/extract-money-report-header-actions
TMisiukiewicz Apr 9, 2026
c46c02b
Merge remote-tracking branch 'origin/main' into perf/extract-money-re…
TMisiukiewicz Apr 9, 2026
2d95972
Add delegateEmail to approve/selectPaymentType calls after merge
TMisiukiewicz Apr 9, 2026
2e1736d
fix codex comments
TMisiukiewicz Apr 9, 2026
43b7c57
Merge origin/main into perf/extract-money-report-header-actions
TMisiukiewicz Apr 10, 2026
a577b13
Propagate main changes into extracted MoneyReportHeaderSelectionDropdown
TMisiukiewicz Apr 10, 2026
f6fcd9c
Fix archived chat check and hold guard in selection dropdown
TMisiukiewicz Apr 10, 2026
dee3d1a
fix prettier
TMisiukiewicz Apr 10, 2026
cfb56c2
address CR comments
TMisiukiewicz Apr 10, 2026
79ee634
Merge remote-tracking branch 'origin/main' into perf/extract-money-re…
TMisiukiewicz Apr 13, 2026
2de9329
fix: address PR review feedback for MoneyReportHeader decomposition
TMisiukiewicz Apr 13, 2026
669ee47
CR comments
TMisiukiewicz Apr 14, 2026
7832945
Merge remote-tracking branch 'origin/main' into perf/extract-money-re…
TMisiukiewicz Apr 14, 2026
2ec79e7
Merge remote-tracking branch 'origin/main' into perf/extract-money-re…
TMisiukiewicz Apr 14, 2026
5220aed
CR Updates
TMisiukiewicz Apr 14, 2026
17c3333
Merge remote-tracking branch 'origin/main' into perf/extract-money-re…
TMisiukiewicz Apr 15, 2026
87d8edc
remove non-reimbursable payment modal logic deleted on main
TMisiukiewicz Apr 15, 2026
d915471
address PR review comments
TMisiukiewicz Apr 15, 2026
e9a5713
Add isAccountLocked guard to secondary actions payment flow and JSDoc…
TMisiukiewicz Apr 15, 2026
d02a6af
Remove unnecessary checkForNecessaryAction from onPaymentSelect
TMisiukiewicz Apr 15, 2026
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
2,044 changes: 54 additions & 1,990 deletions src/components/MoneyReportHeader.tsx

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

101 changes: 101 additions & 0 deletions src/components/MoneyReportHeaderActions/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, {useEffect, useRef} from 'react';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import type {ButtonWithDropdownMenuRef} from '@components/ButtonWithDropdownMenu/types';
import MoneyReportHeaderPrimaryAction from '@components/MoneyReportHeaderPrimaryAction';
import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext';
import useExportAgainModal from '@hooks/useExportAgainModal';
import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
import useThemeStyles from '@hooks/useThemeStyles';
import useTransactionThreadReport from '@hooks/useTransactionThreadReport';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import MoneyReportHeaderSecondaryActions from './MoneyReportHeaderSecondaryActions';
import MoneyReportHeaderSelectionDropdown from './MoneyReportHeaderSelectionDropdown';
import type {MoneyReportHeaderActionsProps} from './types';

/**
* Narrow the wide primaryAction union to what report-level secondary actions accept.
* TRANSACTION_PRIMARY_ACTIONS values (e.g. "keepThisOne") are irrelevant here.
*/
function narrowPrimaryAction(primaryAction: MoneyReportHeaderActionsProps['primaryAction']): ValueOf<typeof CONST.REPORT.PRIMARY_ACTIONS> | '' {
if ((Object.values(CONST.REPORT.PRIMARY_ACTIONS) as string[]).includes(primaryAction)) {
return primaryAction as ValueOf<typeof CONST.REPORT.PRIMARY_ACTIONS>;
}
return '';
}

function MoneyReportHeaderActions({reportID, primaryAction, isReportInSearch, backTo}: MoneyReportHeaderActionsProps) {
const styles = useThemeStyles();
const dropdownMenuRef = useRef<ButtonWithDropdownMenuRef>(null) as React.RefObject<ButtonWithDropdownMenuRef>;

// We need isSmallScreenWidth for the hold expense modal layout https://github.com/Expensify/App/pull/47990#issuecomment-2362382026
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout();
const shouldDisplayNarrowVersion = shouldUseNarrowLayout || isMediumScreenWidth;
const {isWideRHPDisplayedOnWideLayout, isSuperWideRHPDisplayedOnWideLayout} = useResponsiveLayoutOnWideRHP();
const shouldDisplayNarrowMoreButton = !shouldDisplayNarrowVersion || isWideRHPDisplayedOnWideLayout || isSuperWideRHPDisplayedOnWideLayout;

const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(moneyRequestReport?.chatReportID)}`);

const {transactionThreadReportID} = useTransactionThreadReport(reportID);

const {triggerExportOrConfirm} = useExportAgainModal(moneyRequestReport?.reportID, moneyRequestReport?.policyID);

const {selectedTransactionIDs} = useSearchStateContext();
const {clearSelectedTransactions} = useSearchActionsContext();
const hasSelectedTransactions = !!selectedTransactionIDs.length;
const isTransactionThread = !!transactionThreadReportID;

useEffect(() => {
if (!transactionThreadReportID) {
return;
}

clearSelectedTransactions(true);
}, [transactionThreadReportID]); // eslint-disable-line react-hooks/exhaustive-deps

const narrowedPrimaryAction = narrowPrimaryAction(primaryAction);

if (hasSelectedTransactions && !isTransactionThread) {
Comment thread
TMisiukiewicz marked this conversation as resolved.
Comment thread
TMisiukiewicz marked this conversation as resolved.
return (
<View style={shouldDisplayNarrowMoreButton ? undefined : [styles.dFlex, styles.w100, styles.ph5, styles.pb3]}>
<MoneyReportHeaderSelectionDropdown
reportID={reportID}
primaryAction={narrowedPrimaryAction}
isReportInSearch={isReportInSearch}
wrapperStyle={shouldDisplayNarrowMoreButton ? undefined : styles.w100}
/>
</View>
);
}

return (
<View style={[styles.flexRow, styles.gap2, ...(!shouldDisplayNarrowMoreButton ? [styles.pb3, styles.ph5, styles.w100, styles.alignItemsCenter, styles.justifyContentCenter] : [])]}>
{!!primaryAction && (
<View style={!shouldDisplayNarrowMoreButton ? [styles.flex1] : undefined}>
<MoneyReportHeaderPrimaryAction
reportID={reportID}
chatReportID={chatReport?.reportID}
primaryAction={primaryAction}
onExportModalOpen={() => triggerExportOrConfirm(CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION)}
/>
</View>
)}
<MoneyReportHeaderSecondaryActions
Comment thread
TMisiukiewicz marked this conversation as resolved.
reportID={reportID}
primaryAction={narrowedPrimaryAction}
isReportInSearch={isReportInSearch}
backTo={backTo}
dropdownMenuRef={dropdownMenuRef}
/>
</View>
);
}

export default MoneyReportHeaderActions;
export type {MoneyReportHeaderActionsProps};
16 changes: 16 additions & 0 deletions src/components/MoneyReportHeaderActions/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type {ValueOf} from 'type-fest';
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import type CONST from '@src/CONST';
import type {Route} from '@src/ROUTES';

type SecondaryActionEntry = DropdownOption<ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>> & Pick<PopoverMenuItem, 'backButtonText' | 'rightIcon'>;

type MoneyReportHeaderActionsProps = {
reportID: string | undefined;
primaryAction: ValueOf<typeof CONST.REPORT.PRIMARY_ACTIONS> | ValueOf<typeof CONST.REPORT.TRANSACTION_PRIMARY_ACTIONS> | '';
isReportInSearch?: boolean;
backTo?: Route;
};

export type {SecondaryActionEntry, MoneyReportHeaderActionsProps};
Comment thread
TMisiukiewicz marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import Button from '@components/Button';
import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
Expand All @@ -9,10 +10,10 @@ import useConfirmApproval from './useConfirmApproval';

type ApprovePrimaryActionProps = {
reportID: string | undefined;
startApprovedAnimation: () => void;
};

function ApprovePrimaryAction({reportID, startApprovedAnimation}: ApprovePrimaryActionProps) {
function ApprovePrimaryAction({reportID}: ApprovePrimaryActionProps) {
const {startApprovedAnimation} = usePaymentAnimationsContext();
const {translate} = useLocalize();

const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {hasSeenTourSelector} from '@selectors/Onboarding';
import React from 'react';
import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider';
import {useMoneyReportHeaderModals} from '@components/MoneyReportHeaderModalsContext';
import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext';
import {useSearchStateContext} from '@components/Search/SearchContext';
import AnimatedSettlementButton from '@components/SettlementButton/AnimatedSettlementButton';
import type {PaymentActionParams} from '@components/SettlementButton/types';
Expand Down Expand Up @@ -29,14 +30,10 @@ import useTransactionThreadData from './useTransactionThreadData';
type PayPrimaryActionProps = {
reportID: string | undefined;
chatReportID: string | undefined;
isPaidAnimationRunning: boolean;
isApprovedAnimationRunning: boolean;
stopAnimation: () => void;
startAnimation: () => void;
startApprovedAnimation: () => void;
};

function PayPrimaryAction({reportID, chatReportID, isPaidAnimationRunning, isApprovedAnimationRunning, stopAnimation, startAnimation, startApprovedAnimation}: PayPrimaryActionProps) {
function PayPrimaryAction({reportID, chatReportID}: PayPrimaryActionProps) {
const {isPaidAnimationRunning, isApprovedAnimationRunning, stopAnimation, startAnimation, startApprovedAnimation} = usePaymentAnimationsContext();
const {isOffline} = useNetwork();
const {accountID, email} = useCurrentUserPersonalDetails();
const {isDelegateAccessRestricted} = useDelegateNoAccessState();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {delegateEmailSelector} from '@selectors/Account';
import React from 'react';
import AnimatedSubmitButton from '@components/AnimatedSubmitButton';
import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext';
import {useSearchStateContext} from '@components/Search/SearchContext';
import useConfirmPendingRTERAndProceed from '@hooks/useConfirmPendingRTERAndProceed';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
Expand All @@ -24,12 +25,10 @@ import ONYXKEYS from '@src/ONYXKEYS';

type SubmitPrimaryActionProps = {
reportID: string | undefined;
isSubmittingAnimationRunning: boolean;
stopAnimation: () => void;
startSubmittingAnimation: () => void;
};

function SubmitPrimaryAction({reportID, isSubmittingAnimationRunning, stopAnimation, startSubmittingAnimation}: SubmitPrimaryActionProps) {
function SubmitPrimaryAction({reportID}: SubmitPrimaryActionProps) {
const {isSubmittingAnimationRunning, stopAnimation, startSubmittingAnimation} = usePaymentAnimationsContext();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
const {accountID, email} = useCurrentUserPersonalDetails();
Expand Down
35 changes: 3 additions & 32 deletions src/components/MoneyReportHeaderPrimaryAction/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,53 +10,24 @@ import ReviewDuplicatesPrimaryAction from './ReviewDuplicatesPrimaryAction';
import SubmitPrimaryAction from './SubmitPrimaryAction';
import type {MoneyReportHeaderPrimaryActionProps} from './types';

function MoneyReportHeaderPrimaryAction({
reportID,
chatReportID,
primaryAction,
isPaidAnimationRunning,
isApprovedAnimationRunning,
isSubmittingAnimationRunning,
stopAnimation,
startAnimation,
startApprovedAnimation,
startSubmittingAnimation,
onExportModalOpen,
}: MoneyReportHeaderPrimaryActionProps) {
function MoneyReportHeaderPrimaryAction({reportID, chatReportID, primaryAction, onExportModalOpen}: MoneyReportHeaderPrimaryActionProps) {
if (!primaryAction) {
return null;
}

if (primaryAction === CONST.REPORT.PRIMARY_ACTIONS.SUBMIT) {
return (
<SubmitPrimaryAction
reportID={reportID}
isSubmittingAnimationRunning={isSubmittingAnimationRunning}
stopAnimation={stopAnimation}
startSubmittingAnimation={startSubmittingAnimation}
/>
);
return <SubmitPrimaryAction reportID={reportID} />;
}

if (primaryAction === CONST.REPORT.PRIMARY_ACTIONS.APPROVE) {
return (
<ApprovePrimaryAction
reportID={reportID}
startApprovedAnimation={startApprovedAnimation}
/>
);
return <ApprovePrimaryAction reportID={reportID} />;
}

if (primaryAction === CONST.REPORT.PRIMARY_ACTIONS.PAY) {
return (
<PayPrimaryAction
reportID={reportID}
chatReportID={chatReportID}
isPaidAnimationRunning={isPaidAnimationRunning}
isApprovedAnimationRunning={isApprovedAnimationRunning}
stopAnimation={stopAnimation}
startAnimation={startAnimation}
startApprovedAnimation={startApprovedAnimation}
/>
);
}
Expand Down
7 changes: 0 additions & 7 deletions src/components/MoneyReportHeaderPrimaryAction/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@ type MoneyReportHeaderPrimaryActionProps = {
reportID: string | undefined;
chatReportID: string | undefined;
primaryAction: ValueOf<typeof CONST.REPORT.PRIMARY_ACTIONS> | ValueOf<typeof CONST.REPORT.TRANSACTION_PRIMARY_ACTIONS> | '';
isPaidAnimationRunning: boolean;
isApprovedAnimationRunning: boolean;
isSubmittingAnimationRunning: boolean;
stopAnimation: () => void;
startAnimation: () => void;
startApprovedAnimation: () => void;
startSubmittingAnimation: () => void;
onExportModalOpen: () => void;
};

Expand Down
21 changes: 21 additions & 0 deletions src/components/PaymentAnimationsContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, {createContext, useContext} from 'react';
import usePaymentAnimations from '@hooks/usePaymentAnimations';

type PaymentAnimationsContextType = ReturnType<typeof usePaymentAnimations>;

const PaymentAnimationsContext = createContext<PaymentAnimationsContextType | null>(null);

function usePaymentAnimationsContext(): PaymentAnimationsContextType {
const context = useContext(PaymentAnimationsContext);
if (!context) {
throw new Error('usePaymentAnimationsContext must be used within a PaymentAnimationsProvider');
}
return context;
}

function PaymentAnimationsProvider({children}: {children: React.ReactNode}) {
const animations = usePaymentAnimations();
return <PaymentAnimationsContext.Provider value={animations}>{children}</PaymentAnimationsContext.Provider>;
}

export {PaymentAnimationsProvider, usePaymentAnimationsContext};
Loading
Loading