Skip to content

feat(discover): discover v2 swipe nav + cutover#7553

Open
DanielSinclair wants to merge 29 commits into
developfrom
daniel/discover-v2-swipe-nav-cutover
Open

feat(discover): discover v2 swipe nav + cutover#7553
DanielSinclair wants to merge 29 commits into
developfrom
daniel/discover-v2-swipe-nav-cutover

Conversation

@DanielSinclair
Copy link
Copy Markdown
Contributor

@DanielSinclair DanielSinclair commented Jun 3, 2026

Discover v2 was built but never mounted; this makes it the live Discover screen behind a remote flag, leaving the legacy home dormant for the cleanup PRs on top.

What changed

  • The Discover tab is registered behind discover_enabled (replacing discover_placements_enabled), with visible tab routes derived from active-route predicates.
  • The v2 surface runtime is mounted as the Discover screen in place of the legacy home, and the empty-wallet entry point is gated.
  • Pull-to-refresh moves into its own component so refresh state no longer re-renders the screen, and search results are sized below the v2 header.
  • IS_TEST gating on Discover sources is dropped; the Discover e2e flow targets the v2 tab nav.

What to test

  • With discover_enabled on, the Discover tab shows the v2 surface; with it off, the tab is absent.
  • Pull to refresh, run a search, and switch tabs — no clipping under the header.
  • An empty wallet routes to the gated entry point.

Copy link
Copy Markdown
Contributor Author

DanielSinclair commented Jun 3, 2026

@DanielSinclair DanielSinclair marked this pull request as draft June 3, 2026 08:17
@DanielSinclair DanielSinclair changed the title refactor(navigation): derive visible tab routes from active-route predicates feat(discover): discover v2 swipe nav + cutover Jun 3, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 3, 2026

Launch in simulator or device for a93d169

@DanielSinclair DanielSinclair marked this pull request as ready for review June 3, 2026 20:15
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 626e66c502

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/model/remoteConfig.ts
Comment thread src/languages/en_US.json
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 3, 2026

Launch in simulator or device for a93d169

@DanielSinclair DanielSinclair force-pushed the daniel/discover-v2-sparkline-pointer branch from 2d9b47d to 8557323 Compare June 4, 2026 10:32
@DanielSinclair DanielSinclair force-pushed the daniel/discover-v2-swipe-nav-cutover branch from 626e66c to 5b69baa Compare June 4, 2026 10:32
@DanielSinclair DanielSinclair force-pushed the daniel/discover-v2-sparkline-pointer branch from 8557323 to ac49258 Compare June 4, 2026 11:03
@DanielSinclair DanielSinclair force-pushed the daniel/discover-v2-swipe-nav-cutover branch from 5b69baa to e0e844e Compare June 4, 2026 11:03
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0e844eb01

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/features/discover/components/PredictionsSection.tsx Outdated
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 2026

Launch in simulator or device for 5746811

DanielSinclair added a commit that referenced this pull request Jun 4, 2026
…library

The original branch bundled cutover runtime, new V2 analytics, perps
coupling, and dead exports into one ~2.7k-line change that nothing mounted,
which review flagged as unscoped dead code spanning several concerns.

Root cause: the card/layout island, its pager/header runtime, the analytics
event schema, the liveTokens split, and source-specific price math all
landed in one commit, so the branch read as multiple scopes and coupled the
generic cards directly to perps unit math.

