From 67e37b9f54dc4752f45a6ee65af4b1600d50af34 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 12 Aug 2024 11:44:24 +0200 Subject: [PATCH 01/16] create raw WorkspaceInvoiceVBASection --- .../invoices/WorkspaceInvoiceVBASection.tsx | 46 +++++++++++++++++++ .../invoices/WorkspaceInvoicesPage.tsx | 2 + 2 files changed, 48 insertions(+) create mode 100644 src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx diff --git a/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx b/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx new file mode 100644 index 000000000000..1ff2c7e85856 --- /dev/null +++ b/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import {useOnyx} from 'react-native-onyx'; +import Section from '@components/Section'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type WorkspaceInvoiceVBASectionProps = { + /** The policy ID currently being configured */ + policyID: string; +}; + +function WorkspaceInvoiceVBASection({policyID}: WorkspaceInvoiceVBASectionProps) { + const styles = useThemeStyles(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {translate} = useLocalize(); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + + return ( +
+ console.debug('onPress')} + // actionPaymentMethodType={shouldShowDefaultDeleteMenu ? paymentMethod.selectedPaymentMethodType : ''} + activePaymentMethodID={policy?.invoice?.bankAccount?.transferBankAccountID ?? ''} + // buttonRef={addPaymentMethodAnchorRef} + // onListContentSizeChange={shouldShowAddPaymentMenu || shouldShowDefaultDeleteMenu ? setMenuPosition : () => {}} + shouldEnableScroll={false} + style={[styles.mt5, [shouldUseNarrowLayout ? styles.mhn5 : styles.mhn8]]} + listItemStyle={shouldUseNarrowLayout ? styles.ph5 : styles.ph8} + /> +
+ ); +} + +WorkspaceInvoiceVBASection.displayName = 'WorkspaceInvoiceVBASection'; + +export default WorkspaceInvoiceVBASection; diff --git a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx index 40a8239b9ab1..2946f918c043 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx @@ -10,6 +10,7 @@ import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; import WorkspaceInvoicesNoVBAView from './WorkspaceInvoicesNoVBAView'; import WorkspaceInvoicesVBAView from './WorkspaceInvoicesVBAView'; +import WorkspaceInvoiceVBASection from './WorkspaceInvoiceVBASection'; type WorkspaceInvoicesPageProps = StackScreenProps; @@ -28,6 +29,7 @@ function WorkspaceInvoicesPage({route}: WorkspaceInvoicesPageProps) { > {(hasVBA?: boolean, policyID?: string) => ( + {policyID && } {!hasVBA && policyID && } {hasVBA && policyID && } From c2b9af42c3c90bab1969a1e747ba3960aee666b0 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 12 Aug 2024 14:02:20 +0200 Subject: [PATCH 02/16] add invoice types --- src/types/onyx/Policy.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 9724229361ec..5512b7a5b56f 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1299,6 +1299,15 @@ type PolicyInvoicingDetails = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Stripe Connect company website */ companyWebsite?: string; + + /** Back account */ + bankAccount?: { + /** Account balance */ + stripeConnectAccountBalance?: number; + + /** bankAccountID of selected BBA for payouts */ + transferBankAccountID?: number; + }; }>; /** Names of policy features */ From be904818353197e5f303ac1d9ebdef7da3bf9c9a Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 12 Aug 2024 16:21:45 +0200 Subject: [PATCH 03/16] integrate bank accounts logic --- .../invoices/WorkspaceInvoiceVBASection.tsx | 294 +++++++++++++++++- 1 file changed, 289 insertions(+), 5 deletions(-) diff --git a/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx b/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx index 1ff2c7e85856..828c562ac71d 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx @@ -1,11 +1,46 @@ -import React from 'react'; +import type {RefObject} from 'react'; +import React, {useCallback, useRef, useState} from 'react'; +import {View} from 'react-native'; +import type {GestureResponderEvent} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; +import ConfirmModal from '@components/ConfirmModal'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; +import Popover from '@components/Popover'; import Section from '@components/Section'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import getClickedTargetLocation from '@libs/getClickedTargetLocation'; +import Navigation from '@libs/Navigation/Navigation'; +import * as PaymentUtils from '@libs/PaymentUtils'; import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList'; +import type {FormattedSelectedPaymentMethodIcon} from '@pages/settings/Wallet/WalletPage/types'; +import variables from '@styles/variables'; +import * as BankAccounts from '@userActions/BankAccounts'; +import * as PaymentMethods from '@userActions/PaymentMethods'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {AccountData} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type FormattedSelectedPaymentMethod = { + title: string; + icon?: FormattedSelectedPaymentMethodIcon; + description?: string; + type?: string; +}; + +type PaymentMethodState = { + isSelectedPaymentMethodDefault: boolean; + selectedPaymentMethod: AccountData; + formattedSelectedPaymentMethod: FormattedSelectedPaymentMethod; + methodID: string | number; + selectedPaymentMethodType: string; +}; type WorkspaceInvoiceVBASectionProps = { /** The policy ID currently being configured */ @@ -15,8 +50,186 @@ type WorkspaceInvoiceVBASectionProps = { function WorkspaceInvoiceVBASection({policyID}: WorkspaceInvoiceVBASectionProps) { const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {windowWidth} = useWindowDimensions(); const {translate} = useLocalize(); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST); + const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET); + const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); + const addPaymentMethodAnchorRef = useRef(null); + const paymentMethodButtonRef = useRef(null); + const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false); + const [showConfirmDeleteModal, setShowConfirmDeleteModal] = useState(false); + const [shouldShowDefaultDeleteMenu, setShouldShowDefaultDeleteMenu] = useState(false); + const [paymentMethod, setPaymentMethod] = useState({ + isSelectedPaymentMethodDefault: false, + selectedPaymentMethod: {}, + formattedSelectedPaymentMethod: { + title: '', + }, + methodID: '', + selectedPaymentMethodType: '', + }); + const [anchorPosition, setAnchorPosition] = useState({ + anchorPositionHorizontal: 0, + anchorPositionVertical: 0, + anchorPositionTop: 0, + anchorPositionRight: 0, + }); + const hasBankAccount = !isEmptyObject(bankAccountList) || !isEmptyObject(fundList); + const hasWallet = !isEmptyObject(userWallet); + const hasAssignedCard = !isEmptyObject(cardList); + const shouldShowEmptyState = !hasBankAccount && !hasWallet && !hasAssignedCard; + // Determines whether or not the modal popup is mounted from the bottom of the screen instead of the side mount on Web or Desktop screens + const isPopoverBottomMount = anchorPosition.anchorPositionTop === 0 || shouldUseNarrowLayout; + const shouldShowMakeDefaultButton = + !paymentMethod.isSelectedPaymentMethodDefault && + !(paymentMethod.formattedSelectedPaymentMethod.type === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT && paymentMethod.selectedPaymentMethod.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS); + + /** + * Set position of the payment menu + */ + const setMenuPosition = useCallback(() => { + if (!paymentMethodButtonRef.current) { + return; + } + + const position = getClickedTargetLocation(paymentMethodButtonRef.current); + + setAnchorPosition({ + anchorPositionTop: position.top + position.height - variables.bankAccountActionPopoverTopSpacing, + // We want the position to be 23px to the right of the left border + anchorPositionRight: windowWidth - position.right + variables.bankAccountActionPopoverRightSpacing, + anchorPositionHorizontal: position.x + (shouldShowEmptyState ? -variables.addPaymentMethodLeftSpacing : variables.addBankAccountLeftSpacing), + anchorPositionVertical: position.y, + }); + }, [shouldShowEmptyState, windowWidth]); + + /** + * Display the delete/default menu, or the add payment method menu + */ + const paymentMethodPressed = ( + nativeEvent?: GestureResponderEvent | KeyboardEvent, + accountType?: string, + account?: AccountData, + icon?: FormattedSelectedPaymentMethodIcon, + isDefault?: boolean, + methodID?: string | number, + ) => { + if (shouldShowAddPaymentMenu) { + setShouldShowAddPaymentMenu(false); + return; + } + + if (shouldShowDefaultDeleteMenu) { + setShouldShowDefaultDeleteMenu(false); + return; + } + paymentMethodButtonRef.current = nativeEvent?.currentTarget as HTMLDivElement; + + // The delete/default menu + if (accountType) { + let formattedSelectedPaymentMethod: FormattedSelectedPaymentMethod = { + title: '', + }; + if (accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { + formattedSelectedPaymentMethod = { + title: account?.addressName ?? '', + icon, + description: PaymentUtils.getPaymentMethodDescription(accountType, account), + type: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, + }; + } + setPaymentMethod({ + isSelectedPaymentMethodDefault: !!isDefault, + selectedPaymentMethod: account ?? {}, + selectedPaymentMethodType: accountType, + formattedSelectedPaymentMethod, + methodID: methodID ?? '-1', + }); + setShouldShowDefaultDeleteMenu(true); + setMenuPosition(); + return; + } + setShouldShowAddPaymentMenu(true); + setMenuPosition(); + }; + + /** + * Hide the add payment modal + */ + const hideAddPaymentMenu = () => { + setShouldShowAddPaymentMenu(false); + }; + + /** + * Hide the default / delete modal + */ + const hideDefaultDeleteMenu = useCallback(() => { + setShouldShowDefaultDeleteMenu(false); + setShowConfirmDeleteModal(false); + }, [setShouldShowDefaultDeleteMenu, setShowConfirmDeleteModal]); + + const deletePaymentMethod = useCallback(() => { + const bankAccountID = paymentMethod.selectedPaymentMethod.bankAccountID; + if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT && bankAccountID) { + BankAccounts.deletePaymentBankAccount(bankAccountID); + } + }, [paymentMethod.selectedPaymentMethod.bankAccountID, paymentMethod.selectedPaymentMethodType]); + + const makeDefaultPaymentMethod = useCallback(() => { + const paymentCardList = fundList ?? {}; + // Find the previous default payment method so we can revert if the MakeDefaultPaymentMethod command errors + const paymentMethods = PaymentUtils.formatPaymentMethods(bankAccountList ?? {}, paymentCardList, styles); + + const previousPaymentMethod = paymentMethods.find((method) => !!method.isDefault); + const currentPaymentMethod = paymentMethods.find((method) => method.methodID === paymentMethod.methodID); + if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { + PaymentMethods.makeDefaultPaymentMethod(paymentMethod.selectedPaymentMethod.bankAccountID ?? -1, 0, previousPaymentMethod, currentPaymentMethod); + } else if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.DEBIT_CARD) { + PaymentMethods.makeDefaultPaymentMethod(0, paymentMethod.selectedPaymentMethod.fundID ?? -1, previousPaymentMethod, currentPaymentMethod); + } + }, [ + paymentMethod.methodID, + paymentMethod.selectedPaymentMethod.bankAccountID, + paymentMethod.selectedPaymentMethod.fundID, + paymentMethod.selectedPaymentMethodType, + bankAccountList, + fundList, + styles, + ]); + + const resetSelectedPaymentMethodData = useCallback(() => { + // Reset to same values as in the constructor + setPaymentMethod({ + isSelectedPaymentMethodDefault: false, + selectedPaymentMethod: {}, + formattedSelectedPaymentMethod: { + title: '', + }, + methodID: '', + selectedPaymentMethodType: '', + }); + }, [setPaymentMethod]); + + /** + * Navigate to the appropriate payment type addition screen + */ + const addPaymentMethodTypePressed = (paymentType: string) => { + hideAddPaymentMenu(); + + if (paymentType === CONST.PAYMENT_METHODS.DEBIT_CARD) { + Navigation.navigate(ROUTES.SETTINGS_ADD_DEBIT_CARD); + return; + } + if (paymentType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT || paymentType === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) { + BankAccounts.openPersonalBankAccountSetupView(); + return; + } + + throw new Error('Invalid payment method type selected'); + }; return (
console.debug('onPress')} - // actionPaymentMethodType={shouldShowDefaultDeleteMenu ? paymentMethod.selectedPaymentMethodType : ''} + onPress={paymentMethodPressed} activePaymentMethodID={policy?.invoice?.bankAccount?.transferBankAccountID ?? ''} - // buttonRef={addPaymentMethodAnchorRef} - // onListContentSizeChange={shouldShowAddPaymentMenu || shouldShowDefaultDeleteMenu ? setMenuPosition : () => {}} + actionPaymentMethodType={shouldShowDefaultDeleteMenu ? paymentMethod.selectedPaymentMethodType : ''} + buttonRef={addPaymentMethodAnchorRef} shouldEnableScroll={false} style={[styles.mt5, [shouldUseNarrowLayout ? styles.mhn5 : styles.mhn8]]} listItemStyle={shouldUseNarrowLayout ? styles.ph5 : styles.ph8} /> + + } + > + {!showConfirmDeleteModal && ( + + {isPopoverBottomMount && ( + + )} + {shouldShowMakeDefaultButton && ( + { + makeDefaultPaymentMethod(); + setShouldShowDefaultDeleteMenu(false); + }} + wrapperStyle={[styles.pv3, styles.ph5, !shouldUseNarrowLayout ? styles.sidebarPopover : {}]} + /> + )} + setShowConfirmDeleteModal(true)} + wrapperStyle={[styles.pv3, styles.ph5, !shouldUseNarrowLayout ? styles.sidebarPopover : {}]} + /> + + )} + { + deletePaymentMethod(); + hideDefaultDeleteMenu(); + }} + onCancel={hideDefaultDeleteMenu} + title={translate('walletPage.deleteAccount')} + prompt={translate('walletPage.deleteConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + shouldShowCancelButton + danger + onModalHide={resetSelectedPaymentMethodData} + /> + + + addPaymentMethodTypePressed(method)} + anchorRef={addPaymentMethodAnchorRef} + shouldShowPersonalBankAccountOption + />
); } From 8e9d73eee7496c1aa7ded7559e84492f9f34982c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 12 Aug 2024 16:44:47 +0200 Subject: [PATCH 04/16] add translations --- src/languages/en.ts | 3 ++- src/languages/es.ts | 3 ++- src/pages/settings/Wallet/WalletPage/WalletPage.tsx | 2 +- src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx | 6 ++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index bfe0eef70178..f59e24a26dbe 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -373,6 +373,7 @@ export default { filterLogs: 'Filter Logs', network: 'Network', reportID: 'Report ID', + bankAccounts: 'Bank accounts', }, location: { useCurrent: 'Use current location', @@ -1210,7 +1211,6 @@ export default { enableWalletToSendAndReceiveMoney: 'Enable your wallet to send and receive money with friends.', walletEnabledToSendAndReceiveMoney: 'Your wallet has been enabled to send and receive money with friends.', enableWallet: 'Enable wallet', - bankAccounts: 'Bank accounts', addBankAccountToSendAndReceive: 'Adding a bank account allows you to get paid back for expenses you submit to a workspace.', addBankAccount: 'Add bank account', assignedCards: 'Assigned cards', @@ -3321,6 +3321,7 @@ export default { payingAsIndividual: 'Paying as an individual', payingAsBusiness: 'Paying as a business', }, + bankAccountsSubtitle: 'Add a bank account to receive invoice payments.', }, travel: { unlockConciergeBookingTravel: 'Unlock Concierge travel booking', diff --git a/src/languages/es.ts b/src/languages/es.ts index 76d55a096808..3360fa18b00d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -363,6 +363,7 @@ export default { filterLogs: 'Registros de filtrado', network: 'La red', reportID: 'ID del informe', + bankAccounts: 'Cuentas bancarias', }, connectionComplete: { title: 'Conexión completa', @@ -1219,7 +1220,6 @@ export default { enableWalletToSendAndReceiveMoney: 'Habilita tu Billetera Expensify para comenzar a enviar y recibir dinero con amigos.', walletEnabledToSendAndReceiveMoney: 'Tu billetera ha sido habilitada para enviar y recibir dinero con amigos.', enableWallet: 'Habilitar billetera', - bankAccounts: 'Cuentas bancarias', addBankAccountToSendAndReceive: 'Agregar una cuenta bancaria te permite recibir reembolsos por los gastos que envíes a un espacio de trabajo.', addBankAccount: 'Añadir cuenta bancaria', assignedCards: 'Tarjetas asignadas', @@ -3372,6 +3372,7 @@ export default { payingAsIndividual: 'Pago individual', payingAsBusiness: 'Pagar como una empresa', }, + bankAccountsSubtitle: 'Agrega una cuenta bancaria para recibir pagos de facturas.', }, travel: { unlockConciergeBookingTravel: 'Desbloquea la reserva de viajes con Concierge', diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index 7e242a5c8782..5674d6e0608c 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -369,7 +369,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi >
@@ -249,7 +249,6 @@ function WorkspaceInvoiceVBASection({policyID}: WorkspaceInvoiceVBASectionProps) style={[styles.mt5, [shouldUseNarrowLayout ? styles.mhn5 : styles.mhn8]]} listItemStyle={shouldUseNarrowLayout ? styles.ph5 : styles.ph8} /> - - Date: Mon, 12 Aug 2024 17:12:57 +0200 Subject: [PATCH 05/16] integrate add bank account button --- .../settings/Wallet/PaymentMethodList.tsx | 39 +++++++++++++------ .../invoices/WorkspaceInvoiceVBASection.tsx | 3 +- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 0d127e3346ae..a2e073714e76 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -91,6 +91,9 @@ type PaymentMethodListProps = PaymentMethodListOnyxProps & { /** Whether the add Payment button be shown on the list */ shouldShowAddPaymentMethodButton?: boolean; + /** Whether the add Bank account button be shown on the list */ + shouldShowAddBankAccountButton?: boolean; + /** Whether the assigned cards should be shown on the list */ shouldShowAssignedCards?: boolean; @@ -183,6 +186,7 @@ function PaymentMethodList({ onPress, shouldShowSelectedState = false, shouldShowAddPaymentMethodButton = true, + shouldShowAddBankAccountButton = false, shouldShowAddBankAccount = true, shouldShowEmptyListMessage = true, shouldShowAssignedCards = false, @@ -313,19 +317,30 @@ function PaymentMethodList({ const renderListEmptyComponent = () => {translate('paymentMethodList.addFirstPaymentMethod')}; const renderListFooterComponent = useCallback( - () => ( - - ), + () => + shouldShowAddBankAccountButton ? ( +