[MOO-2404] - fix intro screen - #573
Open
YogendraShelke wants to merge 1 commit into
Open
Conversation
YogendraShelke
force-pushed
the
moo/MOO-2404-intro-screen
branch
from
July 29, 2026 10:23
42c7164 to
44d06bc
Compare
YogendraShelke
force-pushed
the
moo/MOO-2404-intro-screen
branch
from
July 30, 2026 03:35
44d06bc to
09dbee9
Compare
YogendraShelke
force-pushed
the
moo/MOO-2404-intro-screen
branch
from
July 30, 2026 07:30
09dbee9 to
3d9b426
Compare
…e (MOO-2404) The IntroScreen did not open on the slide named by the active slide attribute, and swiping between slides did not work reliably on slower Android devices. Three separate causes, all of them races that only lose on a slow device: Which slide is showing is now asked of the list rather than worked out from scroll offsets and gesture phases. A drag lifted with no velocity produces no momentum phase, so a slow swipe moved the slide while the indicators, buttons and attribute stayed behind. flash-list reports viewability from its own scroll handling, so such a slide is reported like any other. flash-list's waitForInteraction cannot gate those reports: it is set only from onScroll and only once a 100ms timer has fired, so a swipe made before that dropped every report for the whole gesture. A touch is the one signal the list cannot produce by scrolling itself, and it needs no timer. The list is held back until the attribute has a value to open on. It arrives Loading with no value on a fresh page and the real value follows a render or more later, but flash-list applies initialScrollIndex at most once, from a commit effect behind a 100ms timer, with no retry. Mounting during that window opened the widget on slide 1 and left it there while the indicators and buttons followed the value that landed moments later. The wait ends once the list is up: writing to the attribute returns it to Loading with no value, and waiting there would remount the list and spring the slide back on every swipe. maintainVisibleContentPosition is turned off. It is on by default in flash-list 2.x and anchors the list to the first visible item, scrolling back by however far a re-render moved it — which silently undid the scrollToOffset that navigation depends on, so buttons and indicators advanced while the slide on screen did not. The Maestro flow waits for the app to go idle before each swipe. A slide change writes the attribute and waits for the runtime to hand the value back, so the JS thread is still busy when the assertions after it pass. A swipe starting there loses its opening touch-move events: measured on a CI failure, the finger moved 75% of the width while the list moved 45%, leaving it short of the halfway point when the touch lifted, so it snapped back to the slide it started on with every counter correctly still on that slide. Gesture duration is left at the default on purpose — a paging list commits on position or lift-off velocity, so a slower drag travels no further and arrives with less velocity to carry it over. Verified on a CPU-throttled emulator that reproduces the failures reliably, and on CI, where Android and iOS both pass. The swipe flake above was seen once across roughly a dozen Android runs and cleared on retry, so the settle is not yet proven by repetition.
YogendraShelke
force-pushed
the
moo/MOO-2404-intro-screen
branch
from
July 30, 2026 08:53
3d9b426 to
f505f5e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Three defects in how the IntroScreen decided which slide was showing:
FINISHbutton.Root cause
The widget was working out for itself which gesture phase ended a swipe. It rounded
contentOffset.x / widthinonMomentumScrollEndand gated that behind a hand-keptisInitializinglatch. Both halves were wrong:slide !== activeIndexbranch. Start with the attribute agreeing with the slide the widget opens on and that branch never runs, so the latch is never cleared, so the first swipe is swallowed — and the sync effect then scrolls back, reverting the slide the user swiped to.Separately,
@shopify/flash-list2.x enablesmaintainVisibleContentPositionby default. It anchors the list to whichever item was first visible and, whenever a re-render moves that item, scrolls back by the difference. Navigating re-renders the slides, so that correction silently undid thescrollToOffsetingoToSlide— state advanced, pixels did not. Confirmed on-device: aftergoToSlide(2)requested offset 822.86 (valid — content width 1234),onScrollreported x=0. Not a clamp, which would have given 411.Fix
Which slide is showing is asked of the list through
onViewableItemsChangedinstead of inferred from offsets and gesture phases. flash-list computes viewability inside its own scroll handling, so a drag lifted with no velocity is reported like any other arrival, andwaitForInteraction: truesupplies the "not the initial scroll" distinction the widget was keeping by hand — flash-list already withholds reports until the first real interaction (RecyclerView.tsxonly callsrecordInteraction()onceisInitialScrollComplete).itemVisiblePercentThreshold: 60is unambiguous because slides are exactly one window wide;minimumViewTime: 250keeps positions merely passed through from counting as arrivals.Four gesture handlers, a manual dragging flag and a settle-timeout fallback are replaced by one handler.
Also in the same sync path:
maintainVisibleContentPositiondisabled — paged slides are all one window wide and every position here is asked for explicitly, so there is nothing to preserve.onLayoutreports a width, with a measuring placeholder before that.initialScrollIndexwas passed the live active index, so flash-list's post-mount re-apply raced our ownscrollToOffset. The slide the list opens on is frozen once mounted.Also in this PR
Two infrastructure fixes that are independent of the widget but were found while chasing it. Both are broken on
mx/11.12.xtoday.CI was testing the wrong code.
upload-artifactstrips the least-common ancestor of the paths it is given. Addingresources-manifest.txtat the repo root moved that ancestor frompackages/to the root, every path gained apackages/prefix, and the overlay'sresources/pluggableWidgets/**/dist/*/*.mpkglob silently stopped matching — no built mpk was installed, and the e2e suite tested the baseline project's widgets instead of the commit under test. The mpk and both jsActions overlays now locate their sources withfindrather than a fixed-depth glob, and installing nothing while widgets were built is a hard failure instead of a silent pass. (Thenanoflow-actionsguard was also checking the mobile-resources directory, so it moved either both or neither.)Maestro e2e. Swipes use explicit start/end points inside the slide: an edge-anchored swipe loses travel to the system back gesture, and the backward one fell short of halfway and snapped back to where it started — a flake independent of the widget fix.
NEXTis what becomesFINISHon the last slide, so the flow now waits for the slide before reaching for the button.Maestro diagnostics. A failed attempt's video and view hierarchy are moved aside before the retry, which reuses both paths — a flake that passed on retry previously left nothing to diagnose from.
🤖 Generated with Claude Code