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-10-20] [Exploratory] Chart- Inline chart is empty when minimize and reopen app #101396
Version Number: 9.4.81-0 Reproducible in staging?: Yes Reproducible in production?: Yes If this was caught during regression testing, add the test name, ID and link from BrowserStack:#99661 Email or phone of affected tester (no customers):duytestnguyen+384@gmail.com Issue reported by: Applause Internal Team Bug source: Exploratory - Significant User Experience Deterioration Device used: Android 16 / Pixel 7A App Component: Other
Action Performed:
Preconditions:
Use account that has many expenses from previous days, months
Open Hybrid app
Navigate to Concierge chat
Ask for "Top spenders", "Top categories"...
Tap on "expand" button in the inline chat
Miminize the app
Reopen the app
Expected Result:
The chart should be displayed after reopen the app
The chart data is fine — it's the Skia canvas that never repaints.
When Android backgrounds the app, the Skia TextureView backing the chart destroys its surface. On resume the surface is recreated, but nothing on the JS side pushes a new picture into it, so the canvas comes back blank. This is the long-standing upstream bug Shopify/react-native-skia#2135 ("[android] Canvas disappear in Modal when app is resumed from background"), still open.
Nothing in App recovers from it. There is no AppState listener anywhere in the chart path — grep -rn "AppState" src/components/Charts src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer src/components/MultiGestureCanvas returns zero matches. Since the chart props are static, React never re-renders it, so the canvas is never asked to redraw.
Why tapping "expand" first is part of the repro:VictoryChartExpandable.tsx:70 latches shouldRenderModal to true on first expand and deliberately keeps the modal mounted after close (comment at :28-30). From that point the chart lives under an Android Modal, which is a separate window — exactly the configuration #2135 is filed against.
Three things I checked and ruled out:
What is NOT the cause
The data. The chart is re-derived from the Onyx-persisted report action HTML on every render — BaseVictoryChartRenderer.tsx:23 calls processVictoryChartTree(tnode, …) in the render body. There is no module-level store, so a remount cannot lose it.
The Skia font cache. It's module-level and never invalidated, but typefaces are CPU-side and Update version to 1.0.7-2 on master #2135 reproduces with a bare <Circle> — no text involved.
Secondary contributor (not fully verified): the inline chart is the only variant whose canvas size comes from ephemeral layout state — getChartLayoutModeProps.ts:6-12 returns {} when there's no explicitSize, so victory-native measures via a single onLayout into useState. A 0×0 measurement while the activity is stopped behind a fullscreen dialog would stick. The expanded chart passes explicitSize and bypasses this. This matches the title saying inline, but I could not prove Android delivers that 0×0 layout without a device repro.
What changes do you think we should make in order to solve the problem?
Remount the chart's Skia Canvas when the app returns to the foreground — the same workaround the App already uses for Lottie, which breaks the same way.
Apply the same shape in the chart renderer: hook useAppState, bump a counter each time the app goes background → active, and pass it as key on the chart container so the Canvas unmounts and remounts with a live surface. This needs to cover both the inline chart and the one inside VictoryChartExpandModal, since both sit on Skia canvases.
Remounting is cheap here — the chart model is re-derived from the HTML anyway, and the font cache is module-level so no typefaces are reloaded.
What alternative solutions did you explore?
Set androidWarmup on the Canvas. RN Skia's androidWarmup path blits the picture into a plain Android Bitmap via onDraw, independent of the TextureView surface, and the patch-003 canvasProps passthrough would already carry it. Its documented purpose is first-frame warmup, so I'm not confident it survives surface loss — worth a quick try since it's a one-line change, but I wouldn't lead with it.
Unmount the expand modal on close instead of latching shouldRenderModal. This removes the Modal window that Update version to 1.0.7-2 on master #2135 is specifically about, but the comment at :28-30 says keeping it mounted is a deliberate perf choice, and it wouldn't help the inline canvas if that one blanks too.
I could not reproduce this on a device — this run is scoped to web, and the bug is Android-app-only, so a browser cannot exercise the TextureView surface-destroy path at all. Two things worth confirming before implementing:
Does the blank canvas in the video belong to the inline chart or the still-open expand modal? Both share the root cause and the fix, but it determines which one to test first.
Does it still reproduce if you never tap expand? If yes, the inline canvas blanks on its own and the fix must cover it independently of the modal. If no, the Modal window is the trigger.
adb logcat filtered on SkiaTextureView/SkiaView during the repro would show whether onSurfaceTextureAvailable fires on resume and paints nothing, versus never firing.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/101396) to create a draft PR. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal.
Please mark it as "Ready for review" when it is ready for review.
What it does: a new useSkiaCanvasRemountKey hook returns a value that changes on every background → active transition, and that value is passed as a key on the chart so the Skia Canvas remounts against a live surface. Same shape as the existing Lottie workaround. On web the hook returns a constant, so nothing changes there.
Two things to look at while you validate:
The key is applied inside VictoryChartContent and VictoryChartInteractiveContent, which covers all three mount sites — inline chart, the fluid chart in VictoryChartExpandModal, and ExpandedChartBox. So it should recover whichever canvas is actually blanking, which also answers the open question from the analysis about which one it is.
Only a full background transition bumps the key, not inactive. If it turns out Android blanks the canvas on a transition that never reports background, the key won't fire and you'll see no change — that's the most likely way this fails.
I could not verify it. The bug is Android-app-only and this run was scoped to web, so no device repro. The web test session also failed during onboarding, so there's no App-level verification at all in the PR. Local checks that did pass: lint, typecheck, React Compiler compliance (both Babel and OXC), cspell, and 307 chart tests including 4 new ones for the hook.
[Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.
Link to the comment on the PR:
Link to the comment on the Issue:
[Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.
Link to discussion:
[Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.
Regression Test Proposal
Precondition:
an account with expenses from earlier days and months.
Test:
On Android, open Concierge and ask for "Top categories" (pie chart).
Tap the expand button on the chart.
Minimize the app, then reopen it.
Verify the expanded chart is still displayed.
Repeat steps 3-4, locking and unlocking the screen instead of minimizing.
Repeat steps 1-4 with "Top spenders" (bar chart).
Close the expanded chart, minimize and reopen the app, and verify the inline chart is still displayed.
Activity
applause-bot commentedon Sep 17, 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/7eeb6c9d41c6a101271204ea83cb512550677fb0adcb9134ccf2c01cc934a0d5
situchan commentedon Sep 17, 2026
@MelvinBot investigate this
MelvinBot commentedon Sep 17, 2026
Proposal
What is the root cause of that problem?
The chart data is fine — it's the Skia canvas that never repaints.
When Android backgrounds the app, the Skia
TextureViewbacking the chart destroys its surface. On resume the surface is recreated, but nothing on the JS side pushes a new picture into it, so the canvas comes back blank. This is the long-standing upstream bug Shopify/react-native-skia#2135 ("[android] Canvas disappear in Modal when app is resumed from background"), still open.Nothing in App recovers from it. There is no
AppStatelistener anywhere in the chart path —grep -rn "AppState" src/components/Charts src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer src/components/MultiGestureCanvasreturns zero matches. Since the chart props are static, React never re-renders it, so the canvas is never asked to redraw.Why tapping "expand" first is part of the repro:
VictoryChartExpandable.tsx:70latchesshouldRenderModaltotrueon first expand and deliberately keeps the modal mounted after close (comment at :28-30). From that point the chart lives under an AndroidModal, which is a separate window — exactly the configuration #2135 is filed against.Three things I checked and ruled out:
What is NOT the cause
BaseVictoryChartRenderer.tsx:23callsprocessVictoryChartTree(tnode, …)in the render body. There is no module-level store, so a remount cannot lose it.getStaticChartCanvasProps/index.native.ts:4returnsundefined— it's a web-only no-op. That PR hits this bug, it did not introduce it;VictoryChartExpandablepredates it.<Circle>— no text involved.Secondary contributor (not fully verified): the inline chart is the only variant whose canvas size comes from ephemeral layout state —
getChartLayoutModeProps.ts:6-12returns{}when there's noexplicitSize, so victory-native measures via a singleonLayoutintouseState. A 0×0 measurement while the activity is stopped behind a fullscreen dialog would stick. The expanded chart passesexplicitSizeand bypasses this. This matches the title saying inline, but I could not prove Android delivers that 0×0 layout without a device repro.What changes do you think we should make in order to solve the problem?
Remount the chart's Skia
Canvaswhen the app returns to the foreground — the same workaround the App already uses for Lottie, which breaks the same way.src/components/Lottie/index.tsxreadsuseAppState()at :31, renders a plain placeholderViewwhileappState.isBackgroundat :110, and forces a fresh mount via akeyprop at :128.Apply the same shape in the chart renderer: hook
useAppState, bump a counter each time the app goesbackground → active, and pass it askeyon the chart container so theCanvasunmounts and remounts with a live surface. This needs to cover both the inline chart and the one insideVictoryChartExpandModal, since both sit on Skia canvases.Remounting is cheap here — the chart model is re-derived from the HTML anyway, and the font cache is module-level so no typefaces are reloaded.
What alternative solutions did you explore?
androidWarmupon theCanvas. RN Skia'sandroidWarmuppath blits the picture into a plain AndroidBitmapviaonDraw, independent of theTextureViewsurface, and the patch-003canvasPropspassthrough would already carry it. Its documented purpose is first-frame warmup, so I'm not confident it survives surface loss — worth a quick try since it's a one-line change, but I wouldn't lead with it.shouldRenderModal. This removes theModalwindow that Update version to 1.0.7-2 on master #2135 is specifically about, but the comment at :28-30 says keeping it mounted is a deliberate perf choice, and it wouldn't help the inline canvas if that one blanks too.What I could not verify
I could not reproduce this on a device — this run is scoped to web, and the bug is Android-app-only, so a browser cannot exercise the
TextureViewsurface-destroy path at all. Two things worth confirming before implementing:Modalwindow is the trigger.adb logcatfiltered onSkiaTextureView/SkiaViewduring the repro would show whetheronSurfaceTextureAvailablefires on resume and paints nothing, versus never firing.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/101396)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run
abbasifaizan70 commentedon Sep 17, 2026
@situchan @luacmartins Can i investigate it?
situchan commentedon Sep 18, 2026
@MelvinBot draft PR for testing. I'll validate if remount solution works.
MelvinBot commentedon Sep 18, 2026
🤖 Draft PR created: Remount chart Skia canvas when the app returns to the foreground
Please mark it as "Ready for review" when it is ready for review.
What it does: a new
useSkiaCanvasRemountKeyhook returns a value that changes on everybackground → activetransition, and that value is passed as akeyon the chart so the SkiaCanvasremounts against a live surface. Same shape as the existing Lottie workaround. On web the hook returns a constant, so nothing changes there.Two things to look at while you validate:
keyis applied insideVictoryChartContentandVictoryChartInteractiveContent, which covers all three mount sites — inline chart, the fluid chart inVictoryChartExpandModal, andExpandedChartBox. So it should recover whichever canvas is actually blanking, which also answers the open question from the analysis about which one it is.backgroundtransition bumps the key, notinactive. If it turns out Android blanks the canvas on a transition that never reportsbackground, the key won't fire and you'll see no change — that's the most likely way this fails.I could not verify it. The bug is Android-app-only and this run was scoped to web, so no device repro. The web test session also failed during onboarding, so there's no App-level verification at all in the PR. Local checks that did pass: lint, typecheck, React Compiler compliance (both Babel and OXC), cspell, and 307 chart tests including 4 new ones for the hook.
view run
melvin-bot commentedon Sep 21, 2026
@situchan Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!
25 remaining items
melvin-bot commentedon Oct 19, 2026
Payment Summary
Resolving PRs:
Redraw Skia TextureView when its window becomes visible #103398
Reviewer: @situchan owed $175 via NewDot
BugZero Checklist (@mallenexpensify)
mallenexpensify commentedon Oct 20, 2026
Payment Summary
Contributor: @situchan due $250 via NewDot
Contributor+: @aimane-chnaif due $250 via NewDot
@aimane-chnaif plz complete the Contributor+ checklist and tag me in a post once you have. Thx
situchan commentedon Oct 20, 2026
@mallenexpensify I am also eligible for payment as PR author.
mallenexpensify commentedon Oct 20, 2026
@situchan paid, summary updated above. Thx, sorry for missing that
situchan commentedon Oct 20, 2026
@mallenexpensify thanks. Should the amount be $250?
aimane-chnaif commentedon Oct 20, 2026
Contributor+ Checklist:
[Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.
Link to the comment on the PR:
Link to the comment on the Issue:
[Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.
Link to discussion:
[Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.
Regression Test Proposal
Precondition:
an account with expenses from earlier days and months.
Test:
Do we agree 👍 or 👎
mallenexpensify commentedon Oct 21, 2026
@situchan yup, apologies, missed it. Price updated to $250
garrettmknight commentedon Oct 30, 2026
$250 approved for @aimane-chnaif
fix(🤖): latch a TextureView's retained buffer when its window becomes…
fix(🤖): latch a TextureView's retained buffer when its window becomes…