Skip to content

Fix Account and Wallet RBR still showing for personal cards past the 90-day grace period#96364

Draft
wildan-m wants to merge 1 commit into
Expensify:mainfrom
wildan-m:wildan/91451-personal-card-rbr-followup
Draft

Fix Account and Wallet RBR still showing for personal cards past the 90-day grace period#96364
wildan-m wants to merge 1 commit into
Expensify:mainfrom
wildan-m:wildan/91451-personal-card-rbr-followup

Conversation

@wildan-m

@wildan-m wildan-m commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

Follow-up to #93523, which shipped the 90-day grace period for broken card connections. That change correctly removed the time-sensitive Home task, but for personal cards the Account indicator and the Wallet row red dot stayed lit — which the issue explicitly asks us to remove:

Personal cards: Remove the indicator on Account and red dot on Wallet row in the account left hand bar. But keep the error on the card itself on the Wallet page so the card can still be fixed.

Reported on production by @joekaufmanexpensify during the regression period: #91451 (comment)

Root cause. Both indicators are driven by the card's own error — which we intentionally keep so the card stays fixable — through two paths that never consult the grace period:

  1. getShouldShowRBR() in the cardFeedErrors derived value short-circuits on hasFeedErrors (built from card.errors / card.errorFields) before it ever reaches the gated isFeedConnectionBroken.
  2. hasPaymentMethodError() returns true for any personal card with errors. Its only two consumers are exactly the two affected surfaces — useAccountIndicatorChecks (the Account indicator) and InitialSettingsPage (the Wallet row).

So keeping the error (as the issue requires) kept the red dot lit forever. That the Home task was correctly removed is the tell: the grace check itself works, but these two paths bypass it.

Fix. Gate both paths on the existing isBrokenConnectionPastDismissThreshold. card.errors is left completely untouched, so the card's row on the Wallet page keeps its error and the Fix card action — only the Account / Wallet row indicators stop.

This only affects personal cards whose broken connection is past the grace period. Cards within the grace period, and cards with unrelated errors, are unchanged (both covered by new tests).

Fixed Issues

$ #91451
PROPOSAL: #91451 (comment)

Tests

This is shared logic (no platform-specific code), so it behaves the same on web, mWeb, iOS and Android.

Important — what actually reproduces this. It only shows up on a personal card (one added on Account > Wallet, i.e. fundID is absent or '0') whose broken connection also carries an error on the card itself. That error is what lights the Account / Wallet red dots, and we deliberately keep it so the card stays fixable. A company card from a workspace will not reproduce it — it goes through the feed path, which the existing grace check already covers.

Rather than waiting 90 days for real data, paste this in the browser console on a dev build to put your existing personal card into exactly that state:

(async () => {
  // ── config ──────────────────────────────────────────────────────────────
  const DAYS_AGO = 100;   // >= 90 → past the grace period. Set to 1 for the "recently broken" baseline.
  const RESET    = false; // true → restore the card to healthy
  // ────────────────────────────────────────────────────────────────────────
  try {
    if (typeof Onyx === 'undefined' || typeof Onyx.merge !== 'function') {
      console.error('[card-sim] window.Onyx unavailable — use a non-production (dev/adhoc) build.');
      return;
    }
    const cards = (await Onyx.get('cardList')) ?? {};
    // CardUtils.isPersonalCard(): no fundID, or fundID === '0'
    const personal = Object.entries(cards).filter(([, c]) => !c?.fundID || c.fundID === '0');
    console.log(`[card-sim] ${Object.keys(cards).length} card(s), ${personal.length} personal`);
    if (!personal.length) {
      console.warn('[card-sim] no personal card — add one via Account > Wallet > Add personal card first.');
      return;
    }
    if (RESET) {
      for (const [id] of personal) {
        await Onyx.merge('cardList', {[id]: {lastScrapeResult: 200, lastScrape: '', errors: null}});
        console.log(`[card-sim] reset ${id} to healthy`);
      }
      return;
    }
    const p = (n) => String(n).padStart(2, '0');
    const d = new Date(Date.now() - DAYS_AGO * 864e5);
    const scrape = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
    for (const [id] of personal) {
      await Onyx.merge('cardList', {[id]: {
        lastScrapeResult: 403,                                         // a broken status (not in BROKEN_CONNECTION_IGNORED_STATUSES)
        lastScrape: scrape,                                            // last *successful* refresh = how long it has been broken
        errors: {brokenConnection: 'Your card connection is broken.'}, // the error kept on the card — this is what lights the RBR
      }});
      console.log(`[card-sim] personal card ${id} -> broken(403), lastScrape ${scrape} (${DAYS_AGO}d ago)`);
    }
    console.log(DAYS_AGO >= 90
      ? '[card-sim] PAST grace → expect Home task, Account dot and Wallet row dot all GONE; card still listed with its error + "Fix card".'
      : '[card-sim] WITHIN grace → expect Home task, Account dot and Wallet row dot all SHOWING.');
  } catch (e) { console.error('[card-sim] failed:', e); }
})();
  1. On a dev build, make sure the account has a personal card (Account > Wallet > Add personal card). Run the script with DAYS_AGO = 1.
  2. Baseline (recently broken) — unchanged behaviour: confirm all three surface — the time-sensitive task on Home, a red dot on the Account button, and a red dot on the Wallet row in the account left hand bar.
  3. Run the script again with DAYS_AGO = 100.
  4. Past the grace period (this fix): confirm the Home task, the Account red dot, and the Wallet row red dot are all gone.
  5. Confirm the card itself is untouched on the Wallet page: still listed with its own red dot, and opening it still shows "Your card connection is broken." with a working Fix card button (the error is kept so it stays fixable).
  6. Re-run with RESET = true to put the card back to healthy.

