Skip to content

Open validation RHP directly for pending USD bank accounts - #94707

Merged
MonilBhavsar merged 20 commits into
mainfrom
claude-pendingBankAccountValidationNavigation
Jul 10, 2026
Merged

MonilBhavsar merged 20 commits into
mainfrom
claude-pendingBankAccountValidationNavigation

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

When a USD business bank account is in the PENDING (micro-deposit validation) state and the user taps it from Workspace Settings → Workflows, ReimbursementAccountPage is supposed to skip the entry screen and open the "Validate your bank account" RHP directly.

The existing PENDING branch only mutated local/Onyx step state — it never navigated. The actual validation form lives on a different route (BANK_ACCOUNT_USD_SETUP, page VALIDATION), so the page fell through to its terminal render of VerifiedBankAccountFlowEntryPoint, which showed the "Continue setup" / "Start over" menu instead of the validation form.

This change makes the PENDING branch actually perform the forward navigation the inline comment promises — it navigates to ROUTES.BANK_ACCOUNT_USD_SETUP with page = VALIDATION (the same destination continueUSDVBBASetup already produces for a VALIDATION step), sending the user straight to the "Validate your bank account" RHP.

Fixed Issues

$ #93564
PROPOSAL: #93564 (comment)

Tests

  1. Go to Workspace settings > Workflows > Add bank account
  2. Start adding a bank account via "Connect manually" option
  3. Proceed to the final step and press "Continue". A "Validate your bank account" page is displayed
  4. Navigate back to Workflows and press the pending bank account
  5. Verify that "Validate your bank account" page opens
  • Verify that no errors appear in the JS console

Offline tests

  1. Go to Workspace settings > Workflows > Add bank account
  2. Start adding a bank account via "Connect manually" option
  3. Proceed to the final step and press "Continue". A "Validate your bank account" page is displayed
  4. Navigate back to Workflows and press the pending bank account
  5. Verify that "Validate your bank account" page opens

QA Steps

  1. Go to Workspace settings > Workflows > Add bank account
  2. Start adding a bank account via "Connect manually" option
  3. Proceed to the final step and press "Continue". A "Validate your bank account" page is displayed
  4. Navigate back to Workflows and press the pending bank account
  5. Verify that "Validate your bank account" page opens
  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

Co-authored-by: Monil Bhavsar <MonilBhavsar@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team June 26, 2026 14:42
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I created this PR from an upstream branch since you don't have push access to a fork.

To take ownership of this branch so you can push updates, run:

git remote add upstream https://github.com/Expensify/App.git
git fetch upstream claude-pendingBankAccountValidationNavigation
git checkout -b claude-pendingBankAccountValidationNavigation upstream/claude-pendingBankAccountValidationNavigation
git push -u origin claude-pendingBankAccountValidationNavigation

Then you can close this PR and open a new one from your fork.

@bernhardoj

Copy link
Copy Markdown
Contributor

@MelvinBot looks like you added changes outside of your own proposal. Please only apply the changes from your proposal

… RHP

Co-authored-by: Bernhard Owen Josephus <bernhardoj@users.noreply.github.com>
@codecov

codecov Bot commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/pages/settings/Wallet/WalletPage/index.tsx 0.00% <ø> (ø)
...rkspace/ConnectExistingBusinessBankAccountPage.tsx 0.00% <ø> (ø)
.../workspace/invoices/WorkspaceInvoiceVBASection.tsx 0.00% <ø> (ø)
...ges/workspace/workflows/WorkspaceWorkflowsPage.tsx 74.36% <ø> (ø)
...orkspace/workflows/WorkspaceWorkflowsPayerPage.tsx 0.00% <ø> (ø)
...nkAccount/components/BankAccountValidationForm.tsx 0.00% <0.00%> (ø)
...rc/libs/actions/ReimbursementAccount/navigation.ts 0.00% <0.00%> (ø)
...ount/USD/ConnectBankAccount/ConnectBankAccount.tsx 0.00% <0.00%> (ø)
src/components/KYCWall/BaseKYCWall.tsx 27.01% <0.00%> (-0.32%) ⬇️
... and 9 files with indirect coverage changes