Narrow #7539 to just the unmounted card/layout/section library that the
upstack source renderers (#7540 token, #7541 perp, #7542 prediction) and
the cutover (#7553) consume:

- Move cutover runtime to #7553: delete DiscoverHeader, DiscoverSectionsPager,
  discoverNavigationStore, and navigation.ts; revert SmoothPager.fillHeight and
  the DiscoverScreenContext section-ref machinery (now byte-identical to
  develop); restore the legacy scrollViewRef scroll-to-top fallback, fixing the
  live single-scrollview regression.
- Make SectionLayout callback-only via onPressSeeAll; delete getHeaderPress and
  the unused getInitialRenderedItemCount; compute showHeaderCaret once.
- Strip new V2 analytics: event.ts and trackInteraction.ts revert to develop;
  the render-callback contract is now (item, width) for carousel/grid and
  (item) for list, with no analytics arg or DiscoverCardAnalyticsContext.
- Decouple generic cards from perps: new src/framework/ui/price/formatPriceChange.ts
  owns the arrow glyphs and the percent-unit formatter ("5.23" => 5.23%, no
  10_000 factor); MarketPill/MarketPriceChange no longer import @/features/perps.
- Clean MarketDisplayItem: drop onNavigate, pressMetadata, chartMaxPoints.
- Remove dead placement/surfaceId plumbing from the generic layout layer.
- Add focused tests: computeSnapToOffsets, formatNormalizedPercentChange,
  isEventCardDisplay.
DanielSinclair added a commit that referenced this pull request Jun 4, 2026
…t + fixes

Restacked onto the slimmed #7539 library and reconciled with its new
source-neutral contract, then applied the review-driven token cleanups.

Cascade adaptation (compile against #7539):
- Drop the analytics plumbing inherited from the old contract: remove
  DiscoverCardAnalyticsContext, the analyticsContext renderItem arg, and the
  card analyticsContext props; the token descriptors now match the generic
  (item, width) / (item) callback signatures.
- Remove navigateDiscoverDestination (deleted in #7539); the token "See All"
  header press uses onTapSearch() only, gated to token-owned destinations.
  Non-token CMS routing is deferred to the #7553 cutover.
- Stop setting onNavigate/pressMetadata on the token MarketDisplayItem
  (those fields were removed from the type); drop the dead placement/surfaceId
  arguments from the renderSectionLayout call.

Token-renderer fixes:
- Fix a 10_000x price-change bug. The token helper divided relativeChange24h
  by 10_000, which only worked because the generic card multiplied it back;
  #7539 removed that multiply, so the divide now under-reported every token
  change as ~0.00%. The token API already returns percent units
  ("5.23" == 5.23%), so normalizeTokenPercentChange now passes the value
  through with a nullish + finite guard and no scaling.
- Guard optional asset.price (price?.relativeChange24h/value/updatedAt);
  missing change becomes "0" and missing value a formatted zero.
- Use Number(x ?? 0) instead of || 0 to preserve valid zero values.
- Delegate token chart color to the canonical getPriceChangeColor.
- Normalize token chart timestamps to seconds at the source boundary via
  normalizeTokenChartTimestampSeconds.
- Make a forced Discover refresh bypass the module-level token-ref cache via
  clearTokenRefCache so it refetches inside TOKEN_REFS_STALE_TIME.
- Extract aggregateLineChartFetches (keeps partial successes, records the
  first error, throws only if every fetch failed) and share it between the
  token and Hyperliquid line-chart stores.
DanielSinclair added a commit that referenced this pull request Jun 4, 2026
… + fixes

Restacked onto the cleaned #7540 and reconciled with the source-neutral
contract, then applied the perp cleanups and review fixes.

Cascade adaptation (compile against the new contract):
- Drop the analytics plumbing (DiscoverCardAnalyticsContext, the
  analyticsContext renderItem arg and card props); perp descriptors now match
  the generic (item, width) / (item) callbacks.
- Remove navigateDiscoverDestination; the perp "See All" header press uses
  onTapSearch() gated to perp-owned destinations, mirroring the token path.
  Non-perp CMS routing is deferred to the #7553 cutover.
- Stop setting onNavigate/pressMetadata on the perp MarketDisplayItem and drop
  the dead placement/surfaceId arguments to renderSectionLayout.

Perp-renderer fixes:
- Fix the perp price-change units. Perps store priceChange['24h'] with an
  extra /100 baked in (so a +5.23% move is "0.000523"), and the Hyperliquid
  live adapter feeds change24hPct from that same stored value. The old generic
  card multiplied by 10_000; #7539 removed that, so usePerpMarketDisplay now
  converts to percent via convertStoredPerpPriceChangeToPercent for the initial
  value, the live priceChangeSelector, and the pill-width input. Importing the
  perps util here is correct -- this is the perp source hook, not a generic card.
- Restore the forced token-ref cache bypass. The Hyperliquid refresh addition
  had dropped clearTokenRefCache() from the rainbow branch of
  refreshDiscoverSurface; without it a forced refresh within TOKEN_REFS_STALE_TIME
  serves stale token data and the helper is dead. Re-add the cache clear and
  keep the force-aware Hyperliquid refetch alongside it.

Keep perps_enabled fallback true and the token/perp renderers as separate
source-specific code (no consolidation), per plan.
…stack

runtime moved here. This restores that runtime so the cutover compiles and
mounts the V2 surfaces, and wires the generic destination routing.

Re-add the runtime moved out of #7539 (step 31):
- discoverNavigationStore and the section-scroll refs it backs in
  DiscoverScreenContext, restoring active-section scroll-to-top.
- navigation.ts / navigateDiscoverDestination.
- DiscoverHeader, with its new V2 tab analytics stripped out -- analytics
  belongs to the dedicated top analytics branch; the tab-press handler keeps
  its navigation/scroll side effects.
- SmoothPager.fillHeight, used by DiscoverSectionsPager.

Wire generic CMS destination header presses now that all source renderers
exist (step 31): prediction and perp section "See All" route through
navigateDiscoverDestination (predictions -> Polymarket, perps -> perps screen);
the token section keeps onTapSearch (token "See All" opens Discover search,
and navigateDiscoverDestination is intentionally a no-op for tokens).

Preserve the resolved cutover fixes (step 32): DiscoverSearch results offset by
DISCOVER_HEADER_HEIGHT, pull-to-refresh state owned by DiscoverRefreshControl
with no screen-level isRefreshing and no renderRefreshControl prop chain. No
new V2 analytics here and event.ts stays byte-identical to develop (step 33).
These symbols are still consumed by the v1 placement stores and carousel
components, which are deleted in the next PR. Removing them here breaks
standalone compilation of this branch.
Reanimated can fail to resolve a host instance on Android when the Discover section Animated.ScrollView mounts with a UI-thread scroll handler.

Use a plain ScrollView with a JS scroll handler on Android while keeping the animated scroll path on iOS, and narrow the stored section ref to the scrollTo surface used by the header.
Android ScrollView wraps the native scroll view by cloning the provided refreshControl and passing the scroll content as children. DiscoverRefreshControl owned the refresh state but did not forward those injected children, so switching Discover sections to a plain Android ScrollView avoided the Reanimated crash but dropped the page body.

Forward the injected style and children into RefreshControl so Android keeps the ScrollView content while preserving Discover's refresh behavior.
MarketTileCard accepted an onPress prop but never forwarded it to ButtonPressAnimation, so tile cards rendered as tappable while dropping their destination handler. Pass the handler through to match the list and pill market card variants.
The floating swipe tab bar shell used pointerEvents=box-none, so only child icon pressables handled touches and gaps inside the visible bar could fall through to the Wallet screen.

Let the visible tab bar container receive pointer events while preserving the existing animated pointerEvents style that disables the bar during browser tab-view transitions.
Discover uses SmoothPager with fillHeight as a full-screen tab surface, but the pager wrapper still inherited box-none hit testing. Let the existing fillHeight mode receive touches so horizontal pans can start in blank For You space without adding a new pager prop or touching DiscoverSectionsPager.
MarketTileCard rendered light-mode cards with a transparent body, so Android elevation exposed the Discover surface through Perps tiles. Give the tile body the same translucent white light surface used by the compact Perps market card while keeping the shadow host shadow-only.
@DanielSinclair DanielSinclair force-pushed the daniel/discover-v2-swipe-nav-cutover branch from bd6ab9a to f817e16 Compare June 6, 2026 00:44
@DanielSinclair DanielSinclair changed the base branch from graphite-base/7553 to develop June 6, 2026 00:45
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 6, 2026

Launch in simulator or device for 972d922

Android elevation draws the host view as the visible shadow surface. Give the light market tile shadow host the same rounded design-system white surface as the card and use the Perps-equivalent black shadow token so Android no longer shows an inner tile box while iOS keeps rendering the card body.
Market tile shadows extend past the carousel item bounds, but the carousel only accounted for vertical bleed. Add an optional horizontal bleed to the existing descriptor path and opt market tiles into the same named bleed so edge-card shadows are not clipped.
Small prediction tiles render through PolymarketEventsListItem, so apply the matching rounded shadow style through its existing style prop instead of changing the shared Polymarket component. Opt the prediction tile carousel into the same bleed so the new shadow is visible at the carousel edges.
The browser tab is rendered as a content-width column so it can shrink into back/forward controls. Its resting width was still the fixed five-tab pill width, which made four-tab layouts allocate a smaller browser column than the tab-position math expected. Use the computed tab slot width for the resting browser tab and the matching browser-control bar resize delta.
@DanielSinclair DanielSinclair force-pushed the daniel/discover-v2-swipe-nav-cutover branch from f817e16 to b489343 Compare June 6, 2026 01:02
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 6, 2026

Launch in simulator or device for 35622c2

Comment on lines +20 to +23
navigate: section => {
if (get().activeSection === section) return;
set({ activeSection: section });
},
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would change to:

Suggested change
navigate: section => {
if (get().activeSection === section) return;
set({ activeSection: section });
},
navigate: section =>
set(state => {
if (state.activeSection === section) return state;
return { activeSection: section };
}),

Comment on lines 28 to +32
export const usePerpsEnabled = createDerivedStore<boolean>(
$ => $(useRemoteConfigStore, state => state.getRemoteConfigKey('perps_enabled')) && !IS_TEST,
{ fastMode: true }
$ => $(useRemoteConfigStore, state => state.getRemoteConfigKey('perps_enabled')),
{
fastMode: true,
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine to handle in a follow-up but this derived store is unnecessary, it's not deriving anything. Can just use the remote config store directly.

Comment on lines +35 to +48
# Navigate the Discover v2 tabs by POSITION. Number-based IDs are stable across the
# server-driven surface, and page assertions verify the active tab content changed.
- extendedWaitUntil:
visible:
id: tab-1
timeout: 30000
- tapOn:
id: tab-2
- assertVisible:
id: discover-section-page-2
- tapOn:
id: tab-1
- assertVisible:
id: discover-section-page-1
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is failing? In the CI screen recording artifact, it displays the first page correctly but doesn't switch to the second page.

Could just remove so this doesn't block merging.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a bit stricter, which destinations are accepted for each section type.

I see some of this code differs in #7557, so if anything would apply this outside of this PR as a follow-up.

Might do something like:

type DestinationForRoot<Root extends DestinationRoot> = [Root, ...string[]];
type NavigableDiscoverDestination = DestinationForRoot<'perps'> | DestinationForRoot<'predictions'>;

export function navigateDiscoverDestination(destination: NavigableDiscoverDestination): void {
  const [root, ...segments] = destination;

  switch (root) {
    case 'predictions': {
      const [category, league] = segments;
      if (category === 'sports') navigateToPolymarketSportsLeague(league ?? DEFAULT_SPORTS_LEAGUE_KEY);
      else if (category) navigateToPolymarketCategory(category);
      else navigateToPolymarket();
      return;
    }
    case 'perps':
      if (segments.length) navigateToPerpsSearch();
      else navigateToPerps();
      return;
  }
}

export function hasDestinationRoot<Root extends DestinationRoot>(
  destination: Destination,
  root: Root
): destination is DestinationForRoot<Root> {
  return destination?.[0] === root;
}

Then can do e.g.:

const perpsDestination = hasDestinationRoot(surface.destination, 'perps') ? surface.destination : null;

const onPress = useCallback(() => {
  if (perpsDestination) navigateDiscoverDestination(perpsDestination);
}, [perpsDestination]);

return renderSectionLayout({
  data: perpsResult.items,
  descriptor: perpDescriptor,
  loading: perpsResult.isLoading,
  onPress: perpsDestination && perpsResult.placement ? onPress : undefined,
  placement: perpsResult.placement,
  section: surface,
  surfaceId,
});

(Rather than the various copies of hasDestination that just check if a destination exists. Also note this is based on what seems to be the updated structure in #7557.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

e2e Tags a PR as needing e2e testing. Re-runs the suites automatically on each push.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants