diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ffa163f75107..9983abc0b699 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1040,6 +1040,9 @@ const ONYXKEYS = { NVP_EXPENSIFY_REPORT_PDF_FILENAME: 'nvp_expensify_report_PDFFilename_', + /** Marker written when the user has already requested to unlock a locked business bank account */ + NVP_LOCKED_VBA_UNLOCK_REQUESTED: 'nvp_expensify_vbaUnlockRequested_', + /** The last submission method (Submit / Submit via PDF) the user chose on a given workspace, so the Submit button can default to it. Keyed by policyID. */ NVP_PREFERRED_REPORT_SUBMISSION_METHOD: 'preferredReportSubmissionMethod_', @@ -1557,6 +1560,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.SELECTED_DISTANCE_REQUEST_TAB]: OnyxTypes.SelectedTabRequest; [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; [ONYXKEYS.COLLECTION.NVP_EXPENSIFY_REPORT_PDF_FILENAME]: string; + [ONYXKEYS.COLLECTION.NVP_LOCKED_VBA_UNLOCK_REQUESTED]: string; [ONYXKEYS.COLLECTION.NVP_PREFERRED_REPORT_SUBMISSION_METHOD]: ValueOf; [ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember; [ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress; diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index 0157b263dbcb..5e0a69e830ce 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -26,7 +26,7 @@ import useVerifyAccountAndResume from '@hooks/useVerifyAccountAndResume'; import {createWorkspace, generateDefaultWorkspaceName, isCurrencySupportedForDirectReimbursement, isCurrencySupportedForGlobalReimbursement} from '@libs/actions/Policy/Policy'; import {navigateToBankAccountRoute} from '@libs/actions/ReimbursementAccount'; import {getLastPolicyBankAccountID, getLastPolicyPaymentMethod} from '@libs/actions/Search'; -import {isBankAccountPartiallySetup} from '@libs/BankAccountUtils'; +import {isBankAccountPartiallySetup, showUnlockAlreadyRequestedModal} from '@libs/BankAccountUtils'; import Navigation from '@libs/Navigation/Navigation'; import {formatPaymentMethods, getActivePaymentType, getBusinessBankAccountOptions, matchesCurrency} from '@libs/PaymentUtils'; import {isPaidGroupPolicy, isPolicyAdmin, sortPoliciesByName} from '@libs/PolicyUtils'; @@ -158,6 +158,9 @@ function SettlementButton({ const {isBetaEnabled} = usePermissions(); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); + // eslint-disable-next-line rulesdir/no-default-id-values + const [unlockRequestedAt] = useOnyx(`${ONYXKEYS.COLLECTION.NVP_LOCKED_VBA_UNLOCK_REQUESTED}${policy?.achAccount?.bankAccountID ?? CONST.DEFAULT_NUMBER_ID}`); + const [initiatingBankAccountUnlock] = useOnyx(ONYXKEYS.INITIATING_BANK_ACCOUNT_UNLOCK); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const delegateAccountID = useDelegateAccountID(); @@ -183,26 +186,30 @@ function SettlementButton({ // interrupted by account validation resumes, since the validation gate skipped them. const checkForPostValidationBlockers = () => { if (isBankAccountLocked) { - showConfirmModal({ - title: translate('bankAccount.lockedBankAccount'), - prompt: ( - - - - ), - confirmText: translate('bankAccount.unlockBankAccount'), - cancelText: translate('common.cancel'), - shouldDisableConfirmButtonWhenOffline: true, - }).then(({action}) => { - if (action !== ModalActions.CONFIRM) { - return; - } - if (policy?.achAccount?.bankAccountID === undefined) { - return; - } - pressLockedBankAccount(policy?.achAccount?.bankAccountID, translate, conciergeReportID, delegateAccountID); - navigateToConciergeChat({conciergeReportID, introSelected, currentUserAccountID, isSelfTourViewed, betas}); - }); + if (unlockRequestedAt) { + showUnlockAlreadyRequestedModal(showConfirmModal, translate); + } else { + showConfirmModal({ + title: translate('bankAccount.lockedBankAccount'), + prompt: ( + + + + ), + confirmText: translate('bankAccount.unlockBankAccount'), + cancelText: translate('common.cancel'), + shouldDisableConfirmButtonWhenOffline: true, + }).then(({action}) => { + if (action !== ModalActions.CONFIRM) { + return; + } + if (policy?.achAccount?.bankAccountID === undefined) { + return; + } + pressLockedBankAccount(policy?.achAccount?.bankAccountID, translate, conciergeReportID, delegateAccountID, initiatingBankAccountUnlock); + navigateToConciergeChat({conciergeReportID, introSelected, currentUserAccountID, isSelfTourViewed, betas}); + }); + } return true; } diff --git a/src/languages/de.ts b/src/languages/de.ts index e13a339a1636..36eb0af53ad2 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -3910,6 +3910,8 @@ ${amount} für ${merchant} – ${date}`, 'Dieses Bankkonto kann nicht gelöscht werden, da es für Zahlungen mit der Expensify Karte verwendet wird. Wenn Sie dieses Konto trotzdem löschen möchten, wenden Sie sich bitte an Concierge.', sameDepositAndWithdrawalAccount: 'Die Einzahlungs- und Auszahlungskonten sind identisch.', }, + unlockAlreadyRequestedTitle: 'Anfrage bereits eingereicht', + unlockAlreadyRequestedDescription: 'Ihre Anfrage zur Entsperrung dieses Bankkontos wurde bereits gesendet. Concierge meldet sich bei Ihnen, falls noch etwas benötigt wird.', }, addPersonalBankAccount: { countrySelectionStepHeader: 'Wo befindet sich dein Bankkonto?', diff --git a/src/languages/el.ts b/src/languages/el.ts index 9316bf5535d2..ce5797026c53 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -3964,6 +3964,9 @@ ${amount} για ${merchant} - ${date}`, 'Αυτός ο τραπεζικός λογαριασμός δεν μπορεί να διαγραφεί επειδή χρησιμοποιείται για πληρωμές με την Κάρτα Expensify. Αν εξακολουθείτε να θέλετε να διαγράψετε αυτόν τον λογαριασμό, παρακαλούμε επικοινωνήστε με το Concierge.', sameDepositAndWithdrawalAccount: 'Οι λογαριασμοί κατάθεσης και ανάληψης είναι οι ίδιοι.', }, + unlockAlreadyRequestedTitle: 'Το αίτημα έχει ήδη υποβληθεί', + unlockAlreadyRequestedDescription: + 'Το αίτημά σας για ξεκλείδωμα αυτού του τραπεζικού λογαριασμού έχει ήδη αποσταλεί. Το Concierge θα επικοινωνήσει μαζί σας αν χρειαστεί οτιδήποτε άλλο.', }, addPersonalBankAccount: { countrySelectionStepHeader: 'Πού βρίσκεται ο τραπεζικός σας λογαριασμός;', diff --git a/src/languages/en.ts b/src/languages/en.ts index 202d778c81b4..1bd6bd460f63 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3975,6 +3975,8 @@ const translations = { lockedBankAccount: 'Locked bank account', unlockBankAccount: 'Unlock bank account', youCantPayThis: `You can't pay this report because you have a locked bank account. Tap below and Concierge will help with the next steps to unlock it.`, + unlockAlreadyRequestedTitle: 'Request already submitted', + unlockAlreadyRequestedDescription: 'Your request to unlock this bank account has already been sent. Concierge will reach out if anything else is needed.', htmlUnlockMessage: (maskedAccountNumber: string) => `

Expensify Business Bank Account ${maskedAccountNumber}

Thank you for submitting a request to unlock your bank account. Withdrawal requests can be rejected due to insufficient funds, or if the bank account has not been enabled for direct debit. We will review your case and reach out to you if we need anything else to resolve this issue.

`, textUnlockMessage: (maskedAccountNumber: string) => diff --git a/src/languages/es.ts b/src/languages/es.ts index 5dd472c89df1..9e17311d5d6f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3832,6 +3832,8 @@ ${amount} para ${merchant} - ${date}`, 'Esta cuenta bancaria no se puede eliminar porque se utiliza para pagos con la tarjeta Expensify. Si aún deseas eliminar esta cuenta, por favor contacta con Concierge.', sameDepositAndWithdrawalAccount: 'Las cuentas de depósito y retiro son las mismas.', }, + unlockAlreadyRequestedTitle: 'Solicitud ya enviada', + unlockAlreadyRequestedDescription: 'Tu solicitud para desbloquear esta cuenta bancaria ya ha sido enviada. Concierge se pondrá en contacto contigo si se necesita algo más.', }, addPersonalBankAccount: { countrySelectionStepHeader: '¿Dónde está ubicada tu cuenta bancaria?', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 0e0e4175a027..b98530943420 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -3916,6 +3916,8 @@ ${amount} pour ${merchant} - ${date}`, 'Ce compte bancaire ne peut pas être supprimé car il est utilisé pour les paiements par Carte Expensify. Si vous souhaitez tout de même supprimer ce compte, veuillez contacter Concierge.', sameDepositAndWithdrawalAccount: 'Les comptes de dépôt et de retrait sont identiques.', }, + unlockAlreadyRequestedTitle: 'Demande déjà soumise', + unlockAlreadyRequestedDescription: 'Votre demande de déverrouillage de ce compte bancaire a déjà été envoyée. Concierge vous contactera si autre chose est nécessaire.', }, addPersonalBankAccount: { countrySelectionStepHeader: 'Où se situe votre compte bancaire ?', diff --git a/src/languages/it.ts b/src/languages/it.ts index 695af9a26c9f..0db69943e75d 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -3890,6 +3890,8 @@ ${amount} per ${merchant} - ${date}`, 'Questo conto bancario non può essere eliminato perché viene utilizzato per i pagamenti con Carta Expensify. Se desideri comunque eliminare questo conto, contatta Concierge.', sameDepositAndWithdrawalAccount: 'I conti di deposito e prelievo sono gli stessi.', }, + unlockAlreadyRequestedTitle: 'Richiesta già inviata', + unlockAlreadyRequestedDescription: 'La tua richiesta di sblocco di questo conto bancario è già stata inviata. Concierge ti contatterà se servirà altro.', }, addPersonalBankAccount: { countrySelectionStepHeader: 'Dove si trova il tuo conto bancario?', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 4f586cd415db..81629b494494 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -3858,6 +3858,8 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' deletePaymentBankAccount: 'この銀行口座は Expensify カードの支払いに使用されているため、削除できません。この口座を削除したい場合は、Concierge までご連絡ください。', sameDepositAndWithdrawalAccount: '入金口座と出金口座が同じです。', }, + unlockAlreadyRequestedTitle: 'リクエストは既に送信されています', + unlockAlreadyRequestedDescription: 'この銀行口座のロック解除リクエストは既に送信されています。追加で必要なことがある場合は、Concierge からご連絡します。', }, addPersonalBankAccount: { countrySelectionStepHeader: '銀行口座はどこにありますか?', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 8106da1265ca..6323e3d2eb3e 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -3891,6 +3891,8 @@ ${amount} voor ${merchant} - ${date}`, 'Deze bankrekening kan niet worden verwijderd omdat hij wordt gebruikt voor betalingen met de Expensify Kaart. Als je deze rekening toch wilt verwijderen, neem dan contact op met Concierge.', sameDepositAndWithdrawalAccount: 'De stortings- en opname­rekeningen zijn hetzelfde.', }, + unlockAlreadyRequestedTitle: 'Verzoek al ingediend', + unlockAlreadyRequestedDescription: 'Je verzoek om deze bankrekening te deblokkeren is al verzonden. Concierge neemt contact met je op als er nog iets anders nodig is.', }, addPersonalBankAccount: { countrySelectionStepHeader: 'Waar is je bankrekening gevestigd?', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 286eafbed75e..32726211b106 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -3919,6 +3919,8 @@ ${amount} dla ${merchant} - ${date}`, 'To konto bankowe nie może zostać usunięte, ponieważ jest używane do płatności Kartą Expensify. Jeśli mimo to chcesz usunąć to konto, skontaktuj się z Concierge.', sameDepositAndWithdrawalAccount: 'Konta wpłat i wypłat są takie same.', }, + unlockAlreadyRequestedTitle: 'Wniosek został już złożony', + unlockAlreadyRequestedDescription: 'Twoja prośba o odblokowanie tego konta bankowego została już wysłana. Concierge skontaktuje się z tobą, jeśli będzie potrzebne coś jeszcze.', }, addPersonalBankAccount: { countrySelectionStepHeader: 'Gdzie znajduje się Twoje konto bankowe?', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 31a5427d02f1..103dda4f4b20 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -3879,6 +3879,8 @@ ${amount} para ${merchant} - ${date}`, 'Essa conta bancária não pode ser excluída porque é usada para pagamentos do Cartão Expensify. Se ainda assim quiser excluir essa conta, entre em contato com o Concierge.', sameDepositAndWithdrawalAccount: 'As contas de depósito e saque são as mesmas.', }, + unlockAlreadyRequestedTitle: 'Solicitação já enviada', + unlockAlreadyRequestedDescription: 'Sua solicitação para desbloquear esta conta bancária já foi enviada. O Concierge vai entrar em contato se for necessário mais alguma coisa.', }, addPersonalBankAccount: { countrySelectionStepHeader: 'Onde fica localizada a sua conta bancária?', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index b094806bdab1..445d7314b48c 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -3765,6 +3765,8 @@ ${amount},商户:${merchant} - 日期:${date}`, deletePaymentBankAccount: '此银行账户无法删除,因为它被用于 Expensify 卡付款。如果您仍希望删除此账户,请联系 Concierge。', sameDepositAndWithdrawalAccount: '存款账户和取款账户相同。', }, + unlockAlreadyRequestedTitle: '请求已提交', + unlockAlreadyRequestedDescription: '您解锁此银行账户的请求已发送。如需其他信息,Concierge 会与您联系。', }, addPersonalBankAccount: { countrySelectionStepHeader: '您的银行账户位于哪个国家/地区?', diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index 68b8799a73b5..f34ad0e810ce 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -1,3 +1,7 @@ +import type {LocaleContextProps} from '@components/LocaleContextProvider'; + +import type useConfirmModal from '@hooks/useConfirmModal'; + import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; @@ -284,6 +288,15 @@ function getRequiredKYBDocuments(externalApiResponses: KYBVerificationResponses) return requiredDocuments; } +function showUnlockAlreadyRequestedModal(showConfirmModal: ReturnType['showConfirmModal'], translate: LocaleContextProps['translate']) { + showConfirmModal({ + title: translate('bankAccount.unlockAlreadyRequestedTitle'), + prompt: translate('bankAccount.unlockAlreadyRequestedDescription'), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + }); +} + export { getBankAccountSearchLabel, isFilterableBankAccount, @@ -301,6 +314,7 @@ export { doesPolicyHavePartiallySetupBankAccount, isPersonalBankAccountMissingInfo, getCompletedStepsForBankAccount, + showUnlockAlreadyRequestedModal, PERSONAL_INFO_STEP, }; export type {BankAccountConnectionStatus, KYBVerificationResponses}; diff --git a/src/libs/ExportOnyxState/common.ts b/src/libs/ExportOnyxState/common.ts index 4971d9f0f545..29674c168a42 100644 --- a/src/libs/ExportOnyxState/common.ts +++ b/src/libs/ExportOnyxState/common.ts @@ -382,6 +382,7 @@ const onyxKeysToMaskFragileData = new Set([ ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD, ONYXKEYS.COLLECTION.MERGE_TRANSACTION, ONYXKEYS.COLLECTION.NVP_EXPENSIFY_ON_CARD_WAITLIST, + ONYXKEYS.COLLECTION.NVP_LOCKED_VBA_UNLOCK_REQUESTED, ONYXKEYS.COLLECTION.PASSKEY_CREDENTIALS, ONYXKEYS.COLLECTION.PENDING_CONCIERGE_RESPONSE, ONYXKEYS.COLLECTION.POLICY_CATEGORIES, diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index e9a25dc55adf..ed47f8dd45ae 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -40,7 +40,7 @@ import ROUTES, {DYNAMIC_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 {BankAccountList, LastPaymentMethod, LastPaymentMethodType, PersonalBankAccount} from '@src/types/onyx'; +import type {BankAccountList, InitiatingBankAccountUnlock, LastPaymentMethod, LastPaymentMethodType, PersonalBankAccount} from '@src/types/onyx'; import type {BankAccountAdditionalData} from '@src/types/onyx/BankAccount'; import type PlaidBankAccount from '@src/types/onyx/PlaidBankAccount'; import type {BankAccountStep, ReimbursementAccountStep, ReimbursementAccountSubStep} from '@src/types/onyx/ReimbursementAccount'; @@ -1766,7 +1766,9 @@ function openBankAccountSharePage() { function initiateBankAccountUnlock(bankAccountID: number, conciergeReportID: string | undefined, optimisticReportActionID: string | null | undefined) { const authToken = NetworkStore.getAuthToken(); - const onyxData: OnyxData = { + const nvpUnlockRequestedKey = `${ONYXKEYS.COLLECTION.NVP_LOCKED_VBA_UNLOCK_REQUESTED}${bankAccountID}` as const; + + const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -1776,6 +1778,11 @@ function initiateBankAccountUnlock(bankAccountID: number, conciergeReportID: str isSuccess: false, }, }, + { + onyxMethod: Onyx.METHOD.SET, + key: nvpUnlockRequestedKey, + value: new Date().toISOString(), + }, ], successData: [ { @@ -1809,6 +1816,11 @@ function initiateBankAccountUnlock(bankAccountID: number, conciergeReportID: str errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), }, }, + { + onyxMethod: Onyx.METHOD.SET, + key: nvpUnlockRequestedKey, + value: null, + }, ...(optimisticReportActionID && conciergeReportID ? [ { @@ -1824,7 +1836,17 @@ function initiateBankAccountUnlock(bankAccountID: number, conciergeReportID: str return API.write(WRITE_COMMANDS.INITIATE_BANK_ACCOUNT_UNLOCK, {bankAccountID, authToken, optimisticReportActionID}, onyxData); } -function pressLockedBankAccount(bankAccountID: number, translate: LocalizedTranslate, conciergeReportID: string | undefined, delegateAccountID: number | undefined) { +function pressLockedBankAccount( + bankAccountID: number, + translate: LocalizedTranslate, + conciergeReportID: string | undefined, + delegateAccountID: number | undefined, + initiatingBankAccountUnlock: OnyxEntry, +) { + if (initiatingBankAccountUnlock?.isLoading && initiatingBankAccountUnlock?.bankAccountIDToUnlock === bankAccountID) { + return; + } + let optimisticReportActionID: string | undefined; if (conciergeReportID) { @@ -1866,6 +1888,9 @@ function pressLockedBankAccount(bankAccountID: number, translate: LocalizedTrans bankAccountIDToUnlock: bankAccountID, optimisticReportActionID: optimisticReportActionID ?? null, }); + + // Write the NVP immediately so the "already requested" guard fires on the next press. + Onyx.merge(`${ONYXKEYS.COLLECTION.NVP_LOCKED_VBA_UNLOCK_REQUESTED}${bankAccountID}`, new Date().toISOString()); } export { diff --git a/src/pages/home/TimeSensitiveSection/items/UnlockBankAccount.tsx b/src/pages/home/TimeSensitiveSection/items/UnlockBankAccount.tsx index 9053575830a5..d6e824b2c857 100644 --- a/src/pages/home/TimeSensitiveSection/items/UnlockBankAccount.tsx +++ b/src/pages/home/TimeSensitiveSection/items/UnlockBankAccount.tsx @@ -1,5 +1,6 @@ import BaseWidgetItem from '@components/BaseWidgetItem'; +import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDelegateAccountID from '@hooks/useDelegateAccountID'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -8,6 +9,7 @@ import useOnyx from '@hooks/useOnyx'; import {pressLockedBankAccount} from '@libs/actions/BankAccounts'; import {navigateToConciergeChat} from '@libs/actions/Report'; +import {showUnlockAlreadyRequestedModal} from '@libs/BankAccountUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -32,6 +34,9 @@ function UnlockBankAccount({bankAccountID, policyName}: UnlockBankAccountProps) const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const delegateAccountID = useDelegateAccountID(); + const [unlockRequestedAt] = useOnyx(`${ONYXKEYS.COLLECTION.NVP_LOCKED_VBA_UNLOCK_REQUESTED}${bankAccountID}`); + const [initiatingBankAccountUnlock] = useOnyx(ONYXKEYS.INITIATING_BANK_ACCOUNT_UNLOCK); + const {showConfirmModal} = useConfirmModal(); const title = policyName ? translate('homePage.timeSensitiveSection.unlockBankAccount.workspaceTitle') : translate('homePage.timeSensitiveSection.unlockBankAccount.personalTitle'); @@ -40,7 +45,11 @@ function UnlockBankAccount({bankAccountID, policyName}: UnlockBankAccountProps) : translate('homePage.timeSensitiveSection.unlockBankAccount.personalSubtitle'); const handleCtaPress = () => { - pressLockedBankAccount(bankAccountID, translate, conciergeReportID, delegateAccountID); + if (unlockRequestedAt) { + showUnlockAlreadyRequestedModal(showConfirmModal, translate); + return; + } + pressLockedBankAccount(bankAccountID, translate, conciergeReportID, delegateAccountID, initiatingBankAccountUnlock); navigateToConciergeChat({conciergeReportID, introSelected, currentUserAccountID, isSelfTourViewed, betas}); }; diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index ef85dfe6cd9e..256d5a085ab5 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -32,7 +32,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {isPersonalBankAccountMissingInfo} from '@libs/BankAccountUtils'; +import {isPersonalBankAccountMissingInfo, showUnlockAlreadyRequestedModal} from '@libs/BankAccountUtils'; import {hasDisplayableAssignedCards, isDirectFeed, maskCardNumber} from '@libs/CardUtils'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; @@ -98,6 +98,8 @@ function WalletPage() { const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const [betas] = useOnyx(ONYXKEYS.BETAS); + const [nvpLockedVbaUnlockRequested] = useOnyx(ONYXKEYS.COLLECTION.NVP_LOCKED_VBA_UNLOCK_REQUESTED); + const [initiatingBankAccountUnlock] = useOnyx(ONYXKEYS.INITIATING_BANK_ACCOUNT_UNLOCK); const delegateAccountID = useDelegateAccountID(); const isUserValidated = userAccount?.validated ?? false; const {isAccountLocked} = useLockedAccountState(); @@ -155,7 +157,11 @@ function WalletPage() { paymentMethodButtonRef.current = event?.currentTarget as HTMLDivElement; if (accountData?.state === CONST.BANK_ACCOUNT.STATE.LOCKED && accountData?.bankAccountID) { - pressLockedBankAccount(accountData?.bankAccountID, translate, conciergeReportID ?? undefined, delegateAccountID); + if (nvpLockedVbaUnlockRequested?.[`${ONYXKEYS.COLLECTION.NVP_LOCKED_VBA_UNLOCK_REQUESTED}${accountData.bankAccountID}`]) { + showUnlockAlreadyRequestedModal(showConfirmModal, translate); + return; + } + pressLockedBankAccount(accountData.bankAccountID, translate, conciergeReportID ?? undefined, delegateAccountID, initiatingBankAccountUnlock); navigateToConciergeChat({conciergeReportID: conciergeReportID ?? undefined, introSelected, currentUserAccountID, isSelfTourViewed, betas}); return; } diff --git a/src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx b/src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx index 488712f6f637..eac43e862317 100644 --- a/src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx +++ b/src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx @@ -25,7 +25,7 @@ import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearPolicyErrorField, isCurrencySupportedForDirectReimbursement, isCurrencySupportedForGlobalReimbursement, setWorkspaceReimbursement} from '@libs/actions/Policy/Policy'; -import {getBankAccountConnectionStatus, isBankAccountPartiallySetup} from '@libs/BankAccountUtils'; +import {getBankAccountConnectionStatus, isBankAccountPartiallySetup, showUnlockAlreadyRequestedModal} from '@libs/BankAccountUtils'; import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getPaymentMethodDescription} from '@libs/PaymentUtils'; @@ -129,6 +129,9 @@ function WorkflowsPaymentsTab({policyID}: WorkflowsPaymentsTabProps) { const bankTitle = addressName.includes(CONST.MASKED_PAN_PREFIX) ? bankName : addressName; const bankAccountID = isBankAccountFullySetup ? policy?.achAccount?.bankAccountID : bankAccountConnectedToWorkspace?.methodID; const state = isBankAccountFullySetup ? (policy?.achAccount?.state ?? '') : (bankAccountConnectedToWorkspace?.accountData?.state ?? ''); + // eslint-disable-next-line rulesdir/no-default-id-values + const [unlockRequestedAt] = useOnyx(`${ONYXKEYS.COLLECTION.NVP_LOCKED_VBA_UNLOCK_REQUESTED}${bankAccountID ?? CONST.DEFAULT_NUMBER_ID}`); + const [initiatingBankAccountUnlock] = useOnyx(ONYXKEYS.INITIATING_BANK_ACCOUNT_UNLOCK); const isAccountInSetupState = isBankAccountPartiallySetup(state); const isBusinessBankAccountLocked = state === CONST.BANK_ACCOUNT.STATE.LOCKED; const canChangePayer = canWritePayments && !isAccountInSetupState; @@ -178,7 +181,11 @@ function WorkflowsPaymentsTab({policyID}: WorkflowsPaymentsTabProps) { } // User who is reimburser can initiate unlocking process if (state === CONST.BANK_ACCOUNT.STATE.LOCKED && bankAccountID && isUserReimburser) { - pressLockedBankAccount(bankAccountID, translate, conciergeReportID ?? undefined, delegateAccountID); + if (unlockRequestedAt) { + showUnlockAlreadyRequestedModal(showConfirmModal, translate); + return; + } + pressLockedBankAccount(bankAccountID, translate, conciergeReportID ?? undefined, delegateAccountID, initiatingBankAccountUnlock); navigateToConciergeChat({conciergeReportID: conciergeReportID ?? undefined, introSelected, currentUserAccountID, isSelfTourViewed, betas}); return; } diff --git a/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx b/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx index 90cb1600c7ec..706da5f7691f 100644 --- a/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx +++ b/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx @@ -13,6 +13,7 @@ import type * as NativeNavigation from '@react-navigation/native'; import Onyx from 'react-native-onyx'; +import {getShowConfirmModalOption, mockShowConfirmModal, resetMockConfirmModal} from '../../../../utils/mockUseConfirmModal'; import waitForBatchedUpdates from '../../../../utils/waitForBatchedUpdates'; jest.mock('@react-navigation/native', () => ({ @@ -65,6 +66,11 @@ jest.mock('@libs/actions/Report', () => ({ navigateToConciergeChat: jest.fn(), })); +jest.mock('@hooks/useConfirmModal', () => ({ + __esModule: true, + default: () => ({showConfirmModal: mockShowConfirmModal, closeModal: jest.fn()}), +})); + const ADMIN_ACCOUNT_ID = 12345; const LOCKED_BANK_ACCOUNT_ID = 99; const POLICY_ID = 'policy_1'; @@ -93,6 +99,7 @@ describe('TimeSensitiveSection - UnlockBankAccount', () => { }); beforeEach(async () => { + resetMockConfirmModal(); await Onyx.clear(); await Onyx.set(ONYXKEYS.ACCOUNT, {primaryLogin: 'admin@example.com'}); await waitForBatchedUpdates(); @@ -300,6 +307,36 @@ describe('TimeSensitiveSection - UnlockBankAccount', () => { } }); + it('shows the already-requested modal and skips pressLockedBankAccount when the NVP is set', async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: 'admin@example.com', accountID: ADMIN_ACCOUNT_ID}); + await Onyx.set(ONYXKEYS.CONCIERGE_REPORT_ID, CONCIERGE_REPORT_ID); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, { + id: POLICY_ID, + name: POLICY_NAME, + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.TEAM, + achAccount: { + bankAccountID: LOCKED_BANK_ACCOUNT_ID, + accountNumber: 'XXXXXXXX1234', + routingNumber: '123456789', + addressName: 'Test Bank', + bankName: 'Test Bank', + reimburser: 'admin@example.com', + state: CONST.BANK_ACCOUNT.STATE.LOCKED, + }, + }); + await Onyx.set(`${ONYXKEYS.COLLECTION.NVP_LOCKED_VBA_UNLOCK_REQUESTED}${LOCKED_BANK_ACCOUNT_ID}`, '2024-01-01T00:00:00.000Z'); + await waitForBatchedUpdates(); + + renderTimeSensitiveSection(); + + const cta = screen.getByText('homePage.timeSensitiveSection.ctaFix'); + fireEvent.press(cta); + + expect(getShowConfirmModalOption('title')).toBe('bankAccount.unlockAlreadyRequestedTitle'); + expect(pressLockedBankAccount).not.toHaveBeenCalled(); + }); + it('calls pressLockedBankAccount and navigates to Concierge when CTA is pressed', async () => { await Onyx.set(ONYXKEYS.SESSION, {email: 'admin@example.com', accountID: ADMIN_ACCOUNT_ID}); await Onyx.set(ONYXKEYS.CONCIERGE_REPORT_ID, CONCIERGE_REPORT_ID); @@ -325,7 +362,7 @@ describe('TimeSensitiveSection - UnlockBankAccount', () => { const cta = screen.getByText('homePage.timeSensitiveSection.ctaFix'); fireEvent.press(cta); - expect(pressLockedBankAccount).toHaveBeenCalledWith(LOCKED_BANK_ACCOUNT_ID, expect.any(Function), CONCIERGE_REPORT_ID, undefined); + expect(pressLockedBankAccount).toHaveBeenCalledWith(LOCKED_BANK_ACCOUNT_ID, expect.any(Function), CONCIERGE_REPORT_ID, undefined, undefined); expect(navigateToConciergeChat).toHaveBeenCalledWith({ conciergeReportID: CONCIERGE_REPORT_ID, introSelected: undefined,