diff --git a/cspell.json b/cspell.json
index 1e1382d4174f..d55ae14f4fed 100644
--- a/cspell.json
+++ b/cspell.json
@@ -667,6 +667,8 @@
"setuptools",
"shareeEmail",
"Sharees",
+ "sharee",
+ "sharees",
"Sharons",
"shellcheck",
"shellenv",
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 56eef0b683ad..8dcb4844bd53 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -2459,6 +2459,16 @@ ${amount} für ${merchant} – ${date}`,
admins: 'Admins',
payer: 'Zahler',
paymentAccount: 'Zahlungskonto',
+ shareBankAccount: {
+ shareTitle: 'Bankkontozugriff teilen?',
+ shareDescription: ({admin}: {admin: string}) => `Sie müssen ${admin} den Bankkontozugriff gewähren, damit dieser als Zahler eingetragen werden kann.`,
+ validationTitle: 'Bankkonto wartet auf Validierung',
+ validationDescription: ({admin}: {admin: string}) =>
+ `Sie müssen dieses Bankkonto validieren. Anschließend können Sie den Zugriff auf das Bankkonto mit ${admin} teilen, um ihn/sie als Zahler festzulegen.`,
+ errorTitle: 'Zahler kann nicht geändert werden',
+ errorDescription: ({admin, owner}: {admin: string; owner: string}) =>
+ `${admin} hat keinen Zugriff auf dieses Bankkonto, daher kann er nicht als Zahler festgelegt werden. Kontaktieren Sie ${owner}, falls das Bankkonto freigegeben werden soll.`,
+ },
},
reportFraudPage: {
title: 'Virtuelle Kartenbetrugsfälle melden',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 9e1b3c985c71..4310a3367f91 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -2500,6 +2500,16 @@ const translations = {
admins: 'Admins',
payer: 'Payer',
paymentAccount: 'Payment account',
+ shareBankAccount: {
+ shareTitle: 'Share bank account access?',
+ shareDescription: ({admin}: {admin: string}) => `You'll need to share bank account access with ${admin} to make them the payer.`,
+ validationTitle: 'Bank account awaiting validation',
+ validationDescription: ({admin}: {admin: string}) =>
+ `You need to validate this bank account. Once that's done, you can share bank account access with ${admin} to make them the payer.`,
+ errorTitle: "Can't change payer",
+ errorDescription: ({admin, owner}: {admin: string; owner: string}) =>
+ `${admin} doesn't have access to this bank account, so you can't make them the payer. Chat with ${owner} if the bank account should be shared.`,
+ },
},
reportFraudPage: {
title: 'Report virtual card fraud',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 28c768351a01..3c11cd118fd9 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -2361,6 +2361,16 @@ ${amount} para ${merchant} - ${date}`,
admins: 'Administradores',
payer: 'Pagador',
paymentAccount: 'Cuenta de pago',
+ shareBankAccount: {
+ shareTitle: '¿Compartir acceso a la cuenta bancaria?',
+ shareDescription: ({admin}: {admin: string}) => `Necesitarás compartir el acceso a la cuenta bancaria con ${admin} para que sea el pagador.`,
+ validationTitle: 'Cuenta bancaria en espera de validación',
+ validationDescription: ({admin}: {admin: string}) =>
+ `Necesitas validar esta cuenta bancaria. Una vez hecho esto, puedes compartir el acceso a la cuenta bancaria con ${admin} para que sea el pagador.`,
+ errorTitle: 'No se puede cambiar el pagador',
+ errorDescription: ({admin, owner}: {admin: string; owner: string}) =>
+ `${admin} no tiene acceso a esta cuenta bancaria, por lo que no puede asignarle el pago. Chatea con ${owner} si la cuenta bancaria debe compartirse.`,
+ },
},
reportFraudPage: {
title: 'Reportar fraude con la tarjeta virtual',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index e3141cb9aa26..01975c47ac03 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -2465,6 +2465,16 @@ ${amount} pour ${merchant} - ${date}`,
admins: 'Administrateurs',
payer: 'Payer',
paymentAccount: 'Compte de paiement',
+ shareBankAccount: {
+ shareTitle: "Partager l'accès au compte bancaire?",
+ shareDescription: ({admin}: {admin: string}) => `Vous devrez partager l'accès au compte bancaire avec ${admin} pour qu'il/elle soit le payeur.`,
+ validationTitle: 'Compte bancaire en attente de validation',
+ validationDescription: ({admin}: {admin: string}) =>
+ `Vous devez valider ce compte bancaire. Une fois cette opération effectuée, vous pourrez partager l'accès au compte bancaire avec ${admin} pour en faire le payeur.`,
+ errorTitle: 'Impossible de modifier le payeur',
+ errorDescription: ({admin, owner}: {admin: string; owner: string}) =>
+ `${admin} n'a pas accès à ce compte bancaire, vous ne pouvez donc pas le désigner comme payeur. Discutez avec ${owner} si le compte bancaire doit être partagé.`,
+ },
},
reportFraudPage: {
title: 'Signaler une fraude à la carte virtuelle',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 2814a3f55957..9ce4dfc1db90 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -2454,6 +2454,16 @@ ${amount} per ${merchant} - ${date}`,
admins: 'Amministratori',
payer: 'Pagatore',
paymentAccount: 'Conto di pagamento',
+ shareBankAccount: {
+ shareTitle: "Condividere l'accesso al conto bancario?",
+ shareDescription: ({admin}: {admin: string}) => `Dovrai condividere l'accesso al conto bancario con ${admin} per renderlo il pagatore.`,
+ validationTitle: 'Conto bancario in attesa di convalida',
+ validationDescription: ({admin}: {admin: string}) =>
+ `Devi convalidare questo conto bancario. Una volta fatto, puoi condividere l'accesso al conto bancario con ${admin} per renderlo il pagatore.`,
+ errorTitle: 'Impossibile cambiare il pagatore',
+ errorDescription: ({admin, owner}: {admin: string; owner: string}) =>
+ `${admin} non ha accesso a questo conto bancario, quindi non puoi impostarlo come pagatore. Chatta con ${owner} se il conto bancario deve essere condiviso.`,
+ },
},
reportFraudPage: {
title: 'Segnala frode con carta virtuale',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 1fb0fb5313e1..008ea153d912 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -2437,6 +2437,16 @@ ${date} の ${merchant} への ${amount}`,
admins: '管理者',
payer: '支払者',
paymentAccount: '支払口座',
+ shareBankAccount: {
+ shareTitle: '銀行口座へのアクセスを共有しますか?',
+ shareDescription: ({admin}: {admin: string}) => `${admin}を支払人にするには、銀行口座へのアクセスを${admin}と共有する必要があります。`,
+ validationTitle: '銀行口座の検証待ち',
+ validationDescription: ({admin}: {admin: string}) =>
+ `この銀行口座を検証する必要があります。検証が完了すると、銀行口座へのアクセス権を${admin}と共有して、支払人にすることができます。`,
+ errorTitle: '支払人を変更できません',
+ errorDescription: ({admin, owner}: {admin: string; owner: string}) =>
+ `${admin} はこの銀行口座へのアクセス権がないため、支払人に指定できません。銀行口座を共有する必要がある場合は、${owner} とチャットしてください。`,
+ },
},
reportFraudPage: {
title: 'バーチャルカードの不正利用を報告',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 5bee400ffe6f..18a54e15818c 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -2452,6 +2452,16 @@ ${amount} voor ${merchant} - ${date}`,
admins: 'Beheerders',
payer: 'Betaler',
paymentAccount: 'Betaalrekening',
+ shareBankAccount: {
+ shareTitle: 'Bankrekeningtoegang delen?',
+ shareDescription: ({admin}: {admin: string}) => `U moet bankrekeningtoegang delen met ${admin} om hem/haar de betaler te maken.`,
+ validationTitle: 'Bankrekening wacht op validatie',
+ validationDescription: ({admin}: {admin: string}) =>
+ `U moet deze bankrekening valideren. Zodra dat is gedaan, kunt u de toegang tot de bankrekening delen met ${admin} om hem/haar de betaler te maken.`,
+ errorTitle: 'Betaler kan niet worden gewijzigd',
+ errorDescription: ({admin, owner}: {admin: string; owner: string}) =>
+ `${admin} heeft geen toegang tot deze bankrekening, dus je kunt hem/haar niet als betaler instellen. Chat met ${owner} als de bankrekening gedeeld moet worden.`,
+ },
},
reportFraudPage: {
title: 'Fraude met virtuele kaart melden',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 3bea0ab6b5ff..0d63f249fe15 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -2447,6 +2447,16 @@ ${amount} dla ${merchant} - ${date}`,
admins: 'Administratorzy',
payer: 'Płatnik',
paymentAccount: 'Konto płatnicze',
+ shareBankAccount: {
+ shareTitle: 'Udostępnić dostęp do konta bankowego?',
+ shareDescription: ({admin}: {admin: string}) => `Musisz udostępnić dostęp do konta bankowego użytkownikowi ${admin}, aby uczynić go płatnikiem.`,
+ validationTitle: 'Konto bankowe oczekuje na walidację',
+ validationDescription: ({admin}: {admin: string}) =>
+ `Musisz zweryfikować to konto bankowe. Po wykonaniu tej czynności możesz udostępnić dostęp do konta bankowego użytkownikowi ${admin}, aby uczynić go płatnikiem.`,
+ errorTitle: 'Nie można zmienić płatnika',
+ errorDescription: ({admin, owner}: {admin: string; owner: string}) =>
+ `${admin} nie ma dostępu do tego konta bankowego, więc nie możesz uczynić go płatnikiem. Porozmawiaj z ${owner}, jeśli konto bankowe powinno być współdzielone.`,
+ },
},
reportFraudPage: {
title: 'Zgłoś oszustwo kartą wirtualną',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index e2c3fb53a3f5..449352d8052a 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -2445,6 +2445,16 @@ ${amount} para ${merchant} - ${date}`,
admins: 'Admins',
payer: 'Pagador',
paymentAccount: 'Conta de pagamento',
+ shareBankAccount: {
+ shareTitle: 'Compartilhar acesso à conta bancária?',
+ shareDescription: ({admin}: {admin: string}) => `Você precisará compartilhar o acesso à conta bancária com ${admin} para torná-lo o pagador.`,
+ validationTitle: 'Conta bancária aguardando validação',
+ validationDescription: ({admin}: {admin: string}) =>
+ `Você precisa validar esta conta bancária. Depois disso, você poderá compartilhar o acesso à conta bancária com ${admin} para torná-lo o pagador.`,
+ errorTitle: 'Não é possível alterar o pagador',
+ errorDescription: ({admin, owner}: {admin: string; owner: string}) =>
+ `${admin} não tem acesso a esta conta bancária, portanto, você não pode torná-lo o pagador. Converse com ${owner} se a conta bancária deve ser compartilhada.`,
+ },
},
reportFraudPage: {
title: 'Denunciar fraude no cartão virtual',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 96415f4fa273..8a533c1c4f1a 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -2397,6 +2397,15 @@ ${amount},商户:${merchant} - 日期:${date}`,
admins: '管理员',
payer: '付款人',
paymentAccount: '支付账户',
+ shareBankAccount: {
+ shareTitle: '共享银行账户访问权限?',
+ shareDescription: ({admin}: {admin: string}) => `您需要与 ${admin} 共享银行账户访问权限,才能将其设置为付款人。`,
+ validationTitle: '银行账户等待验证',
+ validationDescription: ({admin}: {admin: string}) => `您需要验证此银行账户。验证完成后,您可以将银行账户访问权限分享给 ${admin},使其成为付款人。`,
+ errorTitle: '无法更改付款人',
+ errorDescription: ({admin, owner}: {admin: string; owner: string}) =>
+ `${admin} 没有此银行账户的访问权限,因此您无法将其设置为付款人。请联系 ${owner} 了解是否需要共享此银行账户。`,
+ },
},
reportFraudPage: {
title: '报告虚拟卡欺诈',
diff --git a/src/libs/API/parameters/OpenPolicyWorkflowsPageParams.ts b/src/libs/API/parameters/OpenPolicyWorkflowsPageParams.ts
index eea0788b3927..20349d0336b8 100644
--- a/src/libs/API/parameters/OpenPolicyWorkflowsPageParams.ts
+++ b/src/libs/API/parameters/OpenPolicyWorkflowsPageParams.ts
@@ -1,5 +1,6 @@
type OpenPolicyWorkflowsPageParams = {
policyID: string;
+ includeAllBankAccounts?: boolean;
};
export default OpenPolicyWorkflowsPageParams;
diff --git a/src/libs/API/parameters/ShareBankAccountAndSetPayerParams.ts b/src/libs/API/parameters/ShareBankAccountAndSetPayerParams.ts
new file mode 100644
index 000000000000..15f5e1689473
--- /dev/null
+++ b/src/libs/API/parameters/ShareBankAccountAndSetPayerParams.ts
@@ -0,0 +1,7 @@
+type ShareBankAccountAndSetPayerParams = {
+ bankAccountID: number;
+ shareeAccountID: number;
+ policyID: string;
+};
+
+export default ShareBankAccountAndSetPayerParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index f18688d3cee3..3943f534c25e 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -152,6 +152,7 @@ export type {default as AcceptWalletTermsParams} from './AcceptWalletTermsParams
export type {default as ChronosRemoveOOOEventParams} from './ChronosRemoveOOOEventParams';
export type {default as TransferWalletBalanceParams} from './TransferWalletBalanceParams';
export type {default as DeleteWorkspaceParams} from './DeleteWorkspaceParams';
+export type {default as ShareBankAccountAndSetPayerParams} from './ShareBankAccountAndSetPayerParams';
export type {default as CreateWorkspaceParams} from './CreateWorkspaceParams';
export type {default as UpdateWorkspaceGeneralSettingsParams} from './UpdateWorkspaceGeneralSettingsParams';
export type {default as DeleteWorkspaceAvatarParams} from './DeleteWorkspaceAvatarParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 536a0c074a27..ec28e077af9c 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -520,6 +520,7 @@ const WRITE_COMMANDS = {
GET_CORPAY_BANK_ACCOUNT_FIELDS: 'GetCorpayBankAccountFields',
BANK_ACCOUNT_CREATE_CORPAY: 'BankAccount_CreateCorpay',
SHARE_BANK_ACCOUNT: 'ShareBankAccount',
+ SHARE_BANK_ACCOUNT_AND_UPDATE_POLICY_REIMBURSER: 'ShareBankAccountAndUpdatePolicyReimburser',
UPDATE_WORKSPACE_CUSTOM_UNIT: 'UpdateWorkspaceCustomUnit',
GET_ACCESSIBLE_POLICIES: 'GetAccessibleDomainPoliciesForOnyx',
VALIDATE_USER_AND_GET_ACCESSIBLE_POLICIES: 'ValidateUserAndGetAccessiblePolicies',
@@ -746,6 +747,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.RENAME_POLICY_TAG]: Parameters.RenamePolicyTagsParams;
[WRITE_COMMANDS.UPDATE_POLICY_TAG_GL_CODE]: Parameters.UpdatePolicyTagGLCodeParams;
[WRITE_COMMANDS.SHARE_BANK_ACCOUNT]: Parameters.ShareBankAccountParams;
+ [WRITE_COMMANDS.SHARE_BANK_ACCOUNT_AND_UPDATE_POLICY_REIMBURSER]: Parameters.ShareBankAccountAndSetPayerParams;
[WRITE_COMMANDS.SET_POLICY_TAGS_ENABLED]: Parameters.SetPolicyTagsEnabled;
[WRITE_COMMANDS.DELETE_POLICY_TAGS]: Parameters.DeletePolicyTagsParams;
[WRITE_COMMANDS.CREATE_TASK]: Parameters.CreateTaskParams;
diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts
index 6ac536e761e0..af19c37c4924 100644
--- a/src/libs/actions/BankAccounts.ts
+++ b/src/libs/actions/BankAccounts.ts
@@ -14,6 +14,7 @@ import type {
OpenReimbursementAccountPageParams,
SaveCorpayOnboardingBeneficialOwnerParams,
SendReminderForCorpaySignerInformationParams,
+ ShareBankAccountAndSetPayerParams,
ShareBankAccountParams,
UnshareBankAccountParams,
ValidateBankAccountWithTransactionsParams,
@@ -1453,6 +1454,51 @@ function shareBankAccount(bankAccountID: number, emailList: string[]) {
API.write(WRITE_COMMANDS.SHARE_BANK_ACCOUNT, parameters, onyxData);
}
+function shareBankAccountAndSetPayer(bankAccountID: number, shareeAccountID: number, policyID: string) {
+ const parameters: ShareBankAccountAndSetPayerParams = {
+ bankAccountID,
+ shareeAccountID,
+ policyID,
+ };
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.SHARE_BANK_ACCOUNT,
+ value: {
+ isLoading: true,
+ errors: null,
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.SHARE_BANK_ACCOUNT,
+ value: {
+ isLoading: false,
+ errors: null,
+ admins: null,
+ shouldShowSuccess: true,
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.SHARE_BANK_ACCOUNT,
+ value: {
+ isLoading: false,
+ errors: getMicroSecondOnyxErrorWithTranslationKey('walletPage.shareBankAccountFailure'),
+ },
+ },
+ ],
+ };
+
+ API.write(WRITE_COMMANDS.SHARE_BANK_ACCOUNT_AND_UPDATE_POLICY_REIMBURSER, parameters, onyxData);
+}
+
/**
* Get bank account from bankAccountID
*/
@@ -1538,6 +1584,7 @@ export {
clearReimbursementAccountSaveCorpayOnboardingDirectorInformation,
clearCorpayBankAccountFields,
finishCorpayBankAccountOnboarding,
+ shareBankAccountAndSetPayer,
clearReimbursementAccountFinishCorpayBankAccountOnboarding,
enableGlobalReimbursementsForUSDBankAccount,
clearEnableGlobalReimbursementsForUSDBankAccount,
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index 734f9ce93be7..1d62ebb2849d 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -3373,7 +3373,7 @@ function duplicateWorkspace(policy: Policy, options: DuplicatePolicyDataOptions)
return params;
}
-function openPolicyWorkflowsPage(policyID: string) {
+function openPolicyWorkflowsPage(policyID: string, includeAllBankAccounts?: boolean) {
if (!policyID) {
Log.warn('openPolicyWorkflowsPage invalid params', {policyID});
return;
@@ -3409,7 +3409,7 @@ function openPolicyWorkflowsPage(policyID: string) {
],
};
- const params: OpenPolicyWorkflowsPageParams = {policyID};
+ const params: OpenPolicyWorkflowsPageParams = {policyID, includeAllBankAccounts};
API.read(READ_COMMANDS.OPEN_POLICY_WORKFLOWS_PAGE, params, onyxData);
}
diff --git a/src/pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount.tsx b/src/pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount.tsx
index 0718f015a42c..c28583798342 100644
--- a/src/pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount.tsx
+++ b/src/pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount.tsx
@@ -15,6 +15,7 @@ import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
+import {getLatestErrorMessage} from '@libs/ErrorUtils';
import {formatMemberForList, getHeaderMessage, getSearchValueForPhoneOrEmail} from '@libs/OptionsListUtils';
import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
import tokenizedSearch from '@libs/tokenizedSearch';
@@ -42,6 +43,8 @@ function UnshareBankAccount({route}: ShareBankAccountProps) {
const {translate} = useLocalize();
const admins = bankAccountList?.[bankAccountID]?.accountData?.sharees;
const totalAdmins = bankAccountList?.[bankAccountID]?.accountData?.sharees?.length;
+ const error = getLatestErrorMessage(bankAccountList?.[bankAccountID] ?? {});
+ const isExpensifyCardError = error?.includes(CONST.EXPENSIFY_CARD.BANK);
const isExpensifyCardSettlementAccount = bankAccountList?.[bankAccountID]?.isExpensifyCardSettlementAccount ?? false;
const shouldShowTextInput = Number(totalAdmins) >= CONST.STANDARD_LIST_ITEM_LIMIT;
const textInputLabel = shouldShowTextInput ? translate('common.search') : undefined;
@@ -57,6 +60,14 @@ function UnshareBankAccount({route}: ShareBankAccountProps) {
}
}, [totalAdmins, shouldShowSuccess]);
+ useEffect(() => {
+ if (!isExpensifyCardError) {
+ return;
+ }
+ setUnshareUser(undefined);
+ setShowExpensifyCardErrorModal(true);
+ }, [isExpensifyCardError]);
+
const handleUnshare = () => {
if (!bankAccountID || !unshareUser?.login) {
return;
@@ -101,7 +112,10 @@ function UnshareBankAccount({route}: ShareBankAccountProps) {
return adminsToDisplay;
};
- const hideUnshareErrorModal = () => setShowExpensifyCardErrorModal(false);
+ const hideUnshareErrorModal = () => {
+ clearUnshareBankAccountErrors(Number(bankAccountID));
+ setShowExpensifyCardErrorModal(false);
+ };
const itemRightSideComponent = (item: ListItem) => {
return (
@@ -148,7 +162,7 @@ function UnshareBankAccount({route}: ShareBankAccountProps) {
rightHandSideComponent={itemRightSideComponent}
footerContent={
clearUnshareBankAccountErrors(Number(bankAccountID))}
/>
diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
index 8fb4d87a4497..84b23b069512 100644
--- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
+++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
@@ -121,7 +121,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
const onPressAutoReportingFrequency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_AUTOREPORTING_FREQUENCY.getRoute(route.params.policyID)), [route.params.policyID]);
const fetchData = useCallback(() => {
- openPolicyWorkflowsPage(route.params.policyID);
+ openPolicyWorkflowsPage(route.params.policyID, true);
getPaymentMethods(true);
}, [route.params.policyID]);
@@ -460,7 +460,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
brickRoadIndicator={hasReimburserError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
/>
)}
- {shouldShowBankAccount && !isAccountInSetupState && (
+ {shouldShowBankAccount && (
;
};
@@ -44,47 +59,60 @@ type MembersSection = Section;
function WorkspaceWorkflowsPayerPage({route, policy, personalDetails, isLoadingReportData = true}: WorkspaceWorkflowsPayerPageProps) {
const {translate, formatPhoneNumber} = useLocalize();
const policyName = policy?.name ?? '';
+ const policyID = policy?.id;
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
+ const bankAccountConnectedToWorkspace = Object.values(bankAccountList ?? {}).find((account) => account?.accountData?.additionalData?.policyID === policyID);
+ const policyBankAccountID = policy?.achAccount?.bankAccountID;
+ const bankAccountFromList = policyBankAccountID ? bankAccountList?.[policyBankAccountID] : undefined;
+ const bankAccountInfo = bankAccountFromList ?? bankAccountConnectedToWorkspace;
+ const bankAccountID = policyBankAccountID ?? bankAccountInfo?.accountData?.bankAccountID;
+ const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
const {isOffline} = useNetwork();
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE);
const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar'] as const);
const [searchTerm, setSearchTerm] = useState('');
+ const [sharedBankAccountData] = useOnyx(ONYXKEYS.SHARE_BANK_ACCOUNT);
+ const [selectedPayer, setSelectedPayer] = useState(policy?.achAccount?.reimburser);
+ const shouldShowSuccess = sharedBankAccountData?.shouldShowSuccess ?? false;
+ const styles = useThemeStyles();
+ const {showConfirmModal} = useConfirmModal();
+ const isLoading = sharedBankAccountData?.isLoading ?? false;
+ const [isAlertVisible, setIsAlertVisible] = useState(false);
+ const [showValidationModal, setShowValidationModal] = useState(false);
+ const [showErrorModal, setShowErrorModal] = useState(false);
+ const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList);
+ const selectedPayerDetails = selectedPayer ? getPersonalDetailByEmail(selectedPayer) : undefined;
+ const ownerDetails = policy?.owner ? getPersonalDetailByEmail(policy?.owner) : undefined;
+ const accountID = selectedPayer ? policyMemberEmailsToAccountIDs?.[selectedPayer] : '';
+ const authorizedPayerEmail = personalDetails?.[accountID]?.login ?? '';
- const isDeletedPolicyEmployee = useCallback(
- (policyEmployee: PolicyEmployee) => !isOffline && policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyEmployee.errors),
- [isOffline],
- );
+ const isDeletedPolicyEmployee = (policyEmployee: PolicyEmployee) =>
+ !isOffline && policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyEmployee.errors);
- const [formattedPolicyAdmins, formattedAuthorizedPayer] = useMemo(() => {
+ const getPayersAndAdmins = () => {
const policyAdminDetails: MemberOption[] = [];
const authorizedPayerDetails: MemberOption[] = [];
-
- const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList);
-
for (const [email, policyEmployee] of Object.entries(policy?.employeeList ?? {})) {
- const accountID = policyMemberEmailsToAccountIDs?.[email] ?? '';
- const details = personalDetails?.[accountID];
+ const adminAccountID = policyMemberEmailsToAccountIDs?.[email] ?? '';
+ const details = personalDetails?.[adminAccountID];
if (!details) {
- Log.hmmm(`[WorkspaceMembersPage] no personal details found for policy member with accountID: ${accountID}`);
+ Log.hmmm(`[WorkspaceMembersPage] no personal details found for policy member with accountID: ${adminAccountID}`);
continue;
}
-
const isOwner = policy?.owner === details?.login;
const isAdmin = policyEmployee.role === CONST.POLICY.ROLE.ADMIN;
const shouldSkipMember = isDeletedPolicyEmployee(policyEmployee) || isExpensifyTeam(details?.login) || (!isOwner && !isAdmin);
-
if (shouldSkipMember) {
continue;
}
-
const roleBadge = ;
-
- const isAuthorizedPayer = policy?.achAccount?.reimburser === details?.login;
-
+ const isAuthorizedPayer = selectedPayer === details?.login;
const formattedMember = {
- keyForList: String(accountID),
- accountID,
+ keyForList: String(adminAccountID),
+ accountID: adminAccountID,
isSelected: isAuthorizedPayer,
- isDisabled: policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyEmployee.errors),
+ isDisabled: policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyEmployee.errors) || isLoading,
text: formatPhoneNumber(getDisplayNameOrDefault(details)),
alternateText: formatPhoneNumber(details?.login ?? ''),
rightElement: roleBadge,
@@ -93,13 +121,12 @@ function WorkspaceWorkflowsPayerPage({route, policy, personalDetails, isLoadingR
source: details.avatar ?? icons.FallbackAvatar,
name: formatPhoneNumber(details?.login ?? ''),
type: CONST.ICON_TYPE_AVATAR,
- id: accountID,
+ id: adminAccountID,
},
],
errors: policyEmployee.errors,
pendingAction: (policyEmployee.pendingAction ?? isAuthorizedPayer) ? policy?.pendingFields?.reimburser : null,
};
-
if (isAuthorizedPayer) {
authorizedPayerDetails.push(formattedMember);
} else {
@@ -107,25 +134,15 @@ function WorkspaceWorkflowsPayerPage({route, policy, personalDetails, isLoadingR
}
}
return [policyAdminDetails, authorizedPayerDetails];
- }, [
- policy?.employeeList,
- policy?.owner,
- policy?.achAccount?.reimburser,
- policy?.pendingFields?.reimburser,
- personalDetails,
- isDeletedPolicyEmployee,
- translate,
- formatPhoneNumber,
- icons.FallbackAvatar,
- ]);
-
- const sections: MembersSection[] = useMemo(() => {
- const sectionsArray: MembersSection[] = [];
+ };
+
+ const [formattedPolicyAdmins, formattedAuthorizedPayer] = getPayersAndAdmins();
+ const getSections = () => {
+ const sectionsArray: MembersSection[] = [];
if (searchTerm !== '') {
const searchValue = getSearchValueForPhoneOrEmail(searchTerm, countryCode);
const filteredOptions = tokenizedSearch([...formattedPolicyAdmins, ...formattedAuthorizedPayer], searchValue, (option) => [option.text ?? '', option.login ?? '']);
-
return [
{
title: undefined,
@@ -134,52 +151,107 @@ function WorkspaceWorkflowsPayerPage({route, policy, personalDetails, isLoadingR
},
];
}
-
sectionsArray.push({
data: formattedAuthorizedPayer,
sectionIndex: 1,
});
-
sectionsArray.push({
title: translate('workflowsPayerPage.admins'),
data: formattedPolicyAdmins,
sectionIndex: 2,
});
return sectionsArray;
- }, [searchTerm, formattedAuthorizedPayer, translate, formattedPolicyAdmins, countryCode]);
-
- const headerMessage = useMemo(
- () => (searchTerm && !sections.at(0)?.data.length ? translate('common.noResultsFound') : ''),
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [translate, sections],
- );
+ };
- const setPolicyAuthorizedPayer = (member: MemberOption) => {
- const authorizedPayerEmail = personalDetails?.[member.accountID]?.login ?? '';
+ const sections: MembersSection[] = getSections();
+ const headerMessage = searchTerm && !sections.at(0)?.data.length ? translate('common.noResultsFound') : '';
+ const handleConfirm = () => {
+ if (!bankAccountID || !authorizedPayerEmail || !accountID || !policyID) {
+ return;
+ }
+ if (!selectedPayer) {
+ setIsAlertVisible(true);
+ return;
+ }
if (policy?.achAccount?.reimburser === authorizedPayerEmail || policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES) {
Navigation.goBack();
return;
}
+ shareBankAccountAndSetPayer(Number(bankAccountID), accountID, policyID);
+ };
+ const onButtonPress = () => {
+ if (!selectedPayer || !policy || !authorizedPayerEmail) {
+ Navigation.closeRHPFlow();
+ return;
+ }
setWorkspacePayer(policy?.id, authorizedPayerEmail);
- Navigation.goBack();
+ Navigation.closeRHPFlow();
};
- // eslint-disable-next-line rulesdir/no-negated-variables
- const shouldShowNotFoundPage = useMemo(
- () => (isEmptyObject(policy) && !isLoadingReportData) || isPendingDeletePolicy(policy) || policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES,
- [policy, isLoadingReportData],
- );
+ const handleShareBankAccount = () => {
+ // No payer selected — nothing to share with
+ if (!selectedPayer) {
+ return;
+ }
+ const isSelectedPayerOwner = policy?.owner === selectedPayer;
+ const isSelectedAlreadyAPayer = policy?.achAccount?.reimburser === selectedPayer;
+ const isAccountAlreadyShared = bankAccountInfo?.accountData?.sharees ? bankAccountInfo?.accountData.sharees.includes(selectedPayer) : false;
+ const isAccountAlreadySharedOnMainBankAccount = policy?.achAccount?.sharees ? policy?.achAccount.sharees.includes(selectedPayer) : false;
- const totalNumberOfEmployeesEitherOwnerOrAdmin = useMemo(() => {
- return Object.entries(policy?.employeeList ?? {}).filter(([email, policyEmployee]) => {
- const isOwner = policy?.owner === email;
- const isAdmin = policyEmployee.role === CONST.POLICY.ROLE.ADMIN;
- return !isDeletedPolicyEmployee(policyEmployee) && (isOwner || isAdmin);
+ // Selected payer already has access (owner, reimburser, or sharee) — proceed without sharing
+ if (isAccountAlreadyShared || isSelectedPayerOwner || isSelectedAlreadyAPayer || isAccountAlreadySharedOnMainBankAccount) {
+ onButtonPress();
+ return;
+ }
+
+ // Bank account setup incomplete — block and show validation
+ if (isBankAccountPartiallySetup(bankAccountInfo?.accountData?.state)) {
+ setShowValidationModal(true);
+ return;
+ }
+ const isAccountAlreadySharedWithCurrentUser =
+ bankAccountInfo?.accountData?.sharees && currentUserPersonalDetails?.login ? bankAccountInfo?.accountData?.sharees.includes(currentUserPersonalDetails?.login) : false;
+ const isOwner = policy?.owner === currentUserPersonalDetails?.login;
+
+ // Current user has no right to share (not owner and not a sharee) — show error
+ if (!isOwner && !isAccountAlreadyShared && !isAccountAlreadySharedWithCurrentUser) {
+ setShowErrorModal(true);
+ return;
+ }
+ showConfirmModal({
+ title: translate('workflowsPayerPage.shareBankAccount.shareTitle'),
+ success: true,
+ confirmText: translate('common.share'),
+ prompt: (
+
+
+
+ ),
+ }).then((result) => {
+ // User dismissed or cancelled the confirm modal — do not share
+ if (result.action !== ModalActions.CONFIRM) {
+ return;
+ }
+ handleConfirm();
});
- }, [isDeletedPolicyEmployee, policy?.employeeList, policy?.owner]);
+ };
+
+ const setPolicyAuthorizedPayer = (member: MemberOption) => setSelectedPayer(personalDetails?.[member.accountID]?.login);
+
+ const shouldShowBlockingPage =
+ (isEmptyObject(policy) && !isLoadingReportData) || isPendingDeletePolicy(policy) || policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES;
+
+ const totalNumberOfEmployeesEitherOwnerOrAdmin = Object.entries(policy?.employeeList ?? {}).filter(([email, policyEmployee]) => {
+ const isOwner = policy?.owner === email;
+ const isAdmin = policyEmployee.role === CONST.POLICY.ROLE.ADMIN;
+ return !isDeletedPolicyEmployee(policyEmployee) && (isOwner || isAdmin);
+ });
const shouldShowSearchInput = totalNumberOfEmployeesEitherOwnerOrAdmin.length >= CONST.STANDARD_LIST_ITEM_LIMIT;
@@ -190,7 +262,8 @@ function WorkspaceWorkflowsPayerPage({route, policy, personalDetails, isLoadingR
featureName={CONST.POLICY.MORE_FEATURES.ARE_WORKFLOWS_ENABLED}
>
-
+ {shouldShowSuccess && selectedPayer ? (
+
+ ) : (
+
+ }
+ containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto]}
+ />
+ }
+ />
+ )}
+ {
+ setShowValidationModal(false);
+ }}
+ success
+ onCancel={() => setShowValidationModal(false)}
+ prompt={
+
+ {
+ setShowValidationModal(false);
+ navigateToBankAccountRoute({policyID, backTo: ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)});
+ }}
+ html={translate('workflowsPayerPage.shareBankAccount.validationDescription', {
+ admin: selectedPayerDetails?.displayName ?? '',
+ })}
+ />
+
+ }
+ shouldShowCancelButton={false}
+ confirmText={translate('common.buttonConfirm')}
+ />
+ setShowErrorModal(false)}
+ onConfirm={() => {
+ setShowErrorModal(false);
+ }}
+ success
+ prompt={
+
+ {
+ if (!currentUserPersonalDetails?.accountID || !policy?.ownerAccountID) {
+ return;
+ }
+ setShowErrorModal(false);
+ navigateToAndOpenReportWithAccountIDs([policy.ownerAccountID], currentUserPersonalDetails.accountID, introSelected);
+ }}
+ html={translate('workflowsPayerPage.shareBankAccount.errorDescription', {
+ admin: selectedPayerDetails?.displayName ?? '',
+ owner: ownerDetails?.displayName ?? '',
+ })}
+ />
+
+ }
+ shouldShowCancelButton={false}
+ confirmText={translate('common.buttonConfirm')}
+ />
);
}
diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerSuccessPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerSuccessPage.tsx
new file mode 100644
index 000000000000..341b33ad9db6
--- /dev/null
+++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerSuccessPage.tsx
@@ -0,0 +1,50 @@
+import React, {useEffect} from 'react';
+import ConfirmationPage from '@components/ConfirmationPage';
+import ScrollView from '@components/ScrollView';
+import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import {clearShareBankAccount} from '@userActions/BankAccounts';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+function WorkspaceWorkflowsPayerSuccessPage() {
+ const {translate} = useLocalize();
+ const [sharedBankAccountData] = useOnyx(ONYXKEYS.SHARE_BANK_ACCOUNT);
+ const shouldShowSuccess = sharedBankAccountData?.shouldShowSuccess ?? false;
+ const styles = useThemeStyles();
+ const illustrations = useMemoizedLazyIllustrations(['ShareBank', 'Telescope']);
+
+ useEffect(() => {
+ return () => {
+ if (!shouldShowSuccess) {
+ return;
+ }
+ clearShareBankAccount();
+ };
+ }, [shouldShowSuccess]);
+
+ const onButtonPress = () => Navigation.closeRHPFlow();
+
+ return (
+
+
+
+ );
+}
+
+export default WorkspaceWorkflowsPayerSuccessPage;
diff --git a/tests/actions/ShareBankAccountAndWorkspacePayerTest.ts b/tests/actions/ShareBankAccountAndWorkspacePayerTest.ts
new file mode 100644
index 000000000000..b1e72cd8c44c
--- /dev/null
+++ b/tests/actions/ShareBankAccountAndWorkspacePayerTest.ts
@@ -0,0 +1,133 @@
+import Onyx from 'react-native-onyx';
+import {shareBankAccountAndSetPayer} from '@libs/actions/BankAccounts';
+import {setWorkspacePayer} from '@libs/actions/Policy/Policy';
+import {write} from '@libs/API';
+import {WRITE_COMMANDS} from '@libs/API/types';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
+
+jest.mock('@libs/API', () => ({
+ write: jest.fn(),
+}));
+
+describe('actions/ShareBankAccountAndWorkspacePayer', () => {
+ beforeAll(() => {
+ Onyx.init({
+ keys: ONYXKEYS,
+ });
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ return Onyx.clear().then(waitForBatchedUpdates);
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ describe('shareBankAccountAndSetPayer', () => {
+ it('should call API.write with ShareBankAccountAndUpdatePolicyReimburser command and correct parameters', async () => {
+ const bankAccountID = 123;
+ const shareeAccountID = 456;
+ const policyID = 'policy_789';
+
+ shareBankAccountAndSetPayer(bankAccountID, shareeAccountID, policyID);
+ await waitForBatchedUpdates();
+
+ expect(write).toHaveBeenCalledWith(
+ WRITE_COMMANDS.SHARE_BANK_ACCOUNT_AND_UPDATE_POLICY_REIMBURSER,
+ {
+ bankAccountID,
+ shareeAccountID,
+ policyID,
+ },
+ expect.objectContaining({
+ optimisticData: expect.arrayContaining([
+ expect.objectContaining({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.SHARE_BANK_ACCOUNT,
+ value: expect.objectContaining({
+ isLoading: true,
+ errors: null,
+ }),
+ }),
+ ]),
+ successData: expect.arrayContaining([
+ expect.objectContaining({
+ key: ONYXKEYS.SHARE_BANK_ACCOUNT,
+ value: expect.objectContaining({
+ isLoading: false,
+ errors: null,
+ admins: null,
+ shouldShowSuccess: true,
+ }),
+ }),
+ ]),
+ failureData: expect.arrayContaining([
+ expect.objectContaining({
+ key: ONYXKEYS.SHARE_BANK_ACCOUNT,
+ value: expect.objectContaining({
+ isLoading: false,
+ }),
+ }),
+ ]),
+ }),
+ );
+ });
+ });
+
+ describe('setWorkspacePayer', () => {
+ it('should call API.write with SetWorkspacePayer command and correct parameters', async () => {
+ const policyID = 'policy_abc';
+ const reimburserEmail = 'payer@example.com';
+
+ setWorkspacePayer(policyID, reimburserEmail);
+ await waitForBatchedUpdates();
+
+ expect(write).toHaveBeenCalledWith(
+ WRITE_COMMANDS.SET_WORKSPACE_PAYER,
+ {
+ policyID,
+ reimburserEmail,
+ },
+ expect.objectContaining({
+ optimisticData: expect.arrayContaining([
+ expect.objectContaining({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: expect.objectContaining({
+ reimburser: reimburserEmail,
+ achAccount: {reimburser: reimburserEmail},
+ errorFields: {reimburser: null},
+ pendingFields: {reimburser: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
+ }),
+ }),
+ ]),
+ successData: expect.arrayContaining([
+ expect.objectContaining({
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: expect.objectContaining({
+ errorFields: {reimburser: null},
+ pendingFields: {reimburser: null},
+ }),
+ }),
+ ]),
+ failureData: expect.arrayContaining([
+ expect.objectContaining({
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: expect.objectContaining({
+ // Error object shape from ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey
+ errorFields: expect.objectContaining({
+ reimburser: expect.anything(), // eslint-disable-line @typescript-eslint/no-unsafe-assignment -- Jest matcher
+ }),
+ pendingFields: {reimburser: null},
+ }),
+ }),
+ ]),
+ }),
+ );
+ });
+ });
+});