You can also read the underlying state directly instead of eyeballing the dots:

JSON.stringify((await Onyx.get('cardFeedErrors'))?.personalCard)
// past grace, on this branch: {"shouldShowRBR":false,"hasFeedErrors":false,"hasWorkspaceErrors":false,"isFeedConnectionBroken":false}
// past grace, on main today:  {"shouldShowRBR":true, "hasFeedErrors":true, "hasWorkspaceErrors":false,"isFeedConnectionBroken":false}   <- the bug

shouldShowRBR is what drives the Account / Wallet red dots; isFeedConnectionBroken drives the Home task. On main the task is correctly gone but shouldShowRBR stays true — which is exactly what was reported.

  • Verify that no errors appear in the JS console

Offline tests

Same as the Tests above. This is a derived value computed from data already in Onyx plus the device date, so it behaves identically offline — the task and indicators stay hidden for a past-grace connection, and the card keeps its error and Fix card action.

QA Steps

Please treat this as a regression check around the card feature. Behaviour is the same on all platforms, so any one platform is fine.

  1. With a personal card whose connection broke recently (under 90 days), confirm it still surfaces as before: the task on Home, the red dot on the Account button, and the red dot on the Wallet row. This is unchanged and must keep working.
  2. With a personal card whose connection has been broken for 90 days or more, confirm the Home task, the Account red dot, and the Wallet row red dot are all gone — while the card is still listed on the Wallet page with its error and a working Fix card button.
  3. Confirm nothing else in the card feature regressed — adding/removing personal cards, company cards, and cards with other (non-connection) errors still show their red dots as before.

If a 90-days-broken connection isn't available as test data, the removal is also covered by automated unit tests; the essential manual checks are step 1 (recently-broken still prompts) and step 3 (no regressions).

  • 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 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 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

…race period

The 90-day grace correctly removed the home task, but the Account indicator and the
Wallet row red dot stayed lit for personal cards. Both are driven by the card's own
error, which the issue requires us to keep so the card stays fixable:

- getShouldShowRBR short-circuits on hasFeedErrors before the grace check, so a
  past-grace card's error still forced the RBR.
- hasPaymentMethodError counts any personal card with errors, and it feeds both the
  Account indicator (useAccountIndicatorChecks) and the Wallet row (InitialSettingsPage).

Gate both on isBrokenConnectionPastDismissThreshold, leaving card.errors untouched so
the Wallet-page card row keeps its error and Fix card action.

Adds regression tests for a past-grace card that still carries the error (the case the
original fixtures missed), plus guards that within-grace and unrelated errors still show.
@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
...libs/actions/OnyxDerived/configs/cardFeedErrors.ts 97.77% <100.00%> (+2.27%) ⬆️
src/libs/actions/PaymentMethods.ts 32.47% <0.00%> (ø)
... and 13 files with indirect coverage changes

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.

1 participant