You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Spend - The newly added expense is not briefly highlighted when created via FAB #100788
👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:
Identify the pull request that introduced this issue and revert it.
Classification: Frontend bug Causing PR: #98744 - "refactor: the new-transaction highlight rail and the wide RHP width model" by @TaduJR (High confidence) Related Issues: none found
Recommendation: ROLL FORWARD
The regression is a single guard in useAnimatedHighlightStyle that skips the highlight pulse if the transient shouldHighlight flag has already been retracted, and Search retracts that flag (300 ms) before the row's entry animation finishes (350 ms) — so the pulse is always skipped. A targeted fix to that guard is far smaller and safer than reverting a 50-file PR that also carries the wide-RHP width model rewrite and several other highlight fixes; revert only as a fallback if a fix can't land quickly.
Assigned: @TaduJR (PR author), @abzokhattab and @blimpich (approving reviewers) from the highest-confidence causing PR only Labels: No changes. DeployBlockerCash is kept (App-side regression, App deploy is blocked). DeployBlocker was not present, so nothing was removed.
📋 Detailed Analysis
Evidence
Reproducible on staging, not on production — and src/hooks/useAnimatedHighlightStyle/index.ts differs between the two branches. Production still has the old useState-driven implementation that plays the pulse unconditionally from the entry animation's completion callback; staging has the rewritten phase-ref implementation from refactor: the new-transaction highlight rail and the wide RHP width model #98744.
#98744 is in the 9.4.75-0 checklist and merged 2026-09-09T00:07Z (merge commit 3aaa7f70ff1080404ce889ea6da46e558c60c358), just before the 9.4.75-0 staging cut (2b613353, 2026-09-09T19:18Z).
It is the only PR in this release that touches src/hooks/useAnimatedHighlightStyle/index.ts with logic changes (+83/-33). The only later commit on that file is from #100576, which is comment/whitespace-only.
The symptom matches the mechanism exactly: the entry (reveal) animation still runs, so the row appears, but the colour pulse never plays, so it is not highlighted — precisely what the report says.
Platform-independent (reported on iOS App and macOS Safari/Chrome), consistent with a pure JS timing bug rather than a platform issue.
Verification
Affected code path for the "new item highlight" on Spend › Expenses:
Confirmed #98744 modifies file 5 (and file 4's sibling TransactionGroupListExpanded.tsx), and that staging's copy of files 1–5 is byte-identical to the code analysed here.
Root Cause
useAnimatedHighlightStyle on staging defers the colour pulse until the entry animation completes, then re-checks whether the row is still flagged:
useAnimatedHighlightStyle/index.ts#L120-L142 — pulseIfStillHighlighted() returns early unless prevShouldHighlightRef.current is true, and it is invoked from the entry animation's completion callback.
The timings make that check fail every time:
entry = ANIMATED_HIGHLIGHT_ENTRY_DELAY (50) + ANIMATED_HIGHLIGHT_ENTRY_DURATION (300) = 350 ms before the pulse is even considered;
Search retracts shouldHighlight after ANIMATED_HIGHLIGHT_START_DURATION = 300 ms.
So by the time the completion callback runs, prevShouldHighlightRef.current has already been set back to false at L100-L108, and the pulse is dropped. The reveal still runs, which is why the row shows up un-highlighted instead of not showing up at all.
The FAB flow hits a second copy of the same mistake. Creating from the RHP means Search isn't focused, so the hook takes the awaitingFocus branch (L158-L167) and defers the pulse to the next focus event. But playPhaseRef is forced back to 'idle' the moment the flag retracts (L105-L106), so when focus finally arrives the listener's guard at L150 bails out. Both branches lose the pulse.
The underlying assumption — that a still-set shouldHighlight means "this row wasn't recycled" — doesn't hold, because the flag's lifetime (300 ms) is much shorter than the full animation, which is durationHighlightItem = 3460 ms.
Suggested fix (in src/hooks/useAnimatedHighlightStyle/index.ts): decide the pulse when the play is armed, not when the entry finishes. Latch the intent (e.g. a shouldPulseRef set at arm time) and have the entry-completion callback and the focus listener consult that latch rather than prevShouldHighlightRef.current / a phase that gets reset on retraction. Guard against the recycled-row case #98744 was defending against by keying the latch to the row's identity (item.keyForList) or clearing it on unmount, instead of on the transient flag flipping off. Note that two existing tests encode the current (buggy) behaviour and will need updating: tests/unit/useAnimatedHighlightStyleTest.tsx — "does not pulse when the highlight is cleared before the screen regains focus" and "does not pulse a row whose highlight was retracted while the entry was still playing".
An alternative one-liner — widening Search's retract window from ANIMATED_HIGHLIGHT_START_DURATION to durationHighlightItem in useSearchHighlightAndScroll — would also unblock both branches, but it keeps hasQueuedHighlights / handleSelectionListScroll armed ~3.5 s longer and re-stamps rows for that whole window, so it carries more behavioural risk than fixing the guard.
Activity
applause-bot commentedon Sep 10, 2026
While Applause doesn't embed attachments for this bug, here are the links for quick view:
https://platform.applause.com/services/links/v1/external/b354e220695c9655b93377076cb1114654485ba5eca5838f39731d5e8daf57ec
github-actions commentedon Sep 10, 2026
👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:
melvin-bot commentedon Sep 10, 2026
Triggered auto assignment to @NikkiWines (
DeployBlockerCash), see https://stackoverflowteams.com/c/expensify/questions/9980/ for more details.melvin-bot commentedon Sep 10, 2026
💬 A slack conversation has been started in #expensify-open-source
MelvinBot commentedon Sep 10, 2026
🔍 Investigation Summary
Classification: Frontend bug
Causing PR: #98744 - "refactor: the new-transaction highlight rail and the wide RHP width model" by @TaduJR (High confidence)
Related Issues: none found
Recommendation: ROLL FORWARD
The regression is a single guard in
useAnimatedHighlightStylethat skips the highlight pulse if the transientshouldHighlightflag has already been retracted, and Search retracts that flag (300 ms) before the row's entry animation finishes (350 ms) — so the pulse is always skipped. A targeted fix to that guard is far smaller and safer than reverting a 50-file PR that also carries the wide-RHP width model rewrite and several other highlight fixes; revert only as a fallback if a fix can't land quickly.Assigned: @TaduJR (PR author), @abzokhattab and @blimpich (approving reviewers) from the highest-confidence causing PR only
Labels: No changes.
DeployBlockerCashis kept (App-side regression, App deploy is blocked).DeployBlockerwas not present, so nothing was removed.📋 Detailed Analysis
Evidence
src/hooks/useAnimatedHighlightStyle/index.tsdiffers between the two branches. Production still has the olduseState-driven implementation that plays the pulse unconditionally from the entry animation's completion callback; staging has the rewritten phase-ref implementation from refactor: the new-transaction highlight rail and the wide RHP width model #98744.3aaa7f70ff1080404ce889ea6da46e558c60c358), just before the 9.4.75-0 staging cut (2b613353, 2026-09-09T19:18Z).src/hooks/useAnimatedHighlightStyle/index.tswith logic changes (+83/-33). The only later commit on that file is from #100576, which is comment/whitespace-only.Verification
Affected code path for the "new item highlight" on Spend › Expenses:
src/hooks/useSearchHighlightAndScroll.ts#L264— after the post-create refetch, the new transaction key is put innewSearchResultKeys.src/hooks/useSearchHighlightAndScroll.ts#L281-L292—newSearchResultKeysis cleared afterCONST.ANIMATED_HIGHLIGHT_START_DURATION= 300 ms.src/components/Search/hooks/useSearchSnapshot.ts#L328-L347— stampsshouldAnimateInHighlighton the matching row.TransactionListItemWide.tsx#L147-L153andTransactionListItemNarrow.tsx#L86-L92— pass it asshouldHighlightintouseAnimatedHighlightStylewith the default entry timings.src/hooks/useAnimatedHighlightStyle/index.ts— plays the entry then the pulse. This is the file refactor: the new-transaction highlight rail and the wide RHP width model #98744 rewrote.Confirmed #98744 modifies file 5 (and file 4's sibling
TransactionGroupListExpanded.tsx), and that staging's copy of files 1–5 is byte-identical to the code analysed here.Root Cause
useAnimatedHighlightStyleon staging defers the colour pulse until the entry animation completes, then re-checks whether the row is still flagged:useAnimatedHighlightStyle/index.ts#L120-L142—pulseIfStillHighlighted()returns early unlessprevShouldHighlightRef.currentis true, and it is invoked from the entry animation's completion callback.The timings make that check fail every time:
ANIMATED_HIGHLIGHT_ENTRY_DELAY(50) +ANIMATED_HIGHLIGHT_ENTRY_DURATION(300) = 350 ms before the pulse is even considered;shouldHighlightafterANIMATED_HIGHLIGHT_START_DURATION= 300 ms.So by the time the completion callback runs,
prevShouldHighlightRef.currenthas already been set back tofalseat L100-L108, and the pulse is dropped. The reveal still runs, which is why the row shows up un-highlighted instead of not showing up at all.The FAB flow hits a second copy of the same mistake. Creating from the RHP means Search isn't focused, so the hook takes the
awaitingFocusbranch (L158-L167) and defers the pulse to the nextfocusevent. ButplayPhaseRefis forced back to'idle'the moment the flag retracts (L105-L106), so when focus finally arrives the listener's guard at L150 bails out. Both branches lose the pulse.The underlying assumption — that a still-set
shouldHighlightmeans "this row wasn't recycled" — doesn't hold, because the flag's lifetime (300 ms) is much shorter than the full animation, which isdurationHighlightItem= 3460 ms.Suggested fix (in
src/hooks/useAnimatedHighlightStyle/index.ts): decide the pulse when the play is armed, not when the entry finishes. Latch the intent (e.g. ashouldPulseRefset at arm time) and have the entry-completion callback and thefocuslistener consult that latch rather thanprevShouldHighlightRef.current/ a phase that gets reset on retraction. Guard against the recycled-row case #98744 was defending against by keying the latch to the row's identity (item.keyForList) or clearing it on unmount, instead of on the transient flag flipping off. Note that two existing tests encode the current (buggy) behaviour and will need updating:tests/unit/useAnimatedHighlightStyleTest.tsx— "does not pulse when the highlight is cleared before the screen regains focus" and "does not pulse a row whose highlight was retracted while the entry was still playing".An alternative one-liner — widening Search's retract window from
ANIMATED_HIGHLIGHT_START_DURATIONtodurationHighlightIteminuseSearchHighlightAndScroll— would also unblock both branches, but it keepshasQueuedHighlights/handleSelectionListScrollarmed ~3.5 s longer and re-stamps rows for that whole window, so it carries more behavioural risk than fixing the guard.NikkiWines commentedon Sep 10, 2026
#98744 was reverted #100810