Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
12 changes: 8 additions & 4 deletions src/libs/actions/OnyxDerived/configs/cardFeedErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,13 @@ export default createOnyxDerivedValueConfig({
const personalCardsWithBrokenConnection: Record<string, Card> = {};

function addErrorsForPersonalCard(card: Card) {
const hasCardErrors = !isEmptyObject(card.errors) || !isEmptyObject(card.errorFields);
// Stop surfacing the broken connection (task + RBR) once it has been unresolved past the
// grace period; the underlying error on the card is kept so the user can still fix it.
const isPastDismissThreshold = isBrokenConnectionPastDismissThreshold(card);
// The broken connection is reported as an error on the card itself, so past the grace period it must not
// count towards the RBR either — otherwise keeping the error (so the card stays fixable) would keep the
// Account/Wallet red dot lit forever.
const hasCardErrors = (!isEmptyObject(card.errors) || !isEmptyObject(card.errorFields)) && !isPastDismissThreshold;
Comment thread
wildan-m marked this conversation as resolved.
Outdated
const cardErrors = {
...(hasCardErrors
? {
Expand All @@ -66,9 +72,7 @@ export default createOnyxDerivedValueConfig({
: {}),
} as Record<string, CardErrors>;

// Stop surfacing the broken connection (task + RBR) once it has been unresolved past the
// grace period; the underlying error on the card is kept so the user can still fix it.
const isFeedConnectionBroken = isCardConnectionBroken(card) && !isBrokenConnectionPastDismissThreshold(card);
const isFeedConnectionBroken = isCardConnectionBroken(card) && !isPastDismissThreshold;
// Track personal cards with broken feed connection
if (isFeedConnectionBroken) {
personalCardsWithBrokenConnection[card.cardID] = card;
Expand Down
8 changes: 6 additions & 2 deletions src/libs/actions/PaymentMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,8 @@ function dismissSuccessfulTransferBalancePage() {
/**
* Looks through each payment method to see if there is an existing error.
* When session and policies are provided, card list errors are only counted if the current user
* is a member of that card's workspace (for company cards) or always counted for personal cards.
* is a member of that card's workspace (for company cards), or counted for personal cards unless the
* card's broken connection has been unresolved past the dismiss grace period.
* Only cards with non-empty errors are considered (error cards).
*/
function hasPaymentMethodError(
Expand All @@ -437,7 +438,10 @@ function hasPaymentMethodError(
const policyList = Object.values(policies ?? {}).filter(Boolean);
const hasRelevantCardError = cardsWithErrors.some((card) => {
if (CardUtils.isPersonalCard(card)) {
return true;
// Once a personal card's broken connection has been unresolved past the grace period we stop leading the user
// to it, so it must not light the Account/Wallet RBR. The error itself is kept on the card so it can still be
// fixed from the Wallet page.
return !CardUtils.isBrokenConnectionPastDismissThreshold(card);
Comment thread
wildan-m marked this conversation as resolved.
Outdated
}
const workspaceAccountID = Number(card?.fundID);
const policy = policyList.find((p) => p?.policyAccountID === workspaceAccountID);
Expand Down
52 changes: 52 additions & 0 deletions tests/unit/CardFeedErrorsDerivedValueTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,58 @@ describe('CardFeedErrors Derived Value', () => {
expect(result.personalCard.isFeedConnectionBroken).toBe(false);
expect(result.personalCardsWithBrokenConnection).toEqual({});
});

// A real broken personal card also carries the error on the card itself, which we intentionally keep so the
// card stays fixable. That error used to short-circuit getShouldShowRBR, leaving the Account/Wallet red dot
// lit past the grace period even though the home task was correctly removed.
it('should NOT show the RBR for a personal card past the grace period that still carries the broken-connection error', () => {
const card = createCard({
cardID: CARD_IDS.card1,
lastScrapeResult: 403, // Broken connection
lastScrape: '2020-01-01 00:00:00', // Last successful scrape is well beyond the grace period
errors: {connectionError: 'Your card connection is broken.'}, // Kept so the card can still be fixed
});

const globalCardList: CardList = {card1: card};

const result = cardFeedErrorsConfig.compute([globalCardList, {}, {}, undefined], DERIVED_VALUE_CONTEXT);

expect(result.personalCard.shouldShowRBR).toBe(false);
expect(result.personalCard.isFeedConnectionBroken).toBe(false);
});

it('should still show the RBR for a broken personal card within the grace period that carries the error', () => {
// Broken only since yesterday, so it is still well inside the grace period and must keep prompting.
const recentScrape = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().slice(0, 19).replace('T', ' ');
const card = createCard({
cardID: CARD_IDS.card1,
lastScrapeResult: 403,
lastScrape: recentScrape,
errors: {connectionError: 'Your card connection is broken.'},
});

const globalCardList: CardList = {card1: card};

const result = cardFeedErrorsConfig.compute([globalCardList, {}, {}, undefined], DERIVED_VALUE_CONTEXT);

expect(result.personalCard.shouldShowRBR).toBe(true);
expect(result.personalCard.isFeedConnectionBroken).toBe(true);
});

it('should still show the RBR for a personal card with an unrelated error and a healthy connection', () => {
const card = createCard({
cardID: CARD_IDS.card1,
lastScrapeResult: 200, // Not a broken connection, so the grace period does not apply
lastScrape: '2020-01-01 00:00:00',
errors: {unrelatedError: 'Something else went wrong.'},
});

const globalCardList: CardList = {card1: card};

const result = cardFeedErrorsConfig.compute([globalCardList, {}, {}, undefined], DERIVED_VALUE_CONTEXT);

expect(result.personalCard.shouldShowRBR).toBe(true);
});
});

describe('processing cards from workspace cards collection', () => {
Expand Down
Loading