diff --git a/src/hooks/useDuplicateFeedDetection.ts b/src/hooks/useDuplicateFeedDetection.ts new file mode 100644 index 000000000000..ac3bd9566b11 --- /dev/null +++ b/src/hooks/useDuplicateFeedDetection.ts @@ -0,0 +1,63 @@ +import {useCallback, useEffect, useRef} from 'react'; +import Navigation from '@libs/Navigation/Navigation'; +import ROUTES from '@src/ROUTES'; +import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; +import useConfirmModal from './useConfirmModal'; +import useLocalize from './useLocalize'; + +type UseDuplicateFeedDetectionParams = { + policyID: string | undefined; + isPlaid: boolean; +}; + +/** + * Detects and handles duplicate Plaid feed connections by showing a confirmation modal. + * Returns a function that should be called when a new feed connection is detected. + * Returns `true` if a duplicate was detected and handled, so the caller should return early. + */ +function useDuplicateFeedDetection({policyID, isPlaid}: UseDuplicateFeedDetectionParams) { + const hasEverDetectedNewFeed = useRef(false); + const hasShownDuplicateModal = useRef(false); + const {showConfirmModal} = useConfirmModal(); + const {translate} = useLocalize(); + const showConfirmModalRef = useRef(showConfirmModal); + const translateRef = useRef(translate); + + useEffect(() => { + showConfirmModalRef.current = showConfirmModal; + }, [showConfirmModal]); + + useEffect(() => { + translateRef.current = translate; + }, [translate]); + + const checkForDuplicateFeed = useCallback( + (newFeed: CompanyCardFeedWithDomainID | undefined) => { + if (newFeed) { + hasEverDetectedNewFeed.current = true; + } + + const isDuplicateFeed = !newFeed && isPlaid && !hasEverDetectedNewFeed.current; + if (isDuplicateFeed && !hasShownDuplicateModal.current) { + hasShownDuplicateModal.current = true; + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + showConfirmModalRef.current({ + title: translateRef.current('workspace.companyCards.addNewCard.duplicateFeedModal.title'), + prompt: translateRef.current('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), + confirmText: translateRef.current('common.buttonConfirm'), + shouldShowCancelButton: false, + shouldHandleNavigationBack: false, + }); + return true; + } + + return false; + }, + [isPlaid, policyID], + ); + + return {checkForDuplicateFeed}; +} + +export default useDuplicateFeedDetection; diff --git a/src/languages/de.ts b/src/languages/de.ts index 8ac304f70aad..ce8a7c918c24 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -5351,6 +5351,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU `Ups! Du hast ein einzelnes Feld („${duplicateColumn}“) mehreren Spalten zugeordnet. Bitte überprüfe die Zuordnung und versuche es erneut.`, }, fileImportDescription: 'Eine manuelle Option, falls Ihre Bank keinen Feed senden kann.', + duplicateFeedModal: {title: 'Karten-Feed bereits verbunden', prompt: 'Sie können denselben Kartenfeed nicht zweimal zu demselben Workspace hinzufügen.'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: 'Letzter Tag des Monats', diff --git a/src/languages/en.ts b/src/languages/en.ts index bfd66782bc73..c883abf3cd7e 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5346,6 +5346,10 @@ const translations = { confirmText: 'Report issue', cancelText: 'Skip', }, + duplicateFeedModal: { + title: 'Card feed already connected', + prompt: "You can't add the same card feed to the same workspace twice.", + }, csvColumns: { cardNumber: 'Card number', postedDate: 'Date', diff --git a/src/languages/es.ts b/src/languages/es.ts index 59d55843e76c..12aa6d23e25c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5175,6 +5175,7 @@ ${amount} para ${merchant} - ${date}`, requiredColumns: (missingColumns: string) => `Por favor, asigna una columna a cada uno de los atributos: ${missingColumns}.`, duplicateColumns: (duplicateColumn: string) => `¡Ups! Has asignado un solo campo ("${duplicateColumn}") a múltiples columnas. Por favor, revisa y vuelve a intentarlo.`, }, + duplicateFeedModal: {title: 'Feed de tarjeta ya conectado', prompt: 'No puedes añadir el mismo flujo de tarjeta al mismo espacio de trabajo dos veces.'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: 'Último día del mes', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 4ec6dad932ff..3e7434452666 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5359,6 +5359,7 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. duplicateColumns: (duplicateColumn: string) => `Oups ! Vous avez associé un seul champ (« ${duplicateColumn} ») à plusieurs colonnes. Veuillez vérifier et réessayer.`, }, fileImportDescription: 'Une option manuelle si votre banque ne peut pas envoyer de flux.', + duplicateFeedModal: {title: 'Flux de carte déjà connecté', prompt: 'Vous ne pouvez pas ajouter deux fois le même flux de carte au même espace de travail.'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: 'Dernier jour du mois', diff --git a/src/languages/it.ts b/src/languages/it.ts index 7caa746dda7c..ffe035c6bd49 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -5333,6 +5333,7 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. duplicateColumns: (duplicateColumn: string) => `Ops! Hai associato un singolo campo ("${duplicateColumn}") a più colonne. Controlla e riprova.`, }, fileImportDescription: "Un'opzione manuale se la tua banca non può inviare un feed.", + duplicateFeedModal: {title: 'Feed carta già collegato', prompt: 'Non puoi aggiungere due volte lo stesso feed della carta allo stesso spazio di lavoro.'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: 'Ultimo giorno del mese', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c191fbf38b2f..32477988cd59 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5283,6 +5283,7 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO duplicateColumns: (duplicateColumn: string) => `おっと!1 つのフィールド("${duplicateColumn}")を複数の列にマッピングしています。確認して、もう一度お試しください。`, }, fileImportDescription: '銀行からフィードを送信できない場合の手動オプションです。', + duplicateFeedModal: {title: 'カードフィードはすでに接続されています', prompt: '同じカードフィードを同じワークスペースに二重に追加することはできません。'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: '月末最終日', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 107b8f440280..f3339eef11bd 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -5323,6 +5323,7 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ `Oeps! Je hebt één enkel veld (“${duplicateColumn}”) aan meerdere kolommen gekoppeld. Controleer dit en probeer het opnieuw.`, }, fileImportDescription: 'Een handmatige optie als je bank geen feed kan versturen.', + duplicateFeedModal: {title: 'Kaartfeed al verbonden', prompt: 'Je kunt dezelfde kaartfeed niet twee keer aan dezelfde workspace toevoegen.'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: 'Laatste dag van de maand', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index af6f01873175..bf3de127c952 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -5314,6 +5314,7 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy duplicateColumns: (duplicateColumn: string) => `Ups! Przypisałeś jedno pole („${duplicateColumn}”) do wielu kolumn. Sprawdź i spróbuj ponownie.`, }, fileImportDescription: 'Ręczna opcja, jeśli Twój bank nie może wysyłać kanału danych.', + duplicateFeedModal: {title: 'Kanał karty jest już połączony', prompt: 'Nie możesz dodać tego samego źródła danych karty do tego samego obszaru roboczego dwa razy.'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: 'Ostatni dzień miesiąca', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 75592a0d4982..89410dc8a1f7 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -5316,6 +5316,7 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS duplicateColumns: (duplicateColumn: string) => `Ops! Você mapeou um único campo ("${duplicateColumn}") para várias colunas. Revise e tente novamente.`, }, fileImportDescription: 'Uma opção manual caso seu banco não possa enviar um feed.', + duplicateFeedModal: {title: 'Feed do cartão já conectado', prompt: 'Você não pode adicionar o mesmo feed de cartão ao mesmo workspace duas vezes.'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: 'Último dia do mês', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index fe9f58ee820a..aa3b0723a04b 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5198,6 +5198,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM duplicateColumns: (duplicateColumn: string) => `哎呀!你已将单个字段(“${duplicateColumn}”)映射到了多个列。请检查后重试。`, }, fileImportDescription: '如果您的银行无法发送数据馈送时的手动选项。', + duplicateFeedModal: {title: '卡片流水已连接', prompt: '您不能将同一张卡片流水两次添加到同一个工作区。'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: '月末最后一天', diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 324fb3d80da7..0a357f288422 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -7,6 +7,7 @@ import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOffli import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useCardFeeds from '@hooks/useCardFeeds'; +import useDuplicateFeedDetection from '@hooks/useDuplicateFeedDetection'; import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import useLocalize from '@hooks/useLocalize'; @@ -74,6 +75,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {updateBrokenConnection, isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); + const {checkForDuplicateFeed} = useDuplicateFeedDetection({policyID, isPlaid}); const activityReasonAttributes: SkeletonSpanReasonAttributes = { context: 'BankConnection', @@ -139,6 +141,10 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC // Handle add new card flow if (isNewFeedConnected) { + if (checkForDuplicateFeed(newFeed)) { + return; + } + if (newFeed) { updateSelectedFeed(newFeed, policyID); } @@ -163,6 +169,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isFeedConnectionBroken, updateBrokenConnection, isNewFeedHasError, + checkForDuplicateFeed, ]); const checkIfConnectionCompleted = (navState: WebViewNavigation) => { diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index a0f6c335d18b..b581e2a735c7 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -7,6 +7,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useCardFeeds from '@hooks/useCardFeeds'; +import useDuplicateFeedDetection from '@hooks/useDuplicateFeedDetection'; import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; @@ -78,6 +79,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const onImportPlaidAccounts = useImportPlaidAccounts(policyID); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); + const {checkForDuplicateFeed} = useDuplicateFeedDetection({policyID, isPlaid}); const onOpenBankConnectionFlow = useCallback(() => { if (!url) { @@ -152,6 +154,11 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC if (isNewFeedConnected) { setShouldBlockWindowOpen(true); customWindow?.close(); + + if (checkForDuplicateFeed(newFeed)) { + return; + } + if (newFeed) { updateSelectedFeed(newFeed, policyID); } @@ -186,6 +193,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isFeedConnectionBroken, updateBrokenConnection, isNewFeedHasError, + checkForDuplicateFeed, ]); const getContent = () => {