From 27b9cdc7b019070b4192c57228f1503d14e85b81 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Fri, 28 Nov 2025 19:04:44 +0200 Subject: [PATCH 01/20] Unshare bank account --- assets/images/user-minus.svg | 5 + src/ONYXKEYS.ts | 4 + src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + .../Icon/chunks/expensify-icons.chunk.ts | 2 + src/languages/en.ts | 6 + .../parameters/UnshareBankAccountParams.ts | 6 + src/libs/API/parameters/index.ts | 1 + .../ModalStackNavigators/index.tsx | 1 + .../RELATIONS/SETTINGS_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 4 + src/libs/Navigation/types.ts | 3 + src/libs/actions/BankAccounts.ts | 56 +++++ .../UnshareBankAccount/UnshareBankAccount.tsx | 201 ++++++++++++++++++ .../settings/Wallet/WalletPage/WalletPage.tsx | 22 +- src/types/onyx/UnshareBankAccount.ts | 18 ++ src/types/onyx/index.ts | 2 + 17 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 assets/images/user-minus.svg create mode 100644 src/libs/API/parameters/UnshareBankAccountParams.ts create mode 100644 src/pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount.tsx create mode 100644 src/types/onyx/UnshareBankAccount.ts diff --git a/assets/images/user-minus.svg b/assets/images/user-minus.svg new file mode 100644 index 000000000000..c0926d61019b --- /dev/null +++ b/assets/images/user-minus.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ff5685b0cca2..22b9f94f2ba4 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -122,6 +122,9 @@ const ONYXKEYS = { /* Contains meta data for the call to the API to get the joinable policies */ VALIDATE_USER_AND_GET_ACCESSIBLE_POLICIES: 'validateUserAndGetAccessiblePolicies', + /** Stores information about the unshare bank account */ + UNSHARE_BANK_ACCOUNT: 'unshareBankAccount', + /** Information about the current session (authToken, accountID, email, loading, error) */ SESSION: 'session', STASHED_SESSION: 'stashedSession', @@ -1194,6 +1197,7 @@ type OnyxValuesMapping = { [ONYXKEYS.WALLET_STATEMENT]: OnyxTypes.WalletStatement; [ONYXKEYS.PURCHASE_LIST]: OnyxTypes.PurchaseList; [ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; + [ONYXKEYS.UNSHARE_BANK_ACCOUNT]: OnyxTypes.UnshareBankAccount; [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount; [ONYXKEYS.REIMBURSEMENT_ACCOUNT_OPTION_PRESSED]: ValueOf; [ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE]: string | number; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 3893661d7645..a97a0e7c3c7e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -322,6 +322,10 @@ const ROUTES = { SETTINGS_ADD_US_BANK_ACCOUNT: 'settings/wallet/add-us-bank-account', SETTINGS_ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT: `settings/wallet/add-bank-account/select-country/${VERIFY_ACCOUNT}`, SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments', + SETTINGS_WALLET_UNSHARE_BANK_ACCOUNT: { + route: 'settings/wallet/:bankAccountID/unshare-bank-account', + getRoute: (bankAccountID: number | undefined) => `settings/wallet/${bankAccountID}/unshare-bank-account` as const, + }, SETTINGS_WALLET_ENABLE_GLOBAL_REIMBURSEMENTS: { route: 'settings/wallet/:bankAccountID/enable-global-reimbursements', getRoute: (bankAccountID: number | undefined) => `settings/wallet/${bankAccountID}/enable-global-reimbursements` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 9584b2a5f359..831d7b52a070 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -161,6 +161,7 @@ const SCREENS = { REPORT_VIRTUAL_CARD_FRAUD_CONFIRM_MAGIC_CODE: 'Settings_Wallet_ReportVirtualCardFraud_ConfirmMagicCode', REPORT_VIRTUAL_CARD_FRAUD_CONFIRMATION: 'Settings_Wallet_ReportVirtualCardFraudConfirmation', CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS: 'Settings_Wallet_Cards_Digital_Details_Update_Address', + UNSHARE_BANK_ACCOUNT: 'Settings_Wallet_Unshare_Bank_Account', ENABLE_GLOBAL_REIMBURSEMENTS: 'Settings_Wallet_Enable_Global_Reimbursements', }, diff --git a/src/components/Icon/chunks/expensify-icons.chunk.ts b/src/components/Icon/chunks/expensify-icons.chunk.ts index 61d793bcc2c2..a5461688fe9c 100644 --- a/src/components/Icon/chunks/expensify-icons.chunk.ts +++ b/src/components/Icon/chunks/expensify-icons.chunk.ts @@ -214,6 +214,7 @@ import Upload from '@assets/images/upload.svg'; import UserCheck from '@assets/images/user-check.svg'; import UserEye from '@assets/images/user-eye.svg'; import UserLock from '@assets/images/user-lock.svg'; +import UserMinus from '@assets/images/user-minus.svg'; import UserPlus from '@assets/images/user-plus.svg'; import User from '@assets/images/user.svg'; import Users from '@assets/images/users.svg'; @@ -330,6 +331,7 @@ const Expensicons = { LinkCopy, Location, Lock, + UserMinus, Luggage, MagnifyingGlass, Mail, diff --git a/src/languages/en.ts b/src/languages/en.ts index 74a57a3bd8d4..87c2b1402e38 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -330,6 +330,7 @@ const translations = { cancel: 'Cancel', dismiss: 'Dismiss', proceed: 'Proceed', + unshare: 'Unshare', yes: 'Yes', no: 'No', ok: 'OK', @@ -2142,6 +2143,11 @@ const translations = { confirmYourBankAccount: 'Confirm your bank account', personalBankAccounts: 'Personal bank accounts', businessBankAccounts: 'Business bank accounts', + unshareBankAccount: 'Unshare bank account', + unshareBankAccountDescription: 'Everyone below has access to this bank account. You can remove access at any point. We’ll still complete any payments in process.', + unshareBankAccountWarning: ({admin}: {admin?: string | null}) => `${admin} will lose access to this business bank account. We’ll still complete any payments in process.`, + reachOutForHelp: 'It’s being used with the Expensify Card. Reach out to Concierge if you need to unshare it.', + unshareErrorModalTitle: 'Can’t unshare bank account', }, cardPage: { expensifyCard: 'Expensify Card', diff --git a/src/libs/API/parameters/UnshareBankAccountParams.ts b/src/libs/API/parameters/UnshareBankAccountParams.ts new file mode 100644 index 000000000000..420f09fa32ca --- /dev/null +++ b/src/libs/API/parameters/UnshareBankAccountParams.ts @@ -0,0 +1,6 @@ +type UnshareBankAccountParams = { + bankAccountID: number; + email: string; +}; + +export default UnshareBankAccountParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 09553e22d5e0..62fd9c973db6 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -135,6 +135,7 @@ export type {default as TogglePolicyUberAutoRemovePageParams} from './TogglePoli export type {default as InviteToRoomParams} from './InviteToRoomParams'; export type {default as InviteToGroupChatParams} from './InviteToGroupChatParams'; export type {default as InviteWorkspaceEmployeesToUberParams} from './InviteWorkspaceEmployeesToUberParams'; +export type {default as UnshareBankAccountParams} from './UnshareBankAccountParams'; export type {default as RemoveFromRoomParams} from './RemoveFromRoomParams'; export type {default as RemoveFromGroupChatParams} from './RemoveFromGroupChatParams'; export type {default as FlagCommentParams} from './FlagCommentParams'; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 14698f195184..50a826f17bd7 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -442,6 +442,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Wallet/TransferBalancePage').default, [SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT]: () => require('../../../../pages/settings/Wallet/ChooseTransferAccountPage').default, [SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS]: () => require('../../../../pages/EnablePayments/EnablePayments').default, + [SCREENS.SETTINGS.WALLET.UNSHARE_BANK_ACCOUNT]: () => require('../../../../pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount').default, [SCREENS.SETTINGS.WALLET.ENABLE_GLOBAL_REIMBURSEMENTS]: () => require('../../../../pages/settings/Wallet/EnableGlobalReimbursements').default, [SCREENS.SETTINGS.ADD_DEBIT_CARD]: () => require('../../../../pages/settings/Wallet/AddDebitCardPage').default, [SCREENS.SETTINGS.ADD_BANK_ACCOUNT_VERIFY_ACCOUNT]: () => require('../../../../pages/settings/Wallet/NewBankAccountVerifyAccountPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts index 9ee3dd508f81..2990a6ce6342 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts @@ -38,6 +38,7 @@ const SETTINGS_TO_RHP: Partial['config'] = { path: ROUTES.SETTINGS_ENABLE_PAYMENTS, exact: true, }, + [SCREENS.SETTINGS.WALLET.UNSHARE_BANK_ACCOUNT]: { + path: ROUTES.SETTINGS_WALLET_UNSHARE_BANK_ACCOUNT.route, + exact: true, + }, [SCREENS.SETTINGS.WALLET.ENABLE_GLOBAL_REIMBURSEMENTS]: { path: ROUTES.SETTINGS_WALLET_ENABLE_GLOBAL_REIMBURSEMENTS.route, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b2ef587a0955..f5616bc5eb9e 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -196,6 +196,9 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE]: undefined; [SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT]: undefined; [SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS]: undefined; + [SCREENS.SETTINGS.WALLET.UNSHARE_BANK_ACCOUNT]: { + bankAccountID: string; + }; [SCREENS.SETTINGS.WALLET.ENABLE_GLOBAL_REIMBURSEMENTS]: { bankAccountID: string; }; diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 01d686632d1d..3175d866b479 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -14,6 +14,7 @@ import type { OpenReimbursementAccountPageParams, SaveCorpayOnboardingBeneficialOwnerParams, SendReminderForCorpaySignerInformationParams, + UnshareBankAccountParams, ValidateBankAccountWithTransactionsParams, VerifyIdentityForBankAccountParams, } from '@libs/API/parameters'; @@ -1222,6 +1223,58 @@ function fetchCorpayFields(bankCountry: string, bankCurrency?: string, isWithdra ); } +function clearUnshareBankAccount() { + Onyx.set(ONYXKEYS.UNSHARE_BANK_ACCOUNT, null); +} + +function clearUnshareBankAccountErrors() { + Onyx.merge(ONYXKEYS.UNSHARE_BANK_ACCOUNT, {errors: null}); +} + +function unshareBankAccount(bankAccountID: number, email: string) { + const parameters: UnshareBankAccountParams = { + bankAccountID, + email, + }; + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.UNSHARE_BANK_ACCOUNT, + value: { + isLoading: true, + errors: null, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.UNSHARE_BANK_ACCOUNT, + value: { + isLoading: false, + errors: null, + admins: null, + shouldShowSuccess: true, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.UNSHARE_BANK_ACCOUNT, + value: { + isLoading: false, + errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + ], + }; + + API.write(WRITE_COMMANDS.UNSHARE_BANK_ACCOUNT, parameters, onyxData); +} + function createCorpayBankAccountForWalletFlow(data: InternationalBankAccountForm, classification: string, destinationCountry: string, preferredMethod: string) { const inputData = { ...data, @@ -1323,6 +1376,9 @@ export { createCorpayBankAccountForWalletFlow, getCorpayOnboardingFields, saveCorpayOnboardingCompanyDetails, + unshareBankAccount, + clearUnshareBankAccountErrors, + clearUnshareBankAccount, clearReimbursementAccountSaveCorpayOnboardingCompanyDetails, saveCorpayOnboardingBeneficialOwners, saveCorpayOnboardingDirectorInformation, diff --git a/src/pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount.tsx b/src/pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount.tsx new file mode 100644 index 000000000000..e62901f06d24 --- /dev/null +++ b/src/pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount.tsx @@ -0,0 +1,201 @@ +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; +import ConfirmModal from '@components/ConfirmModal'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import RenderHTML from '@components/RenderHTML'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionListWithSections'; +import type {ListItem} from '@components/SelectionListWithSections/types'; +import UserListItem from '@components/SelectionListWithSections/UserListItem'; +import Text from '@components/Text'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getLatestErrorMessage} from '@libs/ErrorUtils'; +import {formatMemberForList, getHeaderMessage, getSearchValueForPhoneOrEmail} from '@libs/OptionsListUtils'; +import type {MemberForList} from '@libs/OptionsListUtils'; +import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; +import tokenizedSearch from '@libs/tokenizedSearch'; +import Navigation from '@navigation/Navigation'; +import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import {clearUnshareBankAccount, clearUnshareBankAccountErrors, unshareBankAccount} from '@userActions/BankAccounts'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type ShareBankAccountProps = PlatformStackScreenProps; + +const DEFAULT_OBJECT = {}; +function UnshareBankAccount({route}: ShareBankAccountProps) { + const bankAccountID = route.params?.bankAccountID; + const styles = useThemeStyles(); + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); + + const {isOffline} = useNetwork(); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + + const [unsharedBankAccountData] = useOnyx(ONYXKEYS.UNSHARE_BANK_ACCOUNT, {canBeMissing: true}); + const isLoading = unsharedBankAccountData?.isLoading ?? false; + + const [unshareUser, setUnshareUser] = useState<{login?: string | null; text?: string | null} | undefined>(undefined); + const error = getLatestErrorMessage(unsharedBankAccountData ?? DEFAULT_OBJECT); + + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const {translate} = useLocalize(); + const admins = ['n.arefyev91@gmail.com', '5552@gmail.com', '5553@gmail.com'] ?? bankAccountList?.[bankAccountID]?.accountData?.sharees; + // const shouldShowTextInput = admins && admins?.length >= CONST.STANDARD_LIST_ITEM_LIMIT; + const shouldShowTextInput = admins && admins?.length >= 2; + const textInputLabel = shouldShowTextInput ? translate('common.search') : undefined; + + useEffect(() => { + return () => { + if (isLoading) { + return; + } + clearUnshareBankAccount(); + }; + }, [isLoading]); + + useEffect(() => { + if (isOffline) { + return; + } + // openBankAccountSharePage(); + }, [isOffline]); + + const handleUnshare = useCallback(() => { + if (!bankAccountID || !unshareUser?.login) { + return; + } + unshareBankAccount(Number(bankAccountID), unshareUser.login); + setUnshareUser(undefined); + }, [bankAccountID, unshareUser?.login]); + + const adminsList = useMemo(() => { + if (admins?.length === 0) { + return []; + } + + const adminsWithInfo = + admins?.map((admin) => { + const personalDetails = getPersonalDetailByEmail(admin); + return formatMemberForList({ + text: personalDetails?.displayName, + alternateText: personalDetails?.login, + keyForList: personalDetails?.login, + accountID: personalDetails?.accountID, + login: personalDetails?.login, + pendingAction: personalDetails?.pendingAction, + reportID: '', + }); + }) ?? []; + + let adminsToDisplay = [...adminsWithInfo]; + + // Apply search filter if there's a search term + if (debouncedSearchTerm) { + const searchValue = getSearchValueForPhoneOrEmail(debouncedSearchTerm, countryCode).toLowerCase(); + adminsToDisplay = tokenizedSearch(adminsWithInfo, searchValue, (option) => [option.text ?? '', option.alternateText ?? '']); + } + + return adminsToDisplay; + }, [admins, countryCode, debouncedSearchTerm]); + + const hideUnshareErrorModal = useCallback(() => { + clearUnshareBankAccountErrors(); + }, []); + + const onSelectRow = useCallback((item: MemberForList) => { + setUnshareUser({login: item?.login, text: item?.text}); + }, []); + + const itemRightSideComponent = useCallback( + (item: ListItem) => { + return ( +