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
[Due for payment 2026-07-17] iOS-Scan-Report preview is blank after dismissing the "Enjoying New Expensify" pop up #95721
👋 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.
This PR decomposed the entire MoneyRequestReportPreview render path and moved its width-measurement + transition-pending gating into a new provider/nested-FlashList structure that fails to render on iOS narrow layout after a modal dismissal until a manual re-layout is forced (tap). It is on staging but not in production, matching the staging-only repro. A clean revert restores the previously-working single-component render.
Assigned: @OlimpiaZurek (PR author), @abzokhattab, @cristipaval, @mountiny (approving reviewers) Labels: No change. Kept DeployBlockerCash (an App PR caused the App-deploy-blocking bug). No DeployBlocker label was present.
📋 Detailed Analysis
Evidence
The "Enjoying New Expensify?" popup is ProactiveAppReviewModalManager (en.ts key proactiveAppReview.title). Its files were not changed in this release (last substantive change 2026-05-12), so the popup itself is not the culprit — it is only the trigger that leaves a modal-dismissal transition in flight.
git log of MoneyRequestReportPreviewContent.tsx shows the two newest commits (f2a132e66 "perf: decompose MoneyRequestReportPreview into provider + components", 97935445e "fix stale report-preview derivations") both belong to [NOQA] perf: add MoneyRequestReportPreview layout components #94681.
Staging-vs-production check confirms this is staging-only:
compare production...<94681 merge sha> → behind (not an ancestor of production → not in production)
compare <94681 merge sha>...staging → ahead (is an ancestor of staging → on staging)
This exactly matches "Reproducible in staging: Yes / production: No."
"Use Staging Server ON" requirement: the defect is timing-sensitive on the scan/report API response. Against the staging backend the preview mounts under the condition that exposes the render gate; against production timing it does not. The rendering defect itself is entirely client-side in the App.
Confirmed the PR modifies these files via gh pr view 94681 --json files.
Root Cause
The preview's rendered width is derived from onLayout-measured state, and the carousel is gated behind a loading state:
index.tsx initializes width state to {currentWidth: 0, currentWrapperWidth: 0} and only populates it from onCarouselLayout / onWrapperLayout (index.tsx:72-102).
isTransitionPending initializes to true whenever there is a pending DISMISS_MODAL_AND_OPEN_REPORT follow-up action for this report (MoneyRequestReportPreviewProvider.tsx:104-107) — precisely the state left behind by dismissing the "Enjoying New Expensify" modal after a scan. It is only cleared by useFocusEffect + TransitionTracker.runAfterTransitions (:115-126).
While shouldShowPreviewLoading is true (transition pending, or currentWidth === 0), TransactionReportCarousel.tsx:28-44 renders only an ActivityIndicator — the transaction content does not render.
The nested horizontal FlashList (TransactionReportCarousel.tsx:56-79) even keys items on the measured width (keyExtractor uses reportPreviewStyles.transactionPreviewCarouselStyle.width). On iOS narrow layout, getMoneyRequestReportPreviewStyle computes transactionPreviewWidth = currentWidth - padding - peek, which is 0/negative when currentWidth is the initial 0 (the ?? 256 fallback does not apply because 0 is passed explicitly, not undefined).
When the modal covers the list on mount, the report-action onLayout/transition-tracker path does not deliver a usable width or clear isTransitionPending, so the preview stays in the loading/zero-width state — blank. Tapping navigates/forces a re-layout, which fires onLayout and/or clears the pending transition, and the preview finally renders. Before #94681 this render path was a single component without this provider + nested-FlashList measurement pipeline, so the regression is scoped to this PR.
@OlimpiaZurek since you authored the decomposition, please confirm — reverting #94681 is the safest path to unblock the App deploy.
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.4.32-3 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
changed the title [-]iOS-Scan-Report preview is blank after dismissing the "Enjoying New Expensify" pop up[/-][+][Due for payment 2026-07-17] iOS-Scan-Report preview is blank after dismissing the "Enjoying New Expensify" pop up[/+]on Jul 10, 2026
@abzokhattab The PR fixing this issue has been merged! The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the BugZero Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]
@abzokhattab The PR fixing this issue has been merged! The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the BugZero Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]
All five blockers share one root cause — the coalescing defers the derived write to a macrotask, past React's render, so within that window consumers observe raw-updated-but-derived-stale state (a stale frame for #95696/#95698/#95700/#95721; a stale read that trips openReport's destructive fallback for #95753). The fix is to flush the coalesced recompute on a microtask (queueMicrotask) instead of a macrotask: the flush drains at the end of the current task — before paint and before React's next render — so derived data is consistent when consumers read it.
Measurements
[fabio] dependency changed / compute counts the derived recomputes; ManualSendMessage is the send-path span; the OnyxDerivedCompute_reportAttributes rows are the recompute count + total time for the busiest derived key.
Metric
main (sync)
macrotask
microtask
recomputes (per 51 dep-changes)
51
20
20
send latency (mean of 5)
177.2 ms
82.0 ms
84.2 ms
reportAttributes computes
11 (~295 ms)
5 (~242 ms)
5 (~238 ms)
Microtask matches macrotask on coalescing (51 → 20), send latency (~84 vs ~82 ms; ~53% faster than main), and reportAttributes recompute count (11 → 5). There is no measurable perf cost to using microtask over macrotask, and it removes the stale-frame/stale-read window.
Activity
applause-bot commentedon Jul 9, 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/a2c8b133df60028278c2f0f5f8b28334ee050c893edee1ca79c5b703ad76ccbf
melvin-bot commentedon Jul 9, 2026
Triggered auto assignment to @chiragsalian (
DeployBlockerCash), see https://stackoverflowteams.com/c/expensify/questions/9980/ for more details.melvin-bot commentedon Jul 9, 2026
💬 A slack conversation has been started in #expensify-open-source
github-actions commentedon Jul 9, 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:
mitarachim commentedon Jul 9, 2026
I cannot reproduce issue while
Use Staging Serveris disable/OFFScreenRecording_07-09-2026.23.MP4
MelvinBot commentedon Jul 9, 2026
🔍 Investigation Summary
Classification: Frontend bug
Causing PR: #94681 - "[NOQA] perf: add MoneyRequestReportPreview layout components" by @OlimpiaZurek (High confidence)
Related Issues: None identified
Recommendation: REVERT
This PR decomposed the entire
MoneyRequestReportPreviewrender path and moved its width-measurement + transition-pending gating into a new provider/nested-FlashList structure that fails to render on iOS narrow layout after a modal dismissal until a manual re-layout is forced (tap). It is on staging but not in production, matching the staging-only repro. A clean revert restores the previously-working single-component render.Assigned: @OlimpiaZurek (PR author), @abzokhattab, @cristipaval, @mountiny (approving reviewers)
Labels: No change. Kept
DeployBlockerCash(an App PR caused the App-deploy-blocking bug). NoDeployBlockerlabel was present.📋 Detailed Analysis
Evidence
ProactiveAppReviewModalManager(en.tskeyproactiveAppReview.title). Its files were not changed in this release (last substantive change 2026-05-12), so the popup itself is not the culprit — it is only the trigger that leaves a modal-dismissal transition in flight.MoneyRequestReportPreview. That component and its whole subtree were rewritten by [NOQA] perf: add MoneyRequestReportPreview layout components #94681 (14 files, +1315/−590), merged 2026-07-06:git logofMoneyRequestReportPreviewContent.tsxshows the two newest commits (f2a132e66"perf: decompose MoneyRequestReportPreview into provider + components",97935445e"fix stale report-preview derivations") both belong to [NOQA] perf: add MoneyRequestReportPreview layout components #94681.compare production...<94681 merge sha>→ behind (not an ancestor of production → not in production)compare <94681 merge sha>...staging→ ahead (is an ancestor of staging → on staging)Verification
Affected files (all rewritten by #94681):
src/components/ReportActionItem/MoneyRequestReportPreview/index.tsxsrc/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewProvider.tsxsrc/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewBody.tsxsrc/components/ReportActionItem/MoneyRequestReportPreview/TransactionReportCarousel.tsxConfirmed the PR modifies these files via
gh pr view 94681 --json files.Root Cause
The preview's rendered width is derived from
onLayout-measured state, and the carousel is gated behind a loading state:index.tsxinitializes width state to{currentWidth: 0, currentWrapperWidth: 0}and only populates it fromonCarouselLayout/onWrapperLayout(index.tsx:72-102).MoneyRequestReportPreviewProvider.tsx:149:shouldShowPreviewLoading = isTransitionPending || shouldShowLoading || shouldShowLoadingDeferred || (!currentWidth && !shouldShowPreviewPlaceholder).isTransitionPendinginitializes totruewhenever there is a pendingDISMISS_MODAL_AND_OPEN_REPORTfollow-up action for this report (MoneyRequestReportPreviewProvider.tsx:104-107) — precisely the state left behind by dismissing the "Enjoying New Expensify" modal after a scan. It is only cleared byuseFocusEffect+TransitionTracker.runAfterTransitions(:115-126).shouldShowPreviewLoadingis true (transition pending, orcurrentWidth === 0),TransactionReportCarousel.tsx:28-44renders only anActivityIndicator— the transaction content does not render.FlashList(TransactionReportCarousel.tsx:56-79) even keys items on the measured width (keyExtractorusesreportPreviewStyles.transactionPreviewCarouselStyle.width). On iOS narrow layout,getMoneyRequestReportPreviewStylecomputestransactionPreviewWidth = currentWidth - padding - peek, which is 0/negative whencurrentWidthis the initial0(the?? 256fallback does not apply because0is passed explicitly, notundefined).When the modal covers the list on mount, the report-action
onLayout/transition-tracker path does not deliver a usable width or clearisTransitionPending, so the preview stays in the loading/zero-width state — blank. Tapping navigates/forces a re-layout, which firesonLayoutand/or clears the pending transition, and the preview finally renders. Before #94681 this render path was a single component without this provider + nested-FlashList measurement pipeline, so the regression is scoped to this PR.@OlimpiaZurek since you authored the decomposition, please confirm — reverting #94681 is the safest path to unblock the App deploy.
4 remaining items
melvin-bot commentedon Jul 10, 2026
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.4.32-3 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
If no regressions arise, payment will be issued on 2026-07-17. 🎊
For reference, here are some details about the assignees on this issue:
[-]iOS-Scan-Report preview is blank after dismissing the "Enjoying New Expensify" pop up[/-][+][Due for payment 2026-07-17] iOS-Scan-Report preview is blank after dismissing the "Enjoying New Expensify" pop up[/+]melvin-bot commentedon Jul 10, 2026
@abzokhattab The PR fixing this issue has been merged! The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the BugZero Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]
melvin-bot commentedon Jul 14, 2026
@abzokhattab The PR fixing this issue has been merged! The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the BugZero Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]
fabioh8010 commentedon Jul 22, 2026
Updates:
All five blockers share one root cause — the coalescing defers the derived write to a macrotask, past React's render, so within that window consumers observe raw-updated-but-derived-stale state (a stale frame for #95696/#95698/#95700/#95721; a stale read that trips
openReport's destructive fallback for #95753). The fix is to flush the coalesced recompute on a microtask (queueMicrotask) instead of a macrotask: the flush drains at the end of the current task — before paint and before React's next render — so derived data is consistent when consumers read it.Measurements
[fabio] dependency changed / computecounts the derived recomputes;ManualSendMessageis the send-path span; theOnyxDerivedCompute_reportAttributesrows are the recompute count + total time for the busiest derived key.reportAttributescomputesMicrotask matches macrotask on coalescing (51 → 20), send latency (~84 vs ~82 ms; ~53% faster than
main), andreportAttributesrecompute count (11 → 5). There is no measurable perf cost to using microtask over macrotask, and it removes the stale-frame/stale-read window.Here's the v2 PR: #96785
mountiny commentedon Jul 23, 2026
Really great investigation, thanks for the fix!
lanitochka17 commentedon Jul 28, 2026
Deploy Blocker #97250 was identified to be related to the PR #96785 that attempted to fix this issue.