// If USD bank account is in pending state, we should navigate straight to the validation step and skip Continue step
if (policyCurrency === CONST.CURRENCY.USD && achData?.state === CONST.BANK_ACCOUNT.STATE.PENDING) {
setUSDBankAccountStep(CONST.BANK_ACCOUNT.STEP.VALIDATION);

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 let's remove 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.

Done in 8f47310 — removed the setUSDBankAccountStep(CONST.BANK_ACCOUNT.STEP.VALIDATION) call. It was redundant: this branch navigates to the separate BANK_ACCOUNT_USD_SETUP (page VALIDATION) route and returns early, so updating this page's local step state had no effect before it navigated away. Lint and typecheck pass.

Co-authored-by: Bernhard Owen Josephus <bernhardoj@users.noreply.github.com>
@bernhardoj

Copy link
Copy Markdown
Contributor

@MelvinBot add this as the QA step. Test and Offline steps are the same.

  1. Go to Workspace settings > Workflows > Add bank account
  2. Start adding a bank account via "Connect manually" option
  3. Proceed to the final step and press "Continue". A "Validate your bank account" page is displayed
  4. Navigate back to Workflows and press the pending bank account
  5. Verify that "Validate your bank account" page opens

setUSDBankAccountStep(CONST.BANK_ACCOUNT.STEP.VALIDATION);
goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.VALIDATION);
setShouldShowContinueSetupButton(shouldShowContinueSetupButtonValue);
Navigation.navigate(ROUTES.BANK_ACCOUNT_USD_SETUP.getRoute({policyID: policyIDParam, page: CONST.BANK_ACCOUNT.PAGE_NAMES.VALIDATION, backTo}));

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 Remove this navigation code.

Navigating immediately in useEffect after mount somehow causes this page to be unmounted (on small screen), which resets the achData to the default state.

So, instead of navigating here, let's navigate to the validation page from navigateToBankAccountRoute. We will need to add policyCurrency and achDataState params. policyCurrency from policy.outputCurrency, while achDataState is from reimbursementAccount.achData.state.

function navigateToBankAccountRoute({
    policyID = '',
    policyCurrency,
    bankAccountID,
    achDataState,
    backTo,
    navigationOptions,
}: {
    policyID?: string;
    policyCurrency: string;
    bankAccountID?: number;
    achDataState: ACHData['state'];
    backTo?: string;
    navigationOptions?: LinkToOptions;
}) {
    if (policyCurrency === CONST.CURRENCY.USD && achDataState === CONST.BANK_ACCOUNT.STATE.PENDING) {
        Navigation.navigate(ROUTES.BANK_ACCOUNT_USD_SETUP.getRoute({policyID, page: CONST.BANK_ACCOUNT.PAGE_NAMES.VALIDATION, backTo}));
        return;
    }
    Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID, bankAccountID, backTo}), navigationOptions);
}

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 nevermind, revert to the previous changes.

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 first investigate why navigating inside the useEffect causes the ReimbursementAccountPage to be unmounted

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 please investigate why navigating to validation page inside the useEffect causes the ReimbursementAccountPage to be unmounted

MelvinBot and others added 2 commits June 29, 2026 04:22
Co-authored-by: Bernhard Owen Josephus <bernhardoj@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

⚠️ MelvinBot CI Fix Loop Detected

MelvinBot has made 5 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR.

Next steps: A human should review the recent commits and resolve the conflicting CI errors manually.

1 similar comment
@MelvinBot

Copy link
Copy Markdown
Contributor Author

⚠️ MelvinBot CI Fix Loop Detected

MelvinBot has made 5 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR.

Next steps: A human should review the recent commits and resolve the conflicting CI errors manually.

@bernhardoj

Copy link
Copy Markdown
Contributor

@MelvinBot please investigate why navigating to validation page inside the useEffect causes the ReimbursementAccountPage to be unmounted

1 similar comment
@bernhardoj

Copy link
Copy Markdown
Contributor

@MelvinBot please investigate why navigating to validation page inside the useEffect causes the ReimbursementAccountPage to be unmounted

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Why ReimbursementAccountPage unmounts when you navigate to the validation page

