From 6c7a985c165cb1957a8c95b6fbde75815d181c8d Mon Sep 17 00:00:00 2001 From: Getabalew Date: Mon, 21 Jul 2025 14:16:24 +0300 Subject: [PATCH 01/19] revert the revert and apply fixes --- src/CONST/index.ts | 6 +- src/components/Button/index.tsx | 14 +- .../ButtonWithDropdownMenu/index.tsx | 49 +- .../ButtonWithDropdownMenu/types.ts | 11 + src/components/KYCWall/BaseKYCWall.tsx | 63 ++- src/components/KYCWall/types.ts | 5 +- src/components/MoneyReportHeader.tsx | 13 +- src/components/PopoverMenu.tsx | 11 +- src/components/ProcessMoneyReportHoldMenu.tsx | 2 +- src/components/SettlementButton/index.tsx | 494 ++++++++++++++++-- src/components/SettlementButton/types.ts | 8 +- src/languages/de.ts | 24 +- src/languages/en.ts | 24 +- src/languages/es.ts | 25 +- src/languages/fr.ts | 26 +- src/languages/it.ts | 25 +- src/languages/ja.ts | 25 +- src/languages/nl.ts | 25 +- src/languages/params.ts | 9 + src/languages/pl.ts | 25 +- src/languages/pt-BR.ts | 24 +- src/languages/zh-hans.ts | 24 +- .../MoveIOUReportToExistingPolicyParams.ts | 1 + ...UReportToPolicyAndInviteSubmitterParams.ts | 1 + src/libs/DebugUtils.ts | 4 + src/libs/IOUUtils.ts | 23 +- src/libs/MoneyRequestReportUtils.ts | 2 +- src/libs/ReportUtils.ts | 97 +++- src/libs/actions/BankAccounts.ts | 113 +++- src/libs/actions/IOU.ts | 43 +- src/libs/actions/Policy/Policy.ts | 55 ++ .../resetUSDBankAccount.ts | 234 +++++---- src/libs/actions/Report.ts | 38 +- src/libs/actions/Search.ts | 65 ++- .../home/report/PureReportActionItem.tsx | 15 + .../home/report/ReportActionItemMessage.tsx | 2 +- .../settings/Wallet/WalletPage/WalletPage.tsx | 5 +- .../TransactionPreviewContent.stories.tsx | 3 +- src/styles/index.ts | 24 + src/styles/utils/index.ts | 13 + src/types/onyx/LastPaymentMethod.ts | 32 +- src/types/onyx/OriginalMessage.ts | 6 + src/types/onyx/Report.ts | 2 +- src/types/onyx/ReportAction.ts | 6 + tests/actions/IOUTest.ts | 8 +- tests/unit/OnyxDerivedTest.ts | 3 +- tests/unit/SidebarUtilsTest.ts | 2 +- 47 files changed, 1417 insertions(+), 312 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 65280a0ced2c..80648be36a68 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6877,9 +6877,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 6fe5fab79382..73318dff4f4c 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -13,6 +13,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'; @@ -55,8 +56,11 @@ function ButtonWithDropdownMenuInner(props: ButtonWithDropdownMenuPr testID, secondLineText = '', icon, - shouldUseModalPaddingStyle = true, - shouldUseOptionIcon = false, + shouldPopoverUseScrollView = false, + containerStyles, + shouldUseModalPaddingStyle = true, + shouldUseShortForm = false, + shouldUseOptionIcon = false, } = props; const theme = useTheme(); @@ -78,9 +82,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]); + useEffect(() => { if (!dropdownAnchor.current) { return; @@ -134,6 +143,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) => { @@ -167,12 +177,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} /> @@ -188,16 +199,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} > - + @@ -249,18 +269,27 @@ function ButtonWithDropdownMenuInner(props: ButtonWithDropdownMenuPr shouldShowSelectedItemCheck={shouldShowSelectedItemCheck} // eslint-disable-next-line react-compiler/react-compiler anchorRef={nullCheckRef(dropdownAnchor)} - withoutOverlay - shouldUseScrollView scrollContainerStyle={!shouldUseModalPaddingStyle && isSmallScreenWidth && styles.pv4} - 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..ca629fa3d384 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -5,19 +5,20 @@ import type {EmitterSubscription, GestureResponderEvent, View} from 'react-nativ import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; 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 {getPolicyExpenseChat, getBankAccountRoute, 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'; @@ -103,16 +104,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) ?? {}; + savePreferredPaymentMethod(iouReport.policyID, policy.id, CONST.LAST_PAYMENT_METHOD.IOU); + 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); + 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) { + savePreferredPaymentMethod(iouReport.policyID, policyID, CONST.LAST_PAYMENT_METHOD.IOU); + } completePaymentOnboarding(CONST.PAYMENT_SELECTED.BBA, adminsChatReportID, policyID); if (workspaceChatReportID) { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(workspaceChatReportID, reportPreviewReportActionID)); @@ -120,7 +146,6 @@ 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); @@ -137,7 +162,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 +196,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 +221,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 ((paymentMethod || policy) && !hasActivatedWallet) { + 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 0bca126c77a6..30771b15428d 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -115,6 +115,7 @@ import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu'; import {useSearchContext} from './Search/SearchContext'; import AnimatedSettlementButton from './SettlementButton/AnimatedSettlementButton'; import Text from './Text'; +import getPlatform from "@libs/getPlatform"; type MoneyReportHeaderProps = { /** The report currently being looked at */ @@ -330,13 +331,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], @@ -545,6 +550,7 @@ function MoneyReportHeader({ isPaidAnimationRunning={isPaidAnimationRunning} isApprovedAnimationRunning={isApprovedAnimationRunning} onAnimationFinish={stopAnimation} + formattedAmount={totalAmount} canIOUBePaid onlyShowPayElsewhere={onlyShowPayElsewhere} currency={moneyRequestReport?.currency} @@ -944,11 +950,12 @@ function MoneyReportHeader({ }} buttonRef={buttonRef} shouldAlwaysShowDropdownMenu + shouldPopoverUseScrollView={applicableSecondaryActions.length >= 5} customText={translate('common.more')} options={applicableSecondaryActions} isSplitButton={false} wrapperStyle={shouldDisplayNarrowVersion && [!primaryAction && styles.flex1]} - shouldUseModalPaddingStyle={false} + shouldUseModalPaddingStyle={applicableSecondaryActions.length <= 5} /> )} diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 89a624531471..efaa5da05de1 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 && 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/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index e89c582ffc66..42ac44b4d321 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -1,24 +1,54 @@ -import React, {useContext} from 'react'; +import isEmpty from 'lodash/isEmpty'; +import truncate from 'lodash/truncate'; +import React, {useCallback, 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, + isBusinessInvoiceRoom, + 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,83 +83,473 @@ 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 [lastPaymentMethod, lastPaymentMethodResult] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, { + canBeMissing: true, + selector: (paymentMethod) => getLastPolicyPaymentMethod(policyIDKey, paymentMethod, iouReport?.type as keyof LastPaymentMethodType, isIOUReport(iouReport)), + }); + + const lastBankAccountID = getLastPolicyBankAccountID(policyIDKey, 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 getLastPaymentMethodType = () => { + if (isInvoiceReport) { + return CONST.LAST_PAYMENT_METHOD.INVOICE; + } + + if (policy) { + return CONST.LAST_PAYMENT_METHOD.EXPENSE; + } + + return CONST.LAST_PAYMENT_METHOD.IOU; + }; + + 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: onlyShowPayElsewhere ? 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; + + 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 formattedPaymentMethods = formatPaymentMethods(bankAccountList, fundList, styles); + 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('workspace.invoices.paymentMethods.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('workspace.invoices.paymentMethods.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 ?? lastPaymentPolicy); + 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; + } + + 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)}); + } - const filteredPaymentOptions = paymentButtonOptions.filter((option) => option.value !== undefined) as Array>; + if (!bankAccountToDisplay?.accountData?.accountNumber) { + return; + } - const onPaymentSelect = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) => { + 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) ?? ''}); + } + + 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} 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 736f7e1a7e11..8bc80be27861 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -35,6 +35,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryDomainRoomPartOneParams, @@ -45,6 +46,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1133,10 +1135,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`, @@ -1171,8 +1183,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.', @@ -1833,6 +1845,7 @@ const translations = { enableWallet: 'Wallet aktivieren', addBankAccountToSendAndReceive: 'Erhalten Sie eine Rückerstattung für Ausgaben, die Sie an einen Arbeitsbereich einreichen.', addBankAccount: 'Bankkonto hinzufügen', + 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', @@ -2046,6 +2059,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 b3feb0833787..3db4c1e9d231 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -23,6 +23,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryDomainRoomPartOneParams, @@ -33,6 +34,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1118,10 +1120,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`, @@ -1156,8 +1168,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', @@ -1810,6 +1822,7 @@ const translations = { enableWallet: 'Enable wallet', addBankAccountToSendAndReceive: 'Get paid back for expenses you submit to a workspace.', addBankAccount: 'Add bank account', + 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', @@ -2019,6 +2032,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 dad6c6c7b2c4..81849370503c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -22,6 +22,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryDomainRoomPartOneParams, @@ -32,6 +33,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1113,10 +1115,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}`, @@ -1151,8 +1164,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', @@ -1809,6 +1822,7 @@ const translations = { enableWallet: 'Habilitar billetera', addBankAccountToSendAndReceive: 'Recibe el reembolso de los gastos que envíes a un espacio de trabajo.', addBankAccount: 'Añadir cuenta bancaria', + 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', @@ -2019,6 +2033,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 0a93058fe15f..9a4c87b1036d 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -35,6 +35,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryDomainRoomPartOneParams, @@ -45,6 +46,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1134,10 +1136,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}`, @@ -1172,8 +1186,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', @@ -1834,6 +1848,7 @@ const translations = { enableWallet: 'Activer le portefeuille', addBankAccountToSendAndReceive: 'Soyez remboursé pour les dépenses que vous soumettez à un espace de travail.', addBankAccount: 'Ajouter un compte bancaire', + 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', @@ -2048,6 +2063,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 0eb90ebe2243..ced62f93f8db 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -35,6 +35,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryDomainRoomPartOneParams, @@ -45,6 +46,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1129,10 +1131,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}`, @@ -1167,8 +1180,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', @@ -1826,6 +1839,7 @@ const translations = { enableWallet: 'Abilita portafoglio', addBankAccountToSendAndReceive: "Ricevi il rimborso per le spese che invii a un'area di lavoro.", addBankAccount: 'Aggiungi conto bancario', + 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', @@ -2038,6 +2052,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 2f1832db095e..967aaddad2ea 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -35,6 +35,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryDomainRoomPartOneParams, @@ -45,6 +46,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1132,10 +1134,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} 請求書を送信`, @@ -1170,8 +1183,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: 'このレポートには無効な金額が含まれています', @@ -1824,6 +1837,7 @@ const translations = { enableWallet: 'ウォレットを有効にする', addBankAccountToSendAndReceive: 'ワークスペースに提出した経費の払い戻しを受ける。', addBankAccount: '銀行口座を追加', + addDebitOrCreditCard: 'デビットカードまたはクレジットカードを追加', assignedCards: '割り当てられたカード', assignedCardsDescription: 'これらは、会社の支出を管理するためにワークスペース管理者によって割り当てられたカードです。', expensifyCard: 'Expensify Card', @@ -2032,6 +2046,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 5d1c2e5184ac..5ab7d682332d 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -35,6 +35,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryDomainRoomPartOneParams, @@ -45,6 +46,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1130,10 +1132,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`, @@ -1168,8 +1181,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.', @@ -1826,6 +1839,7 @@ const translations = { enableWallet: 'Portemonnee inschakelen', addBankAccountToSendAndReceive: 'Word terugbetaald voor uitgaven die je indient bij een werkruimte.', addBankAccount: 'Bankrekening toevoegen', + 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', @@ -2038,6 +2052,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 e90b92435fe8..061c4cd7a455 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -141,6 +141,11 @@ type WorkspacesListRouteParams = { workspacesListRoute: string; }; +type BusinessBankAccountParams = { + amount?: string; + last4Digits?: string; +}; + type WorkspaceRouteParams = { workspaceRoute: string; }; @@ -205,6 +210,8 @@ type TransferParams = {amount: string}; type InstantSummaryParams = {rate: string; minAmount: string}; +type BankAccountLastFourParams = {lastFour: string}; + type NotYouParams = {user: string}; type DateShouldBeBeforeParams = {dateString: string}; @@ -1046,6 +1053,7 @@ export type { SettlementDateParams, PolicyExpenseChatNameParams, YourPlanPriceValueParams, + BusinessBankAccountParams, NeedCategoryForExportToIntegrationParams, UpdatedPolicyAuditRateParams, UpdatedPolicyManualApprovalThresholdParams, @@ -1059,6 +1067,7 @@ export type { UpdatedPolicyCategoryExpenseLimitTypeParams, UpdatedPolicyCategoryMaxAmountNoReceiptParams, SubscriptionSettingsSummaryParams, + BankAccountLastFourParams, ReviewParams, CreateExpensesParams, CurrencyInputDisabledTextParams, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 7175fe2e439d..aa801f39ddba 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -35,6 +35,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryDomainRoomPartOneParams, @@ -45,6 +46,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1128,10 +1130,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}`, @@ -1166,8 +1179,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ę', @@ -1822,6 +1835,7 @@ const translations = { enableWallet: 'Włącz portfel', addBankAccountToSendAndReceive: 'Otrzymaj zwrot kosztów za wydatki, które zgłaszasz do przestrzeni roboczej.', addBankAccount: 'Dodaj konto bankowe', + 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', @@ -2034,6 +2048,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 1d43d58ff60f..4eb8cd5ef083 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -35,6 +35,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryDomainRoomPartOneParams, @@ -45,6 +46,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1130,10 +1132,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}`, @@ -1168,8 +1180,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', @@ -1825,6 +1837,7 @@ const translations = { enableWallet: 'Ativar carteira', addBankAccountToSendAndReceive: 'Receba reembolso pelas despesas que você enviar para um espaço de trabalho.', addBankAccount: 'Adicionar conta bancária', + 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', @@ -2037,6 +2050,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 5d7b2e39b964..702e2b6e594e 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -35,6 +35,7 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, + BankAccountLastFourParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryDomainRoomPartOneParams, @@ -45,6 +46,7 @@ import type { BillingBannerInsufficientFundsParams, BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, + BusinessBankAccountParams, BusinessTaxIDParams, CanceledRequestParams, CardEndingParams, @@ -1120,10 +1122,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} 发票`, @@ -1156,8 +1168,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: '此报告的金额无效', @@ -1808,6 +1820,7 @@ const translations = { enableWallet: '启用钱包', addBankAccountToSendAndReceive: '获得报销您提交到工作区的费用。', addBankAccount: '添加银行账户', + addDebitOrCreditCard: '添加借记卡或信用卡', assignedCards: '已分配的卡片', assignedCardsDescription: '这些是由工作区管理员分配的卡片,用于管理公司支出。', expensifyCard: 'Expensify Card', @@ -2015,6 +2028,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 410386bccaf8..60464e14f56f 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -817,6 +817,8 @@ function validateReportActionDraftProperty(key: keyof ReportAction, value: strin ...CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION, }, deleted: 'string', + bankAccountID: 'string', + payAsBusiness: 'string', }), () => validateObject>(value, { @@ -900,6 +902,8 @@ function validateReportActionDraftProperty(key: keyof ReportAction, value: strin expenseReportID: 'string', resolution: 'string', deleted: 'string', + bankAccountID: 'string', + payAsBusiness: 'string', }), ); } diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index e7b50ef35c79..a9687da48c58 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -1,10 +1,11 @@ import Onyx from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {OnyxInputOrEntry, PersonalDetails, Report} from '@src/types/onyx'; +import type {LastPaymentMethod, LastPaymentMethodType, OnyxInputOrEntry, PersonalDetails, Report} from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; import type {IOURequestType} from './actions/IOU'; import {getCurrencyUnit} from './CurrencyUtils'; @@ -20,6 +21,12 @@ Onyx.connect({ callback: (val) => (lastLocationPermissionPrompt = val ?? ''), }); +let lastUsedPaymentMethods: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + callback: (value) => (lastUsedPaymentMethods = value), +}); + function navigateToStartMoneyRequestStep(requestType: IOURequestType, iouType: IOUType, transactionID: string, reportID: string, iouAction?: IOUAction): void { if (iouAction === CONST.IOU.ACTION.CATEGORIZE || iouAction === CONST.IOU.ACTION.SUBMIT || iouAction === CONST.IOU.ACTION.SHARE) { Navigation.goBack(); @@ -216,6 +223,18 @@ function shouldStartLocationPermissionFlow() { ); } +function getLastUsedPaymentMethods() { + return lastUsedPaymentMethods; +} + +function getLastUsedPaymentMethod(policyID?: string): LastPaymentMethodType | undefined { + if (!policyID) { + return; + } + + return lastUsedPaymentMethods?.[policyID] as LastPaymentMethodType; +} + export { calculateAmount, insertTagIntoTransactionTagsString, @@ -228,4 +247,6 @@ export { formatCurrentUserToAttendee, shouldStartLocationPermissionFlow, navigateToParticipantPage, + getLastUsedPaymentMethods, + getLastUsedPaymentMethod, }; 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 2a2dbac31ecb..e967e76ebc5a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -372,6 +372,8 @@ type BuildOptimisticIOUReportActionParams = { isOwnPolicyExpenseChat?: boolean; created?: string; linkedExpenseReportAction?: OnyxEntry; + payAsBusiness?: boolean; + bankAccountID?: number | undefined; isPersonalTrackingExpense?: boolean; reportActionID?: string; }; @@ -1316,7 +1318,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; } @@ -1349,7 +1352,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; } @@ -1565,6 +1569,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; @@ -4565,7 +4573,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}); @@ -4580,7 +4588,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); } @@ -4597,7 +4607,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) { @@ -4611,13 +4620,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) { @@ -5129,11 +5147,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'); } @@ -6123,9 +6150,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) @@ -6166,7 +6206,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}); @@ -6217,6 +6264,8 @@ function buildOptimisticIOUReportAction(params: BuildOptimisticIOUReportActionPa created = DateUtils.getDBTime(), linkedExpenseReportAction, isPersonalTrackingExpense = false, + payAsBusiness, + bankAccountID, reportActionID, } = params; @@ -6229,6 +6278,8 @@ function buildOptimisticIOUReportAction(params: BuildOptimisticIOUReportActionPa IOUTransactionID: transactionID, IOUReportID, type, + payAsBusiness, + bankAccountID, }; const delegateAccountDetails = getPersonalDetailByEmail(delegateEmail); @@ -6290,7 +6341,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)); @@ -9137,20 +9188,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: @@ -9158,16 +9208,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; @@ -11447,6 +11503,7 @@ export { generateReportName, navigateToLinkedReportAction, buildOptimisticUnreportedTransactionAction, + isBusinessInvoiceRoom, buildOptimisticResolvedDuplicatesReportAction, getTitleReportField, getReportFieldsByPolicyID, diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 604fe4fc09b2..646d125201f4 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -18,8 +18,10 @@ import type {SaveCorpayOnboardingCompanyDetails} from '@libs/API/parameters/Save import type SaveCorpayOnboardingDirectorInformationParams from '@libs/API/parameters/SaveCorpayOnboardingDirectorInformationParams'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; +import {getLastUsedPaymentMethod} from '@libs/IOUUtils'; 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 +29,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'; @@ -216,7 +218,27 @@ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAcc policyID, }; - API.write(WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID, parameters, getVBBADataForOnyx()); + const onyxData = getVBBADataForOnyx(); + const lastUsedPaymentMethod = getLastUsedPaymentMethod(policyID); + + if (!lastUsedPaymentMethod?.expense?.name) { + 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?.lastUsed?.name ?? CONST.IOU.PAYMENT_TYPE.VBBA, + }, + }, + }, + }); + } + + API.write(WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID, parameters, onyxData); } /** @@ -242,6 +264,9 @@ function addPersonalBankAccount(account: PlaidBankAccount, policyID?: string, so parameters.source = source; } + const personalPolicy = getPersonalPolicy(); + const lastUsedPaymentMethod = getLastUsedPaymentMethod(personalPolicy?.id); + const onyxData: OnyxData = { optimisticData: [ { @@ -284,6 +309,44 @@ function addPersonalBankAccount(account: PlaidBankAccount, policyID?: string, so ], }; + if (personalPolicy?.id && !lastUsedPaymentMethod) { + 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 +359,8 @@ function deletePaymentBankAccount(bankAccountID: number, lastUsedPaymentMethods? pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, }; + const personalPolicy = getPersonalPolicy(); + const onyxData: OnyxData = { optimisticData: [ { @@ -326,6 +391,50 @@ function deletePaymentBankAccount(bankAccountID: number, lastUsedPaymentMethods? ], }; + Object.keys(lastUsedPaymentMethods ?? {}).forEach((paymentMethodID) => { + const lastUsedPaymentMethod = lastUsedPaymentMethods?.[paymentMethodID] as LastPaymentMethodType; + + 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]: revertedLastUsedPaymentMethod, + }, + }); + + onyxData.failureData?.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + value: { + [personalPolicy?.id]: lastUsedPaymentMethod.iou.name, + }, + }); + } + + 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]: revertedLastUsedPaymentMethod, + }, + }); + + onyxData.failureData?.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + value: { + [paymentMethodID]: lastUsedPaymentMethod.expense.name, + }, + }); + } + }); + 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 deb46f044718..727a9ead87d6 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -48,6 +48,7 @@ import GoogleTagManager from '@libs/GoogleTagManager'; import { calculateAmount as calculateIOUAmount, formatCurrentUserToAttendee, + getLastUsedPaymentMethod, isMovingTransactionFromTrackExpense as isMovingTransactionFromTrackExpenseIOUUtils, navigateToStartMoneyRequestStep, updateIOUOwnerAndTotal, @@ -8956,6 +8957,8 @@ function getPayMoneyRequestParams( paymentMethodType: PaymentMethodType, full: boolean, payAsBusiness?: boolean, + bankAccountID?: number, + paymentPolicyID?: string | undefined, ): 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,24 @@ function getPayMoneyRequestParams( ); if (iouReport?.policyID) { + const lastUsedPaymentMethod = (getLastUsedPaymentMethod(iouReport.policyID) ?? {}) as OnyxTypes.LastPaymentMethodType; + 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 +10367,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 +10377,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 +10413,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); @@ -11158,9 +11175,17 @@ 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) { + if (!policyID) { + return; + } + + // to make it easier to revert to the previous last payment method, we will save it to this key + const prevPaymentMethod = (getLastUsedPaymentMethod(policyID) ?? {}) as OnyxTypes.LastPaymentMethodType; + Onyx.merge(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, { + [policyID]: type ? {[type]: {name: paymentMethod}, [CONST.LAST_PAYMENT_METHOD.LAST_USED]: {name: 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 27e58d2c400c..6e1d72e5ba57 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -67,6 +67,7 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import {createFile} from '@libs/fileDownload/FileUtils'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import GoogleTagManager from '@libs/GoogleTagManager'; +import {getLastUsedPaymentMethod, getLastUsedPaymentMethods} from '@libs/IOUUtils'; import {translate, translateLocal} from '@libs/Localize'; import Log from '@libs/Log'; import * as NetworkStore from '@libs/Network/NetworkStore'; @@ -482,6 +483,32 @@ function deleteWorkspace(policyID: string, policyName: string) { } }); + const lastUsedPaymentMethods = getLastUsedPaymentMethods(); + 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 : '', + }, + }, + }, + }); + } + }); + const params: DeleteWorkspaceParams = {policyID}; API.write(WRITE_COMMANDS.DELETE_WORKSPACE, params, {optimisticData, finallyData, failureData}); @@ -2173,6 +2200,34 @@ function buildPolicyData(options: BuildPolicyDataOptions = {}) { successData.push(...optimisticCategoriesData.successData); } + if (getAdminPolicies().length === 0) { + Object.values(allReports ?? {}) + .filter((iouReport) => iouReport?.type === CONST.REPORT.TYPE.IOU) + .forEach((iouReport) => { + const lastUsedPaymentMethod = getLastUsedPaymentMethod(iouReport?.policyID); + + // 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..e88606bc1a80 100644 --- a/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts +++ b/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts @@ -2,10 +2,12 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import {WRITE_COMMANDS} from '@libs/API/types'; +import {getLastUsedPaymentMethod} from '@libs/IOUUtils'; 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({ @@ -26,120 +28,142 @@ function resetUSDBankAccount(bankAccountID: number | undefined, session: OnyxEnt } const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? ({} as OnyxTypes.Policy); + const lastUsedPaymentMethod = getLastUsedPaymentMethod(policy.id); + 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 08647148b8bc..c135a9a37287 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, @@ -4984,8 +4985,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 @@ -4998,7 +5000,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; } @@ -5143,10 +5145,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}); @@ -5157,7 +5173,7 @@ 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) { +function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: string): {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 @@ -5171,6 +5187,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)) { @@ -5381,15 +5398,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 7f485b0075dc..600c2ffc604c 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -10,29 +10,23 @@ import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs import {getCommandURL} from '@libs/ApiUtils'; import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import fileDownload from '@libs/fileDownload'; +import {getLastUsedPaymentMethod, getLastUsedPaymentMethods} from '@libs/IOUUtils'; import enhanceParameters from '@libs/Network/enhanceParameters'; import {rand64} from '@libs/NumberUtils'; -import {getSubmitToAccountID, getValidConnectedIntegration} from '@libs/PolicyUtils'; -import {buildOptimisticExportIntegrationAction, hasHeldExpenses} from '@libs/ReportUtils'; +import {getPersonalPolicy, getSubmitToAccountID, getValidConnectedIntegration} from '@libs/PolicyUtils'; +import {buildOptimisticExportIntegrationAction, hasHeldExpenses, isExpenseReport, isInvoiceReport, isIOUReport} from '@libs/ReportUtils'; import {isTransactionGroupListItemType, isTransactionListItemType} from '@libs/SearchUIUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {FILTER_KEYS} from '@src/types/form/SearchAdvancedFiltersForm'; import type {SearchAdvancedFiltersForm} from '@src/types/form/SearchAdvancedFiltersForm'; +import type {PaymentInformation} from '@src/types/onyx/LastPaymentMethod'; import type {LastPaymentMethod, LastPaymentMethodType, Policy, SearchResults} from '@src/types/onyx'; 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'; -let lastPaymentMethod: OnyxEntry; -Onyx.connect({ - key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, - callback: (val) => { - lastPaymentMethod = val; - }, -}); - let allSnapshots: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.SNAPSHOT, @@ -86,24 +80,54 @@ function handleActionButtonPress(hash: number, item: TransactionListItemType | T } } -function getLastPolicyPaymentMethod(policyID: string | undefined, lastPaymentMethods: OnyxEntry) { + +function getLastPolicyBankAccountID(policyID: string | undefined, reportType: keyof LastPaymentMethodType = 'lastUsed'): number | undefined { + if (!policyID) { + return undefined; + } + const lastPolicyPaymentMethod = getLastUsedPaymentMethod(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 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 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(isIOUReport(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(hash: number, item: TransactionListItemType | TransactionReportGroupListItemType, goToItem: () => void) { - const lastPolicyPaymentMethod = getLastPolicyPaymentMethod(item.policyID, lastPaymentMethod); + const lastPaymentMethods = (getLastUsedPaymentMethods() ?? {}) as OnyxEntry; + const lastPolicyPaymentMethod = getLastPolicyPaymentMethod(item.policyID, lastPaymentMethods, getReportType(item.reportID)); - if (!lastPolicyPaymentMethod) { + if (!lastPolicyPaymentMethod || !Object.values(CONST.IOU.PAYMENT_TYPE).includes(lastPolicyPaymentMethod)) { goToItem(); return; } @@ -518,5 +542,6 @@ export { openSearchFiltersCardPage, openSearchPage as openSearch, getLastPolicyPaymentMethod, + getLastPolicyBankAccountID, exportToIntegrationOnSearch, }; diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index cb4af099ff56..234d1294db13 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -1027,8 +1027,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 74cf5ec2944b..82dfe86cb2a6 100644 --- a/src/pages/home/report/ReportActionItemMessage.tsx +++ b/src/pages/home/report/ReportActionItemMessage.tsx @@ -91,7 +91,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 b1fe6d95c659..786e4ec50818 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,11 @@ 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/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 9c9e40df311b..4b5f0b3458b6 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -440,6 +440,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, @@ -4461,6 +4466,15 @@ const styles = (theme: ThemeColors) => paddingLeft: 0, }, + dropDownButtonCartIcon: { + minWidth: 22, + }, + + dropDownSmallButtonArrowContain: { + marginLeft: 3, + marginRight: 6, + }, + dropDownMediumButtonArrowContain: { marginLeft: 12, marginRight: 16, @@ -4674,6 +4688,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 df52b0c09416..259a49becfcf 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1217,6 +1217,18 @@ function getItemBackgroundColorStyle(isSelected: boolean, isFocused: boolean, is return {}; } +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, @@ -1300,6 +1312,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..0338c7c6a7ef 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 */ + 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 09cd4797b973..9671ca610893 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 b6ba318ab743..fc31d4e7498f 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 38baa1950a52..7742f7629b60 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -2708,7 +2708,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(); }) @@ -2837,7 +2837,7 @@ describe('actions/IOU', () => { .then(() => { mockFetch?.fail?.(); if (chatReport && expenseReport) { - payMoneyRequest('ACH', chatReport, expenseReport); + payMoneyRequest('ACH', chatReport, expenseReport, undefined); } return waitForBatchedUpdates(); }) @@ -2977,7 +2977,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(() => { @@ -3052,7 +3052,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/OnyxDerivedTest.ts b/tests/unit/OnyxDerivedTest.ts index 1adbb5b6635c..cd6c960f2224 100644 --- a/tests/unit/OnyxDerivedTest.ts +++ b/tests/unit/OnyxDerivedTest.ts @@ -4,6 +4,7 @@ import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; import initOnyxDerivedValues from '@userActions/OnyxDerived'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; import type {ReportActions} from '@src/types/onyx/ReportAction'; import {createRandomReport} from '../utils/collections/reports'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -19,7 +20,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 31d14880e9b1..62461c95d7b9 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -1056,7 +1056,7 @@ describe('SidebarUtils', () => { parentReportID: policyExpenseChat.reportID, parentReportActionID: lastReportPreviewAction.reportActionID, chatReportID: policyExpenseChat.reportID, - }; + } as Report; const iouAction = { actionName: CONST.REPORT.ACTIONS.TYPE.IOU, originalMessage: { From 3fea2cb0d6c41661556376ac378dea12ed6f69d5 Mon Sep 17 00:00:00 2001 From: Getabalew Date: Mon, 21 Jul 2025 14:55:17 +0300 Subject: [PATCH 02/19] fix: lint --- src/components/ButtonWithDropdownMenu/index.tsx | 6 +++--- src/components/KYCWall/BaseKYCWall.tsx | 2 +- src/components/MoneyReportHeader.tsx | 2 +- src/components/SettlementButton/index.tsx | 2 +- src/libs/actions/Policy/Policy.ts | 2 +- src/libs/actions/Search.ts | 11 +++++------ src/pages/settings/Wallet/WalletPage/WalletPage.tsx | 9 ++++++++- 7 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 73318dff4f4c..32bd0d08dce6 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -57,10 +57,10 @@ function ButtonWithDropdownMenuInner(props: ButtonWithDropdownMenuPr secondLineText = '', icon, shouldPopoverUseScrollView = false, - containerStyles, - shouldUseModalPaddingStyle = true, + containerStyles, + shouldUseModalPaddingStyle = true, shouldUseShortForm = false, - shouldUseOptionIcon = false, + shouldUseOptionIcon = false, } = props; const theme = useTheme(); diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index ca629fa3d384..d73ac55320f8 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -11,7 +11,7 @@ import getClickedTargetLocation from '@libs/getClickedTargetLocation'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import {hasExpensifyPaymentMethod} from '@libs/PaymentUtils'; -import {getPolicyExpenseChat, 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'; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index c6b383af04ee..414cfa97059d 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -18,6 +18,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import {deleteAppReport, downloadReportPDF, exportReportToCSV, exportReportToPDF, exportToIntegration, markAsManuallyExported, openUnreportedExpense} from '@libs/actions/Report'; +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'; @@ -115,7 +116,6 @@ import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu'; import {useSearchContext} from './Search/SearchContext'; import AnimatedSettlementButton from './SettlementButton/AnimatedSettlementButton'; import Text from './Text'; -import getPlatform from "@libs/getPlatform"; type MoneyReportHeaderProps = { /** The report currently being looked at */ diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index 42ac44b4d321..2b25a73fec5e 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -511,7 +511,7 @@ function SettlementButton({ return false; }); - const shouldUseSplitButton = hasPreferredPaymentMethod || !!lastPaymentPolicy || isExpenseReportUtil(iouReport) && hasIntentToPay; + const shouldUseSplitButton = hasPreferredPaymentMethod || !!lastPaymentPolicy || (isExpenseReportUtil(iouReport) && hasIntentToPay); const shouldLimitWidth = shouldUseShortForm && shouldUseSplitButton && !paymentButtonOptions.length; return ( diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 56d60d0776be..6a431c5c668d 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -502,7 +502,7 @@ function deleteWorkspace(policyID: string, policyName: string) { Object.keys(lastUsedPaymentMethods ?? {})?.forEach((paymentMethodKey) => { const lastUsedPaymentMethod = lastUsedPaymentMethods?.[paymentMethodKey]; - if(typeof lastUsedPaymentMethod === 'string' || !lastUsedPaymentMethod) { + if (typeof lastUsedPaymentMethod === 'string' || !lastUsedPaymentMethod) { return; } diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index ae78abf279b9..fd88c87c6941 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -15,7 +15,7 @@ import enhanceParameters from '@libs/Network/enhanceParameters'; import {rand64} from '@libs/NumberUtils'; import {getPersonalPolicy, getSubmitToAccountID, getValidConnectedIntegration} from '@libs/PolicyUtils'; import type {OptimisticExportIntegrationAction} from '@libs/ReportUtils'; -import {buildOptimisticExportIntegrationAction, hasHeldExpenses, isExpenseReport, isInvoiceReport, isIOUReport} from '@libs/ReportUtils'; +import {buildOptimisticExportIntegrationAction, hasHeldExpenses, isExpenseReport, isInvoiceReport, isIOUReport as isIOUReportUtil} from '@libs/ReportUtils'; import type {SuggestedSearchKey} from '@libs/SearchUIUtils'; import {isTransactionGroupListItemType, isTransactionListItemType} from '@libs/SearchUIUtils'; import playSound, {SOUNDS} from '@libs/Sound'; @@ -23,8 +23,8 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {FILTER_KEYS} from '@src/types/form/SearchAdvancedFiltersForm'; import type {SearchAdvancedFiltersForm} from '@src/types/form/SearchAdvancedFiltersForm'; -import type {PaymentInformation} from '@src/types/onyx/LastPaymentMethod'; import type {LastPaymentMethod, LastPaymentMethodType, Policy, SearchResults} 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'; @@ -88,7 +88,6 @@ function handleActionButtonPress( } } - function getLastPolicyBankAccountID(policyID: string | undefined, reportType: keyof LastPaymentMethodType = 'lastUsed'): number | undefined { if (!policyID) { return undefined; @@ -116,15 +115,15 @@ function getLastPolicyPaymentMethod( } function getReportType(reportID?: string) { - if(isIOUReport(reportID)) { + if (isIOUReportUtil(reportID)) { return CONST.REPORT.TYPE.IOU; } - if(isInvoiceReport(reportID)) { + if (isInvoiceReport(reportID)) { return CONST.REPORT.TYPE.INVOICE; } - if(isExpenseReport(reportID)) { + if (isExpenseReport(reportID)) { return CONST.REPORT.TYPE.EXPENSE; } diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index 786e4ec50818..7da6f0fcbdb4 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -300,7 +300,14 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { } else if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.DEBIT_CARD && fundID) { deletePaymentCard(fundID); } - }, [paymentMethod.selectedPaymentMethod.bankAccountID, paymentMethod.selectedPaymentMethod.fundID, paymentMethod.selectedPaymentMethodType, lastUsedPaymentMethods, 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 From cffc5e8f968b927bd0e7ab8290ef6ab05f8b51eb Mon Sep 17 00:00:00 2001 From: Getabalew Date: Mon, 21 Jul 2025 16:11:01 +0300 Subject: [PATCH 03/19] fix: more lint --- src/components/SettlementButton/index.tsx | 24 +++++------------------ 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index 2b25a73fec5e..79891b566a5e 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -1,6 +1,6 @@ import isEmpty from 'lodash/isEmpty'; import truncate from 'lodash/truncate'; -import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; +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'; @@ -25,7 +25,6 @@ import {hasRequestFromCurrentAccount} from '@libs/ReportActionsUtils'; import { doesReportBelongToWorkspace, getBankAccountRoute, - isBusinessInvoiceRoom, isExpenseReport as isExpenseReportUtil, isIndividualInvoiceRoom as isIndividualInvoiceRoomUtil, isInvoiceReport as isInvoiceReportUtil, @@ -106,7 +105,7 @@ function SettlementButton({ }); const lastBankAccountID = getLastPolicyBankAccountID(policyIDKey, iouReport?.type as keyof LastPaymentMethodType); - const [fundList = {}] = useOnyx(ONYXKEYS.FUND_LIST, {canBeMissing: true}); + const [fundList] = useOnyx(ONYXKEYS.FUND_LIST, {canBeMissing: true}); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const currentUserAccountID = getCurrentUserAccountID().toString(); @@ -118,14 +117,14 @@ function SettlementButton({ 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 [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 formattedPaymentMethods = formatPaymentMethods(bankAccountList ?? {}, fundList ?? {}, styles); const hasIntentToPay = ((formattedPaymentMethods.length === 1 && isIOUReport(iouReport)) || !!policy?.achAccount) && !lastPaymentMethod; useEffect(() => { @@ -165,18 +164,6 @@ function SettlementButton({ return formattedPaymentMethods.filter((ba) => (ba.accountData as AccountData)?.type === CONST.BANK_ACCOUNT.TYPE.PERSONAL); } - const getLastPaymentMethodType = () => { - if (isInvoiceReport) { - return CONST.LAST_PAYMENT_METHOD.INVOICE; - } - - if (policy) { - return CONST.LAST_PAYMENT_METHOD.EXPENSE; - } - - return CONST.LAST_PAYMENT_METHOD.IOU; - }; - const personalBankAccountList = getLatestPersonalBankAccount(); const latestBankItem = getLatestBankAccountItem(); @@ -278,7 +265,6 @@ function SettlementButton({ } if (isInvoiceReport) { - const formattedPaymentMethods = formatPaymentMethods(bankAccountList, fundList, styles); const isCurrencySupported = isCurrencySupportedForDirectReimbursement(currency as CurrencyType); const getPaymentSubitems = (payAsBusiness: boolean) => formattedPaymentMethods.map((formattedPaymentMethod) => ({ From 8fd601d02eadb96de7322baca51e553bb457447e Mon Sep 17 00:00:00 2001 From: Getabalew Date: Mon, 21 Jul 2025 16:53:59 +0300 Subject: [PATCH 04/19] fix: stale default payment method after workspace change --- src/components/SettlementButton/index.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index 79891b566a5e..266b71e226a0 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -98,11 +98,15 @@ function SettlementButton({ 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, lastPaymentMethodResult] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, { - canBeMissing: true, - selector: (paymentMethod) => getLastPolicyPaymentMethod(policyIDKey, paymentMethod, iouReport?.type as keyof LastPaymentMethodType, isIOUReport(iouReport)), - }); + const lastPaymentMethod = useMemo(() => { + if (!iouReport?.type) { + return; + } + + return getLastPolicyPaymentMethod(policyIDKey, lastPaymentMethods, iouReport?.type as keyof LastPaymentMethodType, isIOUReport(iouReport)); + }, [policyIDKey]); const lastBankAccountID = getLastPolicyBankAccountID(policyIDKey, iouReport?.type as keyof LastPaymentMethodType); const [fundList] = useOnyx(ONYXKEYS.FUND_LIST, {canBeMissing: true}); From 8ebd03ff883e1e00f68c52eac61a9d464441c9b0 Mon Sep 17 00:00:00 2001 From: Getabalew Date: Thu, 24 Jul 2025 20:20:09 +0300 Subject: [PATCH 05/19] fix: padding and add doc --- src/components/MoneyReportHeader.tsx | 2 +- src/styles/utils/index.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 414cfa97059d..bc6efe5abb26 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -962,7 +962,7 @@ function MoneyReportHeader({ options={applicableSecondaryActions} isSplitButton={false} wrapperStyle={shouldDisplayNarrowVersion && [!primaryAction && styles.flex1]} - shouldUseModalPaddingStyle={applicableSecondaryActions.length <= 5} + containerStyles={applicableSecondaryActions.length > 5 ? styles.settlementButtonListContainer : {}} /> )} diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 259a49becfcf..344867e2b1a3 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1217,6 +1217,11 @@ 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}; From 6ecce23d6322da1e3cc53c8689a5c763234f6409 Mon Sep 17 00:00:00 2001 From: Getabalew Date: Thu, 24 Jul 2025 22:45:14 +0300 Subject: [PATCH 06/19] fix: address comments and bugs --- src/components/KYCWall/BaseKYCWall.tsx | 10 ++- src/components/MoneyReportHeader.tsx | 4 +- src/components/PopoverMenu.tsx | 2 +- .../Search/ReportListItemHeader.tsx | 12 ++- src/components/SettlementButton/index.tsx | 5 +- src/libs/IOUUtils.ts | 23 +---- src/libs/actions/App.ts | 24 ++++- src/libs/actions/BankAccounts.ts | 18 ++-- src/libs/actions/IOU.ts | 18 ++-- src/libs/actions/Policy/Policy.ts | 41 ++++++--- .../resetUSDBankAccount.ts | 9 +- src/libs/actions/Search.ts | 21 +++-- .../BaseOnboardingAccounting.tsx | 13 ++- .../BaseOnboardingWorkspaceConfirmation.tsx | 9 +- .../USD/BankInfo/BankInfo.tsx | 2 + src/pages/Travel/TravelUpgrade.tsx | 9 +- .../request/step/IOURequestStepUpgrade.tsx | 7 +- .../workspace/WorkspaceConfirmationPage.tsx | 18 +++- src/pages/workspace/WorkspaceOverviewPage.tsx | 3 +- .../WorkspaceResetBankAccountModal.tsx | 7 +- src/pages/workspace/WorkspacesListPage.tsx | 3 +- tests/actions/IOUTest.ts | 36 +++++--- tests/actions/PolicyTest.ts | 87 ++++++++++++++++--- tests/unit/GoogleTagManagerTest.tsx | 6 +- 24 files changed, 283 insertions(+), 104 deletions(-) diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index d73ac55320f8..b1937b5c0299 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -65,6 +65,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) { @@ -119,11 +121,11 @@ function KYCWall({ const policyExpenseChatReportID = getPolicyExpenseChat(iouReport.ownerAccountID, policy.id)?.reportID; if (!policyExpenseChatReportID) { const {policyExpenseChatReportID: newPolicyExpenseChatReportID} = moveIOUReportToPolicyAndInviteSubmitter(iouReport.reportID, policy.id) ?? {}; - savePreferredPaymentMethod(iouReport.policyID, policy.id, CONST.LAST_PAYMENT_METHOD.IOU); + 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); + savePreferredPaymentMethod(iouReport.policyID, policy.id, CONST.LAST_PAYMENT_METHOD.IOU, lastPaymentMethod?.[policy.id]); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(policyExpenseChatReportID)); } @@ -136,8 +138,8 @@ function KYCWall({ } const {policyID, workspaceChatReportID, reportPreviewReportActionID, adminsChatReportID} = createWorkspaceFromIOUPayment(iouReport) ?? {}; - if (policyID) { - savePreferredPaymentMethod(iouReport.policyID, policyID, CONST.LAST_PAYMENT_METHOD.IOU); + 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) { diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index bc6efe5abb26..2c5a9bb72468 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -957,12 +957,12 @@ function MoneyReportHeader({ }} buttonRef={buttonRef} shouldAlwaysShowDropdownMenu - shouldPopoverUseScrollView={applicableSecondaryActions.length >= 5} + shouldPopoverUseScrollView={shouldDisplayNarrowVersion && applicableSecondaryActions.length >= 5} customText={translate('common.more')} options={applicableSecondaryActions} isSplitButton={false} wrapperStyle={shouldDisplayNarrowVersion && [!primaryAction && styles.flex1]} - containerStyles={applicableSecondaryActions.length > 5 ? styles.settlementButtonListContainer : {}} + shouldUseModalPaddingStyle /> )} diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index efaa5da05de1..e1820d6af168 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -324,7 +324,7 @@ function PopoverMenu({ }} wrapperStyle={[ StyleUtils.getItemBackgroundColorStyle(!!item.isSelected, focusedIndex === menuIndex, item.disabled ?? false, theme.activeComponentBG, theme.hoverComponentBG), - shouldUseScrollView && StyleUtils.getOptionMargin(menuIndex, currentMenuItems.length - 1), + shouldUseScrollView && !shouldUseModalPaddingStyle && StyleUtils.getOptionMargin(menuIndex, currentMenuItems.length - 1), ]} shouldRemoveHoverBackground={item.isSelected} titleStyle={StyleSheet.flatten([styles.flex1, item.titleStyle])} diff --git a/src/components/SelectionList/Search/ReportListItemHeader.tsx b/src/components/SelectionList/Search/ReportListItemHeader.tsx index 8294da996494..e3584779dcf2 100644 --- a/src/components/SelectionList/Search/ReportListItemHeader.tsx +++ b/src/components/SelectionList/Search/ReportListItemHeader.tsx @@ -7,6 +7,7 @@ import ReportSearchHeader from '@components/ReportSearchHeader'; import {useSearchContext} from '@components/Search/SearchContext'; import type {ListItem, TransactionReportGroupListItemType} from '@components/SelectionList/types'; import TextWithTooltip from '@components/TextWithTooltip'; +import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -14,6 +15,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import {handleActionButtonPress} from '@userActions/Search'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import ActionCell from './ActionCell'; import UserInfoAndActionButtonRow from './UserInfoAndActionButtonRow'; @@ -171,6 +173,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); @@ -179,7 +182,14 @@ function ReportListItemHeader({ theme.highlightBG; const handleOnButtonPress = () => { - handleActionButtonPress(currentSearchHash, reportItem, () => onSelectRow(reportItem as unknown as TItem), shouldUseNarrowLayout && !!canSelectMultiple, currentSearchKey); + handleActionButtonPress( + currentSearchHash, + reportItem, + () => onSelectRow(reportItem as unknown as TItem), + shouldUseNarrowLayout && !!canSelectMultiple, + currentSearchKey, + lastPaymentMethod, + ); }; return !isLargeScreenWidth ? ( diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index 266b71e226a0..5aed97e63b3f 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -106,9 +106,9 @@ function SettlementButton({ } return getLastPolicyPaymentMethod(policyIDKey, lastPaymentMethods, iouReport?.type as keyof LastPaymentMethodType, isIOUReport(iouReport)); - }, [policyIDKey]); + }, [policyIDKey, iouReport, lastPaymentMethods]); - const lastBankAccountID = getLastPolicyBankAccountID(policyIDKey, iouReport?.type as keyof LastPaymentMethodType); + 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(); @@ -545,6 +545,7 @@ function SettlementButton({ anchorAlignment={paymentMethodDropdownAnchorAlignment} enterKeyEventListenerPriority={enterKeyEventListenerPriority} useKeyboardShortcuts={useKeyboardShortcuts} + shouldUseModalPaddingStyle={paymentButtonOptions.length <= 5} /> )} diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index a9687da48c58..e7b50ef35c79 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -1,11 +1,10 @@ import Onyx from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {LastPaymentMethod, LastPaymentMethodType, OnyxInputOrEntry, PersonalDetails, Report} from '@src/types/onyx'; +import type {OnyxInputOrEntry, PersonalDetails, Report} from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; import type {IOURequestType} from './actions/IOU'; import {getCurrencyUnit} from './CurrencyUtils'; @@ -21,12 +20,6 @@ Onyx.connect({ callback: (val) => (lastLocationPermissionPrompt = val ?? ''), }); -let lastUsedPaymentMethods: OnyxEntry; -Onyx.connect({ - key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, - callback: (value) => (lastUsedPaymentMethods = value), -}); - function navigateToStartMoneyRequestStep(requestType: IOURequestType, iouType: IOUType, transactionID: string, reportID: string, iouAction?: IOUAction): void { if (iouAction === CONST.IOU.ACTION.CATEGORIZE || iouAction === CONST.IOU.ACTION.SUBMIT || iouAction === CONST.IOU.ACTION.SHARE) { Navigation.goBack(); @@ -223,18 +216,6 @@ function shouldStartLocationPermissionFlow() { ); } -function getLastUsedPaymentMethods() { - return lastUsedPaymentMethods; -} - -function getLastUsedPaymentMethod(policyID?: string): LastPaymentMethodType | undefined { - if (!policyID) { - return; - } - - return lastUsedPaymentMethods?.[policyID] as LastPaymentMethodType; -} - export { calculateAmount, insertTagIntoTransactionTagsString, @@ -247,6 +228,4 @@ export { formatCurrentUserToAttendee, shouldStartLocationPermissionFlow, navigateToParticipantPage, - getLastUsedPaymentMethods, - getLastUsedPaymentMethod, }; diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 93a49400f2b8..47e01f1e89d7 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -438,6 +438,7 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt( currency?: string, file?: File, routeToNavigateAfterCreate?: Route, + lastUsedPaymentMethod?: OnyxTypes.LastPaymentMethodType, ) { const policyIDWithDefault = policyID || generatePolicyID(); createDraftInitialWorkspace(policyOwnerEmail, policyName, policyIDWithDefault, makeMeAdmin, currency, file); @@ -448,7 +449,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); @@ -464,8 +465,25 @@ 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) { - createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID, CONST.ONBOARDING_CHOICES.MANAGE_TEAM, currency, file); +function savePolicyDraftByNewWorkspace( + policyID?: string, + policyName?: string, + policyOwnerEmail = '', + makeMeAdmin = false, + currency = '', + file?: File, + lastUsedPaymentMethod?: OnyxTypes.LastPaymentMethodType, +) { + createWorkspace({ + policyOwnerEmail, + makeMeAdmin, + policyName, + policyID, + engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + currency, + file, + lastUsedPaymentMethod, + }); } /** diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 646d125201f4..5c7166dd5ac8 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -18,7 +18,6 @@ import type {SaveCorpayOnboardingCompanyDetails} from '@libs/API/parameters/Save import type SaveCorpayOnboardingDirectorInformationParams from '@libs/API/parameters/SaveCorpayOnboardingDirectorInformationParams'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; -import {getLastUsedPaymentMethod} from '@libs/IOUUtils'; import {translateLocal} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import {getPersonalPolicy} from '@libs/PolicyUtils'; @@ -205,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, @@ -219,9 +218,9 @@ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAcc }; const onyxData = getVBBADataForOnyx(); - const lastUsedPaymentMethod = getLastUsedPaymentMethod(policyID); + const lastUsedPaymentMethod = typeof lastPaymentMethod === 'string' ? lastPaymentMethod : lastPaymentMethod?.expense?.name; - if (!lastUsedPaymentMethod?.expense?.name) { + if (!lastUsedPaymentMethod) { onyxData.successData?.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, @@ -231,7 +230,7 @@ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAcc name: CONST.IOU.PAYMENT_TYPE.VBBA, }, lastUsed: { - name: lastUsedPaymentMethod?.lastUsed?.name ?? CONST.IOU.PAYMENT_TYPE.VBBA, + name: lastUsedPaymentMethod ?? CONST.IOU.PAYMENT_TYPE.VBBA, }, }, }, @@ -246,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, @@ -265,7 +264,6 @@ function addPersonalBankAccount(account: PlaidBankAccount, policyID?: string, so } const personalPolicy = getPersonalPolicy(); - const lastUsedPaymentMethod = getLastUsedPaymentMethod(personalPolicy?.id); const onyxData: OnyxData = { optimisticData: [ @@ -309,7 +307,7 @@ function addPersonalBankAccount(account: PlaidBankAccount, policyID?: string, so ], }; - if (personalPolicy?.id && !lastUsedPaymentMethod) { + if (personalPolicy?.id && !lastPaymentMethod) { onyxData.optimisticData?.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, @@ -394,6 +392,10 @@ 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; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 740d0d676227..e08743925d0d 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -48,7 +48,6 @@ import GoogleTagManager from '@libs/GoogleTagManager'; import { calculateAmount as calculateIOUAmount, formatCurrentUserToAttendee, - getLastUsedPaymentMethod, isMovingTransactionFromTrackExpense as isMovingTransactionFromTrackExpenseIOUUtils, navigateToStartMoneyRequestStep, updateIOUOwnerAndTotal, @@ -8942,6 +8941,7 @@ function getPayMoneyRequestParams( 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 @@ -9083,7 +9083,6 @@ function getPayMoneyRequestParams( ); if (iouReport?.policyID) { - const lastUsedPaymentMethod = (getLastUsedPaymentMethod(iouReport.policyID) ?? {}) as OnyxTypes.LastPaymentMethodType; const prevLastUsedPaymentMethod = lastUsedPaymentMethod?.lastUsed?.name; const usedPaymentOption = paymentPolicyID ?? paymentMethodType; @@ -11159,15 +11158,24 @@ function checkIfScanFileCanBeRead( } /** Save the preferred payment method for a policy or personal DM */ -function savePreferredPaymentMethod(policyID: string | undefined, paymentMethod: string, type: ValueOf | undefined) { +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 - const prevPaymentMethod = (getLastUsedPaymentMethod(policyID) ?? {}) as OnyxTypes.LastPaymentMethodType; Onyx.merge(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, { - [policyID]: type ? {[type]: {name: paymentMethod}, [CONST.LAST_PAYMENT_METHOD.LAST_USED]: {name: prevPaymentMethod?.lastUsed?.name ?? paymentMethod}} : paymentMethod, + [policyID]: type + ? { + [type]: {name: paymentMethod}, + [CONST.LAST_PAYMENT_METHOD.LAST_USED]: {name: typeof prevPaymentMethod === 'string' ? prevPaymentMethod : (prevPaymentMethod?.lastUsed?.name ?? paymentMethod)}, + } + : paymentMethod, }); } diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 6a431c5c668d..1bd370f5e68b 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -67,7 +67,6 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import {createFile} from '@libs/fileDownload/FileUtils'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import GoogleTagManager from '@libs/GoogleTagManager'; -import {getLastUsedPaymentMethod, getLastUsedPaymentMethods} from '@libs/IOUUtils'; import {translate, translateLocal} from '@libs/Localize'; import Log from '@libs/Log'; import * as NetworkStore from '@libs/Network/NetworkStore'; @@ -89,6 +88,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type { IntroSelected, InvitedEmailsToAccountIDs, + LastPaymentMethod, + LastPaymentMethodType, PersonalDetailsList, Policy, PolicyCategory, @@ -150,8 +151,22 @@ type BuildPolicyDataOptions = { shouldAddOnboardingTasks?: boolean; companySize?: OnboardingCompanySize; userReportedIntegration?: OnboardingAccounting; + lastUsedPaymentMethod?: LastPaymentMethodType; }; +type CreateWorkspaceArguments = { + policyOwnerEmail?: string; + makeMeAdmin?: boolean; + policyName?: string; + policyID?: string; + engagementChoice?: OnboardingPurpose; + currency?: string; + file?: File; + shouldAddOnboardingTasks?: boolean; + companySize?: OnboardingCompanySize; + userReportedIntegration?: OnboardingAccounting; + lastUsedPaymentMethod?: LastPaymentMethodType; +}; const allPolicies: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY, @@ -337,7 +352,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; } @@ -498,7 +513,6 @@ function deleteWorkspace(policyID: string, policyName: string) { } }); - const lastUsedPaymentMethods = getLastUsedPaymentMethods(); Object.keys(lastUsedPaymentMethods ?? {})?.forEach((paymentMethodKey) => { const lastUsedPaymentMethod = lastUsedPaymentMethods?.[paymentMethodKey]; @@ -506,7 +520,7 @@ function deleteWorkspace(policyID: string, policyName: string) { return; } - if (lastUsedPaymentMethod.iou.name === policyID) { + if (lastUsedPaymentMethod?.iou?.name === policyID) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, @@ -1912,6 +1926,7 @@ function buildPolicyData(options: BuildPolicyDataOptions = {}) { shouldAddOnboardingTasks = true, companySize, userReportedIntegration, + lastUsedPaymentMethod, } = options; const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); @@ -2215,12 +2230,10 @@ function buildPolicyData(options: BuildPolicyDataOptions = {}) { successData.push(...optimisticCategoriesData.successData); } - if (getAdminPolicies().length === 0) { + if (getAdminPolicies().length === 0 && lastUsedPaymentMethod) { Object.values(allReports ?? {}) .filter((iouReport) => iouReport?.type === CONST.REPORT.TYPE.IOU) .forEach((iouReport) => { - const lastUsedPaymentMethod = getLastUsedPaymentMethod(iouReport?.policyID); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (lastUsedPaymentMethod?.iou?.name || !iouReport?.policyID) { return; @@ -2303,18 +2316,19 @@ function buildPolicyData(options: BuildPolicyDataOptions = {}) { return {successData, optimisticData, failureData, params}; } -function createWorkspace( +function createWorkspace({ policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), - engagementChoice: OnboardingPurpose = CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + engagementChoice = CONST.ONBOARDING_CHOICES.MANAGE_TEAM as OnboardingPurpose, currency = '', - file?: File, + file, shouldAddOnboardingTasks = true, - companySize?: OnboardingCompanySize, - userReportedIntegration?: OnboardingAccounting, -): CreateWorkspaceParams { + companySize, + userReportedIntegration, + lastUsedPaymentMethod, +}: CreateWorkspaceArguments): CreateWorkspaceParams { const {optimisticData, failureData, successData, params} = buildPolicyData({ policyOwnerEmail, makeMeAdmin, @@ -2326,6 +2340,7 @@ function createWorkspace( shouldAddOnboardingTasks, companySize, userReportedIntegration, + lastUsedPaymentMethod, }); API.write(WRITE_COMMANDS.CREATE_WORKSPACE, params, {optimisticData, successData, failureData}); diff --git a/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts b/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts index e88606bc1a80..0b6c4bd089f8 100644 --- a/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts +++ b/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts @@ -2,7 +2,6 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import {WRITE_COMMANDS} from '@libs/API/types'; -import {getLastUsedPaymentMethod} from '@libs/IOUUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; @@ -19,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'); } @@ -28,7 +32,6 @@ function resetUSDBankAccount(bankAccountID: number | undefined, session: OnyxEnt } const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? ({} as OnyxTypes.Policy); - const lastUsedPaymentMethod = getLastUsedPaymentMethod(policy.id); const isLastUsedPaymentMethodBBA = lastUsedPaymentMethod?.expense?.name === CONST.IOU.PAYMENT_TYPE.VBBA; const isPreviousLastUsedPaymentMethodBBA = lastUsedPaymentMethod?.lastUsed?.name === CONST.IOU.PAYMENT_TYPE.VBBA; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index fd88c87c6941..c87872c6a5a8 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -10,7 +10,6 @@ import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs import {getCommandURL} from '@libs/ApiUtils'; import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import fileDownload from '@libs/fileDownload'; -import {getLastUsedPaymentMethod, getLastUsedPaymentMethods} from '@libs/IOUUtils'; import enhanceParameters from '@libs/Network/enhanceParameters'; import {rand64} from '@libs/NumberUtils'; import {getPersonalPolicy, getSubmitToAccountID, getValidConnectedIntegration} from '@libs/PolicyUtils'; @@ -44,6 +43,7 @@ function handleActionButtonPress( goToItem: () => void, isInMobileSelectionMode: boolean, currentSearchKey?: SuggestedSearchKey, + lastPaymentMethods?: OnyxEntry, ) { // The transactionIDList is needed to handle actions taken on `status:""` where transactions on single expense reports can be approved/paid. // We need the transactionID to display the loading indicator for that list item's action. @@ -58,7 +58,7 @@ function handleActionButtonPress( switch (item.action) { case CONST.SEARCH.ACTION_TYPES.PAY: - getPayActionCallback(hash, item, goToItem, currentSearchKey); + getPayActionCallback(hash, item, goToItem, currentSearchKey, lastPaymentMethods); return; case CONST.SEARCH.ACTION_TYPES.APPROVE: approveMoneyRequestOnSearch(hash, [item.reportID], transactionID, currentSearchKey); @@ -88,11 +88,15 @@ function handleActionButtonPress( } } -function getLastPolicyBankAccountID(policyID: string | undefined, reportType: keyof LastPaymentMethodType = 'lastUsed'): number | undefined { +function getLastPolicyBankAccountID( + policyID: string | undefined, + lastPaymentMethods: OnyxEntry, + reportType: keyof LastPaymentMethodType = 'lastUsed', +): number | undefined { if (!policyID) { return undefined; } - const lastPolicyPaymentMethod = getLastUsedPaymentMethod(policyID); + const lastPolicyPaymentMethod = lastPaymentMethods?.[policyID]; return typeof lastPolicyPaymentMethod === 'string' ? undefined : (lastPolicyPaymentMethod?.[reportType] as PaymentInformation)?.bankAccountID; } @@ -130,8 +134,13 @@ function getReportType(reportID?: string) { return undefined; } -function getPayActionCallback(hash: number, item: TransactionListItemType | TransactionReportGroupListItemType, goToItem: () => void, currentSearchKey?: SuggestedSearchKey) { - const lastPaymentMethods = (getLastUsedPaymentMethods() ?? {}) as OnyxEntry; +function getPayActionCallback( + hash: number, + item: TransactionListItemType | TransactionReportGroupListItemType, + goToItem: () => void, + currentSearchKey?: SuggestedSearchKey, + lastPaymentMethods?: OnyxEntry, +) { const lastPolicyPaymentMethod = getLastPolicyPaymentMethod(item.policyID, lastPaymentMethods, getReportType(item.reportID)); if (!lastPolicyPaymentMethod || !Object.values(CONST.IOU.PAYMENT_TYPE).includes(lastPolicyPaymentMethod)) { diff --git a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx index 5b7098881d68..b0d66c969da3 100644 --- a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx +++ b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx @@ -210,7 +210,18 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount // We need `adminsChatReportID` for `completeOnboarding`, but at the same time, we don't want to call `createWorkspace` more than once. // If we have already created a workspace, we want to reuse the `onboardingAdminsChatReportID` and `onboardingPolicyID`. const {adminsChatReportID, policyID} = shouldCreateWorkspace - ? createWorkspace(undefined, true, '', generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM, '', undefined, false, onboardingCompanySize, userReportedIntegration) + ? createWorkspace({ + policyOwnerEmail: undefined, + makeMeAdmin: true, + policyName: '', + policyID: generatePolicyID(), + engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + currency: '', + file: undefined, + shouldAddOnboardingTasks: false, + companySize: onboardingCompanySize, + userReportedIntegration, + }) : {adminsChatReportID: onboardingAdminsChatReportID, policyID: onboardingPolicyID}; if (shouldCreateWorkspace) { diff --git a/src/pages/OnboardingWorkspaceConfirmation/BaseOnboardingWorkspaceConfirmation.tsx b/src/pages/OnboardingWorkspaceConfirmation/BaseOnboardingWorkspaceConfirmation.tsx index 47fe16ffa2f8..df05f3cb8373 100644 --- a/src/pages/OnboardingWorkspaceConfirmation/BaseOnboardingWorkspaceConfirmation.tsx +++ b/src/pages/OnboardingWorkspaceConfirmation/BaseOnboardingWorkspaceConfirmation.tsx @@ -63,7 +63,14 @@ function BaseOnboardingWorkspaceConfirmation({shouldUseNativeStyles}: BaseOnboar // We need `adminsChatReportID` for `completeOnboarding`, but at the same time, we don't want to call `createWorkspace` more than once. // If we have already created a workspace, we want to reuse the `onboardingAdminsChatReportID` and `onboardingPolicyID`. const {adminsChatReportID, policyID} = shouldCreateWorkspace - ? createWorkspace(undefined, true, name, generatePolicyID(), CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE, currency, undefined, false) + ? createWorkspace({ + makeMeAdmin: true, + policyName: name, + policyID: generatePolicyID(), + engagementChoice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE, + currency, + shouldAddOnboardingTasks: false, + }) : {adminsChatReportID: onboardingAdminsChatReportID, policyID: onboardingPolicyID}; if (shouldCreateWorkspace) { diff --git a/src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx b/src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx index d3423a6f030a..19bc513ba75e 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,6 +83,7 @@ function BankInfo({onBackButtonPress, policyID, setUSDBankAccountStep}: BankInfo [BANK_INFO_STEP_KEYS.IS_SAVINGS]: data[BANK_INFO_STEP_KEYS.IS_SAVINGS] ?? false, }, policyID, + lastPaymentMethod?.[policyID], ); } }, diff --git a/src/pages/Travel/TravelUpgrade.tsx b/src/pages/Travel/TravelUpgrade.tsx index af4554da5489..31e677cc9dc9 100644 --- a/src/pages/Travel/TravelUpgrade.tsx +++ b/src/pages/Travel/TravelUpgrade.tsx @@ -32,7 +32,14 @@ function TravelUpgrade({route}: TravelUpgradeProps) { createDraftWorkspace('', false, params.name, params.policyID, params.currency, params.avatarFile as File); setShouldShowConfirmation(false); setIsUpgraded(true); - createWorkspace('', false, params.name, params.policyID, undefined, params.currency, params.avatarFile as File); + createWorkspace({ + policyOwnerEmail: '', + makeMeAdmin: false, + policyName: params.name, + policyID: params.policyID, + currency: params.currency, + file: params.avatarFile as File, + }); }; const onClose = () => { diff --git a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx index 822245b93d67..6840103408d1 100644 --- a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx +++ b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx @@ -69,7 +69,12 @@ function IOURequestStepUpgrade({ { - const policyData = Policy.createWorkspace('', false, '', undefined, CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE); + const policyData = Policy.createWorkspace({ + policyOwnerEmail: '', + makeMeAdmin: false, + policyName: '', + engagementChoice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE, + }); setIsUpgraded(true); policyDataRef.current = policyData; }} 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 10b4713c3297..e8d33ebdecce 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,7 +183,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa return; } - deleteWorkspace(policy.id, policyName); + deleteWorkspace(policy.id, policyName, lastPaymentMethod); setIsDeleteModalOpen(false); if (!shouldUseNarrowLayout) { diff --git a/src/pages/workspace/WorkspaceResetBankAccountModal.tsx b/src/pages/workspace/WorkspaceResetBankAccountModal.tsx index 794de00ccda3..9201bcbac7f2 100644 --- a/src/pages/workspace/WorkspaceResetBankAccountModal.tsx +++ b/src/pages/workspace/WorkspaceResetBankAccountModal.tsx @@ -43,6 +43,11 @@ function WorkspaceResetBankAccountModal({ 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 d592fdb1e88a..0f28094c09c4 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -117,6 +117,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>(); @@ -158,7 +159,7 @@ function WorkspacesListPage() { return; } - deleteWorkspace(policyIDToDelete, policyNameToDelete); + deleteWorkspace(policyIDToDelete, policyNameToDelete, lastPaymentMethod); setIsDeleteModalOpen(false); }; diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 7742f7629b60..085022887aa5 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -2652,7 +2652,7 @@ describe('actions/IOU', () => { Onyx.set(ONYXKEYS.SESSION, {email: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}); return waitForBatchedUpdates() .then(() => { - createWorkspace(CARLOS_EMAIL, true, "Carlos's Workspace"); + createWorkspace({policyOwnerEmail: CARLOS_EMAIL, makeMeAdmin: true, policyName: "Carlos's Workspace"}); return waitForBatchedUpdates(); }) .then( @@ -2780,7 +2780,7 @@ describe('actions/IOU', () => { Onyx.set(ONYXKEYS.SESSION, {email: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}); return waitForBatchedUpdates() .then(() => { - createWorkspace(CARLOS_EMAIL, true, "Carlos's Workspace"); + createWorkspace({policyOwnerEmail: CARLOS_EMAIL, makeMeAdmin: true, policyName: "Carlos's Workspace"}); return waitForBatchedUpdates(); }) .then( @@ -3005,7 +3005,7 @@ describe('actions/IOU', () => { return waitForBatchedUpdates() .then(() => { // Which owns a workspace - createWorkspace(CARLOS_EMAIL, true, "Carlos's Workspace"); + createWorkspace({policyOwnerEmail: CARLOS_EMAIL, makeMeAdmin: true, policyName: "Carlos's Workspace"}); return waitForBatchedUpdates(); }) .then(() => @@ -4038,8 +4038,7 @@ describe('actions/IOU', () => { return waitForBatchedUpdates() .then(() => { const policyID = generatePolicyID(); - createWorkspace(CARLOS_EMAIL, true, "Carlos's Workspace", policyID); - + createWorkspace({policyOwnerEmail: CARLOS_EMAIL, makeMeAdmin: true, policyName: "Carlos's Workspace", policyID}); // Change the approval mode for the policy since default is Submit and Close setWorkspaceApprovalMode(policyID, CARLOS_EMAIL, CONST.POLICY.APPROVAL_MODE.BASIC); return waitForBatchedUpdates(); @@ -4150,7 +4149,12 @@ describe('actions/IOU', () => { return waitForBatchedUpdates() .then(() => { - createWorkspace(CARLOS_EMAIL, true, "Carlos's Workspace", undefined, CONST.ONBOARDING_CHOICES.CHAT_SPLIT); + createWorkspace({ + policyOwnerEmail: CARLOS_EMAIL, + makeMeAdmin: true, + policyName: "Carlos's Workspace", + engagementChoice: CONST.ONBOARDING_CHOICES.CHAT_SPLIT, + }); return waitForBatchedUpdates(); }) .then( @@ -4315,7 +4319,12 @@ describe('actions/IOU', () => { return waitForBatchedUpdates() .then(() => { - createWorkspace(CARLOS_EMAIL, true, "Carlos's Workspace", undefined, CONST.ONBOARDING_CHOICES.CHAT_SPLIT); + createWorkspace({ + policyOwnerEmail: CARLOS_EMAIL, + makeMeAdmin: true, + policyName: "Carlos's Workspace", + engagementChoice: CONST.ONBOARDING_CHOICES.CHAT_SPLIT, + }); return waitForBatchedUpdates(); }) .then( @@ -5828,7 +5837,7 @@ describe('actions/IOU', () => { Onyx.set(ONYXKEYS.SESSION, {email: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}); // Which owns a workspace await waitForBatchedUpdates(); - createWorkspace(CARLOS_EMAIL, true, "Carlos's Workspace"); + createWorkspace({policyOwnerEmail: CARLOS_EMAIL, makeMeAdmin: true, policyName: "Carlos's Workspace"}); await waitForBatchedUpdates(); // Get the policy expense chat report @@ -5908,7 +5917,7 @@ describe('actions/IOU', () => { Onyx.set(ONYXKEYS.SESSION, {email: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}); // Which owns a workspace await waitForBatchedUpdates(); - createWorkspace(CARLOS_EMAIL, true, "Carlos's Workspace"); + createWorkspace({policyOwnerEmail: CARLOS_EMAIL, makeMeAdmin: true, policyName: "Carlos's Workspace"}); await waitForBatchedUpdates(); // Get the policy expense chat report @@ -6271,7 +6280,7 @@ describe('actions/IOU', () => { const creatorPersonalDetails = personalDetailsList?.[CARLOS_ACCOUNT_ID] ?? {accountID: CARLOS_ACCOUNT_ID}; const policyID = generatePolicyID(); - createWorkspace(CARLOS_EMAIL, true, "Carlos's Workspace", policyID); + createWorkspace({policyOwnerEmail: CARLOS_EMAIL, makeMeAdmin: true, policyName: "Carlos's Workspace", policyID}); createNewReport(creatorPersonalDetails, policyID); // Create a tracked expense const selfDMReport: Report = { @@ -6378,7 +6387,12 @@ describe('actions/IOU', () => { let originalTransactionID; const policyID = generatePolicyID(); - createWorkspace(CARLOS_EMAIL, true, "Carlos's Workspace", policyID); + createWorkspace({ + policyOwnerEmail: CARLOS_EMAIL, + makeMeAdmin: true, + policyName: "Carlos's Workspace", + policyID, + }); // Change the approval mode for the policy since default is Submit and Close setWorkspaceApprovalMode(policyID, CARLOS_EMAIL, CONST.POLICY.APPROVAL_MODE.BASIC); diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index d8187cdd9279..bd4c3418c0c3 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -60,7 +60,13 @@ describe('actions/Policy', () => { let expenseReportID; const policyID = Policy.generatePolicyID(); - Policy.createWorkspace(ESH_EMAIL, true, WORKSPACE_NAME, policyID, CONST.ONBOARDING_CHOICES.MANAGE_TEAM); + Policy.createWorkspace({ + policyOwnerEmail: ESH_EMAIL, + makeMeAdmin: true, + policyName: WORKSPACE_NAME, + policyID, + engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + }); await waitForBatchedUpdates(); let policy: OnyxEntry | OnyxCollection = await new Promise((resolve) => { @@ -246,7 +252,13 @@ describe('actions/Policy', () => { it('creates a new workspace with BASIC approval mode if the introSelected is MANAGE_TEAM', async () => { const policyID = Policy.generatePolicyID(); // When a new workspace is created with introSelected set to MANAGE_TEAM - Policy.createWorkspace(ESH_EMAIL, true, WORKSPACE_NAME, policyID, CONST.ONBOARDING_CHOICES.MANAGE_TEAM); + Policy.createWorkspace({ + policyOwnerEmail: ESH_EMAIL, + makeMeAdmin: true, + policyName: WORKSPACE_NAME, + policyID, + engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + }); await waitForBatchedUpdates(); const policy: OnyxEntry | OnyxCollection = await new Promise((resolve) => { @@ -266,7 +278,13 @@ describe('actions/Policy', () => { it('creates a new workspace with OPTIONAL approval mode if the introSelected is TRACK_WORKSPACE', async () => { const policyID = Policy.generatePolicyID(); // When a new workspace is created with introSelected set to TRACK_WORKSPACE - Policy.createWorkspace(ESH_EMAIL, true, WORKSPACE_NAME, policyID, CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE); + Policy.createWorkspace({ + policyOwnerEmail: ESH_EMAIL, + makeMeAdmin: true, + policyName: WORKSPACE_NAME, + policyID, + engagementChoice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE, + }); await waitForBatchedUpdates(); const policy: OnyxEntry | OnyxCollection = await new Promise((resolve) => { @@ -290,7 +308,12 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); (fetch as MockFetch)?.fail?.(); - Policy.createWorkspace(ESH_EMAIL, true, WORKSPACE_NAME, undefined, CONST.ONBOARDING_CHOICES.LOOKING_AROUND); + Policy.createWorkspace({ + policyOwnerEmail: ESH_EMAIL, + makeMeAdmin: true, + policyName: WORKSPACE_NAME, + engagementChoice: CONST.ONBOARDING_CHOICES.LOOKING_AROUND, + }); await waitForBatchedUpdates(); (fetch as MockFetch)?.resume?.(); @@ -310,7 +333,13 @@ describe('actions/Policy', () => { it('create a new workspace with delayed submission set to manually if the onboarding choice is newDotManageTeam or newDotLookingAround', async () => { const policyID = Policy.generatePolicyID(); // When a new workspace is created with introSelected set to MANAGE_TEAM - Policy.createWorkspace(ESH_EMAIL, true, WORKSPACE_NAME, policyID, CONST.ONBOARDING_CHOICES.MANAGE_TEAM); + Policy.createWorkspace({ + policyOwnerEmail: ESH_EMAIL, + makeMeAdmin: true, + policyName: WORKSPACE_NAME, + policyID, + engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + }); await waitForBatchedUpdates(); await TestHelper.getOnyxData({ @@ -327,7 +356,12 @@ describe('actions/Policy', () => { it('create a new workspace with delayed submission set to manually if the onboarding choice is not selected', async () => { const policyID = Policy.generatePolicyID(); - Policy.createWorkspace(ESH_EMAIL, true, WORKSPACE_NAME, policyID, undefined); + Policy.createWorkspace({ + policyOwnerEmail: ESH_EMAIL, + makeMeAdmin: true, + policyName: WORKSPACE_NAME, + policyID, + }); await waitForBatchedUpdates(); await TestHelper.getOnyxData({ @@ -345,7 +379,13 @@ describe('actions/Policy', () => { it('create a new workspace with enabled workflows if the onboarding choice is newDotManageTeam', async () => { const policyID = Policy.generatePolicyID(); // When a new workspace is created with introSelected set to MANAGE_TEAM - Policy.createWorkspace(ESH_EMAIL, true, WORKSPACE_NAME, policyID, CONST.ONBOARDING_CHOICES.MANAGE_TEAM); + Policy.createWorkspace({ + policyOwnerEmail: ESH_EMAIL, + makeMeAdmin: true, + policyName: WORKSPACE_NAME, + policyID, + engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + }); await waitForBatchedUpdates(); await TestHelper.getOnyxData({ @@ -361,7 +401,13 @@ describe('actions/Policy', () => { it('create a new workspace with enabled workflows if the onboarding choice is newDotLookingAround', async () => { const policyID = Policy.generatePolicyID(); // When a new workspace is created with introSelected set to LOOKING_AROUND - Policy.createWorkspace(ESH_EMAIL, true, WORKSPACE_NAME, policyID, CONST.ONBOARDING_CHOICES.LOOKING_AROUND); + Policy.createWorkspace({ + policyOwnerEmail: ESH_EMAIL, + makeMeAdmin: true, + policyName: WORKSPACE_NAME, + policyID, + engagementChoice: CONST.ONBOARDING_CHOICES.LOOKING_AROUND, + }); await waitForBatchedUpdates(); await TestHelper.getOnyxData({ @@ -377,7 +423,13 @@ describe('actions/Policy', () => { it('create a new workspace with enabled workflows if the onboarding choice is newDotTrackWorkspace', async () => { const policyID = Policy.generatePolicyID(); // When a new workspace is created with introSelected set to TRACK_WORKSPACE - Policy.createWorkspace(ESH_EMAIL, true, WORKSPACE_NAME, policyID, CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE); + Policy.createWorkspace({ + policyOwnerEmail: ESH_EMAIL, + makeMeAdmin: true, + policyName: WORKSPACE_NAME, + policyID, + engagementChoice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE, + }); await waitForBatchedUpdates(); await TestHelper.getOnyxData({ @@ -393,7 +445,13 @@ describe('actions/Policy', () => { it('create a new workspace with disabled workflows if the onboarding choice is newDotEmployer', async () => { const policyID = Policy.generatePolicyID(); // When a new workspace is created with introSelected set to EMPLOYER - Policy.createWorkspace(ESH_EMAIL, true, WORKSPACE_NAME, policyID, CONST.ONBOARDING_CHOICES.EMPLOYER); + Policy.createWorkspace({ + policyOwnerEmail: ESH_EMAIL, + makeMeAdmin: true, + policyName: WORKSPACE_NAME, + policyID, + engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER, + }); await waitForBatchedUpdates(); await TestHelper.getOnyxData({ @@ -409,7 +467,14 @@ describe('actions/Policy', () => { it('create a new workspace with disabled workflows if the onboarding choice is newDotSplitChat', async () => { const policyID = Policy.generatePolicyID(); // When a new workspace is created with introSelected set to CHAT_SPLIT - Policy.createWorkspace(ESH_EMAIL, true, WORKSPACE_NAME, policyID, CONST.ONBOARDING_CHOICES.CHAT_SPLIT); + Policy.createWorkspace({ + policyOwnerEmail: ESH_EMAIL, + makeMeAdmin: true, + policyName: WORKSPACE_NAME, + policyID, + engagementChoice: CONST.ONBOARDING_CHOICES.CHAT_SPLIT, + }); + await waitForBatchedUpdates(); await TestHelper.getOnyxData({ 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); From af68fdff7c34efde5b15bf725087dafa8aa25d40 Mon Sep 17 00:00:00 2001 From: Getabalew Date: Thu, 24 Jul 2025 22:54:18 +0300 Subject: [PATCH 07/19] fix: lint --- src/languages/de.ts | 2 +- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/languages/fr.ts | 2 +- src/languages/it.ts | 2 +- src/languages/ja.ts | 2 +- src/languages/nl.ts | 2 +- src/languages/pl.ts | 2 +- src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 2 +- .../BaseOnboardingInterestedFeatures.tsx | 12 +++++++++++- 11 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 9289fe7b361e..83fc61823295 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -35,8 +35,8 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, - BeginningOfArchivedRoomParams, BankAccountLastFourParams, + BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, BeginningOfChatHistoryDomainRoomParams, diff --git a/src/languages/en.ts b/src/languages/en.ts index ad6083c0a33f..43fc5b2b7076 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -23,8 +23,8 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, - BeginningOfArchivedRoomParams, BankAccountLastFourParams, + BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, BeginningOfChatHistoryDomainRoomParams, diff --git a/src/languages/es.ts b/src/languages/es.ts index fd156153a7ea..ec217bd25cec 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -22,8 +22,8 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, - BeginningOfArchivedRoomParams, BankAccountLastFourParams, + BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, BeginningOfChatHistoryDomainRoomParams, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index c34267423b11..16fda528e8b4 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -35,8 +35,8 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, - BeginningOfArchivedRoomParams, BankAccountLastFourParams, + BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, BeginningOfChatHistoryDomainRoomParams, diff --git a/src/languages/it.ts b/src/languages/it.ts index 9df68ef16ee0..d3aec286620d 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -35,8 +35,8 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, - BeginningOfArchivedRoomParams, BankAccountLastFourParams, + BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, BeginningOfChatHistoryDomainRoomParams, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index ef89dec714e3..1441711bbdaf 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -35,8 +35,8 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, - BeginningOfArchivedRoomParams, BankAccountLastFourParams, + BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, BeginningOfChatHistoryDomainRoomParams, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 6384d5c4e905..7baf95cebc4e 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -35,8 +35,8 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, - BeginningOfArchivedRoomParams, BankAccountLastFourParams, + BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, BeginningOfChatHistoryDomainRoomParams, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index ade50ed1c71f..06d386419b8b 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -35,8 +35,8 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, - BeginningOfArchivedRoomParams, BankAccountLastFourParams, + BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, BeginningOfChatHistoryDomainRoomParams, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 31bf4bb00091..d438b5e1bd57 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -35,8 +35,8 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, - BeginningOfArchivedRoomParams, BankAccountLastFourParams, + BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, BeginningOfChatHistoryDomainRoomParams, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 065e3f252b6c..370b64021da4 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -35,8 +35,8 @@ import type { AuthenticationErrorParams, AutoPayApprovedReportsLimitErrorParams, BadgeFreeTrialParams, - BeginningOfArchivedRoomParams, BankAccountLastFourParams, + BeginningOfArchivedRoomParams, BeginningOfChatHistoryAdminRoomParams, BeginningOfChatHistoryAnnounceRoomParams, BeginningOfChatHistoryDomainRoomParams, diff --git a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx index f51bca6d69cb..51a8f6709fdb 100644 --- a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx +++ b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx @@ -174,7 +174,17 @@ function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardin // We need `adminsChatReportID` for `completeOnboarding`, but at the same time, we don't want to call `createWorkspace` more than once. // If we have already created a workspace, we want to reuse the `onboardingAdminsChatReportID` and `onboardingPolicyID`. const {adminsChatReportID, policyID} = shouldCreateWorkspace - ? createWorkspace(undefined, true, '', generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM, '', undefined, false, onboardingCompanySize, newUserReportedIntegration) + ? createWorkspace({ + policyOwnerEmail: undefined, + makeMeAdmin: true, + policyName: '', + policyID: generatePolicyID(), + engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + currency: '', + shouldAddOnboardingTasks: false, + companySize: onboardingCompanySize, + userReportedIntegration: newUserReportedIntegration, + }) : {adminsChatReportID: onboardingAdminsChatReportID, policyID: onboardingPolicyID}; if (policyID) { From 4e6aa8461ef0627238c8bcaa4cc943599677daa9 Mon Sep 17 00:00:00 2001 From: Getabalew Date: Thu, 24 Jul 2025 23:00:31 +0300 Subject: [PATCH 08/19] fix: lint --- src/pages/workspace/WorkspaceResetBankAccountModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceResetBankAccountModal.tsx b/src/pages/workspace/WorkspaceResetBankAccountModal.tsx index 9201bcbac7f2..2750f8bd9271 100644 --- a/src/pages/workspace/WorkspaceResetBankAccountModal.tsx +++ b/src/pages/workspace/WorkspaceResetBankAccountModal.tsx @@ -36,7 +36,7 @@ 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; From 17419c91510967fe40d8ae6cac5599b93d415635 Mon Sep 17 00:00:00 2001 From: Getabalew Date: Tue, 29 Jul 2025 17:05:34 +0300 Subject: [PATCH 09/19] update type description --- src/types/onyx/LastPaymentMethod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/LastPaymentMethod.ts b/src/types/onyx/LastPaymentMethod.ts index 0338c7c6a7ef..4b856daf3510 100644 --- a/src/types/onyx/LastPaymentMethod.ts +++ b/src/types/onyx/LastPaymentMethod.ts @@ -2,7 +2,7 @@ * PaymentInformation object */ type PaymentInformation = { - /** The name of the */ + /** 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; From db6eb4b6c2fff1fa6dc7326a17ffb690c47e8700 Mon Sep 17 00:00:00 2001 From: Getabalew Date: Wed, 30 Jul 2025 16:32:56 +0300 Subject: [PATCH 10/19] address comments --- src/libs/actions/BankAccounts.ts | 28 ++++++++++++++++++++++------ src/libs/actions/Policy/Policy.ts | 15 +++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 62fc512430b7..471924ea7b65 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -396,14 +396,18 @@ function deletePaymentBankAccount(bankAccountID: number, lastUsedPaymentMethods? 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; + 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]: revertedLastUsedPaymentMethod, + [personalPolicy?.id]: { + iou: { + name: revertedLastUsedPaymentMethod + } + }, }, }); @@ -411,7 +415,11 @@ function deletePaymentBankAccount(bankAccountID: number, lastUsedPaymentMethods? onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, value: { - [personalPolicy?.id]: lastUsedPaymentMethod.iou.name, + [personalPolicy?.id]: { + iou: { + name: lastUsedPaymentMethod.iou.name + } + }, }, }); } @@ -423,7 +431,11 @@ function deletePaymentBankAccount(bankAccountID: number, lastUsedPaymentMethods? onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, value: { - [paymentMethodID]: revertedLastUsedPaymentMethod, + [paymentMethodID]: { + expense: { + name: revertedLastUsedPaymentMethod + } + }, }, }); @@ -431,7 +443,11 @@ function deletePaymentBankAccount(bankAccountID: number, lastUsedPaymentMethods? onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, value: { - [paymentMethodID]: lastUsedPaymentMethod.expense.name, + [paymentMethodID]: { + expense: { + name: lastUsedPaymentMethod.expense?.name + } + }, }, }); } diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 90bc00ce2dc1..b2a5dce64396 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -538,6 +538,21 @@ function deleteWorkspace(policyID: string, policyName: string, lastUsedPaymentMe }, }, }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + value: { + [paymentMethodKey]: { + iou: { + name: lastUsedPaymentMethod?.iou?.name, + }, + lastUsed: { + name: lastUsedPaymentMethod?.iou?.name, + }, + }, + }, + }); } }); From 4b03b9f2e202491a9bf5d2b43c09727a3d7e3daa Mon Sep 17 00:00:00 2001 From: Getabalew Date: Thu, 31 Jul 2025 16:00:20 +0300 Subject: [PATCH 11/19] fix: show amount on pay button and show correct payment options --- .../MoneyRequestReportPreviewContent.tsx | 6 +++++- src/components/SettlementButton/index.tsx | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index 3fd942bbba81..81167f4a8344 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -74,6 +74,7 @@ import type {Transaction} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import EmptyMoneyRequestReportPreview from './EmptyMoneyRequestReportPreview'; import type {MoneyRequestReportPreviewContentProps} from './types'; +import { getTotalAmountForIOUReportPreviewButton } from "@libs/MoneyRequestReportUtils"; function MoneyRequestReportPreviewContent({ iouReportID, @@ -474,7 +475,8 @@ 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]: (