diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 86501b1b6ee5..daad7958c7be 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6899,9 +6899,9 @@ const CONST = { }, LAST_PAYMENT_METHOD: { LAST_USED: 'lastUsed', - IOU: 'Iou', - EXPENSE: 'Expense', - INVOICE: 'Invoice', + IOU: 'iou', + EXPENSE: 'expense', + INVOICE: 'invoice', }, SKIPPABLE_COLLECTION_MEMBER_IDS: [String(DEFAULT_NUMBER_ID), '-1', 'undefined', 'null', 'NaN'] as string[], SETUP_SPECIALIST_LOGIN: 'Setup Specialist', diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index d68eeebcaef0..b7a62c7aa063 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -310,7 +310,19 @@ function Button( const textComponent = secondLineText ? ( {primaryText} - {secondLineText} + + {secondLineText} + ) : ( primaryText diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index d9351922f858..ee0badb49dab 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -14,6 +14,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import mergeRefs from '@libs/mergeRefs'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; import type {ButtonWithDropdownMenuProps} from './types'; @@ -56,7 +57,10 @@ function ButtonWithDropdownMenuInner(props: ButtonWithDropdownMenuPr testID, secondLineText = '', icon, + shouldPopoverUseScrollView = false, + containerStyles, shouldUseModalPaddingStyle = true, + shouldUseShortForm = false, shouldUseOptionIcon = false, } = props; @@ -79,9 +83,14 @@ function ButtonWithDropdownMenuInner(props: ButtonWithDropdownMenuPr const areAllOptionsDisabled = options.every((option) => option.disabled); const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(buttonSize); const isButtonSizeLarge = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE; + const isButtonSizeSmall = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.SMALL; const nullCheckRef = (refParam: RefObject) => refParam ?? null; const shouldShowButtonRightIcon = !!options.at(0)?.shouldShowButtonRightIcon; + useEffect(() => { + setSelectedItemIndex(defaultSelectedIndex); + }, [defaultSelectedIndex]); + const {paddingBottom} = useSafeAreaPaddings(true); useEffect(() => { @@ -153,6 +162,7 @@ function ButtonWithDropdownMenuInner(props: ButtonWithDropdownMenuPr }, ); const splitButtonWrapperStyle = isSplitButton ? [styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter] : {}; + const isTextTooLong = customText && customText?.length > 6; const handlePress = useCallback( (event?: GestureResponderEvent | KeyboardEvent) => { @@ -186,12 +196,13 @@ function ButtonWithDropdownMenuInner(props: ButtonWithDropdownMenuPr large={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE} medium={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} small={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.SMALL} - innerStyles={[innerStyleDropButton, !isSplitButton && styles.dropDownButtonCartIconView]} + innerStyles={[innerStyleDropButton, !isSplitButton && styles.dropDownButtonCartIconView, isTextTooLong && shouldUseShortForm && {...styles.pl2, ...styles.pr1}]} enterKeyEventListenerPriority={enterKeyEventListenerPriority} iconRight={Expensicons.DownArrow} shouldShowRightIcon={!isSplitButton} isSplitButton={isSplitButton} testID={testID} + textStyles={[isTextTooLong && shouldUseShortForm ? {...styles.textExtraSmall, ...styles.textBold} : {}]} secondLineText={secondLineText} icon={icon} /> @@ -207,16 +218,25 @@ function ButtonWithDropdownMenuInner(props: ButtonWithDropdownMenuPr large={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE} medium={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} small={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.SMALL} - innerStyles={[styles.dropDownButtonCartIconContainerPadding, innerStyleDropButton]} + innerStyles={[styles.dropDownButtonCartIconContainerPadding, innerStyleDropButton, isButtonSizeSmall && styles.dropDownButtonCartIcon]} enterKeyEventListenerPriority={enterKeyEventListenerPriority} > - + @@ -266,18 +286,27 @@ function ButtonWithDropdownMenuInner(props: ButtonWithDropdownMenuPr shouldShowSelectedItemCheck={shouldShowSelectedItemCheck} // eslint-disable-next-line react-compiler/react-compiler anchorRef={nullCheckRef(dropdownAnchor)} - withoutOverlay - shouldUseScrollView scrollContainerStyle={!shouldUseModalPaddingStyle && isSmallScreenWidth && {...styles.pt4, paddingBottom}} - shouldUseModalPaddingStyle={shouldUseModalPaddingStyle} anchorAlignment={anchorAlignment} + shouldUseModalPaddingStyle={shouldUseModalPaddingStyle} headerText={menuHeaderText} + shouldUseScrollView={shouldPopoverUseScrollView} + containerStyles={containerStyles} menuItems={options.map((item, index) => ({ ...item, onSelected: item.onSelected - ? () => item.onSelected?.() + ? () => { + item.onSelected?.(); + if (item.shouldUpdateSelectedIndex) { + setSelectedItemIndex(index); + } + } : () => { onOptionSelected?.(item); + if (item.shouldUpdateSelectedIndex === false) { + return; + } + setSelectedItemIndex(index); }, shouldCallAfterModalHide: true, diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts index 71b3408cd0ff..85a06bfa5e88 100644 --- a/src/components/ButtonWithDropdownMenu/types.ts +++ b/src/components/ButtonWithDropdownMenu/types.ts @@ -41,6 +41,8 @@ type DropdownOption = { descriptionTextStyle?: StyleProp; wrapperStyle?: StyleProp; displayInDefaultIconColor?: boolean; + /** Whether the selected index should be updated when the option is selected even if we have onSelected callback */ + shouldUpdateSelectedIndex?: boolean; subMenuItems?: PopoverMenuItem[]; backButtonText?: string; avatarSize?: ValueOf; @@ -143,9 +145,18 @@ type ButtonWithDropdownMenuProps = { /** Icon for main button */ icon?: IconAsset; + /** Whether the popover content should be scrollable */ + shouldPopoverUseScrollView?: boolean; + + /** Container style to be applied to the popover of the dropdown menu */ + containerStyles?: StyleProp; + /** Whether to use modal padding style for the popover menu */ shouldUseModalPaddingStyle?: boolean; + /** Whether to use short form for the button */ + shouldUseShortForm?: boolean; + /** Whether to display the option icon when only one option is available */ shouldUseOptionIcon?: boolean; }; diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index cb1f76825fe8..f38a0884745e 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -3,21 +3,23 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import {Dimensions} from 'react-native'; import type {EmitterSubscription, GestureResponderEvent, View} from 'react-native'; import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; +import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import {openPersonalBankAccountSetupView} from '@libs/actions/BankAccounts'; -import {completePaymentOnboarding} from '@libs/actions/IOU'; +import {completePaymentOnboarding, savePreferredPaymentMethod} from '@libs/actions/IOU'; +import {moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter} from '@libs/actions/Report'; import getClickedTargetLocation from '@libs/getClickedTargetLocation'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import {hasExpensifyPaymentMethod} from '@libs/PaymentUtils'; -import {getBankAccountRoute, isExpenseReport as isExpenseReportReportUtils, isIOUReport} from '@libs/ReportUtils'; +import {getBankAccountRoute, getPolicyExpenseChat, isExpenseReport as isExpenseReportReportUtils, isIOUReport} from '@libs/ReportUtils'; import {kycWallRef} from '@userActions/PaymentMethods'; import {createWorkspaceFromIOUPayment} from '@userActions/Policy/Policy'; import {setKYCWallSource} from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {BankAccountList} from '@src/types/onyx'; +import type {BankAccountList, Policy} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import {getEmptyObject} from '@src/types/utils/EmptyObject'; import viewRef from '@src/types/utils/viewRef'; @@ -54,6 +56,8 @@ function KYCWall({ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: true}); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, {canBeMissing: true}); + const {formatPhoneNumber} = useLocalize(); + const anchorRef = useRef(null); const transferBalanceButtonRef = useRef(null); @@ -64,6 +68,8 @@ function KYCWall({ anchorPositionHorizontal: 0, }); + const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true}); + const getAnchorPosition = useCallback( (domRect: DomRect): AnchorPosition => { if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP) { @@ -103,16 +109,41 @@ function KYCWall({ }, [getAnchorPosition]); const selectPaymentMethod = useCallback( - (paymentMethod: PaymentMethod) => { - onSelectPaymentMethod(paymentMethod); + (paymentMethod?: PaymentMethod, policy?: Policy) => { + if (paymentMethod) { + onSelectPaymentMethod(paymentMethod); + } if (paymentMethod === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { openPersonalBankAccountSetupView({shouldSetUpUSBankAccount: isIOUReport(iouReport)}); } else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) { Navigation.navigate(addDebitCardRoute ?? ROUTES.HOME); - } else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) { + } else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT || policy) { if (iouReport && isIOUReport(iouReport)) { + if (policy) { + const policyExpenseChatReportID = getPolicyExpenseChat(iouReport.ownerAccountID, policy.id)?.reportID; + if (!policyExpenseChatReportID) { + const {policyExpenseChatReportID: newPolicyExpenseChatReportID} = moveIOUReportToPolicyAndInviteSubmitter(iouReport.reportID, policy.id, formatPhoneNumber) ?? {}; + savePreferredPaymentMethod(iouReport.policyID, policy.id, CONST.LAST_PAYMENT_METHOD.IOU, lastPaymentMethod?.[policy.id]); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(newPolicyExpenseChatReportID)); + } else { + moveIOUReportToPolicy(iouReport.reportID, policy.id, true); + savePreferredPaymentMethod(iouReport.policyID, policy.id, CONST.LAST_PAYMENT_METHOD.IOU, lastPaymentMethod?.[policy.id]); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(policyExpenseChatReportID)); + } + + if (policy?.achAccount) { + return; + } + // Navigate to the bank account set up flow for this specific policy + Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policy.id)); + return; + } + const {policyID, workspaceChatReportID, reportPreviewReportActionID, adminsChatReportID} = createWorkspaceFromIOUPayment(iouReport) ?? {}; + if (policyID && iouReport?.policyID) { + savePreferredPaymentMethod(iouReport.policyID, policyID, CONST.LAST_PAYMENT_METHOD.IOU, lastPaymentMethod?.[iouReport?.policyID]); + } completePaymentOnboarding(CONST.PAYMENT_SELECTED.BBA, adminsChatReportID, policyID); if (workspaceChatReportID) { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(workspaceChatReportID, reportPreviewReportActionID)); @@ -120,14 +151,13 @@ function KYCWall({ // Navigate to the bank account set up flow for this specific policy Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID)); - return; } const bankAccountRoute = addBankAccountRoute ?? getBankAccountRoute(chatReport); Navigation.navigate(bankAccountRoute); } }, - [addBankAccountRoute, addDebitCardRoute, chatReport, iouReport, onSelectPaymentMethod], + [addBankAccountRoute, addDebitCardRoute, chatReport, iouReport, onSelectPaymentMethod, formatPhoneNumber, lastPaymentMethod], ); /** @@ -137,7 +167,7 @@ function KYCWall({ * */ const continueAction = useCallback( - (event?: GestureResponderEvent | KeyboardEvent, iouPaymentType?: PaymentMethodType) => { + (event?: GestureResponderEvent | KeyboardEvent, iouPaymentType?: PaymentMethodType, paymentMethod?: PaymentMethod, policy?: Policy) => { const currentSource = walletTerms?.source ?? source; /** @@ -171,6 +201,19 @@ function KYCWall({ return; } + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (paymentMethod || policy) { + setShouldShowAddPaymentMenu(false); + selectPaymentMethod(paymentMethod, policy); + return; + } + + if (iouPaymentType && isExpenseReport) { + setShouldShowAddPaymentMenu(false); + selectPaymentMethod(CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT); + return; + } + const clickedElementLocation = getClickedTargetLocation(targetElement as HTMLDivElement); const position = getAnchorPosition(clickedElementLocation); @@ -183,13 +226,20 @@ function KYCWall({ // Ask the user to upgrade to a gold wallet as this means they have not yet gone through our Know Your Customer (KYC) checks const hasActivatedWallet = userWallet?.tierName && [CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM].some((name) => name === userWallet.tierName); - if (!hasActivatedWallet) { + if (!hasActivatedWallet && !policy) { Log.info('[KYC Wallet] User does not have active wallet'); Navigation.navigate(enablePaymentsRoute); return; } + + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (policy || (paymentMethod && (!hasActivatedWallet || paymentMethod !== CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT))) { + setShouldShowAddPaymentMenu(false); + selectPaymentMethod(paymentMethod, policy); + return; + } } Log.info('[KYC Wallet] User has valid payment method and passed KYC checks or did not need them'); diff --git a/src/components/KYCWall/types.ts b/src/components/KYCWall/types.ts index 06fd42d3103a..b2d585970bf6 100644 --- a/src/components/KYCWall/types.ts +++ b/src/components/KYCWall/types.ts @@ -4,7 +4,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; import type {Route} from '@src/ROUTES'; -import type {Report} from '@src/types/onyx'; +import type {Policy, Report} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; @@ -63,6 +63,9 @@ type KYCWallProps = { /** Children to build the KYC */ children: (continueAction: (event: GestureResponderEvent | KeyboardEvent | undefined, method?: PaymentMethodType) => void, anchorRef: RefObject) => void; + + /** The policy used for payment */ + policy?: Policy; }; export type {AnchorPosition, KYCWallProps, PaymentMethod, DomRect, PaymentMethodType, Source}; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 3b794ca46aa7..8893f274f4b3 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -20,6 +20,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import {deleteAppReport, downloadReportPDF, exportReportToCSV, exportReportToPDF, exportToIntegration, markAsManuallyExported, openReport, openUnreportedExpense} from '@libs/actions/Report'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; +import getPlatform from '@libs/getPlatform'; import {getThreadReportIDsForTransactions, getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -343,13 +344,17 @@ function MoneyReportHeader({ if (isDelegateAccessRestricted) { showDelegateNoAccessModal(); } else if (isAnyTransactionOnHold) { - InteractionManager.runAfterInteractions(() => setIsHoldMenuVisible(true)); + if (getPlatform() === CONST.PLATFORM.IOS) { + InteractionManager.runAfterInteractions(() => setIsHoldMenuVisible(true)); + } else { + setIsHoldMenuVisible(true); + } } else if (isInvoiceReport) { startAnimation(); payInvoice(type, chatReport, moneyRequestReport, payAsBusiness, methodID, paymentMethod); } else { startAnimation(); - payMoneyRequest(type, chatReport, moneyRequestReport, true); + payMoneyRequest(type, chatReport, moneyRequestReport, undefined, true); } }, [chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, showDelegateNoAccessModal, isInvoiceReport, moneyRequestReport, startAnimation], @@ -613,6 +618,7 @@ function MoneyReportHeader({ isPaidAnimationRunning={isPaidAnimationRunning} isApprovedAnimationRunning={isApprovedAnimationRunning} onAnimationFinish={stopAnimation} + formattedAmount={totalAmount} canIOUBePaid onlyShowPayElsewhere={onlyShowPayElsewhere} currency={moneyRequestReport?.currency} @@ -996,11 +1002,12 @@ function MoneyReportHeader({ }} buttonRef={buttonRef} shouldAlwaysShowDropdownMenu + shouldPopoverUseScrollView={shouldDisplayNarrowVersion && applicableSecondaryActions.length >= 5} customText={translate('common.more')} options={applicableSecondaryActions} isSplitButton={false} wrapperStyle={shouldDisplayNarrowVersion && [!primaryAction && styles.flex1]} - shouldUseModalPaddingStyle={false} + shouldUseModalPaddingStyle /> )} diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index eda7f49a0055..4fa419266941 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -322,13 +322,10 @@ function PopoverMenu({ } setFocusedIndex(menuIndex); }} - wrapperStyle={StyleUtils.getItemBackgroundColorStyle( - !!item.isSelected, - focusedIndex === menuIndex, - item.disabled ?? false, - theme.activeComponentBG, - theme.hoverComponentBG, - )} + wrapperStyle={[ + StyleUtils.getItemBackgroundColorStyle(!!item.isSelected, focusedIndex === menuIndex, item.disabled ?? false, theme.activeComponentBG, theme.hoverComponentBG), + shouldUseScrollView && !shouldUseModalPaddingStyle && StyleUtils.getOptionMargin(menuIndex, currentMenuItems.length - 1), + ]} shouldRemoveHoverBackground={item.isSelected} titleStyle={StyleSheet.flatten([styles.flex1, item.titleStyle])} // Spread other props dynamically diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index c803de7ea38e..0a155fa4c41b 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -77,7 +77,7 @@ function ProcessMoneyReportHoldMenu({ if (startAnimation) { startAnimation(); } - payMoneyRequest(paymentType, chatReport, moneyRequestReport, full); + payMoneyRequest(paymentType, chatReport, moneyRequestReport, undefined, full); } onClose(); }; diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index 8d0ec21e6536..359c15282d01 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -32,6 +32,7 @@ import {openUnreportedExpense} from '@libs/actions/Report'; import ControlSelection from '@libs/ControlSelection'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; +import {getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils'; import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import {getConnectedIntegration} from '@libs/PolicyUtils'; @@ -476,6 +477,7 @@ function MoneyRequestReportPreviewContent({ ); const isReportDeleted = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + const formattedAmount = getTotalAmountForIOUReportPreviewButton(iouReport, policy, reportPreviewAction); const reportPreviewActions = { [CONST.REPORT.REPORT_PREVIEW_ACTIONS.SUBMIT]: ( @@ -507,6 +509,7 @@ function MoneyRequestReportPreviewContent({ onPress={confirmPayment} onPaymentOptionsShow={onPaymentOptionsShow} onPaymentOptionsHide={onPaymentOptionsHide} + formattedAmount={formattedAmount} confirmApproval={confirmApproval} enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS} shouldHidePaymentOptions={!shouldShowPayButton} diff --git a/src/components/SelectionList/Search/ReportListItemHeader.tsx b/src/components/SelectionList/Search/ReportListItemHeader.tsx index c008c56a6b4f..1ca2f1655a23 100644 --- a/src/components/SelectionList/Search/ReportListItemHeader.tsx +++ b/src/components/SelectionList/Search/ReportListItemHeader.tsx @@ -170,6 +170,7 @@ function ReportListItemHeader({ const theme = useTheme(); const {currentSearchHash, currentSearchKey} = useSearchContext(); const {isLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); + const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true}); const thereIsFromAndTo = !!reportItem?.from && !!reportItem?.to; const showUserInfo = (reportItem.type === CONST.REPORT.TYPE.IOU && thereIsFromAndTo) || (reportItem.type === CONST.REPORT.TYPE.EXPENSE && !!reportItem?.from); const [snapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`, {canBeMissing: true}); @@ -179,7 +180,6 @@ function ReportListItemHeader({ const snapshotPolicy = useMemo(() => { return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as SearchPolicy; }, [snapshot, reportItem.policyID]); - const [lastPaymentMethod] = useOnyx(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, {canBeMissing: true}); const avatarBorderColor = StyleUtils.getItemBackgroundColorStyle(!!reportItem.isSelected, !!isFocused, !!isDisabled, theme.activeComponentBG, theme.hoverComponentBG)?.backgroundColor ?? theme.highlightBG; diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index e89c582ffc66..a6323aa2fd32 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -1,24 +1,53 @@ -import React, {useContext} from 'react'; +import isEmpty from 'lodash/isEmpty'; +import truncate from 'lodash/truncate'; +import React, {useContext, useEffect, useMemo, useRef} from 'react'; +import type {GestureResponderEvent} from 'react-native'; +import type {TupleToUnion} from 'type-fest'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; -import type {DropdownOption, PaymentType} from '@components/ButtonWithDropdownMenu/types'; +import * as Expensicons from '@components/Icon/Expensicons'; +import {Bank} from '@components/Icon/Expensicons'; import KYCWall from '@components/KYCWall'; +import type {PaymentMethod} from '@components/KYCWall/types'; import {LockedAccountContext} from '@components/LockedAccountModalProvider'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; -import usePaymentOptions from '@hooks/usePaymentOptions'; -import {selectPaymentType} from '@libs/PaymentUtils'; -import type {KYCFlowEvent, TriggerKYCFlow} from '@libs/PaymentUtils'; +import usePolicy from '@hooks/usePolicy'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {isCurrencySupportedForDirectReimbursement} from '@libs/actions/Policy/Policy'; +import {getCurrentUserAccountID} from '@libs/actions/Report'; +import {getLastPolicyBankAccountID, getLastPolicyPaymentMethod} from '@libs/actions/Search'; +import Navigation from '@libs/Navigation/Navigation'; +import {formatPaymentMethods} from '@libs/PaymentUtils'; import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; -import {doesReportBelongToWorkspace, isInvoiceReport as isInvoiceReportUtil} from '@libs/ReportUtils'; -import {savePreferredPaymentMethod as savePreferredPaymentMethodIOU} from '@userActions/IOU'; +import {getActiveAdminWorkspaces, hasVBBA} from '@libs/PolicyUtils'; +import {hasRequestFromCurrentAccount} from '@libs/ReportActionsUtils'; +import { + doesReportBelongToWorkspace, + getBankAccountRoute, + isExpenseReport as isExpenseReportUtil, + isIndividualInvoiceRoom as isIndividualInvoiceRoomUtil, + isInvoiceReport as isInvoiceReportUtil, + isIOUReport, +} from '@libs/ReportUtils'; +import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; +import {setPersonalBankAccountContinueKYCOnSuccess} from '@userActions/BankAccounts'; +import {approveMoneyRequest} from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {AccountData, BankAccount, LastPaymentMethodType, Policy} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import type SettlementButtonProps from './types'; +type KYCFlowEvent = GestureResponderEvent | KeyboardEvent | undefined; + +type TriggerKYCFlow = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, paymentMethod?: PaymentMethod, policy?: Policy) => void; + +type CurrencyType = TupleToUnion; + function SettlementButton({ addDebitCardRoute = ROUTES.IOU_SEND_ADD_DEBIT_CARD, kycWallAnchorAlignment = { @@ -53,88 +82,470 @@ function SettlementButton({ onPaymentOptionsHide, onlyShowPayElsewhere, wrapperStyle, + shouldUseShortForm = false, + hasOnlyHeldExpenses = false, }: SettlementButtonProps) { + const styles = useThemeStyles(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); + // The app would crash due to subscribing to the entire report collection if chatReportID is an empty string. So we should have a fallback ID here. // eslint-disable-next-line rulesdir/no-default-id-values const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || CONST.DEFAULT_NUMBER_ID}`, {canBeMissing: true}); const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.validated, canBeMissing: true}); const policyEmployeeAccountIDs = policyID ? getPolicyEmployeeAccountIDs(policyID) : []; const reportBelongsToWorkspace = policyID ? doesReportBelongToWorkspace(chatReport, policyEmployeeAccountIDs, policyID) : false; - const policyIDKey = reportBelongsToWorkspace ? policyID : CONST.POLICY.ID_FAKE; - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: false}); + const policyIDKey = reportBelongsToWorkspace ? policyID : (iouReport?.policyID ?? CONST.POLICY.ID_FAKE); + const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET, {canBeMissing: true}); + const hasActivatedWallet = ([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM] as string[]).includes(userWallet?.tierName ?? ''); + const [lastPaymentMethods, lastPaymentMethodResult] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true}); + + const lastPaymentMethod = useMemo(() => { + if (!iouReport?.type) { + return; + } + + return getLastPolicyPaymentMethod(policyIDKey, lastPaymentMethods, iouReport?.type as keyof LastPaymentMethodType, isIOUReport(iouReport)); + }, [policyIDKey, iouReport, lastPaymentMethods]); + + const lastBankAccountID = getLastPolicyBankAccountID(policyIDKey, lastPaymentMethods, iouReport?.type as keyof LastPaymentMethodType); + const [fundList] = useOnyx(ONYXKEYS.FUND_LIST, {canBeMissing: true}); + const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); + const currentUserAccountID = getCurrentUserAccountID().toString(); + + const activeAdminPolicies = getActiveAdminWorkspaces(policies, currentUserAccountID).sort((a, b) => (a.name || '').localeCompare(b.name || '')); + const reportID = iouReport?.reportID; + + const hasPreferredPaymentMethod = !!lastPaymentMethod; + const isLoadingLastPaymentMethod = isLoadingOnyxValue(lastPaymentMethodResult); + const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; + const lastPaymentPolicy = usePolicy(lastPaymentMethod); + + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); + const bankAccount = bankAccountList?.[lastBankAccountID ?? CONST.DEFAULT_NUMBER_ID]; + const isExpenseReport = isExpenseReportUtil(iouReport); + // whether the user has single policy and the expense is p2p + const hasSinglePolicy = !isExpenseReport && activeAdminPolicies.length === 1; + const hasMultiplePolicies = !isExpenseReport && activeAdminPolicies.length > 1; + const lastPaymentMethodRef = useRef(lastPaymentMethod); + const formattedPaymentMethods = formatPaymentMethods(bankAccountList ?? {}, fundList ?? {}, styles); + const hasIntentToPay = ((formattedPaymentMethods.length === 1 && isIOUReport(iouReport)) || !!policy?.achAccount) && !lastPaymentMethod; + + useEffect(() => { + if (isLoadingLastPaymentMethod) { + return; + } + lastPaymentMethodRef.current = lastPaymentMethod; + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [isLoadingLastPaymentMethod]); + const isInvoiceReport = (!isEmptyObject(iouReport) && isInvoiceReportUtil(iouReport)) || false; const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext); + const shouldShowPayWithExpensifyOption = !shouldHidePaymentOptions; + const shouldShowPayElsewhereOption = !shouldHidePaymentOptions && !isInvoiceReport; - const paymentButtonOptions = usePaymentOptions({ - currency, + function getLatestBankAccountItem() { + if (!hasVBBA(policy?.id)) { + return; + } + const policyBankAccounts = formattedPaymentMethods.filter((method) => method.methodID === policy?.achAccount?.bankAccountID); + + return policyBankAccounts.map((formattedPaymentMethod) => { + const {icon, title, description, methodID} = formattedPaymentMethod ?? {}; + + return { + text: title ?? '', + description: description ?? '', + icon: typeof icon === 'number' ? Bank : icon, + onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.EXPENSIFY, true, undefined), + methodID, + value: CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT, + }; + }); + } + + function getLatestPersonalBankAccount() { + return formattedPaymentMethods.filter((ba) => (ba.accountData as AccountData)?.type === CONST.BANK_ACCOUNT.TYPE.PERSONAL); + } + + const personalBankAccountList = getLatestPersonalBankAccount(); + const latestBankItem = getLatestBankAccountItem(); + + const paymentButtonOptions = useMemo(() => { + const buttonOptions = []; + const paymentMethods = { + [CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT]: { + text: hasActivatedWallet ? translate('iou.settleWallet', {formattedAmount: ''}) : translate('iou.settlePersonal', {formattedAmount: ''}), + icon: Expensicons.User, + value: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, + shouldUpdateSelectedIndex: false, + }, + [CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]: { + text: translate('iou.settleBusiness', {formattedAmount: ''}), + icon: Expensicons.Building, + value: CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT, + shouldUpdateSelectedIndex: false, + }, + [CONST.IOU.PAYMENT_TYPE.ELSEWHERE]: { + text: translate('iou.payElsewhere', {formattedAmount}), + icon: Expensicons.CheckCircle, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + shouldUpdateSelectedIndex: false, + }, + }; + + const approveButtonOption = { + text: translate('iou.approve', {formattedAmount}), + icon: Expensicons.ThumbsUp, + value: CONST.IOU.REPORT_ACTION_TYPE.APPROVE, + disabled: !!shouldDisableApproveButton, + }; + + const canUseWallet = !isExpenseReport && !isInvoiceReport && currency === CONST.CURRENCY.USD; + const canUseBusinessBankAccount = isExpenseReport || (isIOUReport(iouReport) && reportID && !hasRequestFromCurrentAccount(reportID, Number(currentUserAccountID) ?? -1)); + + const canUsePersonalBankAccount = shouldShowPersonalBankAccountOption || isIOUReport(iouReport); + + const isPersonalOnlyOption = canUsePersonalBankAccount && !canUseBusinessBankAccount; + + // Only show the Approve button if the user cannot pay the expense + if (shouldHidePaymentOptions && shouldShowApproveButton) { + return [approveButtonOption]; + } + + if (onlyShowPayElsewhere) { + return [paymentMethods[CONST.IOU.PAYMENT_TYPE.ELSEWHERE]]; + } + + // To achieve the one tap pay experience we need to choose the correct payment type as default. + if (canUseWallet) { + if (personalBankAccountList.length && canUsePersonalBankAccount) { + buttonOptions.push({ + text: translate('iou.settleWallet', {formattedAmount: ''}), + value: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, + icon: Expensicons.Wallet, + }); + } else if (canUsePersonalBankAccount) { + buttonOptions.push(paymentMethods[CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT]); + } + + if (activeAdminPolicies.length === 0 && !isPersonalOnlyOption) { + buttonOptions.push(paymentMethods[CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]); + } + } + + const shouldShowBusinessBankAccountOptions = isExpenseReport && shouldShowPayWithExpensifyOption && !isPersonalOnlyOption; + + if (shouldShowBusinessBankAccountOptions) { + if (!isEmpty(latestBankItem) && latestBankItem) { + buttonOptions.push({ + text: latestBankItem.at(0)?.text ?? '', + icon: latestBankItem.at(0)?.icon, + value: CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT, + description: latestBankItem.at(0)?.description, + }); + } else { + buttonOptions.push(paymentMethods[CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]); + } + } + + if ((hasMultiplePolicies || hasSinglePolicy) && canUseWallet && !isPersonalOnlyOption) { + activeAdminPolicies.forEach((activePolicy) => { + const policyName = activePolicy.name; + buttonOptions.push({ + text: translate('iou.payWithPolicy', {policyName: truncate(policyName, {length: CONST.ADDITIONAL_ALLOWED_CHARACTERS}), formattedAmount: ''}), + icon: Expensicons.Building, + value: activePolicy.id, + shouldUpdateSelectedIndex: false, + }); + }); + } + + if (shouldShowPayElsewhereOption) { + buttonOptions.push({ + ...paymentMethods[CONST.IOU.PAYMENT_TYPE.ELSEWHERE], + ...(!buttonOptions.length && shouldUseShortForm ? {text: translate('iou.pay')} : {}), + }); + } + + if (isInvoiceReport) { + const isCurrencySupported = isCurrencySupportedForDirectReimbursement(currency as CurrencyType); + const getPaymentSubitems = (payAsBusiness: boolean) => + formattedPaymentMethods.map((formattedPaymentMethod) => ({ + text: formattedPaymentMethod?.title ?? '', + description: formattedPaymentMethod?.description ?? '', + icon: formattedPaymentMethod?.icon, + onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.EXPENSIFY, payAsBusiness, formattedPaymentMethod.methodID, formattedPaymentMethod.accountType), + iconStyles: formattedPaymentMethod?.iconStyles, + iconHeight: formattedPaymentMethod?.iconSize, + iconWidth: formattedPaymentMethod?.iconSize, + })); + + if (isIndividualInvoiceRoomUtil(chatReport)) { + buttonOptions.push({ + text: translate('iou.settlePersonal', {formattedAmount}), + icon: Expensicons.User, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + backButtonText: translate('iou.individual'), + subMenuItems: [ + ...(isCurrencySupported ? getPaymentSubitems(false) : []), + { + text: translate('iou.payElsewhere', {formattedAmount: ''}), + icon: Expensicons.Cash, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE), + }, + { + text: translate('bankAccount.addBankAccount'), + icon: Expensicons.Bank, + onSelected: () => { + const bankAccountRoute = getBankAccountRoute(chatReport); + Navigation.navigate(bankAccountRoute); + }, + }, + ], + }); + } + + buttonOptions.push({ + text: translate('iou.settleBusiness', {formattedAmount}), + icon: Expensicons.Building, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + backButtonText: translate('iou.business'), + subMenuItems: [ + ...(isCurrencySupported ? getPaymentSubitems(true) : []), + { + text: translate('bankAccount.addBankAccount'), + icon: Expensicons.Bank, + onSelected: () => { + const bankAccountRoute = getBankAccountRoute(chatReport); + Navigation.navigate(bankAccountRoute); + }, + }, + { + text: translate('iou.payElsewhere', {formattedAmount: ''}), + icon: Expensicons.Cash, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE, true), + }, + ], + }); + } + + if (shouldShowApproveButton) { + buttonOptions.push(approveButtonOption); + } + + return buttonOptions; + // We don't want to reorder the options when the preferred payment method changes while the button is still visible except for component initialization when the last payment method is not initialized yet. + // We need to be sure that onPress should be wrapped in an useCallback to prevent unnecessary updates. + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [ + isLoadingLastPaymentMethod, iouReport, - chatReportID, + translate, formattedAmount, - policyID, - onPress, + shouldDisableApproveButton, + isInvoiceReport, + currency, shouldHidePaymentOptions, shouldShowApproveButton, - shouldDisableApproveButton, + shouldShowPayWithExpensifyOption, + shouldShowPayElsewhereOption, + chatReport, + onPress, onlyShowPayElsewhere, - }); + latestBankItem, + activeAdminPolicies, + ]); + + const selectPaymentType = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType) => { + if (policy && shouldRestrictUserBillableActions(policy.id)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); + return; + } + + if (iouPaymentType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE) { + if (confirmApproval) { + confirmApproval(); + } else { + approveMoneyRequest(iouReport); + } + return; + } + + onPress(iouPaymentType, false); + }; + + const selectPaymentMethod = (event: KYCFlowEvent, triggerKYCFlow: TriggerKYCFlow, paymentMethod?: PaymentMethod, selectedPolicy?: Policy) => { + if (!isUserValidated) { + Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_VERIFY_ACCOUNT.getRoute(Navigation.getActiveRoute())); + return; + } + + if (policy && shouldRestrictUserBillableActions(policy.id)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); + return; + } + + let paymentType; + switch (paymentMethod) { + case CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT: + paymentType = CONST.IOU.PAYMENT_TYPE.EXPENSIFY; + break; + case CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT: + paymentType = CONST.IOU.PAYMENT_TYPE.VBBA; + break; + default: + paymentType = CONST.IOU.PAYMENT_TYPE.ELSEWHERE; + } + triggerKYCFlow(event, paymentType, paymentMethod, selectedPolicy ?? (event ? lastPaymentPolicy : undefined)); + if (paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY || paymentType === CONST.IOU.PAYMENT_TYPE.VBBA) { + setPersonalBankAccountContinueKYCOnSuccess(ROUTES.ENABLE_PAYMENTS); + } + }; + + const getCustomText = () => { + if (shouldUseShortForm) { + return translate('iou.pay'); + } + + if (lastPaymentMethod === CONST.IOU.PAYMENT_TYPE.ELSEWHERE && !isInvoiceReport) { + return translate('iou.payElsewhere', {formattedAmount}); + } + + return translate('iou.settlePayment', {formattedAmount}); + }; + + const getSecondaryText = (): string | undefined => { + if ( + shouldUseShortForm || + isInvoiceReport || + lastPaymentMethod === CONST.IOU.PAYMENT_TYPE.ELSEWHERE || + (paymentButtonOptions.length === 1 && paymentButtonOptions.every((option) => option.value === CONST.IOU.PAYMENT_TYPE.ELSEWHERE)) || + (shouldHidePaymentOptions && (shouldShowApproveButton || onlyShowPayElsewhere)) + ) { + return undefined; + } - const filteredPaymentOptions = paymentButtonOptions.filter((option) => option.value !== undefined) as Array>; + if (lastPaymentPolicy) { + return lastPaymentPolicy.name; + } + + const bankAccountToDisplay = hasIntentToPay ? (formattedPaymentMethods.at(0) as BankAccount) : bankAccount; + if (lastPaymentMethod === CONST.IOU.PAYMENT_TYPE.EXPENSIFY || (hasIntentToPay && isInvoiceReportUtil(iouReport))) { + if (!personalBankAccountList.length) { + return; + } + + return translate('common.wallet'); + } + + if ((lastPaymentMethod === CONST.IOU.PAYMENT_TYPE.VBBA || hasIntentToPay) && !!policy?.achAccount) { + if (policy?.achAccount?.accountNumber) { + return translate('paymentMethodList.bankAccountLastFour', {lastFour: policy?.achAccount?.accountNumber?.slice(-4)}); + } + + if (!bankAccountToDisplay?.accountData?.accountNumber) { + return; + } + + return translate('paymentMethodList.bankAccountLastFour', {lastFour: bankAccountToDisplay?.accountData?.accountNumber?.slice(-4)}); + } + + if (bankAccount?.accountData?.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS && isExpenseReportUtil(iouReport)) { + return translate('paymentMethodList.bankAccountLastFour', {lastFour: bankAccount?.accountData?.accountNumber?.slice(-4) ?? ''}); + } - const onPaymentSelect = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) => { + return undefined; + }; + + const handlePaymentSelection = ( + event: GestureResponderEvent | KeyboardEvent | undefined, + selectedOption: PaymentMethodType | PaymentMethod, + triggerKYCFlow: (event: GestureResponderEvent | KeyboardEvent | undefined, method?: PaymentMethodType) => void, + ) => { if (isAccountLocked) { showLockedAccountModal(); return; } - selectPaymentType(event, iouPaymentType, triggerKYCFlow, policy, onPress, isUserValidated, confirmApproval, iouReport); - }; - const savePreferredPaymentMethod = (id: string, value: PaymentMethodType) => { - savePreferredPaymentMethodIOU(id, value, undefined); + const isPaymentMethod = Object.values(CONST.PAYMENT_METHODS).includes(selectedOption as PaymentMethod); + const shouldSelectPaymentMethod = (isPaymentMethod ?? lastPaymentPolicy ?? !isEmpty(latestBankItem)) && !shouldShowApproveButton && !shouldHidePaymentOptions; + const selectedPolicy = activeAdminPolicies.find((activePolicy) => activePolicy.id === selectedOption); + + if (!!selectedPolicy || shouldSelectPaymentMethod) { + selectPaymentMethod(event, triggerKYCFlow, selectedOption as PaymentMethod, selectedPolicy); + return; + } + + selectPaymentType(event, selectedOption as PaymentMethodType); }; + const customText = getCustomText(); + const secondaryText = truncate(getSecondaryText(), {length: CONST.FORM_CHARACTER_LIMIT}); + + const defaultSelectedIndex = paymentButtonOptions.findIndex((paymentOption) => { + if (lastPaymentMethod === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { + return paymentOption.value === CONST.IOU.PAYMENT_TYPE.ELSEWHERE; + } + + if (latestBankItem?.length) { + return paymentOption.value === latestBankItem.at(0)?.value; + } + + if (lastPaymentPolicy?.id) { + return paymentOption.value === lastPaymentPolicy.id; + } + + return false; + }); + + const shouldUseSplitButton = hasPreferredPaymentMethod || !!lastPaymentPolicy || (isExpenseReportUtil(iouReport) && hasIntentToPay); + const shouldLimitWidth = shouldUseShortForm && shouldUseSplitButton && !paymentButtonOptions.length; + return ( onPress(paymentType)} + onSuccessfulKYC={(paymentType) => onPress(paymentType, undefined, undefined)} enablePaymentsRoute={enablePaymentsRoute} addDebitCardRoute={addDebitCardRoute} isDisabled={isOffline} source={CONST.KYC_WALL_SOURCE.REPORT} chatReportID={chatReportID} iouReport={iouReport} + policy={lastPaymentPolicy} anchorAlignment={kycWallAnchorAlignment} shouldShowPersonalBankAccountOption={shouldShowPersonalBankAccountOption} > {(triggerKYCFlow, buttonRef) => ( - + onOptionsMenuShow={onPaymentOptionsShow} onOptionsMenuHide={onPaymentOptionsHide} buttonRef={buttonRef} shouldAlwaysShowDropdownMenu={isInvoiceReport && !onlyShowPayElsewhere} - customText={isInvoiceReport ? translate('iou.settlePayment', {formattedAmount}) : undefined} + customText={customText} menuHeaderText={isInvoiceReport ? translate('workspace.invoices.paymentMethods.chooseInvoiceMethod') : undefined} - isSplitButton={!isInvoiceReport} + isSplitButton={shouldUseSplitButton && !isInvoiceReport} isDisabled={isDisabled} isLoading={isLoading} - onPress={(event, iouPaymentType) => { - onPaymentSelect(event, iouPaymentType, triggerKYCFlow); - }} + defaultSelectedIndex={defaultSelectedIndex !== -1 ? defaultSelectedIndex : 0} + onPress={(event, iouPaymentType) => handlePaymentSelection(event, iouPaymentType, triggerKYCFlow)} + success={!hasOnlyHeldExpenses} + secondLineText={secondaryText} pressOnEnter={pressOnEnter} - options={filteredPaymentOptions} - onOptionSelected={(option) => { - if (policyID === '-1') { - return; - } - savePreferredPaymentMethod(policyIDKey, option.value); - }} + options={paymentButtonOptions} + onOptionSelected={(option) => handlePaymentSelection(undefined, option.value, triggerKYCFlow)} style={style} - wrapperStyle={wrapperStyle} + shouldUseShortForm={shouldUseShortForm} + shouldPopoverUseScrollView={paymentButtonOptions.length > 5} + containerStyles={paymentButtonOptions.length > 5 ? styles.settlementButtonListContainer : {}} + wrapperStyle={[wrapperStyle, shouldLimitWidth ? styles.settlementButtonShortFormWidth : {}]} disabledStyle={disabledStyle} buttonSize={buttonSize} anchorAlignment={paymentMethodDropdownAnchorAlignment} enterKeyEventListenerPriority={enterKeyEventListenerPriority} useKeyboardShortcuts={useKeyboardShortcuts} + shouldUseModalPaddingStyle={paymentButtonOptions.length <= 5} /> )} diff --git a/src/components/SettlementButton/types.ts b/src/components/SettlementButton/types.ts index 527c99004063..2fb2d9c99333 100644 --- a/src/components/SettlementButton/types.ts +++ b/src/components/SettlementButton/types.ts @@ -12,7 +12,7 @@ type EnablePaymentsRoute = typeof ROUTES.ENABLE_PAYMENTS | typeof ROUTES.IOU_SEN type SettlementButtonProps = { /** Callback to execute when this button is pressed. Receives a single payment type argument. */ - onPress: (paymentType?: PaymentMethodType, payAsBusiness?: boolean, methodID?: number, paymentMethod?: PaymentMethod) => void; + onPress: (paymentType: PaymentMethodType | undefined, payAsBusiness?: boolean, methodID?: number, paymentMethod?: PaymentMethod | undefined, policyID?: string) => void; /** Callback when the payment options popover is shown */ onPaymentOptionsShow?: () => void; @@ -91,6 +91,12 @@ type SettlementButtonProps = { /** Whether we only show pay elsewhere button */ onlyShowPayElsewhere?: boolean; + + /** Whether to use short form for the button */ + shouldUseShortForm?: boolean; + + /** Whether we the report has only held expenses */ + hasOnlyHeldExpenses?: boolean; }; export default SettlementButtonProps; diff --git a/src/languages/de.ts b/src/languages/de.ts index 8a703022446f..465e85a77147 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -35,6 +35,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, @@ -49,6 +50,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1141,10 +1143,20 @@ const translations = { individual: 'Individuum', business: 'Geschäft', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Bezahle ${formattedAmount} mit Expensify` : `Mit Expensify bezahlen`), - settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Zahlen Sie ${formattedAmount} als Einzelperson` : `Als Einzelperson bezahlen`), + settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Bezahle ${formattedAmount} als Privatperson` : `Mit Privatkonto bezahlen`), + settleWallet: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Bezahle ${formattedAmount} mit Wallet` : `Mit Wallet bezahlen`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Zahlen Sie ${formattedAmount}`, - settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Zahlen Sie ${formattedAmount} als Unternehmen` : `Als Unternehmen bezahlen`), - payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Zahle ${formattedAmount} anderswo` : `Anderswo bezahlen`), + settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Bezahle ${formattedAmount} als Unternehmen` : `Mit Geschäftskonto bezahlen`), + payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `${formattedAmount} als bezahlt markieren` : `Als bezahlt markieren`), + settleInvoicePersonal: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `${amount} mit Privatkonto ${last4Digits} bezahlt` : `Mit Privatkonto bezahlt`), + settleInvoiceBusiness: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `${amount} mit Geschäftskonto ${last4Digits} bezahlt` : `Mit Geschäftskonto bezahlt`), + payWithPolicy: ({formattedAmount, policyName}: SettleExpensifyCardParams & {policyName: string}) => + formattedAmount ? `${formattedAmount} über ${policyName} bezahlen` : `Über ${policyName} bezahlen`, + businessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `${amount} mit Bankkonto ${last4Digits} bezahlt.` : `mit Bankkonto ${last4Digits} bezahlt.`), + automaticallyPaidWithBusinessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => + `heeft ${amount} betaald met bankrekening ${last4Digits}. via werkruimte regels`, + invoicePersonalBank: ({lastFour}: BankAccountLastFourParams) => `Privatkonto • ${lastFour}`, + invoiceBusinessBank: ({lastFour}: BankAccountLastFourParams) => `Geschäftskonto • ${lastFour}`, nextStep: 'Nächste Schritte', finished: 'Fertiggestellt', sendInvoice: ({amount}: RequestAmountParams) => `Sende ${amount} Rechnung`, @@ -1179,8 +1191,8 @@ const translations = { `hat die Zahlung von ${amount} storniert, weil ${submitterDisplayName} ihre Expensify Wallet nicht innerhalb von 30 Tagen aktiviert hat`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} hat ein Bankkonto hinzugefügt. Die Zahlung von ${amount} wurde geleistet.`, - paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}woanders bezahlt`, - paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''} mit Expensify bezahlt`, + paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}als bezahlt markiert`, + paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}mit Wallet bezahlt`, automaticallyPaidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''} mit Expensify über Arbeitsbereichsregeln bezahlt`, noReimbursableExpenses: 'Dieser Bericht hat einen ungültigen Betrag.', @@ -1842,6 +1854,7 @@ const translations = { sendAndReceiveMoney: 'Senden und Empfangen von Geld mit Freunden. Nur US-Bankkonten.', enableWallet: 'Wallet aktivieren', addBankAccountToSendAndReceive: 'Fügen Sie ein Bankkonto hinzu, um Zahlungen zu tätigen oder zu empfangen.', + addDebitOrCreditCard: 'Debit- oder Kreditkarte hinzufügen', assignedCards: 'Zugewiesene Karten', assignedCardsDescription: 'Dies sind Karten, die von einem Workspace-Admin zugewiesen wurden, um die Ausgaben des Unternehmens zu verwalten.', expensifyCard: 'Expensify Card', @@ -2056,6 +2069,7 @@ const translations = { cardLastFour: 'Karte endet mit', addFirstPaymentMethod: 'Fügen Sie eine Zahlungsmethode hinzu, um Zahlungen direkt in der App zu senden und zu empfangen.', defaultPaymentMethod: 'Standardmäßig', + bankAccountLastFour: ({lastFour}: BankAccountLastFourParams) => `Bankkonto • ${lastFour}`, }, preferencesPage: { appSection: { diff --git a/src/languages/en.ts b/src/languages/en.ts index f2610e03b5aa..56fc5b9ab274 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -23,6 +23,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, @@ -37,6 +38,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1127,10 +1129,20 @@ const translations = { individual: 'Individual', business: 'Business', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} with Expensify` : `Pay with Expensify`), - settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} as an individual` : `Pay as an individual`), + settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} as an individual` : `Pay with personal account`), + settleWallet: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} with wallet` : `Pay with wallet`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Pay ${formattedAmount}`, - settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} as a business` : `Pay as a business`), - payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} elsewhere` : `Pay elsewhere`), + settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} as a business` : `Pay with business account`), + payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Mark ${formattedAmount} as paid` : `Mark as paid`), + settleInvoicePersonal: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `Paid ${amount} with personal account ${last4Digits}` : `Paid with personal account`), + settleInvoiceBusiness: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `Paid ${amount} with business account ${last4Digits}` : `Paid with business account`), + payWithPolicy: ({formattedAmount, policyName}: SettleExpensifyCardParams & {policyName: string}) => + formattedAmount ? `Pay ${formattedAmount} via ${policyName}` : `Pay via ${policyName}`, + businessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `Paid ${amount} with bank account ${last4Digits}` : `Paid with bank account ${last4Digits}`), + automaticallyPaidWithBusinessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => + `paid ${amount ? `${amount} ` : ''}with bank account ${last4Digits} via workspace rules`, + invoicePersonalBank: ({lastFour}: BankAccountLastFourParams) => `Personal account • ${lastFour}`, + invoiceBusinessBank: ({lastFour}: BankAccountLastFourParams) => `Business Account • ${lastFour}`, nextStep: 'Next steps', finished: 'Finished', sendInvoice: ({amount}: RequestAmountParams) => `Send ${amount} invoice`, @@ -1165,8 +1177,8 @@ const translations = { `canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} added a bank account. The ${amount} payment has been made.`, - paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}paid elsewhere`, - paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}paid with Expensify`, + paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}marked as paid`, + paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}paid with wallet`, automaticallyPaidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}paid with Expensify via workspace rules`, noReimbursableExpenses: 'This report has an invalid amount', @@ -1820,6 +1832,7 @@ const translations = { sendAndReceiveMoney: 'Send and receive money with friends. US bank accounts only.', enableWallet: 'Enable wallet', addBankAccountToSendAndReceive: 'Add a bank account to make or receive payments.', + addDebitOrCreditCard: 'Add debit or credit card', assignedCards: 'Assigned cards', assignedCardsDescription: 'These are cards assigned by a workspace admin to manage company spend.', expensifyCard: 'Expensify Card', @@ -2030,6 +2043,7 @@ const translations = { cardLastFour: 'Card ending in', addFirstPaymentMethod: 'Add a payment method to send and receive payments directly in the app.', defaultPaymentMethod: 'Default', + bankAccountLastFour: ({lastFour}: BankAccountLastFourParams) => `Bank Account • ${lastFour}`, }, preferencesPage: { appSection: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 465f547397db..44a4f4f4cee1 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -22,6 +22,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, @@ -36,6 +37,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1121,10 +1123,21 @@ const translations = { individual: 'Individual', business: 'Empresa', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} con Expensify` : `Pagar con Expensify`), - settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pago ${formattedAmount} como individuo` : `Pago individual`), + settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pago ${formattedAmount} como individuo` : `Pago con cuenta personal`), + settleWallet: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} con billetera` : `con billetera`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Pagar ${formattedAmount}`, - settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} como negocio` : `Pagar como empresa`), - payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} de otra forma` : `Pagar de otra forma`), + settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} como negocio` : `Pago con cuenta empresarial`), + payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Marcar ${formattedAmount} como pagado` : `Marcar como pagado`), + settleInvoicePersonal: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `Pagado ${amount} con cuenta personal ${last4Digits}` : `Pagado con cuenta personal`), + settleInvoiceBusiness: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `Pagado ${amount} con cuenta de empresa ${last4Digits}` : `Pagado con cuenta de empresa`), + payWithPolicy: ({formattedAmount, policyName}: SettleExpensifyCardParams & {policyName: string}) => + formattedAmount ? `Pay ${formattedAmount} via ${policyName}` : `Pay via ${policyName}`, + businessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => + amount ? `Pagó ${amount} con la cuenta bancaria ${last4Digits}.` : `Pagó con la cuenta bancaria ${last4Digits}`, + automaticallyPaidWithBusinessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => + `pagado ${amount ? `${amount} ` : ''}con la cuenta bancaria terminada en ${last4Digits} vía reglas del espacio de trabajo`, + invoicePersonalBank: ({lastFour}: BankAccountLastFourParams) => `Cuenta personal • ${lastFour}`, + invoiceBusinessBank: ({lastFour}: BankAccountLastFourParams) => `Cuenta de empresa • ${lastFour}`, nextStep: 'Pasos siguientes', finished: 'Finalizado', sendInvoice: ({amount}: RequestAmountParams) => `Enviar factura de ${amount}`, @@ -1159,8 +1172,8 @@ const translations = { `canceló el pago ${amount}, porque ${submitterDisplayName} no habilitó tu Billetera Expensify en un plazo de 30 días.`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} añadió una cuenta bancaria. El pago de ${amount} se ha realizado.`, - paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}pagó de otra forma`, - paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}pagó con Expensify`, + paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}marcó como pagado`, + paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}pagó con la billetera`, automaticallyPaidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}pagó con Expensify via reglas del espacio de trabajo`, noReimbursableExpenses: 'El importe de este informe no es válido', @@ -1818,6 +1831,7 @@ const translations = { sendAndReceiveMoney: 'Envía y recibe dinero desde tu Billetera Expensify. Solo cuentas bancarias de EE. UU.', enableWallet: 'Habilitar billetera', addBankAccountToSendAndReceive: 'Añade una cuenta bancaria para hacer o recibir pagos.', + addDebitOrCreditCard: 'Añadir tarjeta de débito o crédito', assignedCards: 'Tarjetas asignadas', assignedCardsDescription: 'Son tarjetas asignadas por un administrador del espacio de trabajo para gestionar los gastos de la empresa.', expensifyCard: 'Tarjeta Expensify', @@ -2029,6 +2043,7 @@ const translations = { cardLastFour: 'Tarjeta terminada en', addFirstPaymentMethod: 'Añade un método de pago para enviar y recibir pagos directamente desde la aplicación.', defaultPaymentMethod: 'Predeterminado', + bankAccountLastFour: ({lastFour}: BankAccountLastFourParams) => `Cuenta bancaria • ${lastFour}`, }, preferencesPage: { appSection: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 659f112c10a0..c7641fa6f2eb 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -35,6 +35,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, @@ -49,6 +50,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1143,10 +1145,22 @@ const translations = { individual: 'Individuel', business: 'Entreprise', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Payer ${formattedAmount} avec Expensify` : `Payer avec Expensify`), - settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Payer ${formattedAmount} en tant qu'individu` : `Payer en tant qu'individu`), + settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Payer ${formattedAmount} en tant qu'individu` : `Payer avec un compte personnel`), + settleWallet: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Payer ${formattedAmount} avec le portefeuille` : `Payer avec le portefeuille`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Payer ${formattedAmount}`, - settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Payer ${formattedAmount} en tant qu'entreprise` : `Payer en tant qu'entreprise`), - payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Payer ${formattedAmount} ailleurs` : `Payer ailleurs`), + settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Payer ${formattedAmount} en tant qu'entreprise` : `Payer avec un compte professionnel`), + payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Marquer ${formattedAmount} comme payé` : `Marquer comme payé`), + settleInvoicePersonal: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `Payé ${amount} avec le compte personnel ${last4Digits}` : `Payé avec le compte personnel`), + settleInvoiceBusiness: ({amount, last4Digits}: BusinessBankAccountParams) => + amount ? `Payé ${amount} avec le compte professionnel ${last4Digits}` : `Payé avec le compte professionnel`, + payWithPolicy: ({formattedAmount, policyName}: SettleExpensifyCardParams & {policyName: string}) => + formattedAmount ? `Payer ${formattedAmount} via ${policyName}` : `Payer via ${policyName}`, + businessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => + amount ? `Payé ${amount} avec le compte bancaire ${last4Digits}.` : `Payé avec le compte bancaire ${last4Digits}`, + automaticallyPaidWithBusinessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => + `payé ${amount ? `${amount} ` : ''}avec le compte bancaire se terminant par ${last4Digits} via les règles de l’espace de travail`, + invoicePersonalBank: ({lastFour}: BankAccountLastFourParams) => `Compte personnel • ${lastFour}`, + invoiceBusinessBank: ({lastFour}: BankAccountLastFourParams) => `Compte professionnel • ${lastFour}`, nextStep: 'Étapes suivantes', finished: 'Terminé', sendInvoice: ({amount}: RequestAmountParams) => `Envoyer une facture de ${amount}`, @@ -1181,8 +1195,8 @@ const translations = { `a annulé le paiement de ${amount}, car ${submitterDisplayName} n'a pas activé leur Expensify Wallet dans les 30 jours`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} a ajouté un compte bancaire. Le paiement de ${amount} a été effectué.`, - paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''} payé ailleurs`, - paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''} payé avec Expensify`, + paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}marqué comme payé`, + paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}payé avec le portefeuille`, automaticallyPaidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''} payé avec Expensify via les règles de l'espace de travail`, noReimbursableExpenses: 'Ce rapport contient un montant invalide', @@ -1844,6 +1858,7 @@ const translations = { sendAndReceiveMoney: "Envoyez et recevez de l'argent avec des amis. Comptes bancaires américains uniquement.", enableWallet: 'Activer le portefeuille', addBankAccountToSendAndReceive: 'Ajoutez un compte bancaire pour effectuer ou recevoir des paiements.', + addDebitOrCreditCard: 'Ajouter une carte de débit ou de crédit', assignedCards: 'Cartes assignées', assignedCardsDescription: "Ce sont des cartes attribuées par un administrateur d'espace de travail pour gérer les dépenses de l'entreprise.", expensifyCard: 'Expensify Card', @@ -2059,6 +2074,7 @@ const translations = { cardLastFour: 'Carte se terminant par', addFirstPaymentMethod: "Ajoutez un mode de paiement pour envoyer et recevoir des paiements directement dans l'application.", defaultPaymentMethod: 'Par défaut', + bankAccountLastFour: ({lastFour}: BankAccountLastFourParams) => `Bank Account • ${lastFour}`, }, preferencesPage: { appSection: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 279aacbf2426..002b6e22b05c 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -35,6 +35,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, @@ -49,6 +50,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1138,10 +1140,21 @@ const translations = { individual: 'Individuale', business: 'Business', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Paga ${formattedAmount} con Expensify` : `Paga con Expensify`), - settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Paga ${formattedAmount} come individuo` : `Paga come individuo`), + settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Paga ${formattedAmount} come individuo` : `Paga con conto personale`), + settleWallet: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Paga ${formattedAmount} con portafoglio` : `Paga con portafoglio`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Paga ${formattedAmount}`, - settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Paga ${formattedAmount} come azienda` : `Paga come un'azienda`), - payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Paga ${formattedAmount} altrove` : `Paga altrove`), + settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Paga ${formattedAmount} come azienda` : `Paga con conto aziendale`), + payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Segna ${formattedAmount} come pagato` : `Segna come pagato`), + settleInvoicePersonal: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `Pagato ${amount} con conto personale ${last4Digits}` : `Pagato con conto personale`), + settleInvoiceBusiness: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `Pagato ${amount} con conto aziendale ${last4Digits}` : `Pagato con conto aziendale`), + payWithPolicy: ({formattedAmount, policyName}: SettleExpensifyCardParams & {policyName: string}) => + formattedAmount ? `Paga ${formattedAmount} tramite ${policyName}` : `Paga tramite ${policyName}`, + businessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => + amount ? `Pagato ${amount} con conto bancario ${last4Digits}` : `Pagato con conto bancario ${last4Digits}`, + automaticallyPaidWithBusinessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => + `pagato ${amount ? `${amount} ` : ''}con il conto bancario terminante con ${last4Digits} tramite le regole dello spazio di lavoro`, + invoicePersonalBank: ({lastFour}: BankAccountLastFourParams) => `Conto personale • ${lastFour}`, + invoiceBusinessBank: ({lastFour}: BankAccountLastFourParams) => `Conto aziendale • ${lastFour}`, nextStep: 'Prossimi passi', finished: 'Finito', sendInvoice: ({amount}: RequestAmountParams) => `Invia fattura di ${amount}`, @@ -1176,8 +1189,8 @@ const translations = { `annullato il pagamento di ${amount}, perché ${submitterDisplayName} non ha attivato il loro Expensify Wallet entro 30 giorni`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} ha aggiunto un conto bancario. Il pagamento di ${amount} è stato effettuato.`, - paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}pagato altrove`, - paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''} pagato con Expensify`, + paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}segnato come pagato`, + paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}pagato con portafoglio`, automaticallyPaidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''} ha pagato con Expensify tramite regole dello spazio di lavoro`, noReimbursableExpenses: 'Questo rapporto ha un importo non valido', @@ -1836,6 +1849,7 @@ const translations = { sendAndReceiveMoney: 'Invia e ricevi denaro con gli amici. Solo conti bancari statunitensi.', enableWallet: 'Abilita portafoglio', addBankAccountToSendAndReceive: 'Aggiungi un conto bancario per effettuare o ricevere pagamenti.', + addDebitOrCreditCard: 'Aggiungi carta di debito o di credito', assignedCards: 'Carte assegnate', assignedCardsDescription: 'Queste sono carte assegnate da un amministratore del workspace per gestire le spese aziendali.', expensifyCard: 'Expensify Card', @@ -2049,6 +2063,7 @@ const translations = { cardLastFour: 'Carta che termina con', addFirstPaymentMethod: "Aggiungi un metodo di pagamento per inviare e ricevere pagamenti direttamente nell'app.", defaultPaymentMethod: 'Predefinito', + bankAccountLastFour: ({lastFour}: BankAccountLastFourParams) => `Conto bancario • ${lastFour}`, }, preferencesPage: { appSection: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 5383d36032e6..0bf112445719 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -35,6 +35,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, @@ -49,6 +50,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1140,10 +1142,21 @@ const translations = { individual: '個人', business: 'ビジネス', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Expensifyで${formattedAmount}を支払う` : `Expensifyで支払う`), - settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `個人として${formattedAmount}を支払う` : `個人として支払う`), + settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `${formattedAmount}を個人として支払う` : `個人口座で支払う`), + settleWallet: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `ウォレットで${formattedAmount}を支払う` : `ウォレットで支払う`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `${formattedAmount}を支払う`, - settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `${formattedAmount} をビジネスとして支払う` : `ビジネスとして支払う`), - payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `他の場所で${formattedAmount}を支払う` : `他の場所で支払う`), + settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `${formattedAmount}をビジネスとして支払う` : `ビジネス口座で支払う`), + payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `${formattedAmount}を支払い済みにマーク` : `支払い済みにマーク`), + settleInvoicePersonal: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `${amount}を個人口座(${last4Digits})で支払い済み` : `個人口座で支払い済み`), + settleInvoiceBusiness: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `${amount}をビジネス口座(${last4Digits})で支払い済み` : `ビジネス口座で支払い済み`), + payWithPolicy: ({formattedAmount, policyName}: SettleExpensifyCardParams & {policyName: string}) => + formattedAmount ? `${policyName}経由で${formattedAmount}を支払う` : `${policyName}経由で支払う`, + businessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => + amount ? `${amount}を銀行口座(${last4Digits})で支払い済み` : `を銀行口座(${last4Digits})で支払い済み`, + automaticallyPaidWithBusinessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => + `${amount}円が銀行口座(下4桁:${last4Digits})で支払われました ワークスペースのルールによる`, + invoicePersonalBank: ({lastFour}: BankAccountLastFourParams) => `個人口座・${lastFour}`, + invoiceBusinessBank: ({lastFour}: BankAccountLastFourParams) => `ビジネス口座・${lastFour}`, nextStep: '次のステップ', finished: '完了', sendInvoice: ({amount}: RequestAmountParams) => `${amount} 請求書を送信`, @@ -1178,8 +1191,8 @@ const translations = { `${submitterDisplayName}が30日以内にExpensifyウォレットを有効にしなかったため、${amount}の支払いをキャンセルしました。`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName}が銀行口座を追加しました。${amount}の支払いが行われました。`, - paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}は他で支払われました`, - paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}はExpensifyで支払いました`, + paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}支払い済みにマークされました`, + paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}ウォレットで支払い済み`, automaticallyPaidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}はワークスペースルールを通じてExpensifyで支払いました。`, noReimbursableExpenses: 'このレポートには無効な金額が含まれています', @@ -1833,6 +1846,7 @@ const translations = { sendAndReceiveMoney: '友達とお金を送受信する。米国の銀行口座のみ。', enableWallet: 'ウォレットを有効にする', addBankAccountToSendAndReceive: '支払いや受け取りを行うために銀行口座を追加してください。', + addDebitOrCreditCard: 'デビットカードまたはクレジットカードを追加', assignedCards: '割り当てられたカード', assignedCardsDescription: 'これらは、会社の支出を管理するためにワークスペース管理者によって割り当てられたカードです。', expensifyCard: 'Expensify Card', @@ -2042,6 +2056,7 @@ const translations = { cardLastFour: '末尾が', addFirstPaymentMethod: 'アプリ内で直接送受金を行うために支払い方法を追加してください。', defaultPaymentMethod: 'デフォルト', + bankAccountLastFour: ({lastFour}: BankAccountLastFourParams) => `銀行口座・${lastFour}`, }, preferencesPage: { appSection: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index d93229554d33..d4fd5b1203f0 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -35,6 +35,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, @@ -49,6 +50,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1139,10 +1141,21 @@ const translations = { individual: 'Individuueel', business: 'Business', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Betaal ${formattedAmount} met Expensify` : `Betaal met Expensify`), - settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Betaal ${formattedAmount} als individu` : `Betaal als individu`), + settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Betaal ${formattedAmount} als individu` : `Betalen met persoonlijke rekening`), + settleWallet: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Betaal ${formattedAmount} met wallet` : `Betalen met wallet`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Betaal ${formattedAmount}`, - settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Betaal ${formattedAmount} als een bedrijf` : `Betalen als een bedrijf`), - payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Betaal ${formattedAmount} ergens anders` : `Elders betalen`), + settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Betaal ${formattedAmount} als bedrijf` : `Betalen met zakelijke rekening`), + payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `${formattedAmount} als betaald markeren` : `Markeren als betaald`), + settleInvoicePersonal: ({amount, last4Digits}: BusinessBankAccountParams) => + amount ? `${amount} betaald met persoonlijke rekening ${last4Digits}` : `Betaald met persoonlijke rekening`, + settleInvoiceBusiness: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `${amount} betaald met zakelijke rekening ${last4Digits}` : `Betaald met zakelijke rekening`), + payWithPolicy: ({formattedAmount, policyName}: SettleExpensifyCardParams & {policyName: string}) => + formattedAmount ? `Betaal ${formattedAmount} via ${policyName}` : `Betalen via ${policyName}`, + businessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `${amount} betaald via bankrekening ${last4Digits}` : `betaald via bankrekening ${last4Digits}`), + automaticallyPaidWithBusinessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => + `${amount} betaald met bankrekening eindigend op ${last4Digits} via werkruimte regels`, + invoicePersonalBank: ({lastFour}: BankAccountLastFourParams) => `Persoonlijke rekening • ${lastFour}`, + invoiceBusinessBank: ({lastFour}: BankAccountLastFourParams) => `Zakelijke rekening • ${lastFour}`, nextStep: 'Volgende stappen', finished: 'Voltooid', sendInvoice: ({amount}: RequestAmountParams) => `Verstuur ${amount} factuur`, @@ -1177,8 +1190,8 @@ const translations = { `heeft de betaling van ${amount} geannuleerd, omdat ${submitterDisplayName} hun Expensify Wallet niet binnen 30 dagen heeft geactiveerd.`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} heeft een bankrekening toegevoegd. De betaling van ${amount} is gedaan.`, - paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''} elders betaald`, - paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}betaald met Expensify`, + paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}gemarkeerd als betaald`, + paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}betaald met wallet`, automaticallyPaidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}betaald met Expensify via werkruimte regels`, noReimbursableExpenses: 'Dit rapport heeft een ongeldig bedrag.', @@ -1836,6 +1849,7 @@ const translations = { sendAndReceiveMoney: 'Stuur en ontvang geld met vrienden. Alleen Amerikaanse bankrekeningen.', enableWallet: 'Portemonnee inschakelen', addBankAccountToSendAndReceive: 'Voeg een bankrekening toe om betalingen te doen of te ontvangen.', + addDebitOrCreditCard: 'Debet- of creditcard toevoegen', assignedCards: 'Toegewezen kaarten', assignedCardsDescription: 'Dit zijn kaarten die door een werkruimtebeheerder zijn toegewezen om de uitgaven van het bedrijf te beheren.', expensifyCard: 'Expensify Card', @@ -2049,6 +2063,7 @@ const translations = { cardLastFour: 'Kaart eindigend op', addFirstPaymentMethod: 'Voeg een betaalmethode toe om betalingen direct in de app te verzenden en ontvangen.', defaultPaymentMethod: 'Standaard', + bankAccountLastFour: ({lastFour}: BankAccountLastFourParams) => `Bankrekening • ${lastFour}`, }, preferencesPage: { appSection: { diff --git a/src/languages/params.ts b/src/languages/params.ts index 1a77cf89384c..6a20ebef8e01 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -161,6 +161,11 @@ type WorkspacesListRouteParams = { workspacesListRoute: string; }; +type BusinessBankAccountParams = { + amount?: string; + last4Digits?: string; +}; + type WorkspaceRouteParams = { workspaceRoute: string; }; @@ -225,6 +230,8 @@ type TransferParams = {amount: string}; type InstantSummaryParams = {rate: string; minAmount: string}; +type BankAccountLastFourParams = {lastFour: string}; + type NotYouParams = {user: string}; type DateShouldBeBeforeParams = {dateString: string}; @@ -1080,6 +1087,7 @@ export type { SettlementDateParams, PolicyExpenseChatNameParams, YourPlanPriceValueParams, + BusinessBankAccountParams, NeedCategoryForExportToIntegrationParams, UpdatedPolicyAuditRateParams, UpdatedPolicyManualApprovalThresholdParams, @@ -1093,6 +1101,7 @@ export type { UpdatedPolicyCategoryExpenseLimitTypeParams, UpdatedPolicyCategoryMaxAmountNoReceiptParams, SubscriptionSettingsSummaryParams, + BankAccountLastFourParams, ReviewParams, CreateExpensesParams, CurrencyInputDisabledTextParams, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index ef4b5b849aa3..7fbd14e3e9cd 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -35,6 +35,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, @@ -49,6 +50,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1137,10 +1139,21 @@ const translations = { individual: 'Indywidualny', business: 'Biznes', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Zapłać ${formattedAmount} za pomocą Expensify` : `Zapłać z Expensify`), - settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Zapłać ${formattedAmount} jako osoba prywatna` : `Płać jako osoba prywatna`), + settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Zapłać ${formattedAmount} jako osoba prywatna` : `Zapłać z konta osobistego`), + settleWallet: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Zapłać ${formattedAmount} portfelem` : `Zapłać portfelem`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Zapłać ${formattedAmount}`, - settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Zapłać ${formattedAmount} jako firma` : `Płać jako firma`), - payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Zapłać ${formattedAmount} gdzie indziej` : `Zapłać gdzie indziej`), + settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Zapłać ${formattedAmount} jako firma` : `Zapłać z konta firmowego`), + payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Oznacz ${formattedAmount} jako zapłacone` : `Oznacz jako zapłacone`), + settleInvoicePersonal: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `Zapłacono ${amount} z konta osobistego ${last4Digits}` : `Zapłacono z konta osobistego`), + settleInvoiceBusiness: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `Zapłacono ${amount} z konta firmowego ${last4Digits}` : `Zapłacono z konta firmowego`), + payWithPolicy: ({formattedAmount, policyName}: SettleExpensifyCardParams & {policyName: string}) => + formattedAmount ? `Zapłać ${formattedAmount} przez ${policyName}` : `Zapłać przez ${policyName}`, + businessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => + amount ? `Zapłacono ${amount} z konta bankowego ${last4Digits}` : `Zapłacono z konta bankowego ${last4Digits}`, + automaticallyPaidWithBusinessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => + `zapłacono ${amount ? `${amount} ` : ''}z konta bankowego o numerze kończącym się na ${last4Digits} przez zasady przestrzeni roboczej`, + invoicePersonalBank: ({lastFour}: BankAccountLastFourParams) => `Konto osobiste • ${lastFour}`, + invoiceBusinessBank: ({lastFour}: BankAccountLastFourParams) => `Konto firmowe • ${lastFour}`, nextStep: 'Następne kroki', finished: 'Zakończono', sendInvoice: ({amount}: RequestAmountParams) => `Wyślij fakturę na kwotę ${amount}`, @@ -1175,8 +1188,8 @@ const translations = { `anulowano płatność w wysokości ${amount}, ponieważ ${submitterDisplayName} nie aktywował swojego Portfela Expensify w ciągu 30 dni`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} dodał konto bankowe. Płatność w wysokości ${amount} została dokonana.`, - paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}zapłacono gdzie indziej`, - paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}zapłacono za pomocą Expensify`, + paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}oznaczono jako zapłacone`, + paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}zapłacono portfelem`, automaticallyPaidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}zapłacono z Expensify za pomocą zasad przestrzeni roboczej`, noReimbursableExpenses: 'Ten raport ma nieprawidłową kwotę', @@ -1832,6 +1845,7 @@ const translations = { sendAndReceiveMoney: 'Wysyłaj i odbieraj pieniądze z przyjaciółmi. Tylko konta bankowe w USA.', enableWallet: 'Włącz portfel', addBankAccountToSendAndReceive: 'Dodaj konto bankowe, aby dokonywać lub otrzymywać płatności.', + addDebitOrCreditCard: 'Dodaj kartę debetową lub kredytową', assignedCards: 'Przypisane karty', assignedCardsDescription: 'Są to karty przypisane przez administratora przestrzeni roboczej do zarządzania wydatkami firmy.', expensifyCard: 'Expensify Card', @@ -2045,6 +2059,7 @@ const translations = { cardLastFour: 'Karta kończąca się na', addFirstPaymentMethod: 'Dodaj metodę płatności, aby wysyłać i odbierać płatności bezpośrednio w aplikacji.', defaultPaymentMethod: 'Domyślny', + bankAccountLastFour: ({lastFour}: BankAccountLastFourParams) => `Konto bankowe • ${lastFour}`, }, preferencesPage: { appSection: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 9aa4b0841e9c..a9684403648c 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -35,6 +35,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, @@ -49,6 +50,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1139,10 +1141,20 @@ const translations = { individual: 'Individual', business: 'Negócio', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pague ${formattedAmount} com Expensify` : `Pague com Expensify`), - settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} como indivíduo` : `Pagar como indivíduo`), + settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} como indivíduo` : `Pagar com conta pessoal`), + settleWallet: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} com carteira` : `Pagar com carteira`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Pagar ${formattedAmount}`, - settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pague ${formattedAmount} como uma empresa` : `Pagar como empresa`), - payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pague ${formattedAmount} em outro lugar` : `Pague em outro lugar`), + settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} como empresa` : `Pagar com conta empresarial`), + payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Marcar ${formattedAmount} como pago` : `Marcar como pago`), + settleInvoicePersonal: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `Pago ${amount} com conta pessoal ${last4Digits}` : `Pago com conta pessoal`), + settleInvoiceBusiness: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `Pago ${amount} com conta empresarial ${last4Digits}` : `Pago com conta empresarial`), + payWithPolicy: ({formattedAmount, policyName}: SettleExpensifyCardParams & {policyName: string}) => + formattedAmount ? `Pagar ${formattedAmount} via ${policyName}` : `Pagar via ${policyName}`, + businessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `Pago ${amount} com conta bancária ${last4Digits}` : `Pago com conta bancária ${last4Digits}`), + automaticallyPaidWithBusinessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => + `pago ${amount ? `${amount} ` : ''}com a conta bancária terminada em ${last4Digits} via regras do espaço de trabalho`, + invoicePersonalBank: ({lastFour}: BankAccountLastFourParams) => `Conta pessoal • ${lastFour}`, + invoiceBusinessBank: ({lastFour}: BankAccountLastFourParams) => `Conta empresarial • ${lastFour}`, nextStep: 'Próximos passos', finished: 'Concluído', sendInvoice: ({amount}: RequestAmountParams) => `Enviar fatura de ${amount}`, @@ -1177,8 +1189,8 @@ const translations = { `cancelou o pagamento de ${amount}, porque ${submitterDisplayName} não ativou sua Expensify Wallet dentro de 30 dias`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} adicionou uma conta bancária. O pagamento de ${amount} foi realizado.`, - paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''} pago em outro lugar`, - paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''} pagou com Expensify`, + paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}marcado como pago`, + paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}pago com carteira`, automaticallyPaidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''} pagou com Expensify via regras do workspace`, noReimbursableExpenses: 'Este relatório possui um valor inválido', @@ -1835,6 +1847,7 @@ const translations = { sendAndReceiveMoney: 'Envie e receba dinheiro com amigos. Apenas contas bancárias dos EUA.', enableWallet: 'Ativar carteira', addBankAccountToSendAndReceive: 'Adicione uma conta bancária para fazer ou receber pagamentos.', + addDebitOrCreditCard: 'Adicionar cartão de débito ou crédito', assignedCards: 'Cartões atribuídos', assignedCardsDescription: 'Estes são cartões atribuídos por um administrador de espaço de trabalho para gerenciar os gastos da empresa.', expensifyCard: 'Expensify Card', @@ -2048,6 +2061,7 @@ const translations = { cardLastFour: 'Cartão terminando em', addFirstPaymentMethod: 'Adicione um método de pagamento para enviar e receber pagamentos diretamente no aplicativo.', defaultPaymentMethod: 'Padrão', + bankAccountLastFour: ({lastFour}: BankAccountLastFourParams) => `Conta bancária • ${lastFour}`, }, preferencesPage: { appSection: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 6ef632e8a025..8e1f1b2f1d7b 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -35,6 +35,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, @@ -49,6 +50,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1129,10 +1131,20 @@ const translations = { individual: '个人', business: '商务', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `使用 Expensify 支付 ${formattedAmount}` : `使用Expensify支付`), - settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `以个人身份支付${formattedAmount}` : `以个人身份支付`), + settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `以个人身份支付${formattedAmount}` : `用个人账户支付`), + settleWallet: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `用钱包支付${formattedAmount}` : `用钱包支付`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `支付 ${formattedAmount}`, - settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `以企业身份支付${formattedAmount}` : `以企业身份支付`), - payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `在其他地方支付${formattedAmount}` : `在其他地方支付`), + settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `以企业身份支付${formattedAmount}` : `用企业账户支付`), + payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `标记${formattedAmount}为已支付` : `标记为已支付`), + settleInvoicePersonal: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `已用个人账户${last4Digits}支付${amount}` : `已用个人账户支付`), + settleInvoiceBusiness: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `已用企业账户${last4Digits}支付${amount}` : `已用企业账户支付`), + payWithPolicy: ({formattedAmount, policyName}: SettleExpensifyCardParams & {policyName: string}) => + formattedAmount ? `通过${policyName}支付${formattedAmount}` : `通过${policyName}支付`, + businessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `已用银行账户${last4Digits}支付${amount} ` : `已用银行账户${last4Digits}支付 `), + automaticallyPaidWithBusinessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => + `已使用尾号为${last4Digits}的银行账户支付${amount} 通过工作区规则`, + invoicePersonalBank: ({lastFour}: BankAccountLastFourParams) => `个人账户 • ${lastFour}`, + invoiceBusinessBank: ({lastFour}: BankAccountLastFourParams) => `企业账户 • ${lastFour}`, nextStep: '下一步', finished: '完成', sendInvoice: ({amount}: RequestAmountParams) => `发送 ${amount} 发票`, @@ -1165,8 +1177,8 @@ const translations = { adminCanceledRequest: ({manager}: AdminCanceledRequestParams) => `${manager ? `${manager}: ` : ''}取消了付款`, canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) => `取消了${amount}付款,因为${submitterDisplayName}在30天内未启用他们的Expensify Wallet。`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} 添加了一个银行账户。${amount} 付款已完成。`, - paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}在其他地方支付`, - paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}通过Expensify支付`, + paidElsewhere: ({payer}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}已标记为已支付`, + paidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}已用钱包支付`, automaticallyPaidWithExpensify: ({payer}: PaidWithExpensifyParams = {}) => `${payer ? `${payer} ` : ''}通过工作区规则使用Expensify支付`, noReimbursableExpenses: '此报告的金额无效', @@ -1818,6 +1830,7 @@ const translations = { sendAndReceiveMoney: '与朋友发送和接收资金。仅限美国银行账户。', enableWallet: '启用钱包', addBankAccountToSendAndReceive: '添加银行账户以进行付款或收款。', + addDebitOrCreditCard: '添加借记卡或信用卡', assignedCards: '已分配的卡片', assignedCardsDescription: '这些是由工作区管理员分配的卡片,用于管理公司支出。', expensifyCard: 'Expensify Card', @@ -2026,6 +2039,7 @@ const translations = { cardLastFour: '卡号末尾为', addFirstPaymentMethod: '添加支付方式以便直接在应用中发送和接收付款。', defaultPaymentMethod: '默认', + bankAccountLastFour: ({lastFour}: BankAccountLastFourParams) => `银行账户 • ${lastFour}`, }, preferencesPage: { appSection: { diff --git a/src/libs/API/parameters/MoveIOUReportToExistingPolicyParams.ts b/src/libs/API/parameters/MoveIOUReportToExistingPolicyParams.ts index a72c7ff4f552..de125c916e8c 100644 --- a/src/libs/API/parameters/MoveIOUReportToExistingPolicyParams.ts +++ b/src/libs/API/parameters/MoveIOUReportToExistingPolicyParams.ts @@ -2,6 +2,7 @@ type MoveIOUReportToExistingPolicyParams = { iouReportID: string; policyID: string; changePolicyReportActionID: string; + dmMovedReportActionID: string; }; export default MoveIOUReportToExistingPolicyParams; diff --git a/src/libs/API/parameters/MoveIOUReportToPolicyAndInviteSubmitterParams.ts b/src/libs/API/parameters/MoveIOUReportToPolicyAndInviteSubmitterParams.ts index 4868b9d60ab8..9595493ec9bb 100644 --- a/src/libs/API/parameters/MoveIOUReportToPolicyAndInviteSubmitterParams.ts +++ b/src/libs/API/parameters/MoveIOUReportToPolicyAndInviteSubmitterParams.ts @@ -4,6 +4,7 @@ type MoveIOUReportToPolicyAndInviteSubmitterParams = { policyExpenseChatReportID: string; policyExpenseCreatedReportActionID: string; changePolicyReportActionID: string; + dmMovedReportActionID: string; }; export default MoveIOUReportToPolicyAndInviteSubmitterParams; diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 533b125072d3..5f4953431f54 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -823,6 +823,8 @@ function validateReportActionDraftProperty(key: keyof ReportAction, value: strin ...CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION, }, deleted: 'string', + bankAccountID: 'string', + payAsBusiness: 'string', }), () => validateObject>(value, { @@ -906,6 +908,8 @@ function validateReportActionDraftProperty(key: keyof ReportAction, value: strin expenseReportID: 'string', resolution: 'string', deleted: 'string', + bankAccountID: 'string', + payAsBusiness: 'string', }), ); } diff --git a/src/libs/MoneyRequestReportUtils.ts b/src/libs/MoneyRequestReportUtils.ts index 625512b31518..edad81f52a12 100644 --- a/src/libs/MoneyRequestReportUtils.ts +++ b/src/libs/MoneyRequestReportUtils.ts @@ -142,7 +142,7 @@ const getTotalAmountForIOUReportPreviewButton = (report: OnyxEntry, poli } // We shouldn't display the nonHeldAmount as the default option if it's not valid since we cannot pay partially in this case - if (hasHeldExpensesReportUtils(report?.reportID) && canAllowSettlement && hasValidNonHeldAmount) { + if (hasHeldExpensesReportUtils(report?.reportID) && canAllowSettlement && hasValidNonHeldAmount && !hasOnlyHeldExpenses) { return nonHeldAmount; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0baddce2e51b..ea2a13cebcac 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -373,6 +373,8 @@ type BuildOptimisticIOUReportActionParams = { isOwnPolicyExpenseChat?: boolean; created?: string; linkedExpenseReportAction?: OnyxEntry; + payAsBusiness?: boolean; + bankAccountID?: number | undefined; isPersonalTrackingExpense?: boolean; reportActionID?: string; }; @@ -1317,7 +1319,8 @@ function isChatReport(report: OnyxEntry): boolean { return report?.type === CONST.REPORT.TYPE.CHAT; } -function isInvoiceReport(report: OnyxInputOrEntry | SearchReport): boolean { +function isInvoiceReport(reportOrID: OnyxInputOrEntry | SearchReport | string): boolean { + const report = typeof reportOrID === 'string' ? (getReport(reportOrID, allReports) ?? null) : reportOrID; return report?.type === CONST.REPORT.TYPE.INVOICE; } @@ -1350,7 +1353,8 @@ function isReportIDApproved(reportID: string | undefined) { /** * Checks if a report is an Expense report. */ -function isExpenseReport(report: OnyxInputOrEntry | SearchReport): boolean { +function isExpenseReport(reportOrID: OnyxInputOrEntry | SearchReport | string): boolean { + const report = typeof reportOrID === 'string' ? (getReport(reportOrID, allReports) ?? null) : reportOrID; return report?.type === CONST.REPORT.TYPE.EXPENSE; } @@ -1566,6 +1570,10 @@ function isIndividualInvoiceRoom(report: OnyxEntry): boolean { return isInvoiceRoom(report) && report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; } +function isBusinessInvoiceRoom(report: OnyxEntry): boolean { + return isInvoiceRoom(report) && report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS; +} + function isCurrentUserInvoiceReceiver(report: OnyxEntry): boolean { if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { return currentUserAccountID === report.invoiceReceiver.accountID; @@ -4596,7 +4604,7 @@ function getReportPreviewMessage( } const containsNonReimbursable = hasNonReimbursableTransactions(report.reportID); - const {totalDisplaySpend: totalAmount, reimbursableSpend} = getMoneyRequestSpendBreakdown(report); + const {totalDisplaySpend: totalAmount} = getMoneyRequestSpendBreakdown(report); const parentReport = getParentReport(report); const policyName = getPolicyName({report: parentReport ?? report, policy}); @@ -4611,7 +4619,9 @@ function getReportPreviewMessage( }); } - let linkedTransaction: OnyxEntry; + const reportPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; + + let linkedTransaction; if (!isEmptyObject(iouReportAction) && shouldConsiderScanningReceiptOrPendingRoute && iouReportAction && isMoneyRequestAction(iouReportAction)) { linkedTransaction = getLinkedTransaction(iouReportAction); } @@ -4628,7 +4638,6 @@ function getReportPreviewMessage( // Show Paid preview message if it's settled or if the amount is paid & stuck at receivers end for only chat reports. if (isSettled(report.reportID) || (report.isWaitingOnBankAccount && isPreviewMessageForParentChatReport)) { - const formattedReimbursableAmount = convertToDisplayString(reimbursableSpend, report.currency); // A settled report preview message can come in three formats "paid ... elsewhere" or "paid ... with Expensify" let translatePhraseKey: TranslationPaths = 'iou.paidElsewhere'; if (isPreviewMessageForParentChatReport) { @@ -4642,13 +4651,22 @@ function getReportPreviewMessage( if (originalMessage?.automaticAction) { translatePhraseKey = 'iou.automaticallyPaidWithExpensify'; } + + if (originalMessage?.paymentType === CONST.IOU.PAYMENT_TYPE.VBBA) { + translatePhraseKey = 'iou.businessBankAccount'; + } } let actualPayerName = report.managerID === currentUserAccountID ? '' : getDisplayNameForParticipant({accountID: report.managerID, shouldUseShortForm: true}); + actualPayerName = actualPayerName && isForListPreview && !isPreviewMessageForParentChatReport ? `${actualPayerName}:` : actualPayerName; const payerDisplayName = isPreviewMessageForParentChatReport ? payerName : actualPayerName; - return translateLocal(translatePhraseKey, {amount: formattedReimbursableAmount, payer: payerDisplayName ?? ''}); + return translateLocal(translatePhraseKey, { + amount: '', + payer: payerDisplayName ?? '', + last4Digits: reportPolicy?.achAccount?.accountNumber?.slice(-4) ?? '', + }); } if (report.isWaitingOnBankAccount) { @@ -5160,11 +5178,20 @@ function getReportNameInternal({ if (isMoneyRequestAction(parentReportAction)) { const originalMessage = getOriginalMessage(parentReportAction); + const reportPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; + const last4Digits = reportPolicy?.achAccount?.accountNumber.slice(-4) ?? ''; + if (originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { if (originalMessage.paymentType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { return translateLocal('iou.paidElsewhere'); } - if (originalMessage.paymentType === CONST.IOU.PAYMENT_TYPE.VBBA || originalMessage.paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { + if (originalMessage.paymentType === CONST.IOU.PAYMENT_TYPE.VBBA) { + if (originalMessage.automaticAction) { + return translateLocal('iou.automaticallyPaidWithBusinessBankAccount', {last4Digits}); + } + return translateLocal('iou.businessBankAccount', {last4Digits}); + } + if (originalMessage.paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { if (originalMessage.automaticAction) { return translateLocal('iou.automaticallyPaidWithExpensify'); } @@ -6166,9 +6193,22 @@ function getPolicyChangeMessage(action: ReportAction) { * @param currency - IOU currency * @param paymentType - IOU paymentMethodType. Can be oneOf(Elsewhere, Expensify) * @param isSettlingUp - Whether we are settling up an IOU + * @param bankAccountID - Bank account ID + * @param payAsBusiness - Whether the payment is made as a business */ -function getIOUReportActionMessage(iouReportID: string, type: string, total: number, comment: string, currency: string, paymentType = '', isSettlingUp = false): Message[] { +function getIOUReportActionMessage( + iouReportID: string, + type: string, + total: number, + comment: string, + currency: string, + paymentType = '', + isSettlingUp = false, + bankAccountID?: number | undefined, + payAsBusiness = false, +): Message[] { const report = getReportOrDraftReport(iouReportID); + const isInvoice = isInvoiceReport(report); const amount = type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !isEmptyObject(report) ? convertToDisplayString(getMoneyRequestSpendBreakdown(report).totalDisplaySpend, currency) @@ -6209,7 +6249,14 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num iouMessage = `deleted the ${amount} expense${comment && ` for ${comment}`}`; break; case CONST.IOU.REPORT_ACTION_TYPE.PAY: - iouMessage = isSettlingUp ? `paid ${amount}${paymentMethodMessage}` : `sent ${amount}${comment && ` for ${comment}`}${paymentMethodMessage}`; + if (isInvoice && isSettlingUp) { + iouMessage = + paymentType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE + ? translateLocal('iou.payElsewhere', {formattedAmount: amount}) + : translateLocal(payAsBusiness ? 'iou.settleInvoiceBusiness' : 'iou.settleInvoicePersonal', {amount, last4Digits: String(bankAccountID).slice(-4)}); + } else { + iouMessage = isSettlingUp ? `paid ${amount}${paymentMethodMessage}` : `sent ${amount}${comment && ` for ${comment}`}${paymentMethodMessage}`; + } break; case CONST.REPORT.ACTIONS.TYPE.SUBMITTED: iouMessage = translateLocal('iou.expenseAmount', {formattedAmount: amount}); @@ -6260,6 +6307,8 @@ function buildOptimisticIOUReportAction(params: BuildOptimisticIOUReportActionPa created = DateUtils.getDBTime(), linkedExpenseReportAction, isPersonalTrackingExpense = false, + payAsBusiness, + bankAccountID, reportActionID, } = params; @@ -6272,6 +6321,8 @@ function buildOptimisticIOUReportAction(params: BuildOptimisticIOUReportActionPa IOUTransactionID: transactionID, IOUReportID, type, + payAsBusiness, + bankAccountID, }; const delegateAccountDetails = getPersonalDetailByEmail(delegateEmail); @@ -6333,7 +6384,7 @@ function buildOptimisticIOUReportAction(params: BuildOptimisticIOUReportActionPa }, ], avatar: getCurrentUserAvatar(), - message: getIOUReportActionMessage(iouReportID, type, amount, comment, currency, paymentType, isSettlingUp), + message: getIOUReportActionMessage(iouReportID, type, amount, comment, currency, paymentType, isSettlingUp, bankAccountID, payAsBusiness), }; const managerMcTestParticipant = participants.find((participant) => isSelectedManagerMcTest(participant.login)); @@ -9180,20 +9231,19 @@ function getTaskAssigneeChatOnyxData( /** * Return iou report action display message */ -function getIOUReportActionDisplayMessage(reportAction: OnyxEntry, transaction?: OnyxEntry): string { +function getIOUReportActionDisplayMessage(reportAction: OnyxEntry, transaction?: OnyxEntry, report?: Report): string { if (!isMoneyRequestAction(reportAction)) { return ''; } const originalMessage = getOriginalMessage(reportAction); - const {IOUReportID, automaticAction} = originalMessage ?? {}; + const {IOUReportID, automaticAction, payAsBusiness} = originalMessage ?? {}; const iouReport = getReportOrDraftReport(IOUReportID); + const isInvoice = isInvoiceReport(iouReport); + let translationKey: TranslationPaths; if (originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { - // The `REPORT_ACTION_TYPE.PAY` action type is used for both fulfilling existing requests and sending money. To - // differentiate between these two scenarios, we check if the `originalMessage` contains the `IOUDetails` - // property. If it does, it indicates that this is a 'Pay someone' action. - const {amount, currency} = originalMessage?.IOUDetails ?? originalMessage ?? {}; - const formattedAmount = convertToDisplayString(Math.abs(amount), currency) ?? ''; + const reportPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; + const last4Digits = reportPolicy?.achAccount?.accountNumber.slice(-4) ?? ''; switch (originalMessage.paymentType) { case CONST.IOU.PAYMENT_TYPE.ELSEWHERE: @@ -9201,16 +9251,22 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry, break; case CONST.IOU.PAYMENT_TYPE.EXPENSIFY: case CONST.IOU.PAYMENT_TYPE.VBBA: - translationKey = 'iou.paidWithExpensify'; - if (automaticAction) { + if (isInvoice) { + return translateLocal(payAsBusiness ? 'iou.settleInvoiceBusiness' : 'iou.settleInvoicePersonal', {amount: '', last4Digits}); + } + translationKey = 'iou.businessBankAccount'; + if (automaticAction && originalMessage.paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { translationKey = 'iou.automaticallyPaidWithExpensify'; + } else { + translationKey = 'iou.automaticallyPaidWithBusinessBankAccount'; } break; default: translationKey = 'iou.payerPaidAmount'; break; } - return translateLocal(translationKey, {amount: formattedAmount, payer: ''}); + + return translateLocal(translationKey, {amount: '', payer: '', last4Digits}); } const amount = getTransactionAmount(transaction, !isEmptyObject(iouReport) && isExpenseReport(iouReport), transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID) ?? 0; @@ -11604,6 +11660,7 @@ export { generateReportName, navigateToLinkedReportAction, buildOptimisticUnreportedTransactionAction, + isBusinessInvoiceRoom, buildOptimisticResolvedDuplicatesReportAction, getTitleReportField, getReportFieldsByPolicyID, diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 7aba5ed59d8c..eed52c57f459 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -447,6 +447,7 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt( currency?: string, file?: File, routeToNavigateAfterCreate?: Route, + lastUsedPaymentMethod?: OnyxTypes.LastPaymentMethodType, ) { const policyIDWithDefault = policyID || generatePolicyID(); createDraftInitialWorkspace(policyOwnerEmail, policyName, policyIDWithDefault, makeMeAdmin, currency, file); @@ -457,7 +458,7 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt( Navigation.goBack(); } const routeToNavigate = routeToNavigateAfterCreate ?? ROUTES.WORKSPACE_INITIAL.getRoute(policyIDWithDefault, backTo); - savePolicyDraftByNewWorkspace(policyIDWithDefault, policyName, policyOwnerEmail, makeMeAdmin, currency, file); + savePolicyDraftByNewWorkspace(policyIDWithDefault, policyName, policyOwnerEmail, makeMeAdmin, currency, file, lastUsedPaymentMethod); Navigation.navigate(routeToNavigate, {forceReplace: !transitionFromOldDot}); }) .then(endSignOnTransition); @@ -473,7 +474,15 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt( * @param [currency] Optional, selected currency for the workspace * @param [file] Optional, avatar file for workspace */ -function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, policyOwnerEmail = '', makeMeAdmin = false, currency = '', file?: File) { +function savePolicyDraftByNewWorkspace( + policyID?: string, + policyName?: string, + policyOwnerEmail = '', + makeMeAdmin = false, + currency = '', + file?: File, + lastUsedPaymentMethod?: OnyxTypes.LastPaymentMethodType, +) { createWorkspace({ policyOwnerEmail, makeMeAdmin, @@ -482,6 +491,7 @@ function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, p engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, currency, file, + lastUsedPaymentMethod, }); } diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 8b5ec800f97a..e8815ca506bd 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -20,6 +20,7 @@ import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import {translateLocal} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; +import {getPersonalPolicy} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import type {Country} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -27,7 +28,7 @@ import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; import type {InternationalBankAccountForm, PersonalBankAccountForm} from '@src/types/form'; import type {ACHContractStepProps, BeneficialOwnersStepProps, CompanyStepProps, ReimbursementAccountForm, RequestorStepProps} from '@src/types/form/ReimbursementAccountForm'; -import type {LastPaymentMethod, PersonalBankAccount} from '@src/types/onyx'; +import type {LastPaymentMethod, LastPaymentMethodType, PersonalBankAccount} from '@src/types/onyx'; import type PlaidBankAccount from '@src/types/onyx/PlaidBankAccount'; import type {BankAccountStep, ReimbursementAccountStep, ReimbursementAccountSubStep} from '@src/types/onyx/ReimbursementAccount'; import type {OnyxData} from '@src/types/onyx/Request'; @@ -203,7 +204,7 @@ function addBusinessWebsiteForDraft(websiteUrl: string) { /** * Submit Bank Account step with Plaid data so php can perform some checks. */ -function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAccount: PlaidBankAccount, policyID: string) { +function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAccount: PlaidBankAccount, policyID: string, lastPaymentMethod?: LastPaymentMethodType | string) { const parameters: ConnectBankAccountParams = { bankAccountID, routingNumber: selectedPlaidBankAccount.routingNumber, @@ -216,7 +217,27 @@ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAcc policyID, }; - API.write(WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID, parameters, getVBBADataForOnyx()); + const onyxData = getVBBADataForOnyx(); + const lastUsedPaymentMethod = typeof lastPaymentMethod === 'string' ? lastPaymentMethod : lastPaymentMethod?.expense?.name; + + if (!lastUsedPaymentMethod) { + onyxData.successData?.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + value: { + [policyID]: { + expense: { + name: CONST.IOU.PAYMENT_TYPE.VBBA, + }, + lastUsed: { + name: lastUsedPaymentMethod ?? CONST.IOU.PAYMENT_TYPE.VBBA, + }, + }, + }, + }); + } + + API.write(WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID, parameters, onyxData); } /** @@ -224,7 +245,7 @@ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAcc * * TODO: offline pattern for this command will have to be added later once the pattern B design doc is complete */ -function addPersonalBankAccount(account: PlaidBankAccount, policyID?: string, source?: string) { +function addPersonalBankAccount(account: PlaidBankAccount, policyID?: string, source?: string, lastPaymentMethod?: LastPaymentMethodType | string | undefined) { const parameters: AddPersonalBankAccountParams = { addressName: account.addressName ?? '', routingNumber: account.routingNumber, @@ -242,6 +263,8 @@ function addPersonalBankAccount(account: PlaidBankAccount, policyID?: string, so parameters.source = source; } + const personalPolicy = getPersonalPolicy(); + const onyxData: OnyxData = { optimisticData: [ { @@ -284,6 +307,44 @@ function addPersonalBankAccount(account: PlaidBankAccount, policyID?: string, so ], }; + if (personalPolicy?.id && !lastPaymentMethod) { + onyxData.optimisticData?.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + value: { + [personalPolicy?.id]: { + iou: { + name: CONST.IOU.PAYMENT_TYPE.EXPENSIFY, + }, + lastUsed: { + name: CONST.IOU.PAYMENT_TYPE.EXPENSIFY, + }, + }, + }, + }); + onyxData.successData?.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + value: { + [personalPolicy?.id]: { + iou: { + name: CONST.IOU.PAYMENT_TYPE.EXPENSIFY, + }, + lastUsed: { + name: CONST.IOU.PAYMENT_TYPE.EXPENSIFY, + }, + }, + }, + }); + onyxData.failureData?.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + value: { + [personalPolicy?.id]: null, + }, + }); + } + API.write(WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT, parameters, onyxData); } @@ -296,6 +357,8 @@ function deletePaymentBankAccount(bankAccountID: number, lastUsedPaymentMethods? pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, }; + const personalPolicy = getPersonalPolicy(); + const onyxData: OnyxData = { optimisticData: [ { @@ -326,6 +389,84 @@ function deletePaymentBankAccount(bankAccountID: number, lastUsedPaymentMethods? ], }; + Object.keys(lastUsedPaymentMethods ?? {}).forEach((paymentMethodID) => { + const lastUsedPaymentMethod = lastUsedPaymentMethods?.[paymentMethodID] as LastPaymentMethodType; + + if (typeof lastUsedPaymentMethod === 'string' || !lastUsedPaymentMethod) { + return; + } + + if (personalPolicy?.id === paymentMethodID && lastUsedPaymentMethod.iou?.name === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { + const revertedLastUsedPaymentMethod = lastUsedPaymentMethod.lastUsed?.name !== CONST.IOU.PAYMENT_TYPE.EXPENSIFY ? lastUsedPaymentMethod.lastUsed?.name : null; + + onyxData.successData?.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + value: { + [personalPolicy?.id]: { + iou: { + name: revertedLastUsedPaymentMethod, + bankAccountID: null, + }, + ...(!revertedLastUsedPaymentMethod ? {lastUsed: {name: null, bankAccountID: null}} : {}), + }, + }, + }); + + onyxData.failureData?.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + value: { + [personalPolicy?.id]: { + expense: { + name: lastUsedPaymentMethod.iou?.name, + bankAccountID: lastUsedPaymentMethod.iou?.bankAccountID, + }, + lastUsed: { + name: lastUsedPaymentMethod.lastUsed?.name, + bankAccountID: lastUsedPaymentMethod.lastUsed?.bankAccountID, + }, + }, + }, + }); + } + + if (lastUsedPaymentMethod?.expense?.name === CONST.IOU.PAYMENT_TYPE.VBBA) { + const revertedLastUsedPaymentMethod = lastUsedPaymentMethod.lastUsed.name !== CONST.IOU.PAYMENT_TYPE.VBBA ? lastUsedPaymentMethod.lastUsed.name : null; + + onyxData.successData?.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + value: { + [paymentMethodID]: { + expense: { + name: revertedLastUsedPaymentMethod, + bankAccountID: null, + }, + ...(!revertedLastUsedPaymentMethod ? {lastUsed: {name: null, bankAccountID: null}} : {}), + }, + }, + }); + + onyxData.failureData?.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + value: { + [paymentMethodID]: { + expense: { + name: lastUsedPaymentMethod.expense?.name, + bankAccountID: lastUsedPaymentMethod.expense?.bankAccountID, + }, + lastUsed: { + name: lastUsedPaymentMethod.lastUsed?.name, + bankAccountID: lastUsedPaymentMethod.lastUsed?.bankAccountID, + }, + }, + }, + }); + } + }); + API.write(WRITE_COMMANDS.DELETE_PAYMENT_BANK_ACCOUNT, parameters, onyxData); } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 7ae0ff50ddcc..3f7692c3508f 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -8956,6 +8956,9 @@ function getPayMoneyRequestParams( paymentMethodType: PaymentMethodType, full: boolean, payAsBusiness?: boolean, + bankAccountID?: number, + paymentPolicyID?: string | undefined, + lastUsedPaymentMethod?: OnyxTypes.LastPaymentMethodType, ): PayMoneyRequestData { const isInvoiceReport = isInvoiceReportReportUtils(iouReport); // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 @@ -9022,6 +9025,8 @@ function getPayMoneyRequestParams( paymentType: paymentMethodType, iouReportID: iouReport?.reportID, isSettlingUp: true, + payAsBusiness, + bankAccountID, }); // In some instances, the report preview action might not be available to the payer (only whispered to the requestor) @@ -9095,12 +9100,23 @@ function getPayMoneyRequestParams( ); if (iouReport?.policyID) { + const prevLastUsedPaymentMethod = lastUsedPaymentMethod?.lastUsed?.name; + const usedPaymentOption = paymentPolicyID ?? paymentMethodType; + + const optimisticLastPaymentMethod = { + [iouReport.policyID]: { + ...(iouReport.type ? {[iouReport.type]: {name: usedPaymentOption}} : {}), + ...(isInvoiceReport ? {invoice: {name: paymentMethodType, bankAccountID}} : {}), + lastUsed: { + name: prevLastUsedPaymentMethod !== usedPaymentOption && !!prevLastUsedPaymentMethod ? prevLastUsedPaymentMethod : usedPaymentOption, + }, + }, + }; + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, - value: { - [iouReport.policyID]: paymentMethodType, - }, + value: optimisticLastPaymentMethod, }); } @@ -10350,7 +10366,7 @@ function completePaymentOnboarding(paymentSelected: ValueOf, full = true) { +function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.Report, iouReport: OnyxEntry, paymentPolicyID?: string, full = true) { if (chatReport.policyID && shouldRestrictUserBillableActions(chatReport.policyID)) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(chatReport.policyID)); return; @@ -10360,7 +10376,7 @@ function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.R completePaymentOnboarding(paymentSelected); const recipient = {accountID: iouReport?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID}; - const {params, optimisticData, successData, failureData} = getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentType, full); + const {params, optimisticData, successData, failureData} = getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentType, full, undefined, undefined, paymentPolicyID); // For now, we need to call the PayMoneyRequestWithWallet API since PayMoneyRequest was not updated to work with // Expensify Wallets. @@ -10396,7 +10412,7 @@ function payInvoice( ownerEmail, policyName, }, - } = getPayMoneyRequestParams(chatReport, invoiceReport, recipient, paymentMethodType, true, payAsBusiness); + } = getPayMoneyRequestParams(chatReport, invoiceReport, recipient, paymentMethodType, true, payAsBusiness, methodID); const paymentSelected = paymentMethodType === CONST.IOU.PAYMENT_TYPE.VBBA ? CONST.IOU.PAYMENT_SELECTED.BBA : CONST.IOU.PAYMENT_SELECTED.PBA; completePaymentOnboarding(paymentSelected); @@ -11161,9 +11177,26 @@ function checkIfScanFileCanBeRead( return readFileAsync(receiptPath.toString(), receiptFilename, onSuccess, onFailure, receiptType); } -/** Save the preferred payment method for a policy */ -function savePreferredPaymentMethod(policyID: string, paymentMethod: PaymentMethodType, type: ValueOf | undefined) { - Onyx.merge(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, {[policyID]: type ? {[type]: paymentMethod, [CONST.LAST_PAYMENT_METHOD.LAST_USED]: {name: paymentMethod}} : paymentMethod}); +/** Save the preferred payment method for a policy or personal DM */ +function savePreferredPaymentMethod( + policyID: string | undefined, + paymentMethod: string, + type: ValueOf | undefined, + prevPaymentMethod?: OnyxTypes.LastPaymentMethodType | string, +) { + if (!policyID) { + return; + } + + // to make it easier to revert to the previous last payment method, we will save it to this key + Onyx.merge(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, { + [policyID]: type + ? { + [type]: {name: paymentMethod}, + [CONST.LAST_PAYMENT_METHOD.LAST_USED]: {name: typeof prevPaymentMethod === 'string' ? prevPaymentMethod : (prevPaymentMethod?.lastUsed?.name ?? paymentMethod)}, + } + : paymentMethod, + }); } /** Get report policy id of IOU request */ diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 94e6fd0d2d35..675c83428706 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -91,6 +91,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type { IntroSelected, InvitedEmailsToAccountIDs, + LastPaymentMethod, + LastPaymentMethodType, PersonalDetailsList, Policy, PolicyCategory, @@ -153,6 +155,7 @@ type BuildPolicyDataOptions = { companySize?: OnboardingCompanySize; userReportedIntegration?: OnboardingAccounting; featuresMap?: Feature[]; + lastUsedPaymentMethod?: LastPaymentMethodType; }; const allPolicies: OnyxCollection = {}; @@ -340,7 +343,7 @@ function hasActiveChatEnabledPolicies(policies: Array> /** * Delete the workspace */ -function deleteWorkspace(policyID: string, policyName: string) { +function deleteWorkspace(policyID: string, policyName: string, lastUsedPaymentMethods?: LastPaymentMethod) { if (!allPolicies) { return; } @@ -501,6 +504,46 @@ function deleteWorkspace(policyID: string, policyName: string) { } }); + Object.keys(lastUsedPaymentMethods ?? {})?.forEach((paymentMethodKey) => { + const lastUsedPaymentMethod = lastUsedPaymentMethods?.[paymentMethodKey]; + + if (typeof lastUsedPaymentMethod === 'string' || !lastUsedPaymentMethod) { + return; + } + + if (lastUsedPaymentMethod?.iou?.name === policyID) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + value: { + [paymentMethodKey]: { + iou: { + name: policyID !== lastUsedPaymentMethod?.lastUsed?.name ? lastUsedPaymentMethod?.lastUsed?.name : '', + }, + lastUsed: { + name: policyID !== lastUsedPaymentMethod?.lastUsed?.name ? lastUsedPaymentMethod?.lastUsed?.name : '', + }, + }, + }, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + value: { + [paymentMethodKey]: { + iou: { + name: lastUsedPaymentMethod?.iou?.name, + }, + lastUsed: { + name: lastUsedPaymentMethod?.iou?.name, + }, + }, + }, + }); + } + }); + const params: DeleteWorkspaceParams = {policyID}; API.write(WRITE_COMMANDS.DELETE_WORKSPACE, params, {optimisticData, finallyData, failureData}); @@ -1890,6 +1933,7 @@ function buildPolicyData(options: BuildPolicyDataOptions = {}) { companySize, userReportedIntegration, featuresMap, + lastUsedPaymentMethod, } = options; const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); @@ -2194,6 +2238,32 @@ function buildPolicyData(options: BuildPolicyDataOptions = {}) { successData.push(...optimisticCategoriesData.successData); } + if (getAdminPolicies().length === 0 && lastUsedPaymentMethod) { + Object.values(allReports ?? {}) + .filter((iouReport) => iouReport?.type === CONST.REPORT.TYPE.IOU) + .forEach((iouReport) => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (lastUsedPaymentMethod?.iou?.name || !iouReport?.policyID) { + return; + } + + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + value: { + [iouReport?.policyID]: { + iou: { + name: policyID, + }, + lastUsed: { + name: policyID, + }, + }, + }, + }); + }); + } + // We need to clone the file to prevent non-indexable errors. const clonedFile = file ? (createFile(file) as File) : undefined; diff --git a/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts b/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts index e014db04386f..0b6c4bd089f8 100644 --- a/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts +++ b/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts @@ -6,6 +6,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import type * as OnyxTypes from '@src/types/onyx'; +import type {OnyxData} from '@src/types/onyx/Request'; let allPolicies: OnyxCollection; Onyx.connect({ @@ -17,7 +18,12 @@ Onyx.connect({ /** * Reset user's USD reimbursement account. This will delete the bank account */ -function resetUSDBankAccount(bankAccountID: number | undefined, session: OnyxEntry, policyID: string | undefined) { +function resetUSDBankAccount( + bankAccountID: number | undefined, + session: OnyxEntry, + policyID: string | undefined, + lastUsedPaymentMethod?: OnyxTypes.LastPaymentMethodType, +) { if (!bankAccountID) { throw new Error('Missing bankAccountID when attempting to reset free plan bank account'); } @@ -26,120 +32,141 @@ function resetUSDBankAccount(bankAccountID: number | undefined, session: OnyxEnt } const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? ({} as OnyxTypes.Policy); + const isLastUsedPaymentMethodBBA = lastUsedPaymentMethod?.expense?.name === CONST.IOU.PAYMENT_TYPE.VBBA; + const isPreviousLastUsedPaymentMethodBBA = lastUsedPaymentMethod?.lastUsed?.name === CONST.IOU.PAYMENT_TYPE.VBBA; - API.write( - WRITE_COMMANDS.RESTART_BANK_ACCOUNT_SETUP, - { - bankAccountID, - ownerEmail: session.email, - policyID, - }, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - value: { - shouldShowResetModal: false, - isLoading: true, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - achData: null, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - achAccount: null, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.ONFIDO_TOKEN, - value: '', + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + shouldShowResetModal: false, + isLoading: true, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + achData: null, }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.ONFIDO_APPLICANT_ID, - value: '', + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + achAccount: null, }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.PLAID_DATA, - value: CONST.PLAID.DEFAULT_DATA, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.ONFIDO_TOKEN, + value: '', + }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.ONFIDO_APPLICANT_ID, + value: '', + }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.PLAID_DATA, + value: CONST.PLAID.DEFAULT_DATA, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.PLAID_LINK_TOKEN, + value: '', + }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, + value: { + [INPUT_IDS.BENEFICIAL_OWNER_INFO_STEP.OWNS_MORE_THAN_25_PERCENT]: false, + [INPUT_IDS.BENEFICIAL_OWNER_INFO_STEP.HAS_OTHER_BENEFICIAL_OWNERS]: false, + [INPUT_IDS.BENEFICIAL_OWNER_INFO_STEP.BENEFICIAL_OWNERS]: '', + [INPUT_IDS.BANK_INFO_STEP.ACCOUNT_NUMBER]: '', + [INPUT_IDS.BANK_INFO_STEP.ROUTING_NUMBER]: '', + [INPUT_IDS.BANK_INFO_STEP.PLAID_ACCOUNT_ID]: '', + [INPUT_IDS.BANK_INFO_STEP.PLAID_MASK]: '', + [INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_NAME]: '', + [INPUT_IDS.BUSINESS_INFO_STEP.STREET]: '', + [INPUT_IDS.BUSINESS_INFO_STEP.CITY]: '', + [INPUT_IDS.BUSINESS_INFO_STEP.STATE]: '', + [INPUT_IDS.BUSINESS_INFO_STEP.ZIP_CODE]: '', + [INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_PHONE]: '', + [INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_WEBSITE]: undefined, + [INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_TAX_ID]: '', + [INPUT_IDS.BUSINESS_INFO_STEP.INCORPORATION_TYPE]: '', + [INPUT_IDS.BUSINESS_INFO_STEP.INCORPORATION_DATE]: '', + [INPUT_IDS.BUSINESS_INFO_STEP.INCORPORATION_STATE]: '', + [INPUT_IDS.BUSINESS_INFO_STEP.HAS_NO_CONNECTION_TO_CANNABIS]: false, + [INPUT_IDS.PERSONAL_INFO_STEP.FIRST_NAME]: '', + [INPUT_IDS.PERSONAL_INFO_STEP.LAST_NAME]: '', + [INPUT_IDS.PERSONAL_INFO_STEP.STREET]: '', + [INPUT_IDS.PERSONAL_INFO_STEP.CITY]: '', + [INPUT_IDS.PERSONAL_INFO_STEP.STATE]: '', + [INPUT_IDS.PERSONAL_INFO_STEP.ZIP_CODE]: '', + [INPUT_IDS.PERSONAL_INFO_STEP.IS_ONFIDO_SETUP_COMPLETE]: false, + [INPUT_IDS.PERSONAL_INFO_STEP.DOB]: '', + [INPUT_IDS.PERSONAL_INFO_STEP.SSN_LAST_4]: '', + [INPUT_IDS.COMPLETE_VERIFICATION.ACCEPT_TERMS_AND_CONDITIONS]: false, + [INPUT_IDS.COMPLETE_VERIFICATION.CERTIFY_TRUE_INFORMATION]: false, + [INPUT_IDS.COMPLETE_VERIFICATION.IS_AUTHORIZED_TO_USE_BANK_ACCOUNT]: false, + [INPUT_IDS.BANK_INFO_STEP.IS_SAVINGS]: false, + [INPUT_IDS.BANK_INFO_STEP.BANK_NAME]: '', + [INPUT_IDS.BANK_INFO_STEP.PLAID_ACCESS_TOKEN]: '', + [INPUT_IDS.BANK_INFO_STEP.SELECTED_PLAID_ACCOUNT_ID]: '', + [INPUT_IDS.AMOUNT1]: '', + [INPUT_IDS.AMOUNT2]: '', + [INPUT_IDS.AMOUNT3]: '', }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.PLAID_LINK_TOKEN, - value: '', + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: {isLoading: false, pendingAction: null}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + achAccount: policy?.achAccount, }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - value: CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, - value: { - [INPUT_IDS.BENEFICIAL_OWNER_INFO_STEP.OWNS_MORE_THAN_25_PERCENT]: false, - [INPUT_IDS.BENEFICIAL_OWNER_INFO_STEP.HAS_OTHER_BENEFICIAL_OWNERS]: false, - [INPUT_IDS.BENEFICIAL_OWNER_INFO_STEP.BENEFICIAL_OWNERS]: '', - [INPUT_IDS.BANK_INFO_STEP.ACCOUNT_NUMBER]: '', - [INPUT_IDS.BANK_INFO_STEP.ROUTING_NUMBER]: '', - [INPUT_IDS.BANK_INFO_STEP.PLAID_ACCOUNT_ID]: '', - [INPUT_IDS.BANK_INFO_STEP.PLAID_MASK]: '', - [INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_NAME]: '', - [INPUT_IDS.BUSINESS_INFO_STEP.STREET]: '', - [INPUT_IDS.BUSINESS_INFO_STEP.CITY]: '', - [INPUT_IDS.BUSINESS_INFO_STEP.STATE]: '', - [INPUT_IDS.BUSINESS_INFO_STEP.ZIP_CODE]: '', - [INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_PHONE]: '', - [INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_WEBSITE]: undefined, - [INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_TAX_ID]: '', - [INPUT_IDS.BUSINESS_INFO_STEP.INCORPORATION_TYPE]: '', - [INPUT_IDS.BUSINESS_INFO_STEP.INCORPORATION_DATE]: '', - [INPUT_IDS.BUSINESS_INFO_STEP.INCORPORATION_STATE]: '', - [INPUT_IDS.BUSINESS_INFO_STEP.HAS_NO_CONNECTION_TO_CANNABIS]: false, - [INPUT_IDS.PERSONAL_INFO_STEP.FIRST_NAME]: '', - [INPUT_IDS.PERSONAL_INFO_STEP.LAST_NAME]: '', - [INPUT_IDS.PERSONAL_INFO_STEP.STREET]: '', - [INPUT_IDS.PERSONAL_INFO_STEP.CITY]: '', - [INPUT_IDS.PERSONAL_INFO_STEP.STATE]: '', - [INPUT_IDS.PERSONAL_INFO_STEP.ZIP_CODE]: '', - [INPUT_IDS.PERSONAL_INFO_STEP.IS_ONFIDO_SETUP_COMPLETE]: false, - [INPUT_IDS.PERSONAL_INFO_STEP.DOB]: '', - [INPUT_IDS.PERSONAL_INFO_STEP.SSN_LAST_4]: '', - [INPUT_IDS.COMPLETE_VERIFICATION.ACCEPT_TERMS_AND_CONDITIONS]: false, - [INPUT_IDS.COMPLETE_VERIFICATION.CERTIFY_TRUE_INFORMATION]: false, - [INPUT_IDS.COMPLETE_VERIFICATION.IS_AUTHORIZED_TO_USE_BANK_ACCOUNT]: false, - [INPUT_IDS.BANK_INFO_STEP.IS_SAVINGS]: false, - [INPUT_IDS.BANK_INFO_STEP.BANK_NAME]: '', - [INPUT_IDS.BANK_INFO_STEP.PLAID_ACCESS_TOKEN]: '', - [INPUT_IDS.BANK_INFO_STEP.SELECTED_PLAID_ACCOUNT_ID]: '', - [INPUT_IDS.AMOUNT1]: '', - [INPUT_IDS.AMOUNT2]: '', - [INPUT_IDS.AMOUNT3]: '', + }, + ], + }; + + if (isLastUsedPaymentMethodBBA && policyID) { + onyxData.successData?.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + value: { + [policyID]: { + expense: { + name: isPreviousLastUsedPaymentMethodBBA ? '' : lastUsedPaymentMethod?.lastUsed.name, }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - value: {isLoading: false, pendingAction: null}, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - achAccount: policy?.achAccount, + lastUsed: { + name: isPreviousLastUsedPaymentMethodBBA ? '' : lastUsedPaymentMethod?.lastUsed.name, }, }, - ], + }, + }); + } + + API.write( + WRITE_COMMANDS.RESTART_BANK_ACCOUNT_SETUP, + { + bankAccountID, + ownerEmail: session.email, + policyID, }, + onyxData, ); } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index a94926a81acf..1bf709127730 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -107,6 +107,7 @@ import { buildOptimisticExportIntegrationAction, buildOptimisticGroupChatReport, buildOptimisticIOUReportAction, + buildOptimisticMovedReportAction, buildOptimisticRenamedRoomReportAction, buildOptimisticReportPreview, buildOptimisticRoomDescriptionUpdatedReportAction, @@ -5000,8 +5001,9 @@ function deleteAppReport(reportID: string | undefined) { * Moves an IOU report to a policy by converting it to an expense report * @param reportID - The ID of the IOU report to move * @param policyID - The ID of the policy to move the report to + * @param isFromSettlementButton - Whether the action is from report preview */ -function moveIOUReportToPolicy(reportID: string, policyID: string) { +function moveIOUReportToPolicy(reportID: string, policyID: string, isFromSettlementButton?: boolean) { const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line deprecation/deprecation @@ -5014,7 +5016,7 @@ function moveIOUReportToPolicy(reportID: string, policyID: string) { const isReimbursed = isReportManuallyReimbursed(iouReport); // We do not want to create negative amount expenses - if (!isReimbursed && ReportActionsUtils.hasRequestFromCurrentAccount(reportID, iouReport.managerID ?? CONST.DEFAULT_NUMBER_ID)) { + if (!isReimbursed && ReportActionsUtils.hasRequestFromCurrentAccount(reportID, iouReport.managerID ?? CONST.DEFAULT_NUMBER_ID) && !isFromSettlementButton) { return; } @@ -5159,10 +5161,24 @@ function moveIOUReportToPolicy(reportID: string, policyID: string) { }, }); + // Create the MOVED report action and add it to the DM chat which indicates to the user where the report has been moved + const movedReportAction = buildOptimisticMovedReportAction(iouReport.policyID, policyID, expenseChatReportId, iouReportID, policyName); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, + value: {[movedReportAction.reportActionID]: movedReportAction}, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, + value: {[movedReportAction.reportActionID]: null}, + }); + const parameters: MoveIOUReportToExistingPolicyParams = { iouReportID, policyID, changePolicyReportActionID: changePolicyReportAction.reportActionID, + dmMovedReportActionID: movedReportAction.reportActionID, }; API.write(WRITE_COMMANDS.MOVE_IOU_REPORT_TO_EXISTING_POLICY, parameters, {optimisticData, successData, failureData}); @@ -5173,7 +5189,11 @@ function moveIOUReportToPolicy(reportID: string, policyID: string) { * @param reportID - The ID of the IOU report to move * @param policyID - The ID of the policy to move the report to */ -function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: string, formatPhoneNumber: LocaleContextProps['formatPhoneNumber']) { +function moveIOUReportToPolicyAndInviteSubmitter( + reportID: string, + policyID: string, + formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], +): {policyExpenseChatReportID?: string} | undefined { const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line deprecation/deprecation @@ -5187,6 +5207,7 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str const submitterAccountID = iouReport.ownerAccountID; const submitterEmail = PersonalDetailsUtils.getLoginByAccountID(submitterAccountID ?? CONST.DEFAULT_NUMBER_ID); const submitterLogin = PhoneNumber.addSMSDomainIfPhoneNumber(submitterEmail); + const iouReportID = iouReport.reportID; // This flow only works for admins moving an IOU report to a policy where the submitter is NOT yet a member of the policy if (!isPolicyAdmin || !isIOUReportUsingReport(iouReport) || !submitterAccountID || !submitterEmail || isPolicyMember(submitterLogin, policyID)) { @@ -5397,15 +5418,30 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str }, }); + // Create the MOVED report action and add it to the DM chat which indicates to the user where the report has been moved + const movedReportAction = buildOptimisticMovedReportAction(iouReport.policyID, policyID, optimisticPolicyExpenseChatReportID, iouReportID, policy.name); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, + value: {[movedReportAction.reportActionID]: movedReportAction}, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, + value: {[movedReportAction.reportActionID]: null}, + }); + const parameters: MoveIOUReportToPolicyAndInviteSubmitterParams = { iouReportID: reportID, policyID, policyExpenseChatReportID: optimisticPolicyExpenseChatReportID ?? String(CONST.DEFAULT_NUMBER_ID), policyExpenseCreatedReportActionID: optimisticPolicyExpenseChatCreatedReportActionID ?? String(CONST.DEFAULT_NUMBER_ID), changePolicyReportActionID: changePolicyReportAction.reportActionID, + dmMovedReportActionID: movedReportAction.reportActionID, }; API.write(WRITE_COMMANDS.MOVE_IOU_REPORT_TO_POLICY_AND_INVITE_SUBMITTER, parameters, {optimisticData, successData, failureData}); + return {policyExpenseChatReportID: optimisticPolicyExpenseChatReportID}; } /** diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 3333755f4955..d0866de900ae 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -12,9 +12,9 @@ import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import fileDownload from '@libs/fileDownload'; import enhanceParameters from '@libs/Network/enhanceParameters'; import {rand64} from '@libs/NumberUtils'; -import {getSubmitToAccountID, getValidConnectedIntegration} from '@libs/PolicyUtils'; +import {getPersonalPolicy, getSubmitToAccountID, getValidConnectedIntegration} from '@libs/PolicyUtils'; import type {OptimisticExportIntegrationAction} from '@libs/ReportUtils'; -import {buildOptimisticExportIntegrationAction, hasHeldExpenses} from '@libs/ReportUtils'; +import {buildOptimisticExportIntegrationAction, hasHeldExpenses, isExpenseReport, isInvoiceReport, isIOUReport as isIOUReportUtil} from '@libs/ReportUtils'; import type {SearchKey} from '@libs/SearchUIUtils'; import {isTransactionGroupListItemType, isTransactionListItemType} from '@libs/SearchUIUtils'; import playSound, {SOUNDS} from '@libs/Sound'; @@ -23,6 +23,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import {FILTER_KEYS} from '@src/types/form/SearchAdvancedFiltersForm'; import type {SearchAdvancedFiltersForm} from '@src/types/form/SearchAdvancedFiltersForm'; import type {LastPaymentMethod, LastPaymentMethodType, Policy} from '@src/types/onyx'; +import type {PaymentInformation} from '@src/types/onyx/LastPaymentMethod'; import type {ConnectionName} from '@src/types/onyx/Policy'; import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; import type Nullable from '@src/types/utils/Nullable'; @@ -79,18 +80,50 @@ function handleActionButtonPress( } } -function getLastPolicyPaymentMethod(policyID: string | undefined, lastPaymentMethods: OnyxEntry) { +function getLastPolicyBankAccountID( + policyID: string | undefined, + lastPaymentMethods: OnyxEntry, + reportType: keyof LastPaymentMethodType = 'lastUsed', +): number | undefined { if (!policyID) { - return null; + return undefined; } - let lastPolicyPaymentMethod = null; - if (typeof lastPaymentMethods?.[policyID] === 'string') { - lastPolicyPaymentMethod = lastPaymentMethods?.[policyID] as ValueOf; - } else { - lastPolicyPaymentMethod = (lastPaymentMethods?.[policyID] as LastPaymentMethodType)?.lastUsed.name as ValueOf; + const lastPolicyPaymentMethod = lastPaymentMethods?.[policyID]; + return typeof lastPolicyPaymentMethod === 'string' ? undefined : (lastPolicyPaymentMethod?.[reportType] as PaymentInformation)?.bankAccountID; +} + +function getLastPolicyPaymentMethod( + policyID: string | undefined, + lastPaymentMethods: OnyxEntry, + reportType: keyof LastPaymentMethodType = 'lastUsed', + isIOUReport?: boolean, +): ValueOf | undefined { + if (!policyID) { + return undefined; + } + + const personalPolicy = getPersonalPolicy(); + + const lastPolicyPaymentMethod = lastPaymentMethods?.[policyID] ?? (isIOUReport && personalPolicy ? lastPaymentMethods?.[personalPolicy.id] : undefined); + const result = typeof lastPolicyPaymentMethod === 'string' ? lastPolicyPaymentMethod : (lastPolicyPaymentMethod?.[reportType] as PaymentInformation)?.name; + + return result as ValueOf | undefined; +} + +function getReportType(reportID?: string) { + if (isIOUReportUtil(reportID)) { + return CONST.REPORT.TYPE.IOU; + } + + if (isInvoiceReport(reportID)) { + return CONST.REPORT.TYPE.INVOICE; + } + + if (isExpenseReport(reportID)) { + return CONST.REPORT.TYPE.EXPENSE; } - return lastPolicyPaymentMethod; + return undefined; } function getPayActionCallback( @@ -102,9 +135,9 @@ function getPayActionCallback( lastPaymentMethod: OnyxEntry, currentSearchKey?: SearchKey, ) { - const lastPolicyPaymentMethod = getLastPolicyPaymentMethod(item.policyID, lastPaymentMethod); + const lastPolicyPaymentMethod = getLastPolicyPaymentMethod(item.policyID, lastPaymentMethod, getReportType(item.reportID)); - if (!lastPolicyPaymentMethod) { + if (!lastPolicyPaymentMethod || !Object.values(CONST.IOU.PAYMENT_TYPE).includes(lastPolicyPaymentMethod)) { goToItem(); return; } @@ -538,5 +571,6 @@ export { openSearchFiltersCardPage, openSearchPage as openSearch, getLastPolicyPaymentMethod, + getLastPolicyBankAccountID, exportToIntegrationOnSearch, }; diff --git a/src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx b/src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx index 1cfca7ce4e3d..efb1a3310e79 100644 --- a/src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx +++ b/src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx @@ -39,6 +39,7 @@ function BankInfo({onBackButtonPress, policyID, setUSDBankAccountStep}: BankInfo const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: false}); const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, {canBeMissing: false}); const [plaidLinkToken] = useOnyx(ONYXKEYS.PLAID_LINK_TOKEN, {canBeMissing: true}); + const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true}); const {translate} = useLocalize(); const [redirectedFromPlaidToManual, setRedirectedFromPlaidToManual] = React.useState(false); @@ -82,10 +83,11 @@ function BankInfo({onBackButtonPress, policyID, setUSDBankAccountStep}: BankInfo [BANK_INFO_STEP_KEYS.IS_SAVINGS]: data[BANK_INFO_STEP_KEYS.IS_SAVINGS] ?? false, }, policyID, + lastPaymentMethod?.[policyID], ); } }, - [setupType, bankAccountID, policyID], + [setupType, bankAccountID, policyID, lastPaymentMethod], ); const bodyContent = setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID ? plaidSubSteps : manualSubSteps; diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 8fbbd09aea4b..1f780a3f29ff 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -1032,8 +1032,23 @@ function PureReportActionItem({ } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(action)?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { const wasAutoPaid = getOriginalMessage(action)?.automaticAction ?? false; const paymentType = getOriginalMessage(action)?.paymentType; + if (paymentType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { children = ; + } else if (paymentType === CONST.IOU.PAYMENT_TYPE.VBBA) { + const last4Digits = policy?.achAccount?.accountNumber?.slice(-4) ?? ''; + + if (wasAutoPaid) { + const translation = translate('iou.automaticallyPaidWithBusinessBankAccount', {amount: '', last4Digits}); + + children = ( + + ${translation}`} /> + + ); + } else { + children = ; + } } else if (wasAutoPaid) { children = ( diff --git a/src/pages/home/report/ReportActionItemMessage.tsx b/src/pages/home/report/ReportActionItemMessage.tsx index c022423a6219..bc090bb24e75 100644 --- a/src/pages/home/report/ReportActionItemMessage.tsx +++ b/src/pages/home/report/ReportActionItemMessage.tsx @@ -92,7 +92,7 @@ function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHid const originalMessage = action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? getOriginalMessage(action) : null; const iouReportID = originalMessage?.IOUReportID; if (iouReportID) { - iouMessage = getIOUReportActionDisplayMessage(action, transaction); + iouMessage = getIOUReportActionDisplayMessage(action, transaction, report); } } diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index ecf5bb084edb..252ec2007345 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -65,6 +65,7 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { const [walletTerms = getEmptyObject()] = useOnyx(ONYXKEYS.WALLET_TERMS, {canBeMissing: true}); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: false}); const [userAccount] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); + const [lastUsedPaymentMethods] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true}); const isUserValidated = userAccount?.validated ?? false; const {isActingAsDelegate, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext); @@ -295,11 +296,18 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { const fundID = paymentMethod.selectedPaymentMethod.fundID; if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT && bankAccountID) { const bankAccount = bankAccountList?.[paymentMethod.methodID] ?? {}; - deletePaymentBankAccount(bankAccountID, undefined, bankAccount); + deletePaymentBankAccount(bankAccountID, lastUsedPaymentMethods, bankAccount); } else if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.DEBIT_CARD && fundID) { deletePaymentCard(fundID); } - }, [paymentMethod.selectedPaymentMethod.bankAccountID, paymentMethod.selectedPaymentMethod.fundID, paymentMethod.selectedPaymentMethodType, paymentMethod.methodID, bankAccountList]); + }, [ + paymentMethod.selectedPaymentMethod.bankAccountID, + paymentMethod.selectedPaymentMethod.fundID, + paymentMethod.selectedPaymentMethodType, + lastUsedPaymentMethods, + paymentMethod.methodID, + bankAccountList, + ]); /** * Navigate to the appropriate page after completing the KYC flow, depending on what initiated it diff --git a/src/pages/workspace/WorkspaceConfirmationPage.tsx b/src/pages/workspace/WorkspaceConfirmationPage.tsx index 4ef081cbf9aa..43c8a3df3ea5 100644 --- a/src/pages/workspace/WorkspaceConfirmationPage.tsx +++ b/src/pages/workspace/WorkspaceConfirmationPage.tsx @@ -2,22 +2,36 @@ import React from 'react'; import ScreenWrapper from '@components/ScreenWrapper'; import WorkspaceConfirmationForm from '@components/WorkspaceConfirmationForm'; import type {WorkspaceConfirmationSubmitFunctionParams} from '@components/WorkspaceConfirmationForm'; +import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {createWorkspaceWithPolicyDraftAndNavigateToIt} from '@libs/actions/App'; import {generatePolicyID} from '@libs/actions/Policy/Policy'; import getCurrentUrl from '@libs/Navigation/currentUrl'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {LastPaymentMethodType} from '@src/types/onyx'; function WorkspaceConfirmationPage() { // It is necessary to use here isSmallScreenWidth because on a wide layout we should always navigate to ROUTES.WORKSPACE_OVERVIEW. // shouldUseNarrowLayout cannot be used to determine that as this screen is displayed in RHP and shouldUseNarrowLayout always returns true. // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); - + const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true}); const onSubmit = (params: WorkspaceConfirmationSubmitFunctionParams) => { const policyID = params.policyID || generatePolicyID(); const routeToNavigate = isSmallScreenWidth ? ROUTES.WORKSPACE_INITIAL.getRoute(policyID) : ROUTES.WORKSPACE_OVERVIEW.getRoute(policyID); - createWorkspaceWithPolicyDraftAndNavigateToIt('', params.name, false, false, '', policyID, params.currency, params.avatarFile as File, routeToNavigate); + createWorkspaceWithPolicyDraftAndNavigateToIt( + '', + params.name, + false, + false, + '', + policyID, + params.currency, + params.avatarFile as File, + routeToNavigate, + lastPaymentMethod?.[policyID] as LastPaymentMethodType, + ); }; const currentUrl = getCurrentUrl(); // Approved Accountants and Guides can enter a flow where they make a workspace for other users, diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index cf91c61bc4de..11856072a302 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -136,6 +136,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const imageStyle: StyleProp = shouldUseNarrowLayout ? [styles.mhv12, styles.mhn5, styles.mbn5] : [styles.mhv8, styles.mhn8, styles.mbn5]; const shouldShowAddress = !readOnly || !!formattedAddress; const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext); + const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true}); const fetchPolicyData = useCallback(() => { if (policyDraft?.id) { @@ -182,10 +183,10 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa return; } - deleteWorkspace(policy.id, policyName); + deleteWorkspace(policy.id, policyName, lastPaymentMethod); setIsDeleteModalOpen(false); goBackFromInvalidPolicy(); - }, [policy?.id, policyName]); + }, [policy?.id, policyName, lastPaymentMethod]); useEffect(() => { if (isLoadingBill) { diff --git a/src/pages/workspace/WorkspaceResetBankAccountModal.tsx b/src/pages/workspace/WorkspaceResetBankAccountModal.tsx index 794de00ccda3..2750f8bd9271 100644 --- a/src/pages/workspace/WorkspaceResetBankAccountModal.tsx +++ b/src/pages/workspace/WorkspaceResetBankAccountModal.tsx @@ -36,13 +36,18 @@ function WorkspaceResetBankAccountModal({ }: WorkspaceResetBankAccountModalProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [session] = useOnyx(ONYXKEYS.SESSION); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const policyID = reimbursementAccount?.achData?.policyID; const achData = reimbursementAccount?.achData; const isInOpenState = achData?.state === BankAccount.STATE.OPEN; const bankAccountID = achData?.bankAccountID; const bankShortName = `${achData?.addressName ?? ''} ${(achData?.accountNumber ?? '').slice(-4)}`; + const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, { + canBeMissing: true, + selector: (paymentMethods) => (policyID ? (paymentMethods?.[policyID] as OnyxTypes.LastPaymentMethodType) : undefined), + }); + const handleConfirm = () => { if (isNonUSDWorkspace) { resetNonUSDBankAccount(policyID); @@ -55,7 +60,7 @@ function WorkspaceResetBankAccountModal({ setNonUSDBankAccountStep(null); } } else { - resetUSDBankAccount(bankAccountID, session, policyID); + resetUSDBankAccount(bankAccountID, session, policyID, lastPaymentMethod); if (setShouldShowConnectedVerifiedBankAccount) { setShouldShowConnectedVerifiedBankAccount(false); diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 5b3b657cb924..d0277522b539 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -116,6 +116,7 @@ function WorkspacesListPage() { const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); + const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true}); const shouldShowLoadingIndicator = isLoadingApp && !isOffline; const route = useRoute>(); @@ -157,7 +158,7 @@ function WorkspacesListPage() { return; } - deleteWorkspace(policyIDToDelete, policyNameToDelete); + deleteWorkspace(policyIDToDelete, policyNameToDelete, lastPaymentMethod); setIsDeleteModalOpen(false); }; diff --git a/src/stories/TransactionPreviewContent.stories.tsx b/src/stories/TransactionPreviewContent.stories.tsx index 30853d8c5b84..032d0845c588 100644 --- a/src/stories/TransactionPreviewContent.stories.tsx +++ b/src/stories/TransactionPreviewContent.stories.tsx @@ -2,6 +2,7 @@ import type {InputType} from '@storybook/csf'; import type {Meta, StoryFn} from '@storybook/react'; import React from 'react'; import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; import TransactionPreviewContent from '@components/ReportActionItem/TransactionPreview/TransactionPreviewContent'; import type {TransactionPreviewContentProps} from '@components/ReportActionItem/TransactionPreview/types'; import ThemeProvider from '@components/ThemeProvider'; @@ -27,7 +28,7 @@ const modifiedTransaction = ({category, tag, merchant = '', amount = 1000, hold hold: hold ? 'true' : undefined, }, }); -const iouReportWithModifiedType = (type: string) => ({...iouReportR14932, type}); +const iouReportWithModifiedType = (type: ValueOf) => ({...iouReportR14932, type}); const actionWithModifiedPendingAction = (pendingAction: PendingAction) => ({...actionR14932, pendingAction}); const disabledProperties = [ diff --git a/src/styles/index.ts b/src/styles/index.ts index 4190d3757230..bf1a4d4ee0f2 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -441,6 +441,11 @@ const styles = (theme: ThemeColors) => fontSize: variables.fontSizeSmall, }, + textExtraSmall: { + ...FontUtils.fontFamily.platform.EXP_NEUE, + fontSize: variables.fontSizeExtraSmall, + }, + textMicro: { ...FontUtils.fontFamily.platform.EXP_NEUE, fontSize: variables.fontSizeSmall, @@ -4485,6 +4490,15 @@ const styles = (theme: ThemeColors) => paddingLeft: 0, }, + dropDownButtonCartIcon: { + minWidth: 22, + }, + + dropDownSmallButtonArrowContain: { + marginLeft: 3, + marginRight: 6, + }, + dropDownMediumButtonArrowContain: { marginLeft: 12, marginRight: 16, @@ -4698,6 +4712,16 @@ const styles = (theme: ThemeColors) => height: is2FARequired ? variables.modalTopIconHeight : variables.modalTopBigIconHeight, }), + settlementButtonListContainer: { + maxHeight: 500, + paddingBottom: 0, + paddingTop: 0, + }, + + settlementButtonShortFormWidth: { + minWidth: 90, + }, + moneyRequestViewImage: { ...spacing.mh5, overflow: 'hidden', diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 1cec935f323e..3d6138981675 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1234,6 +1234,23 @@ function getItemBackgroundColorStyle(isSelected: boolean, isFocused: boolean, is return {}; } +/** + * In SettlementButton, when the list exceeds a certain number of items, + * we don't want to apply padding to the container. Instead, we want only + * the first last item to have spacing to create the effect of having more items in the list. + */ +function getOptionMargin(itemIndex: number, itemsLen: number) { + if (itemIndex === itemsLen && itemsLen > 5) { + return {marginBottom: 16}; + } + + if (itemIndex === 0 && itemsLen > 5) { + return {marginTop: 16}; + } + + return {}; +} + const staticStyleUtils = { positioning, searchHeaderDefaultOffset, @@ -1318,6 +1335,7 @@ const staticStyleUtils = { getItemBackgroundColorStyle, getNavigationBarType, getSuccessReportCardLostIllustrationStyle, + getOptionMargin, }; const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ diff --git a/src/types/onyx/LastPaymentMethod.ts b/src/types/onyx/LastPaymentMethod.ts index 00a4cd475415..4b856daf3510 100644 --- a/src/types/onyx/LastPaymentMethod.ts +++ b/src/types/onyx/LastPaymentMethod.ts @@ -1,30 +1,28 @@ +/** + * PaymentInformation object + */ +type PaymentInformation = { + /** The name of the payment type used Elsewhere, Expensify, ACH, or a policyID. */ + name: string; + /** The bank account id of the last payment method */ + bankAccountID?: number; +}; + /** * The new lastPaymentMethod object */ type LastPaymentMethodType = { /** The default last payment method */ - lastUsed: { - /** The name of the last payment method */ - name: string; - }; + lastUsed: PaymentInformation; /** The lastPaymentMethod of an IOU */ - Iou: { - /** The name of the last payment method */ - name: string; - }; + iou: PaymentInformation; /** The lastPaymentMethod of an Expense */ - Expense: { - /** The name of the last payment method */ - name: string; - }; + expense: PaymentInformation; /** The lastPaymentMethod of an Invoice */ - Invoice: { - /** The name of the last payment method */ - name: string; - }; + invoice: string | PaymentInformation; }; /** Record of last payment methods, indexed by policy id */ type LastPaymentMethod = Record; -export type {LastPaymentMethodType, LastPaymentMethod}; +export type {LastPaymentMethodType, LastPaymentMethod, PaymentInformation}; diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index bd3412bec52f..48aff18b4d01 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -74,6 +74,12 @@ type OriginalMessageIOU = { /** Collection of accountIDs of users mentioned in message */ whisperedTo?: number[]; + + /** Where the invoice is paid with business account or not */ + payAsBusiness?: boolean; + + /** The bank account id */ + bankAccountID?: number; }; /** Names of moderation decisions */ diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index cea4963592cf..d0d3de64d159 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -135,7 +135,7 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< writeCapability?: WriteCapability; /** The report type */ - type?: string; + type?: ValueOf | ValueOf | ValueOf; /** The report visibility */ visibility?: RoomVisibility; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 5667284fdba1..417de849917a 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -80,6 +80,12 @@ type Message = { /** The time this report action was deleted */ deleted?: string; + + /** The bank account id that was used to pay the invoice */ + bankAccountID?: number | undefined; + + /** Whether the invoice was paid with business account or not */ + payAsBusiness?: boolean; }; /** Model of image */ diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 36bec20a1062..331acef345d5 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -2775,7 +2775,7 @@ describe('actions/IOU', () => { ) .then(() => { if (chatReport && expenseReport) { - payMoneyRequest(CONST.IOU.PAYMENT_TYPE.VBBA, chatReport, expenseReport); + payMoneyRequest(CONST.IOU.PAYMENT_TYPE.VBBA, chatReport, expenseReport, undefined); } return waitForBatchedUpdates(); }) @@ -2908,7 +2908,7 @@ describe('actions/IOU', () => { .then(() => { mockFetch?.fail?.(); if (chatReport && expenseReport) { - payMoneyRequest('ACH', chatReport, expenseReport); + payMoneyRequest('ACH', chatReport, expenseReport, undefined); } return waitForBatchedUpdates(); }) @@ -3048,7 +3048,7 @@ describe('actions/IOU', () => { }) .then(() => { // When partially paying an iou report from the chat report via the report preview - payMoneyRequest(CONST.IOU.PAYMENT_TYPE.ELSEWHERE, {reportID: topMostReportID}, iouReport, false); + payMoneyRequest(CONST.IOU.PAYMENT_TYPE.ELSEWHERE, {reportID: topMostReportID}, iouReport, undefined, false); return waitForBatchedUpdates(); }) .then(() => { @@ -3127,7 +3127,7 @@ describe('actions/IOU', () => { .then(() => { // When the expense report is paid elsewhere (but really, any payment option would work) if (chatReport && expenseReport) { - payMoneyRequest(CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, expenseReport); + payMoneyRequest(CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, expenseReport, undefined); } return waitForBatchedUpdates(); }) diff --git a/tests/unit/GoogleTagManagerTest.tsx b/tests/unit/GoogleTagManagerTest.tsx index f4e865d4260e..a3a862656bac 100644 --- a/tests/unit/GoogleTagManagerTest.tsx +++ b/tests/unit/GoogleTagManagerTest.tsx @@ -71,11 +71,11 @@ describe('GoogleTagManagerTest', () => { test('workspace_created', async () => { // When we run the createWorkspace action a few times - createWorkspace(); + createWorkspace({}); await waitForBatchedUpdates(); - createWorkspace(); + createWorkspace({}); await waitForBatchedUpdates(); - createWorkspace(); + createWorkspace({}); // Then we publish a workspace_created event only once expect(GoogleTagManager.publishEvent).toBeCalledTimes(1); diff --git a/tests/unit/OnyxDerivedTest.ts b/tests/unit/OnyxDerivedTest.ts index ecf1868025fd..0e8f6b8c4370 100644 --- a/tests/unit/OnyxDerivedTest.ts +++ b/tests/unit/OnyxDerivedTest.ts @@ -23,7 +23,7 @@ describe('OnyxDerived', () => { }); describe('reportAttributes', () => { - const mockReport = { + const mockReport: Report = { reportID: `test_1`, reportName: 'Test Report', type: 'chat', diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index de069853fa08..2e42cdbd1a80 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -1057,7 +1057,7 @@ describe('SidebarUtils', () => { parentReportID: policyExpenseChat.reportID, parentReportActionID: lastReportPreviewAction.reportActionID, chatReportID: policyExpenseChat.reportID, - }; + } as Report; const iouAction = { actionName: CONST.REPORT.ACTIONS.TYPE.IOU, originalMessage: {