Short answer: The validation page is a different screen rendered by a different component, so navigating to it is a real cross-screen navigation — and the RHP card stack detaches (fully unmounts) the previous card, it doesn't just hide it.

  • Your current screen is SCREENS.REIMBURSEMENT_ACCOUNT (path bank-account/new), rendered by ReimbursementAccountPage.
  • ROUTES.BANK_ACCOUNT_USD_SETUP with page: VALIDATION resolves to path bank-account/new/us/validationSCREENS.REIMBURSEMENT_ACCOUNT_USD, rendered by USDVerifiedBankAccountFlowPagenot ReimbursementAccountPage.

So the effect navigates you off ReimbursementAccountPage entirely. Because both screens live in the same RHP SettingsModalStackNavigator, this becomes a PUSH inside that one JS stack — the router keeps the old route in state, but the stack's rendering layer detaches the now-inactive card, which unmounts the component (not merely blurs it). This is expected React Navigation behavior for the RHP, not a bug in your change.

Full mechanism (traced, high confidence)
  1. navigate becomes a PUSH. linkTo converts the default NAVIGATE into PUSH at linkTo/index.ts:181-189.
  2. The PUSH targets the inner Settings modal stack, not a new RHP. getMinimalAction unwraps through the already-open RIGHT_MODAL_NAVIGATORSettingsModalStackNavigator (names match the current path) and dispatches into that existing inner stack — getMinimalAction.ts:23-44.
  3. The router keeps the old route. StackRouter's PUSH just appends the new route and moves index; REIMBURSEMENT_ACCOUNT stays in routes. So the router alone would only hide, not unmount.
  4. The card stack detaches the previous card. createModalStackNavigator renders @react-navigation/stack's StackView. RHP cards use a horizontal/slide interpolator, not forModalPresentationIOS (useModalStackScreenOptions.ts:28-30). For non-modal-presentation cards, detachPreviousScreen defaults to true and activeScreensLimit is 1, so the previous card's activityState goes to STATE_INACTIVE. With detachInactiveScreens (true on web), react-native-screens removes that subtree → ReimbursementAccountPage unmounts.

Implication for this PR: running the navigate inside the effect works, but any state you set on ReimbursementAccountPage right before navigating (goToWithdrawalAccountSetupStep, setShouldShowContinueSetupButton) is set on a component that's about to unmount — those calls on the source page are effectively throwaway once you navigate to the separate USDVerifiedBankAccountFlowPage screen. If the validation form needs that state, it should be derived on/for the destination screen rather than set on the page you're leaving.

If you instead wanted ReimbursementAccountPage to stay mounted, you'd need a detachPreviousScreen: false / persistent-screen override on that screen — but that's contrary to how the RHP is intended to behave, so navigating to the dedicated validation screen (as this PR does) is the correct approach.


Next Steps: Reply with @MelvinBot <your feedback> to refine this analysis.

@bernhardoj

Copy link
Copy Markdown
Contributor

@MelvinBot it's not expected. If I delay the navigation (for example by using setTimeout 0), then the ReimbursementAccountPage is not unmounted.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

You're right, and my earlier "this is expected RHP detachment" answer was wrong. If it were just inherent card-detachment, setTimeout(0) would unmount it too — it doesn't. The unmount is a timing race, not a property of navigating to the validation screen.

Root cause: The redirect fires synchronously in ReimbursementAccountPage's mount effect (ReimbursementAccountPage.tsx:264-268) — in the same JS frame as the navigation that just opened this page from Workflows, and before Onyx has finished hydrating achData. That collapses two PUSHes into one navigation commit, the RHP card stack detaches/unmounts REIMBURSEMENT_ACCOUNT while its data is still mid-load, and the local achData-derived state resets to defaults. Deferring one tick lets the first navigation commit and Onyx settle first, so the redirect becomes a clean push and the page's state survives.

This is a known bug class in this codebase — navigation racing an Onyx update. The purpose-built helper for it is Navigation.setNavigationActionToMicrotaskQueue, which resolves a microtask + requestAnimationFrame so Onyx updates land before the navigate. See the near-identical precedent in useParticipantSubmission.ts:198-206: "we wrap navigation in setNavigationActionToMicrotaskQueue so that data loading in Onyx and navigation do not occur simultaneously, which resets the amount to 0." Same symptom you're seeing.

