Skip to content
Open
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
14 changes: 10 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,15 @@ export default createOnyxDerivedValueConfig({
const personalCardsWithBrokenConnection: Record<string, Card> = {};

function addErrorsForPersonalCard(card: Card) {
const hasCardErrors = !isEmptyObject(card.errors) || !isEmptyObject(card.errorFields);
// Once the broken connection is unresolved past the grace period we stop leading the user to it: the
// time-sensitive task and the RBR are removed. The broken-connection error surfaces via
// `errorFields.lastScrape`, so drop only that field when deciding the RBR — any other actionable error on
// the card (a failed reimbursable/start-date update in another `errorFields` entry, a failed remove in
// `card.errors`, etc.) is kept so it still surfaces. The error itself stays on the card so it's fixable.
const isPastDismissThreshold = isBrokenConnectionPastDismissThreshold(card);
const errorFieldsForRBR =
isPastDismissThreshold && card.errorFields ? Object.fromEntries(Object.entries(card.errorFields).filter(([field]) => field !== 'lastScrape')) : card.errorFields;
const hasCardErrors = !isEmptyObject(card.errors) || !isEmptyObject(errorFieldsForRBR);
const cardErrors = {
...(hasCardErrors
? {
Expand All @@ -66,9 +74,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
89 changes: 89 additions & 0 deletions tests/unit/CardFeedErrorsDerivedValueTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,95 @@ describe('CardFeedErrors Derived Value', () => {
expect(result.personalCard.isFeedConnectionBroken).toBe(false);
expect(result.personalCardsWithBrokenConnection).toEqual({});
});

// A broken personal card surfaces its connection error via errorFields.lastScrape (this is what the card
// detail page reads). Past the grace period we stop leading the user to it, so that specific error must not
// light the Account/Wallet RBR — while the error itself is kept so the card can still be fixed.
it('should NOT show the RBR for a personal card past the grace period whose only error is the broken connection', () => {
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
errorFields: {lastScrape: {error: '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', () => {
// 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,
errorFields: {lastScrape: {error: '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);
});

// Past the grace period we suppress ONLY the broken-connection error, not other actionable errors on the same
// card — e.g. a failed reimbursable/start-date update, which lands in a different errorFields entry.
it('should still show the RBR for a past-grace broken card that also has an unrelated field error', () => {
const card = createCard({
cardID: CARD_IDS.card1,
lastScrapeResult: 403,
lastScrape: '2020-01-01 00:00:00',
errorFields: {
lastScrape: {error: 'Your card connection is broken.'},
reimbursable: {error: 'Failed to update the reimbursable setting.'},
},
});

const globalCardList: CardList = {card1: card};

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

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

// ...nor an error written to card.errors, e.g. a failed remove of the card.
it('should still show the RBR for a past-grace broken card that also has a card-level error', () => {
const card = createCard({
cardID: CARD_IDS.card1,
lastScrapeResult: 403,
lastScrape: '2020-01-01 00:00:00',
errorFields: {lastScrape: {error: 'Your card connection is broken.'}},
errors: {removeFailed: 'Failed to remove the card.'},
});

const globalCardList: CardList = {card1: card};

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

expect(result.personalCard.shouldShowRBR).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