Skip to content

Commit f6d66d3

Browse files
Fix offline Your Spend repaid section after paying reports
1 parent 72448a2 commit f6d66d3

2 files changed

Lines changed: 51 additions & 2 deletions

File tree

src/libs/actions/IOU/PayMoneyRequest.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ function getPayMoneyRequestParams({
194194
| typeof ONYXKEYS.NVP_LAST_PAYMENT_METHOD
195195
| typeof ONYXKEYS.COLLECTION.TRANSACTION
196196
| typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS
197+
| typeof ONYXKEYS.COLLECTION.SNAPSHOT
197198
| BuildPolicyDataKeys
198199
> = {
199200
optimisticData: [],
@@ -497,6 +498,20 @@ function getPayMoneyRequestParams({
497498
optimisticHoldReportExpenseActionIDs = JSON.stringify(holdReportOnyxData.optimisticHoldReportExpenseActionIDs);
498499
}
499500

501+
// Paying a report moves it out of "Awaiting approval" and into "Repaid (last 30 days)" in the Your spend widget.
502+
// Home reads those section totals from cached snapshots that are only refreshed online, and the linked Search page
503+
// renders its list from `snapshot.data` — so without this optimistic patch the repaid section opens empty offline.
504+
const yourSpendSnapshotUpdates = getYourSpendSnapshotReportMoveUpdates({
505+
iouReport,
506+
reportTransactions,
507+
fromStatus: {stateNum: iouReport?.stateNum, statusNum: iouReport?.statusNum},
508+
toStatus: {stateNum: CONST.REPORT.STATE_NUM.APPROVED, statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED},
509+
currentUserAccountID: currentUserAccountIDParam,
510+
});
511+
onyxData.optimisticData?.push(...yourSpendSnapshotUpdates.optimisticData);
512+
onyxData.successData?.push(...yourSpendSnapshotUpdates.successData);
513+
onyxData.failureData?.push(...yourSpendSnapshotUpdates.failureData);
514+
500515
return {
501516
params: {
502517
iouReportID: iouReport?.reportID,

src/pages/home/YourSpendSection/useYourSpendData.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import useNetwork from '@hooks/useNetwork';
88
import useOnyx from '@hooks/useOnyx';
99
import {search} from '@libs/actions/Search';
1010
import {getDisplayableExpensifyCards, getDisplayableThirdPartyCards, isPersonalCard, lastFourNumbersFromCardName} from '@libs/CardUtils';
11+
// eslint-disable-next-line no-restricted-imports -- Your Spend scopes approval/payment workflows to paid (Collect/Control) group policies, so this is a genuine billing/paid-only check.
1112
import {arePaymentsEnabled, isPaidGroupPolicy} from '@libs/PolicyUtils';
1213
import {buildSearchQueryJSON} from '@libs/SearchQueryUtils';
1314
import {getSuggestedSearches, getSuggestedSearchesVisibility, TODO_SEARCH_KEYS} from '@libs/SearchUIUtils';
@@ -113,6 +114,24 @@ function getOutstandingReportsSignature(reports: OnyxCollection<Report> | undefi
113114
return ids.sort().join(',');
114115
}
115116

117+
// Signature of the reports the user owns that are currently REPAID (reimbursed). The home query results are
118+
// cached snapshots that are not patched when a report's state changes, so without this the cached "Repaid"
119+
// total can be resurrected after the user cancels the payment of their last repaid expense (the snapshot count
120+
// gets wiped to null by `shouldCalculateTotals: false` searches, which would otherwise re-show the stale total).
121+
// Mirrors `getOutstandingReportsSignature` for the "Awaiting approval" row.
122+
function getRepaidReportsSignature(reports: OnyxCollection<Report> | undefined, accountID: number): string {
123+
if (!reports) {
124+
return '';
125+
}
126+
const ids: string[] = [];
127+
for (const report of Object.values(reports)) {
128+
if (report?.ownerAccountID === accountID && (report.stateNum ?? 0) >= CONST.REPORT.STATE_NUM.APPROVED && report.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED) {
129+
ids.push(report.reportID);
130+
}
131+
}
132+
return ids.sort().join(',');
133+
}
134+
116135
function getYourSpendRowState({isApplicable, isOffline, searchResults}: GetYourSpendRowStateParams): YourSpendRowState {
117136
if (!isApplicable) {
118137
return YOUR_SPEND_ROW_STATE.HIDDEN;
@@ -158,6 +177,12 @@ function useYourSpendData(): UseYourSpendDataReturn {
158177
selector: (reports) => getOutstandingReportsSignature(reports, paidGroupPolicyIDs, accountID),
159178
});
160179

180+
// Signature of the reports the user owns that are currently REPAID. Lets the "Repaid" cached-total fallback
181+
// drop its stale value once the user no longer owns any repaid report (e.g. after cancelling a payment).
182+
const [repaidReportsSignature] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {
183+
selector: (reports) => getRepaidReportsSignature(reports, accountID),
184+
});
185+
161186
// Destructure here so downstream memos depend only on the sub-records, not on
162187
// the parent value that's rebuilt on every CARD_FEED_ERRORS tick.
163188
const {cardsWithBrokenFeedConnection, personalCardsWithBrokenConnection} = useCardFeedErrors();
@@ -355,7 +380,16 @@ function useYourSpendData(): UseYourSpendDataReturn {
355380
cachedApprovalReady !== null &&
356381
cachedApprovalHash === approvalHash &&
357382
outstandingReportsSignature !== '';
358-
const shouldUseCachedPayment = paymentRowStateRaw === YOUR_SPEND_ROW_STATE.HIDDEN_EMPTY && paymentCountIsMissing && paymentSearchResults !== undefined && cachedPaymentReady !== null;
383+
// Only bridge a wiped/missing count with the cached total while the user still owns a REPAID report.
384+
// An empty signature means nothing is repaid, so the row must hide immediately after cancelling the
385+
// payment of the last repaid expense — otherwise the stale cached total is resurrected when a
386+
// `shouldCalculateTotals: false` search wipes the snapshot count to null.
387+
const shouldUseCachedPayment =
388+
paymentRowStateRaw === YOUR_SPEND_ROW_STATE.HIDDEN_EMPTY &&
389+
paymentCountIsMissing &&
390+
paymentSearchResults !== undefined &&
391+
cachedPaymentReady !== null &&
392+
repaidReportsSignature !== '';
359393

360394
const approvalRowState = shouldUseCachedApproval ? YOUR_SPEND_ROW_STATE.READY : approvalRowStateRaw;
361395
const paymentRowState = shouldUseCachedPayment ? YOUR_SPEND_ROW_STATE.READY : paymentRowStateRaw;
@@ -457,5 +491,5 @@ function useYourSpendData(): UseYourSpendDataReturn {
457491
};
458492
}
459493

460-
export {YOUR_SPEND_CARD_KIND, YOUR_SPEND_ROW_STATE, getOutstandingReportsSignature, getYourSpendApplicability, getYourSpendRowState, useYourSpendData};
494+
export {YOUR_SPEND_CARD_KIND, YOUR_SPEND_ROW_STATE, getOutstandingReportsSignature, getRepaidReportsSignature, getYourSpendApplicability, getYourSpendRowState, useYourSpendData};
461495
export type {GetYourSpendRowStateParams, UseYourSpendDataReturn, YourSpendApplicability, YourSpendCardKind, YourSpendCardRow, YourSpendRowState, YourSpendRowTotals};

0 commit comments

Comments
 (0)