Two ways to fix it:

  1. Move the redirect to the caller (cleanest — your original idea). Decide the PENDING case in navigateToBankAccountRoute (ReimbursementAccount/navigation.ts) so ReimbursementAccountPage never mounts for PENDING at all. No throwaway mount, no race, and it drops the dead goToWithdrawalAccountSetupStep/setShouldShowContinueSetupButton calls that run on a component that's about to unmount. This is what the branch head reverted (Revert "Move pending USD VBA validation redirect into navigateToBankAccountRoute") — worth re-attempting now that we know it's the right shape; the caller just needs policyCurrency (policy.outputCurrency) and achData.state, exactly as you sketched earlier.

  2. Keep it in the effect but defer via setNavigationActionToMicrotaskQueue (smallest diff). Prefer this over a raw setTimeout(0) — it's the idiomatic helper for this exact race. Downside: still mounts-then-unmounts the page, so it's strictly inferior to option 1.

Why immediate vs. deferred actually differ (mechanism, traced)
  • Both navigate calls run linkTo synchronously — neither the Workflows caller nor the effect passes waitForTransition (Navigation.ts:331-342).
  • At the moment the mount effect runs, the first navigation's transitionStart hasn't fired yet, so TransitionTracker has no active transition to serialize against and runs the second navigate immediately (TransitionTracker.ts:148-151; transitionStart is wired in ScreenLayout.tsx:24-34). So the queue does not separate the two navigations — they land in one frame.
  • REIMBURSEMENT_ACCOUNT (bank-account/new) and REIMBURSEMENT_ACCOUNT_USD (bank-account/new/us/validation) are siblings in the same inner SettingsModalStackNavigator, so getMinimalAction dispatches the second PUSH into that existing stack (getMinimalAction.ts:23-44).
  • On web/mWeb the inner stack is @react-navigation/stack's CardStack. RHP cards use forHorizontalIOS, not modal presentation (useModalStackScreenOptions.ts:28-30), so CardStack's defaults resolve to activeScreensLimit = 1 / detachPreviousScreen = true and the previous card is detached (unmounted). Native uses native-stack, which is why this reproduces on the small-screen web path. My earlier reply quoted those detach values as if configured — they're actually library defaults, and detachment alone was never the point: the timing of the same-frame double dispatch is what turns "detach after transition" into "unmount before data loads."

Confidence: high that this is an Onyx-load-vs-navigation race fixed by the microtask helper (documented precedent + your setTimeout(0) repro both point here); medium on the exact internal RN reason the deferred variant keeps the card fully mounted vs. merely detaching it later.


Next Steps: Reply with @MelvinBot implement option 1 (move to caller) or @MelvinBot implement option 2 (defer in effect) and I'll open the change, or @MelvinBot <feedback> to refine.

@bernhardoj

Copy link
Copy Markdown
Contributor

@MelvinBot let's use settimeout with 0 delay. explain why with a comment

… unmount race

Co-authored-by: Bernhard Owen Josephus <bernhardoj@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done in 7b11994 — wrapped the validation redirect in setTimeout(…, 0) with a comment explaining why: ReimbursementAccountPage.tsx:264-276.

The comment captures the mechanism from our investigation: navigating synchronously in this mount effect runs in the same frame as the navigation that just opened the page from Workflows, collapsing both into one commit while Onyx is still hydrating achData. On small screens that detaches/unmounts the page mid-load and resets achData to defaults. Deferring one tick lets the first navigation commit and Onyx settle, so the redirect becomes a clean push and the page stays mounted — matching your setTimeout(0) repro.

