Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
211a213
Detect duplicate Plaid feeds using raw card feed data to prevent infi…
MelvinBot Apr 22, 2026
1deaeb4
Fix ESLint errors: use nullish coalescing and ref instead of state in…
MelvinBot Apr 22, 2026
a0b8d1b
Show ConfirmModal for duplicate Plaid card feeds instead of silently …
MelvinBot May 1, 2026
128c0d4
Apply Polyglot Parrot translations for duplicateFeedModal
MelvinBot May 1, 2026
cc9660f
Use common.buttonConfirm for duplicate feed modal confirm text
MelvinBot May 1, 2026
044a48f
Fix duplicate feed modal briefly showing for new Plaid feeds
MelvinBot May 1, 2026
7d97610
Move duplicate feed modal from BankConnection RHP to Company Cards page
MelvinBot May 1, 2026
a228e3d
Show duplicate feed modal in BankConnection instead of Company Cards …
MelvinBot May 1, 2026
1404997
Merge remote-tracking branch 'origin/main' into claude-fixDuplicatePl…
MelvinBot May 1, 2026
70b332b
Remove seatbelt entry for BankConnection set-state-in-effect
MelvinBot May 1, 2026
08a6845
Merge main and resolve conflicts in BankConnection and seatbelt
MelvinBot May 14, 2026
c5e1761
Move duplicate feed modal from BankConnection to WorkspaceCompanyCard…
MelvinBot May 14, 2026
69c1c51
Use common.buttonConfirm for duplicate feed modal
MelvinBot May 14, 2026
9b38686
Show duplicate feed modal inside BankConnection RHP instead of after …
MelvinBot May 15, 2026
89e2cb5
Show duplicate feed info in BankConnection RHP with submit, then moda…
MelvinBot May 15, 2026
a8ab7fd
Remove ConfirmationPage from BankConnection, navigate directly to modal
MelvinBot May 19, 2026
26db145
Merge main into claude-fixDuplicatePlaidFeedInfiniteSpinner
MelvinBot May 20, 2026
ea21afb
Show duplicate feed modal inline instead of via programmatic showConf…
MelvinBot May 20, 2026
53ed266
Revert all previous changes to start fresh
MelvinBot May 20, 2026
532ec65
Show duplicate Plaid feed modal directly in BankConnection
MelvinBot May 20, 2026
9756c43
Revert unrelated language file changes, keep only duplicateFeedModal …
MelvinBot May 21, 2026
8a51509
Remove all unrelated en.ts changes, keep only duplicateFeedModal keys
MelvinBot May 21, 2026
d9cfccf
Reset language files to PR base, keep only duplicateFeedModal in en.ts
MelvinBot May 21, 2026
8298d83
Add duplicateFeedModal translations from Polyglot Parrot
MelvinBot May 21, 2026
6b7e613
Replace deprecated ConfirmModal with useConfirmModal hook
MelvinBot May 21, 2026
5a1e84c
Fix duplicate feed modal showing on first-time Plaid additions
MelvinBot May 21, 2026
b32cbf4
Prevent duplicate feed modal from showing twice
MelvinBot May 21, 2026
157dc45
Fix: Close RHP before showing duplicate feed modal on iOS native
MelvinBot May 21, 2026
0457672
Fix: Disable shouldHandleNavigationBack for duplicate feed modal
MelvinBot May 21, 2026
106c542
Fix: stabilize showConfirmModal and translate in useEffect deps locally
MelvinBot May 22, 2026
0cda279
Fix: Move ref updates into useEffect to fix lint and compiler errors
MelvinBot May 22, 2026
49098bd
Extract duplicate feed detection into useDuplicateFeedDetection hook
MelvinBot May 22, 2026
eaa4e88
Move useDuplicateFeedDetection hook to src/hooks/
MelvinBot May 22, 2026
7368add
Fix: use relative imports for same-directory hooks to satisfy ESLint
MelvinBot May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/hooks/useDuplicateFeedDetection.ts
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]: '月末最終日',
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]: '月末最后一天',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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);
}
Expand All @@ -163,6 +169,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC
isFeedConnectionBroken,
updateBrokenConnection,
isNewFeedHasError,
checkForDuplicateFeed,
]);

const checkIfConnectionCompleted = (navState: WebViewNavigation) => {
Expand Down
8 changes: 8 additions & 0 deletions src/pages/workspace/companyCards/BankConnection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -186,6 +193,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC
isFeedConnectionBroken,
updateBrokenConnection,
isNewFeedHasError,
checkForDuplicateFeed,
]);

const getContent = () => {
Expand Down
Loading