Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/libs/API/parameters/ImportPlaidAccountsParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type ImportPlaidAccountsParams = {
domainName: string;
plaidAccounts: string;
plaidAccessToken?: string;
domainAccountID?: string;
};

export default ImportPlaidAccountsParams;
5 changes: 5 additions & 0 deletions src/libs/actions/Plaid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ function importPlaidAccounts(
domainName: string,
plaidAccounts: string,
plaidAccessToken: string | undefined,
domainAccountID?: number,
) {
const parameters: ImportPlaidAccountsParams = {
publicToken,
Expand All @@ -159,6 +160,10 @@ function importPlaidAccounts(
domainName,
plaidAccounts,
plaidAccessToken,
// When repairing a feed that originated from a Classic domain-level connection surfaced into a workspace,
// forward the originating domain's account ID so the server refreshes credentials on the feed the broken
// cards actually belong to instead of the synthetic workspace-policy domain.
...(domainAccountID ? {domainAccountID: String(domainAccountID)} : {}),
};

const onyxData = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,17 +120,26 @@ function BankConnection({policyID, feed, title}: BankConnectionProps) {
}

// Handle assign card flow
if (feed && !isFeedExpired) {
if (isFeedConnectionBroken) {
updateBrokenConnection();
Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID));
if (feed) {
if (!isFeedExpired) {
if (isFeedConnectionBroken) {
updateBrokenConnection();
Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID));
return;
}
setAssignCardStepAndData({
currentStep: assignCard?.cardToAssign?.dateOption ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.ASSIGNEE,
isEditing: false,
});
return;
}
// Repairing an existing Plaid feed: PlaidConnectionStep already fired importPlaidAccounts with the
// prefixed feed + domainAccountID. Don't queue a second import from the bare institutionId here (it would
// miss the `plaid.` prefix and take the server's create-new-feed branch, duplicating the feed). This
// mirrors the web effect, which returns for a still-expired Plaid feed.
if (isPlaid) {
return;
}
setAssignCardStepAndData({
currentStep: assignCard?.cardToAssign?.dateOption ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.ASSIGNEE,
isEditing: false,
});
return;
}

// Handle add new card flow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';

import {setAddNewCompanyCardStepAndData, setAssignCardStepAndData} from '@libs/actions/CompanyCards';
import {splitCardFeedWithDomainID} from '@libs/CardUtils';
import getPlaidOAuthReceivedRedirectURI from '@libs/getPlaidOAuthReceivedRedirectURI';
import KeyboardShortcut from '@libs/KeyboardShortcut';
import Log from '@libs/Log';
Expand Down Expand Up @@ -141,14 +142,18 @@ function PlaidConnectionStep({feed, policyID, onExit, title}: PlaidConnectionSte

if (feed) {
if (plaidConnectedFeed && addNewCard?.data?.selectedCountry && plaidConnectedFeedName) {
// Repairing an existing feed: send the existing `feed` (which keeps its `plaid.` prefix and
// originating `#domainID`) rather than the freshly-derived bare institution ID, so the server
// takes its repair branch and refreshes the existing feed instead of minting a duplicate.
importPlaidAccounts(
publicToken,
plaidConnectedFeed,
feed,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid queueing a second bare-feed import

When RefreshCardFeedConnectionPage refreshes an expired Plaid feed, this corrected import is immediately followed by setAssignCardStepAndData, which mounts BankConnection; both its web and native effects then call useImportPlaidAccounts(). That hook reads the bare plaidConnectedFeed stored as institutionId, so it queues another ImportPlaidAccounts request without the plaid. prefix or domainAccountID. Because this command has no conflict resolver, the second request can still take the create-new-feed branch and undo the intended duplicate-feed fix; preserve the existing feed in the state consumed by that hook or skip the second import for reconnects.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelvinBot can you address this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in a6893be — the concern was real, but only on native. Pushed a fix.

What was happening (native only): After the repair importPlaidAccounts call, PlaidConnectionStep runs setAssignCardStepAndData({cardToAssign: {institutionId: plaidConnectedFeed /* bare */}}, BANK_CONNECTION) and RefreshCardFeedConnectionPage mounts BankConnection with the feed prop. In the native effect the guard was if (feed && !isFeedExpired) — so while the repair is still in flight and the feed still reads as expired, that block is skipped and execution falls through to if (isPlaid) { onImportPlaidAccounts(); }, which reads the bare institutionId (no plaid. prefix, no domainAccountID) and queues a second ImportPlaidAccounts. With no conflict resolver, that second request can take the server's create-new-feed branch and undo the fix.

Why web was already safe: the web effect uses if (feed) { if (!isFeedExpired) {…} if (isPlaid) { return; } }, so a still-expired Plaid feed returns early and never fires a second import.

Fix: aligned the native effect with the web one so a Plaid reconnect returns instead of re-importing:

src/pages/workspace/companyCards/BankConnection/index.native.tsx:122-139

The add-new-feed path (feed undefined) is unchanged and still imports via onImportPlaidAccounts(); OAuth reconnects (feed present, not Plaid) are also unchanged.

Checks run locally
  • ESLint (scripts/lint.sh on the changed file) — pass
  • React Compiler compliance (check, both Babel + OXC) — compiled
  • tests/unit/RefreshCardFeedConnectionPageTest.tsx — 10/10 pass
  • typecheck-tsgo — no new errors in the changed file (the reported errors are pre-existing, in unrelated files); full tsc OOM'd at the 8 GB ceiling in this environment, not a type error.

plaidConnectedFeedName,
addNewCard.data.selectedCountry,
getDomainNameForPolicy(policyID),
JSON.stringify(metadata?.accounts),
'',
splitCardFeedWithDomainID(feed)?.domainID,
);
}
setAssignCardStepAndData({
Expand Down
Loading