Note: I couldn't run lint/typecheck to completion here — this environment's node_modules are missing the built expensify-common/dist/* artifacts and the eslint-config-expensify export map is broken, so both tools error before reaching this file. The change itself is a trivial wrap of the existing Navigation.navigate call, so please let CI confirm the checks.

// page mounts from Workflows) collapses the two navigations into a single commit while Onyx is still
// hydrating achData. On small screens that detaches/unmounts this page mid-load and resets achData to its
// defaults. Letting the current frame finish so the first navigation commits and Onyx settles makes the
// redirect a clean push, keeping the page mounted.

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 don't mention about onyx hydration. it's unrelated

Co-authored-by: Bernhard Owen Josephus <bernhardoj@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Updated in ffcf059 — removed the Onyx hydration wording. The comment now attributes the unmount solely to the navigation-timing race (two navigations landing in the same frame collapse into one commit, detaching/unmounting the page and resetting its achData-derived local state): ReimbursementAccountPage.tsx:267-274.

@bernhardoj

Copy link
Copy Markdown
Contributor

@MelvinBot copy this to PR Author Checklis

checklist
  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done — replaced the PR Author Checklist section in the PR body with the full checklist you provided, all items checked.

@bernhardoj

Copy link
Copy Markdown
Contributor

@MelvinBot check the Verify that no errors appear in the JS console and mark this PR as ready

@bernhardoj

Copy link
Copy Markdown
Contributor

@MonilBhavsar done

@MonilBhavsar

Copy link
Copy Markdown
Contributor

ES lint is failing

@MonilBhavsar

Copy link
Copy Markdown
Contributor

@codex review

@MonilBhavsar

Copy link
Copy Markdown
Contributor

Were you able to test it? If so could you please attach screenshots in the description?

@bernhardoj

Copy link
Copy Markdown
Contributor

@MelvinBot fix the lint

@MelvinBot

Copy link
Copy Markdown
Contributor Author

There was no actual merge conflict to resolve — GitHub reports the branch as mergeable: true. The branch was just very stale (764 commits behind main), so I brought it up to date with a clean merge.

  • Merged main into the branch via a merge commit (b27dc39, Merge branch 'main' into claude-pendingBankAccountValidationNavigation) — no conflicts.
  • Branch is now behind_by: 0, ahead_by: 18 (up to date with main).

The PR still shows "blocked", but that's branch protection waiting on required status checks (e.g. the failing ESLint you flagged separately) — not a merge conflict. Once those checks pass it'll be mergeable.

Co-authored-by: Bernhard Owen Josephus <bernhardoj@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Fixed the lint in 947e669. The failing rule was @typescript-eslint/no-shadow at BaseKYCWall.tsx:232 — the .find() callback param bankAccount shadowed the outer const bankAccount declared on the same line. Renamed the callback param to account (matching the convention used elsewhere, e.g. BankAccounts.ts).

./scripts/lint.sh now passes on all files changed in this PR.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ac1b782f24

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +67 to +68
const bankAccountConnectedToWorkspace = policyID ? Object.values(bankAccountList ?? {}).find((bankAccount) => bankAccount?.accountData?.policyIDs?.includes(policyID)) : undefined;
const bankAccountState = bankAccountConnectedToWorkspace?.accountData?.state ?? '';

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 Resolve the pending account by the workspace link

When this new direct validation route is opened from Workflows/Wallet, the selected workspace bank account is identified by accountData.additionalData.policyID in the callers, but this screen only searches accountData.policyIDs. For a persisted pending VBA that doesn't have the optimistic policyIDs array, bankAccountConnectedToWorkspace stays undefined, bankAccountState is empty, and the validation form never renders. Please also match additionalData.policyID here (or pass the bankAccountID through the route).

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.

This accountData.policyIDs and accountData.additionalData.policyID is kinda confusing. When I connect the BA to a 2nd workspace, the 2nd workspace will be appended to policyIDs. But after reopening the Workflows page, the policyIDs become a single array again that only contains the 2nd workspace, and accountData.additionalData.policyID is also pointing to the 2nd workspace. I checked the first workspace Workflows and yes, the BA is disconnected. Not sure if that's a bug or not, but I think we shouldn't worry with the comment above. I'd rather not touch it (the workflows and wallet page that uses accountData.additionalData.policyID).

Comment on lines +44 to +45
if (policyCurrency === CONST.CURRENCY.USD && bankAccountState === CONST.BANK_ACCOUNT.STATE.PENDING) {
Navigation.navigate(ROUTES.BANK_ACCOUNT_USD_SETUP.getRoute({policyID, page: CONST.BANK_ACCOUNT.PAGE_NAMES.VALIDATION, backTo}), navigationOptions);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Hydrate reimbursement data before opening validation

Navigating straight to BANK_ACCOUNT_USD_SETUP skips the ReimbursementAccountPage path that calls openReimbursementAccountPage; after leaving the original setup flow that Onyx data can be cleared, while USDVerifiedBankAccountFlowPage still uses reimbursementAccount?.achData?.state to decide whether Back should return to the entry route. In that state, pressing Back from the validation RHP falls through to previous USD setup pages instead of returning to backTo (and max-attempt state is also unavailable). Please fetch/hydrate the reimbursement account before opening this route, or make the validation route self-hydrate from policyID/bank account data.

Useful? React with 👍 / 👎.

@bernhardoj

Copy link
Copy Markdown
Contributor

@MelvinBot we currently get the BA state from reimbursementAccount.

const onBackButtonPress = useCallback(() => {
// When the bank account is pending validation it has already been submitted, so stepping back through the
// setup pages doesn't make sense. Pop back to the entry point screen the user came from.
if (currentEntry?.pageName === PAGE_NAMES.VALIDATION && reimbursementAccount?.achData?.state === CONST.BANK_ACCOUNT.STATE.PENDING) {
Navigation.goBack(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID, backTo}));
return;
}

Let's update it too by getting it from the bankAccountList

Co-authored-by: Bernhard Owen Josephus <bernhardoj@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done in 137b793 — the pending-state check in onBackButtonPress now derives the BA state from bankAccountList (matching the policyIDs?.includes(policyID) pattern) instead of reimbursementAccount.achData.state: USDVerifiedBankAccountFlowPage.tsx:120-127.

const bankAccountConnectedToWorkspace = policyID ? Object.values(bankAccountList ?? {}).find((bankAccount) => bankAccount?.accountData?.policyIDs?.includes(policyID)) : undefined;
const bankAccountState = bankAccountConnectedToWorkspace?.accountData?.state ?? '';

bankAccountState also replaces reimbursementAccount?.achData?.state in the useCallback deps. reimbursementAccount is still used elsewhere in the file, so it stays.

Note: typecheck-tsgo passes for this change. ESLint OOM'd in this environment (lint-changed lints every file changed across the whole PR from the merge-base, which exhausts memory here), so please let CI confirm lint.

@bernhardoj

Copy link
Copy Markdown
Contributor

@MonilBhavsar done, the recordings is on the checklist here

@MonilBhavsar

Copy link
Copy Markdown
Contributor

Thanks!

@MonilBhavsar
MonilBhavsar merged commit 28b6d4c into main Jul 10, 2026
40 checks passed
@MonilBhavsar
MonilBhavsar deleted the claude-pendingBankAccountValidationNavigation branch July 10, 2026 18:56
@github-actions

Copy link
Copy Markdown
Contributor

🚧 MonilBhavsar has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/MonilBhavsar in version: 9.4.34-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Help site review — no changes required

I reviewed the changes in this PR against the help site files under docs/articles. No help-site updates are required.

Why: This PR is a navigation bug fix. When a USD business bank account is in the PENDING (micro-deposit validation) state, tapping it (from Workflows, Wallet, Invoices, etc.) now opens the Validate your bank account RHP directly, instead of falling through to the "Continue setup / Start over" menu. It restores the behavior the help site already documents — it does not change any documented flow, feature name, label, or user-facing copy.

The relevant article, Validate a Business Bank Account, already describes clicking the pending bank account (with the Action required badge) to reveal the Validate your bank account section and enter the test amounts — which is exactly the end state this PR makes reachable. So the docs remain accurate as-is.

I checked both help articles that touch bank-account validation:

  • new-expensify/wallet-and-payments/Validate-a-Business-Bank-Account.md — already reflects the corrected behavior.
  • expensify-classic/.../Share-Verified-Business-Bank-Account.md — Classic-only, unaffected by this NewDot navigation change.

⚠️ Note: browser-based verification of the live UI was unavailable in this run (web prep failed), so I confirmed the feature/label names ("Validate your bank account", "Action required") against the article content and the PR code rather than the running app.

Since no help-site content becomes inaccurate, I did not open a docs PR. If you'd like a doc change anyway (e.g., to explicitly document reaching validation from the Workflows page), let me know and I'll draft one.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/roryabraham in version: 9.4.34-14 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 failure ❌
🍎 iOS 🍎 failure ❌

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/roryabraham in version: 9.4.34-14 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants