Skip to content
Open
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
100 changes: 59 additions & 41 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@
import AnimatedSubmitButton from './AnimatedSubmitButton';
import BrokenConnectionDescription from './BrokenConnectionDescription';
import Button from './Button';
import ButtonWithDropdownMenu from './ButtonWithDropdownMenu';
import type {DropdownOption} from './ButtonWithDropdownMenu/types';
import DecisionModal from './DecisionModal';
import {DelegateNoAccessContext} from './DelegateNoAccessModalProvider';
Expand All @@ -133,7 +132,6 @@
import HoldSubmitterEducationalModal from './HoldSubmitterEducationalModal';
import Icon from './Icon';
import {KYCWallContext} from './KYCWall/KYCWallContext';
import type {PaymentMethod} from './KYCWall/types';
import LoadingBar from './LoadingBar';
import Modal from './Modal';
import {ModalActions} from './Modal/Global/ModalContext';
Expand All @@ -149,6 +147,7 @@
import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu';
import {useSearchContext} from './Search/SearchContext';
import AnimatedSettlementButton from './SettlementButton/AnimatedSettlementButton';
import type {PaymentActionParams} from './SettlementButton/types';
import Text from './Text';
import {WideRHPContext} from './WideRHPContextProvider';

Expand Down Expand Up @@ -431,18 +430,6 @@
);

const [offlineModalVisible, setOfflineModalVisible] = useState(false);
const isOnSearch = route.name.toLowerCase().startsWith('search');
const {options: originalSelectedTransactionsOptions, handleDeleteTransactions} = useSelectedTransactionsActions({
report: moneyRequestReport,
reportActions,
allTransactionsLength: transactions.length,
session,
onExportFailed: () => setIsDownloadErrorModalVisible(true),
onExportOffline: () => setOfflineModalVisible(true),
policy,
beginExportWithTemplate: (templateName, templateType, transactionIDList, policyID) => beginExportWithTemplate(templateName, templateType, transactionIDList, policyID),
isOnSearch,
});

const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]);
const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]);
Expand Down Expand Up @@ -491,11 +478,11 @@

const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID);
const confirmPayment = useCallback(
(type?: PaymentMethodType | undefined, payAsBusiness?: boolean, methodID?: number, paymentMethod?: PaymentMethod) => {
if (!type || !chatReport) {
({paymentType: selectedPaymentType, payAsBusiness, methodID, paymentMethod, skipAnimation}: PaymentActionParams) => {
if (!selectedPaymentType || !chatReport) {
return;
}
setPaymentType(type);
setPaymentType(selectedPaymentType);
setRequestType(CONST.IOU.REPORT_ACTION_TYPE.PAY);
if (isDelegateAccessRestricted) {
showDelegateNoAccessModal();
Expand All @@ -507,9 +494,12 @@
setIsHoldMenuVisible(true);
}
} else if (isInvoiceReport) {
startAnimation();
// Only start animation when skipAnimation is not set (default behavior for header button)
if (!skipAnimation) {
startAnimation();
}
payInvoice({
paymentMethodType: type,
paymentMethodType: selectedPaymentType,
chatReport,
invoiceReport: moneyRequestReport,
introSelected,
Expand All @@ -522,8 +512,11 @@
activePolicy,
});
} else {
startAnimation();
payMoneyRequest(type, chatReport, moneyRequestReport, introSelected, undefined, true, activePolicy);
// Only start animation when skipAnimation is not set (default behavior for header button)
if (!skipAnimation) {
startAnimation();
}
payMoneyRequest(selectedPaymentType, chatReport, moneyRequestReport, introSelected, undefined, true, activePolicy);
if (currentSearchQueryJSON && !isOffline) {
search({
searchKey: currentSearchKey,
Expand Down Expand Up @@ -570,7 +563,7 @@
}
};

const confirmApproval = () => {
const confirmApproval = (skipAnimation?: boolean) => {
if (hasDynamicExternalWorkflow(policy)) {
showDWEModal();
return;
Expand All @@ -581,7 +574,9 @@
} else if (isAnyTransactionOnHold) {
setIsHoldMenuVisible(true);
} else {
startApprovedAnimation();
if (!skipAnimation) {
startApprovedAnimation();
}
approveMoneyRequest(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, true);
}
};
Expand Down Expand Up @@ -911,7 +906,7 @@
[CONST.REPORT.PRIMARY_ACTIONS.APPROVE]: (
<Button
success
onPress={confirmApproval}
onPress={() => confirmApproval()}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-4 (docs)

Creating inline arrow functions as props causes unnecessary re-renders because a new function reference is created on every render. React uses referential equality to determine if props changed, so even if the logic is identical, the new function instance triggers re-renders of child components.

Wrap this function in useCallback to preserve referential stability:

const handleConfirmApproval = useCallback(() => confirmApproval(), [confirmApproval]);

// Then use it in the Button:
onPress={handleConfirmApproval}

Note: For this to be fully effective, confirmApproval itself should also be wrapped in useCallback.

text={translate('iou.approve')}
isDisabled={isBlockSubmitDueToPreventSelfApproval}
/>
Expand Down Expand Up @@ -1090,7 +1085,7 @@
</Text>
);

const secondaryActionsImplementation: Record<

Check warning on line 1088 in src/components/MoneyReportHeader.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

The 'secondaryActionsImplementation' object makes the dependencies of useMemo Hook (at line 1479) change on every render. To fix this, wrap the initialization of 'secondaryActionsImplementation' in its own useMemo() Hook

Check warning on line 1088 in src/components/MoneyReportHeader.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

The 'secondaryActionsImplementation' object makes the dependencies of useMemo Hook (at line 1479) change on every render. To fix this, wrap the initialization of 'secondaryActionsImplementation' in its own useMemo() Hook
ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>,
DropdownOption<ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>> & Pick<PopoverMenuItem, 'backButtonText' | 'rightIcon'>
> = {
Expand Down Expand Up @@ -1145,7 +1140,7 @@
icon: expensifyIcons.ThumbsUp,
value: CONST.REPORT.SECONDARY_ACTIONS.APPROVE,
sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.APPROVE,
onSelected: confirmApproval,
onSelected: () => confirmApproval(true),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-4 (docs)

Creating inline arrow functions in object literals that are recreated on every render causes unnecessary recomputations of dependent values. The secondaryActionsImplementation object is recreated on every render, and since applicableTransactionActions depends on it via useMemo, it will also be recreated, defeating the purpose of memoization.

To fix this, wrap secondaryActionsImplementation in useMemo or extract stable callback references:

const handleApproveWithSkipAnimation = useCallback(() => confirmApproval(true), [confirmApproval]);

// Then use in the object:
[CONST.REPORT.SECONDARY_ACTIONS.APPROVE]: {
    text: translate(iou.approve),
    icon: expensifyIcons.ThumbsUp,
    value: CONST.REPORT.SECONDARY_ACTIONS.APPROVE,
    sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.APPROVE,
    onSelected: handleApproveWithSkipAnimation,
}

Alternatively, wrap the entire secondaryActionsImplementation object in useMemo with appropriate dependencies.

},
[CONST.REPORT.SECONDARY_ACTIONS.UNAPPROVE]: {
text: translate('iou.unapprove'),
Expand Down Expand Up @@ -1474,6 +1469,28 @@
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [transactionThreadReportID]);

const applicableTransactionActions = useMemo(() => {
const allowedActions = [CONST.REPORT.PRIMARY_ACTIONS.SUBMIT, CONST.REPORT.PRIMARY_ACTIONS.APPROVE, CONST.REPORT.PRIMARY_ACTIONS.PAY];
return allowedActions
.filter((actionType) => {
return actionType === primaryAction || secondaryActions.includes(actionType);
})
.map((actionType) => secondaryActionsImplementation[actionType]);
}, [primaryAction, secondaryActions, secondaryActionsImplementation]);

const isOnSearch = route.name.toLowerCase().startsWith('search');
const {options: originalSelectedTransactionsOptions, handleDeleteTransactions} = useSelectedTransactionsActions({
report: moneyRequestReport,
reportActions,
allTransactionsLength: transactions.length,
session,
onExportFailed: () => setIsDownloadErrorModalVisible(true),
onExportOffline: () => setOfflineModalVisible(true),
policy,
beginExportWithTemplate: (templateName, templateType, transactionIDList, policyID) => beginExportWithTemplate(templateName, templateType, transactionIDList, policyID),
reportLevelActions: applicableTransactionActions,
isOnSearch,
});
useEffect(() => {
if (!hasFinishedPDFDownload || !canTriggerAutomaticPDFDownload.current) {
return;
Expand Down Expand Up @@ -1625,36 +1642,37 @@
chatReportID={chatReport?.reportID}
iouReport={moneyRequestReport}
onPaymentSelect={onPaymentSelect}
onSuccessfulKYC={(payment) => confirmPayment(payment)}
onSuccessfulKYC={(kycPaymentType) => confirmPayment({paymentType: kycPaymentType})}
primaryAction={primaryAction}
applicableSecondaryActions={applicableSecondaryActions}
ref={kycWallRef}
/>
)}
{shouldShowSelectedTransactionsButton && (
<View>
<ButtonWithDropdownMenu
onPress={() => null}
options={selectedTransactionsOptions}
customText={translate('workspace.common.selected', {count: selectedTransactionIDs.length})}
isSplitButton={false}
shouldAlwaysShowDropdownMenu
/>
</View>
<MoneyReportHeaderKYCDropdown
chatReportID={chatReport?.reportID}
iouReport={moneyRequestReport}
onPaymentSelect={onPaymentSelect}
onSuccessfulKYC={(kycPaymentType) => confirmPayment({paymentType: kycPaymentType, skipAnimation: true})}
options={selectedTransactionsOptions}
customText={translate('workspace.common.selected', {count: selectedTransactionIDs.length})}
ref={kycWallRef}
/>
)}
</View>
)}
</HeaderWithBackButton>
{!shouldDisplayNarrowMoreButton &&
(shouldShowSelectedTransactionsButton ? (
<View style={[styles.dFlex, styles.w100, styles.ph5, styles.pb3]}>
<ButtonWithDropdownMenu
onPress={() => null}
<MoneyReportHeaderKYCDropdown
chatReportID={chatReport?.reportID}
iouReport={moneyRequestReport}
onPaymentSelect={onPaymentSelect}
onSuccessfulKYC={(kycPaymentType) => confirmPayment({paymentType: kycPaymentType, skipAnimation: true})}
options={selectedTransactionsOptions}
customText={translate('workspace.common.selected', {count: selectedTransactionIDs.length})}
isSplitButton={false}
shouldAlwaysShowDropdownMenu
wrapperStyle={styles.w100}
ref={kycWallRef}
/>
</View>
) : (
Expand All @@ -1665,7 +1683,7 @@
chatReportID={chatReport?.reportID}
iouReport={moneyRequestReport}
onPaymentSelect={onPaymentSelect}
onSuccessfulKYC={(payment) => confirmPayment(payment)}
onSuccessfulKYC={(kycPaymentType) => confirmPayment({paymentType: kycPaymentType})}
primaryAction={primaryAction}
applicableSecondaryActions={applicableSecondaryActions}
ref={kycWallRef}
Expand Down
17 changes: 9 additions & 8 deletions src/components/MoneyReportHeaderKYCDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import KYCWall from './KYCWall';
import type {KYCWallProps} from './KYCWall/types';

type MoneyReportHeaderKYCDropdownProps = Omit<KYCWallProps, 'children' | 'enablePaymentsRoute'> & {
primaryAction: ValueOf<typeof CONST.REPORT.PRIMARY_ACTIONS> | '';

applicableSecondaryActions: Array<DropdownOption<ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>>>;

primaryAction?: ValueOf<typeof CONST.REPORT.PRIMARY_ACTIONS> | '';
applicableSecondaryActions?: Array<DropdownOption<ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>>>;
options?: Array<DropdownOption<string>>;
onPaymentSelect: (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) => void;
customText?: string; // Custom text to display on the button
};

function MoneyReportHeaderKYCDropdown({
Expand All @@ -30,6 +30,7 @@ function MoneyReportHeaderKYCDropdown({
iouReport,
onPaymentSelect,
ref,
options,
...props
}: MoneyReportHeaderKYCDropdownProps) {
const styles = useThemeStyles();
Expand All @@ -38,7 +39,7 @@ function MoneyReportHeaderKYCDropdown({
const {isOffline} = useNetwork();

const shouldDisplayNarrowVersion = shouldUseNarrowLayout || isMediumScreenWidth;

const optionsShown = applicableSecondaryActions ?? options ?? [];
return (
<KYCWall
// eslint-disable-next-line react/jsx-props-no-spreading
Expand Down Expand Up @@ -67,11 +68,11 @@ function MoneyReportHeaderKYCDropdown({
}}
buttonRef={buttonRef}
shouldAlwaysShowDropdownMenu
shouldPopoverUseScrollView={applicableSecondaryActions.length >= CONST.DROPDOWN_SCROLL_THRESHOLD}
shouldPopoverUseScrollView={optionsShown.length >= CONST.DROPDOWN_SCROLL_THRESHOLD}
customText={translate('common.more')}
options={applicableSecondaryActions}
options={optionsShown}
isSplitButton={false}
wrapperStyle={shouldDisplayNarrowVersion && [!primaryAction && styles.flex1]}
wrapperStyle={shouldDisplayNarrowVersion && [!primaryAction && applicableSecondaryActions && styles.flex1, options && styles.w100]}
shouldUseModalPaddingStyle
sentryLabel={CONST.SENTRY_LABEL.MORE_MENU.MORE_BUTTON}
/>
Expand Down
11 changes: 6 additions & 5 deletions src/components/MoneyRequestConfirmationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import SelectionList from './SelectionListWithSections';
import type {SectionListDataType} from './SelectionListWithSections/types';
import UserListItem from './SelectionListWithSections/UserListItem';
import SettlementButton from './SettlementButton';
import type {PaymentActionParams} from './SettlementButton/types';
import Text from './Text';
import EducationalTooltip from './Tooltip/EducationalTooltip';

Expand Down Expand Up @@ -882,7 +883,7 @@ function MoneyRequestConfirmationList({
* @param {String} paymentMethod
*/
const confirm = useCallback(
(paymentMethod: PaymentMethodType | undefined) => {
({paymentType}: PaymentActionParams) => {
if (!!routeError || !transactionID) {
return;
}
Expand Down Expand Up @@ -952,7 +953,7 @@ function MoneyRequestConfirmationList({

onConfirm?.(selectedParticipants);
} else {
if (!paymentMethod) {
if (!paymentType) {
return;
}
if (isDelegateAccessRestricted) {
Expand All @@ -962,8 +963,8 @@ function MoneyRequestConfirmationList({
if (formError) {
return;
}
Log.info(`[IOU] Sending money via: ${paymentMethod}`);
onSendMoney?.(paymentMethod);
Log.info(`[IOU] Sending money via: ${paymentType}`);
onSendMoney?.(paymentType);
}
},
[
Expand Down Expand Up @@ -1073,7 +1074,7 @@ function MoneyRequestConfirmationList({
<View>
<ButtonWithDropdownMenu
pressOnEnter
onPress={(event, value) => confirm(value as PaymentMethodType)}
onPress={(event, value) => confirm({paymentType: value as PaymentMethodType})}
options={splitOrRequestOptions}
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE}
enterKeyEventListenerPriority={1}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
import ConfirmModal from '@components/ConfirmModal';
import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider';
import Icon from '@components/Icon';
import type {PaymentMethod} from '@components/KYCWall/types';
import MoneyReportHeaderStatusBarSkeleton from '@components/MoneyReportHeaderStatusBarSkeleton';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import {PressableWithFeedback} from '@components/Pressable';
Expand All @@ -20,6 +19,7 @@ import ProcessMoneyReportHoldMenu from '@components/ProcessMoneyReportHoldMenu';
import type {ActionHandledType} from '@components/ProcessMoneyReportHoldMenu';
import ExportWithDropdownMenu from '@components/ReportActionItem/ExportWithDropdownMenu';
import AnimatedSettlementButton from '@components/SettlementButton/AnimatedSettlementButton';
import type {PaymentActionParams} from '@components/SettlementButton/types';
import {showContextMenuForReport} from '@components/ShowContextMenuContext';
import Text from '@components/Text';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
Expand Down Expand Up @@ -237,11 +237,11 @@ function MoneyRequestReportPreviewContent({
}, [chatReport, policy, hasReportBeenRetracted, iouReport]);

const confirmPayment = useCallback(
(type: PaymentMethodType | undefined, payAsBusiness?: boolean, methodID?: number, paymentMethod?: PaymentMethod) => {
if (!type) {
({paymentType: selectedPaymentType, payAsBusiness, methodID, paymentMethod}: PaymentActionParams) => {
if (!selectedPaymentType) {
return;
}
setPaymentType(type);
setPaymentType(selectedPaymentType);
setRequestType(CONST.IOU.REPORT_ACTION_TYPE.PAY);
if (isDelegateAccessRestricted) {
showDelegateNoAccessModal();
Expand All @@ -251,7 +251,7 @@ function MoneyRequestReportPreviewContent({
startAnimation();
if (isInvoiceReportUtils(iouReport)) {
payInvoice({
paymentMethodType: type,
paymentMethodType: selectedPaymentType,
chatReport,
invoiceReport: iouReport,
introSelected,
Expand All @@ -264,7 +264,7 @@ function MoneyRequestReportPreviewContent({
activePolicy,
});
} else {
payMoneyRequest(type, chatReport, iouReport, introSelected, undefined, true, activePolicy);
payMoneyRequest(selectedPaymentType, chatReport, iouReport, introSelected, undefined, true, activePolicy);
}
}
},
Expand Down
Loading
Loading