Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,7 @@ const CONST = {
ADD_EXPENSE: 'addExpense',
REOPEN: 'reopen',
MOVE_EXPENSE: 'moveExpense',
PAY: 'pay',
},
PRIMARY_ACTIONS: {
SUBMIT: 'submit',
Expand Down
6 changes: 5 additions & 1 deletion src/components/ButtonWithDropdownMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function ButtonWithDropdownMenu<IValueType>({
onPress,
options,
onOptionSelected,
onSubItemSelected,
onOptionsMenuShow,
onOptionsMenuHide,
enterKeyEventListenerPriority = 0,
Expand Down Expand Up @@ -218,7 +219,10 @@ function ButtonWithDropdownMenu<IValueType>({
onOptionsMenuHide?.();
}}
onModalShow={onOptionsMenuShow}
onItemSelected={() => setIsMenuVisible(false)}
onItemSelected={(selectedSubitem, index, event) => {
onSubItemSelected?.(selectedSubitem, index, event);
setIsMenuVisible(false);
}}
anchorPosition={shouldUseStyleUtilityForAnchorPosition ? styles.popoverButtonDropdownMenuOffset(windowWidth) : popoverAnchorPosition}
shouldShowSelectedItemCheck={shouldShowSelectedItemCheck}
// eslint-disable-next-line react-compiler/react-compiler
Expand Down
3 changes: 3 additions & 0 deletions src/components/ButtonWithDropdownMenu/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ type ButtonWithDropdownMenuProps<TValueType> = {
/** The second line text displays under the first line */
secondLineText?: string;

/** Callback to execute when a dropdown submenu option is selected */
onSubItemSelected?: (selectedItem: PopoverMenuItem, index: number, event?: GestureResponderEvent | KeyboardEvent) => void;

/** Icon for main button */
icon?: IconAsset;
};
Expand Down
118 changes: 88 additions & 30 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
import useNetwork from '@hooks/useNetwork';
import usePaymentAnimations from '@hooks/usePaymentAnimations';
import usePaymentOptions from '@hooks/usePaymentOptions';
import usePermissions from '@hooks/usePermissions';
import useReportIsArchived from '@hooks/useReportIsArchived';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
Expand All @@ -20,9 +21,11 @@ import {deleteAppReport, downloadReportPDF, exportReportToCSV, exportReportToPDF
import {getThreadReportIDsForTransactions, getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils';
import Navigation from '@libs/Navigation/Navigation';
import {buildOptimisticNextStepForPreventSelfApprovalsEnabled} from '@libs/NextStepUtils';
import {isSecondaryActionAPaymentOption, selectPaymentType} from '@libs/PaymentUtils';
import type {KYCFlowEvent, TriggerKYCFlow} from '@libs/PaymentUtils';
import {getValidConnectedIntegration} from '@libs/PolicyUtils';
import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {getReportPrimaryAction} from '@libs/ReportPrimaryActionUtils';
import {getAllExpensesToHoldIfApplicable, getReportPrimaryAction} from '@libs/ReportPrimaryActionUtils';
import {getSecondaryReportActions} from '@libs/ReportSecondaryActionUtils';
import {
changeMoneyRequestHoldStatus,
Expand Down Expand Up @@ -95,6 +98,7 @@ import Header from './Header';
import HeaderWithBackButton from './HeaderWithBackButton';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import KYCWall from './KYCWall';
import type {PaymentMethod} from './KYCWall/types';
import LoadingBar from './LoadingBar';
import Modal from './Modal';
Expand All @@ -103,6 +107,7 @@ import MoneyReportHeaderStatusBarSkeleton from './MoneyReportHeaderStatusBarSkel
import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusBar';
import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar';
import {useMoneyRequestReportContext} from './MoneyRequestReportView/MoneyRequestReportContext';
import type {PopoverMenuItem} from './PopoverMenu';
import type {ActionHandledType} from './ProcessMoneyReportHoldMenu';
import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu';
import AnimatedSettlementButton from './SettlementButton/AnimatedSettlementButton';
Expand Down Expand Up @@ -151,6 +156,7 @@ function MoneyReportHeader({
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID}`, {canBeMissing: true});
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID}`, {canBeMissing: true});
const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.validated, canBeMissing: true});
const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, {canBeMissing: true});
const [reportPDFFilename] = useOnyx(`${ONYXKEYS.COLLECTION.NVP_EXPENSIFY_REPORT_PDF_FILENAME}${moneyRequestReport?.reportID}`, {canBeMissing: true}) ?? null;
const [download] = useOnyx(`${ONYXKEYS.COLLECTION.DOWNLOAD}${reportPDFFilename}`, {canBeMissing: true});
Expand Down Expand Up @@ -316,7 +322,7 @@ function MoneyReportHeader({
if (isDelegateAccessRestricted) {
setIsNoDelegateAccessMenuVisible(true);
} else if (isAnyTransactionOnHold) {
setIsHoldMenuVisible(true);
InteractionManager.runAfterInteractions(() => setIsHoldMenuVisible(true));
} else if (isInvoiceReport) {
startAnimation();
payInvoice(type, chatReport, moneyRequestReport, payAsBusiness, methodID, paymentMethod);
Expand Down Expand Up @@ -445,6 +451,27 @@ function MoneyReportHeader({
}
}, [connectedIntegration, exportModalStatus, moneyRequestReport?.reportID]);

const getAmount = (actionType: ValueOf<typeof CONST.REPORT.REPORT_PREVIEW_ACTIONS>) => ({
formattedAmount: getTotalAmountForIOUReportPreviewButton(moneyRequestReport, policy, actionType),
});

const {formattedAmount: payAmount} = getAmount(CONST.REPORT.PRIMARY_ACTIONS.PAY);
const {formattedAmount: totalAmount} = hasOnlyHeldExpenses ? getAmount(CONST.REPORT.REPORT_PREVIEW_ACTIONS.REVIEW) : getAmount(CONST.REPORT.PRIMARY_ACTIONS.PAY);

const paymentButtonOptions = usePaymentOptions({
addBankAccountRoute: bankAccountRoute,
currency: moneyRequestReport?.currency,
iouReport: moneyRequestReport,
chatReportID: chatReport?.reportID,
formattedAmount: totalAmount,
policyID: moneyRequestReport?.policyID,
onPress: confirmPayment,
shouldHidePaymentOptions: !shouldShowPayButton,
shouldShowApproveButton,
shouldDisableApproveButton,
onlyShowPayElsewhere,
});

const addExpenseDropdownOptions: Array<DropdownOption<ValueOf<typeof CONST.REPORT.ADD_EXPENSE_OPTIONS>>> = useMemo(
() => [
{
Expand All @@ -470,10 +497,6 @@ function MoneyReportHeader({
[moneyRequestReport?.reportID, translate],
);

const getAmount = (actionType: ValueOf<typeof CONST.REPORT.REPORT_PREVIEW_ACTIONS>) => ({
formattedAmount: getTotalAmountForIOUReportPreviewButton(moneyRequestReport, policy, actionType),
});

const primaryActionsImplementation = {
[CONST.REPORT.PRIMARY_ACTIONS.SUBMIT]: (
<Button
Expand Down Expand Up @@ -512,7 +535,7 @@ function MoneyReportHeader({
shouldHidePaymentOptions={!shouldShowPayButton}
shouldShowApproveButton={shouldShowApproveButton}
shouldDisableApproveButton={shouldDisableApproveButton}
formattedAmount={getAmount(CONST.REPORT.PRIMARY_ACTIONS.PAY).formattedAmount}
formattedAmount={payAmount}
isDisabled={isOffline && !canAllowSettlement}
isLoading={!isOffline && !canAllowSettlement}
/>
Expand Down Expand Up @@ -541,6 +564,13 @@ function MoneyReportHeader({
onPress={() => {
const parentReportAction = getReportAction(moneyRequestReport?.parentReportID, moneyRequestReport?.parentReportActionID);

const IOUActions = getAllExpensesToHoldIfApplicable(moneyRequestReport, reportActions);

if (IOUActions.length) {
IOUActions.forEach(changeMoneyRequestHoldStatus);
return;
}

const moneyRequestAction = transactionThreadReportID ? requestParentReportAction : parentReportAction;
if (!moneyRequestAction) {
return;
Expand Down Expand Up @@ -595,7 +625,10 @@ function MoneyReportHeader({
return getSecondaryReportActions(moneyRequestReport, transactions, violations, policy, reportNameValuePairs, reportActions, canUseRetractNewDot, canUseTableReportView, policies);
}, [moneyRequestReport, transactions, violations, policy, reportNameValuePairs, reportActions, canUseRetractNewDot, canUseTableReportView, policies]);

const secondaryActionsImplementation: Record<ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>, DropdownOption<ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>>> = {
const secondaryActionsImplementation: Record<
ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>,
DropdownOption<ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>> & Pick<PopoverMenuItem, 'backButtonText'>
> = {
[CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS]: {
value: CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS,
text: translate('iou.viewDetails'),
Expand Down Expand Up @@ -773,9 +806,9 @@ function MoneyReportHeader({
},
[CONST.REPORT.SECONDARY_ACTIONS.ADD_EXPENSE]: {
text: translate('iou.addExpense'),
backButtonText: translate('iou.addExpense'),
icon: Expensicons.Plus,
value: CONST.REPORT.SECONDARY_ACTIONS.ADD_EXPENSE,
backButtonText: translate('iou.addExpense'),
subMenuItems: addExpenseDropdownOptions,
onSelected: () => {
if (!moneyRequestReport?.reportID) {
Expand All @@ -784,6 +817,13 @@ function MoneyReportHeader({
startMoneyRequest(CONST.IOU.TYPE.SUBMIT, moneyRequestReport?.reportID);
},
},
[CONST.REPORT.SECONDARY_ACTIONS.PAY]: {
text: translate('iou.settlePayment', {formattedAmount: totalAmount}),
icon: Expensicons.Cash,
value: CONST.REPORT.SECONDARY_ACTIONS.PAY,
backButtonText: translate('iou.settlePayment', {formattedAmount: totalAmount}),
subMenuItems: Object.values(paymentButtonOptions),
Comment thread
JakubKorytko marked this conversation as resolved.
},
};

const applicableSecondaryActions = secondaryActions.map((action) => secondaryActionsImplementation[action]).filter((action) => action?.shouldShow !== false);
Expand Down Expand Up @@ -833,6 +873,43 @@ function MoneyReportHeader({
<Text>{translate('iou.reopenExportedReportConfirmation', {connectionName: integrationNameFromExportMessage ?? ''})}</Text>
</Text>
);
const onPaymentSelect = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) =>
selectPaymentType(event, iouPaymentType, triggerKYCFlow, policy, confirmPayment, isUserValidated, confirmApproval, moneyRequestReport);

const KYCMoreDropdown = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Coming from #68688, this caused an issue with the sub-menus not staying stable, since this dropdown is recreated every time the parent component is updated. More details in the proposal here.

<KYCWall
onSuccessfulKYC={(payment) => confirmPayment(payment)}
enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS}
addBankAccountRoute={bankAccountRoute}
isDisabled={isOffline}
source={CONST.KYC_WALL_SOURCE.REPORT}
chatReportID={chatReport?.reportID}
iouReport={moneyRequestReport}
anchorAlignment={{
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, // button is at left, so horizontal anchor is at LEFT
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, // we assume that popover menu opens below the button, anchor is at TOP
}}
>
{(triggerKYCFlow, buttonRef) => (
<ButtonWithDropdownMenu
success={false}
onPress={() => {}}
onSubItemSelected={(item, index, event) => {
if (!isSecondaryActionAPaymentOption(item)) {
return;
}
onPaymentSelect(event, item.value, triggerKYCFlow);
}}
buttonRef={buttonRef}
shouldAlwaysShowDropdownMenu
customText={translate('common.more')}
options={applicableSecondaryActions}
isSplitButton={false}
wrapperStyle={shouldDisplayNarrowVersion && [!primaryAction && styles.flex1]}
/>
)}
</KYCWall>
);

return (
<View style={[styles.pt0, styles.borderBottom]}>
Expand All @@ -850,16 +927,7 @@ function MoneyReportHeader({
{!shouldDisplayNarrowVersion && (
<View style={[styles.flexRow, styles.gap2]}>
{!!primaryAction && !shouldShowSelectedTransactionsButton && primaryActionsImplementation[primaryAction]}
{!!applicableSecondaryActions.length && !shouldShowSelectedTransactionsButton && (
<ButtonWithDropdownMenu
success={false}
onPress={() => {}}
shouldAlwaysShowDropdownMenu
customText={translate('common.more')}
options={applicableSecondaryActions}
isSplitButton={false}
/>
)}
{!!applicableSecondaryActions.length && !shouldShowSelectedTransactionsButton && KYCMoreDropdown}
{shouldShowSelectedTransactionsButton && (
<View>
<ButtonWithDropdownMenu
Expand All @@ -877,17 +945,7 @@ function MoneyReportHeader({
{shouldDisplayNarrowVersion && !shouldShowSelectedTransactionsButton && (
<View style={[styles.flexRow, styles.gap2, styles.pb3, styles.ph5, styles.w100, styles.alignItemsCenter, styles.justifyContentCenter]}>
{!!primaryAction && <View style={[styles.flex1]}>{primaryActionsImplementation[primaryAction]}</View>}
{!!applicableSecondaryActions.length && (
<ButtonWithDropdownMenu
success={false}
onPress={() => {}}
shouldAlwaysShowDropdownMenu
customText={translate('common.more')}
options={applicableSecondaryActions}
isSplitButton={false}
wrapperStyle={[!primaryAction && styles.flex1]}
/>
)}
{!!applicableSecondaryActions.length && KYCMoreDropdown}
</View>
)}
{isMoreContentShown && (
Expand Down
7 changes: 4 additions & 3 deletions src/components/MoneyRequestHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useTransactionViolations from '@hooks/useTransactionViolations';
import {deleteMoneyRequest} from '@libs/actions/IOU';
import Navigation from '@libs/Navigation/Navigation';
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {getOriginalMessage, getReportActions, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {getTransactionThreadPrimaryAction} from '@libs/ReportPrimaryActionUtils';
import {getSecondaryTransactionThreadActions} from '@libs/ReportSecondaryActionUtils';
import {changeMoneyRequestHoldStatus, isSelfDM, navigateToDetailsPage} from '@libs/ReportUtils';
Expand Down Expand Up @@ -197,10 +197,11 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
};

const secondaryActions = useMemo(() => {
if (!parentReport || !transaction) {
const reportActions = !!parentReport && getReportActions(parentReport);
if (!transaction || !reportActions) {
return [];
}
return getSecondaryTransactionThreadActions(parentReport, transaction);
return getSecondaryTransactionThreadActions(parentReport, transaction, Object.values(reportActions));
}, [parentReport, transaction]);

const secondaryActionsImplementation: Record<ValueOf<typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS>, DropdownOption<ValueOf<typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS>>> = {
Expand Down
10 changes: 5 additions & 5 deletions src/components/PopoverMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ type PopoverMenuProps = Partial<PopoverModalProps> & {
isVisible: boolean;

/** Callback to fire when a CreateMenu item is selected */
onItemSelected?: (selectedItem: PopoverMenuItem, index: number) => void;
onItemSelected?: (selectedItem: PopoverMenuItem, index: number, event?: GestureResponderEvent | KeyboardEvent) => void;

/** Menu items to be rendered on the list */
menuItems: PopoverMenuItem[];
Expand Down Expand Up @@ -220,7 +220,7 @@ function PopoverMenu({
const isWebOrDesktop = platform === CONST.PLATFORM.WEB || platform === CONST.PLATFORM.DESKTOP;
const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: currentMenuItemsFocusedIndex, maxIndex: currentMenuItems.length - 1, isActive: isVisible});

const selectItem = (index: number) => {
const selectItem = (index: number, event?: GestureResponderEvent | KeyboardEvent) => {
const selectedItem = currentMenuItems.at(index);
if (!selectedItem) {
return;
Expand All @@ -231,7 +231,7 @@ function PopoverMenu({
const selectedSubMenuItemIndex = selectedItem?.subMenuItems.findIndex((option) => option.isSelected);
setFocusedIndex(selectedSubMenuItemIndex);
} else if (selectedItem.shouldCallAfterModalHide && (!isSafari() || shouldAvoidSafariException)) {
onItemSelected?.(selectedItem, index);
onItemSelected?.(selectedItem, index, event);
close(
() => {
selectedItem.onSelected?.();
Expand All @@ -240,7 +240,7 @@ function PopoverMenu({
selectedItem.shouldCloseAllModals,
);
} else {
onItemSelected?.(selectedItem, index);
onItemSelected?.(selectedItem, index, event);
selectedItem.onSelected?.();
}
};
Expand Down Expand Up @@ -295,7 +295,7 @@ function PopoverMenu({
key={key ?? `${item.text}_${menuIndex}`}
pressableTestID={menuItemTestID ?? `PopoverMenuItem-${item.text}`}
title={text}
onPress={() => selectItem(menuIndex)}
onPress={(event) => selectItem(menuIndex, event)}
focused={focusedIndex === menuIndex}
shouldShowSelectedItemCheck={shouldShowSelectedItemCheck}
shouldCheckActionAllowedOnPress={false}
Expand Down
Loading