Today WebView Bridge
Contract

Today Page V3

Native integration and offline behavior for the mixed Today V3 feed.

Surface and authentication

Load /embed/feeds/v3?language=en&uid=<current-user-id>. This is a static Next.js Embed document with client-side data loading. It uses the existing Embed session exchange and common WebView bridge. uid selects an account's local snapshot; it never authenticates a request. The verified session identity replaces the hint online. Replace uid and reload on account switch.

The existing language, padding, showLoading, and requestId parameters retain their meanings. For a local fixture demonstration, use /embed/feeds/v3?mock=true; &scenario=empty demonstrates an empty feed.

Embed owns /api/embed/v3/today/batch, exact ClientCard revision reads, and V3 bundle downloads through its own BFF. Requests do not depend on a Web server deployment. Batch debug data is excluded. Widget assets use the shared virtual TCK URLs with tck-bundle-source=today-page-v3.

Scroll ownership

V3 uses document scrolling, matching the V2 feed at /embed/feeds/v1. The feed stays in normal document flow; it must not introduce a viewport-height inner scroll container, including a ScrollerVirtualBar wrapper. The native WebView owns scrolling and scrollbar presentation on this surface.

Android uses the WebView's vertical scroll range to gate pull-to-refresh and its scrollY to drive the header fade. iOS uses WKWebView.scrollView for content offsets and native header/footer insets. An independently scrolling DOM element leaves those native values at the top even while cards move. Native window.scrollTo and todayPage.scrollToSection must therefore move the same document scroll position as touch scrolling.

Render and refresh lifecycle

V3 reuses the Today Page render events and visibility lifecycle:

  • webView.loadingPresentation is the loading gate on this surface, and it is the only event Native should key its global loader on. V3 sizes its own skeletons from remembered card heights and reveals a bounded landing-card window in one step. A server batch remains the navigation and read identity, but it is not a rendering budget: an oversized batch is admitted over several card windows. Once batch data lands, the first bounded window is a credible screen and Web takes ownership with owner: 'web' and reason: 'placeholders_visible'. That is typically several hundred milliseconds before the first widget bundle has executed.
  • firstBatchRendered keeps its legacy event name and reports the first bounded presentation window, using that window's first batchId as its identity. It waits for every Widget and ClientCard in that window to reach a render outcome, and remains the refreshResult trigger and render-timing telemetry point. allBatchesRendered waits until every card in the loaded response has been progressively admitted and reached a terminal outcome; it may therefore follow later scrolling. An empty feed completes without waiting for a Widget mount and hands the loader over with reason: 'content_visible' because the empty state has no placeholder.
  • Existing totalWidgetCount, mountedWidgetCount, and failedWidgetCount fields count both card kinds across the loaded response, including cards not yet admitted when firstBatchRendered fires. A failed card may show a fallback while the feed completes with status: 'partial'.
  • todayPage.exposureChanged still produces contentVisible after the ready content remains visible for 300 ms, or contentFailed for a pre-content failure.
  • todayPage.refreshRequested refetches batches, then active ClientCard revisions, and returns one terminal refreshResult for its request id. Cached content remains visible on network errors. A soft refresh result describes data revalidation; newly referenced Widgets mount independently.
  • today_page.v3.updates, visible native foreground events, and browser reconnect trigger revalidation. Native owns the socket subscription and may relay the V3 update as a common native event; this page opens no socket.
  • todayPage.scrollToSection targets the first card of a batch, even when multiple batches share one natural-day section. Web loads older pages inside the continuous feed's retention window, advances toward the target in bounded card windows, and waits for preceding skeletons to settle before scrolling. V3 does not add a section highlight.

Brief entry and repeated navigation

V3 Chat keeps the V2 brief_card and 72-hour split based on pushedAt. Fresh entries show the existing Today surface and send a new JSB request on every click. Older entries open /feed/YYYYMMDD-today-page-v3, which reads that day through the single-batch API without the continuous feed's 14-day retention. The permalink shows current saved content, not a morning snapshot. Server-side V3 eligibility still applies: selecting the V3 reader does not bypass an API feature gate or authorization check. /feeds selects V2/V3 through today_page_v3_enabled; explicit V2 permalinks keep V2.

// Native -> Web, requestId is additive; old callers remain supported.
{ type: 'todayPage.scrollToSection', schemaVersion: 1,
  requestId: '<per-click-id>', batchId: '20260914-today-page-v3',
  behavior: 'auto', highlight: false }
// Web -> Native, only for requests carrying requestId.
{ type: 'todayPage.scrollToSectionResult', schemaVersion: 1,
  requestId: '<per-click-id>', batchId: '20260914-today-page-v3',
  status: 'succeeded' } // also not_found, out_of_range, failed, cancelled
// Native -> Web, e.g. the user reselects Today to scroll to the top.
{ type: 'todayPage.cancelScrollToSection', schemaVersion: 1,
  requestId: '<per-click-id>' }

Latest request wins, including repeat clicks on the same batch. A matching cancel, user scroll gesture, or leaving the visible surface cancels pending movement. Prerendered pages can prepare data but do not scroll until visible. Native sends visibility state before the navigation command, keeps only the latest pending request across document replacement, and never replays a completed request. Web moves the document scroll position; Native must not perform a second final scroll. iOS keeps its native header/footer inset contract.

not_found, out_of_range, and failed let Native fall back to the original permalink. cancelled must not open that fallback. Native may cancel an outstanding request before its own timeout fallback. A late result for an older request must not affect the current navigation.

Agent name

ClientCard and Widget CTAs share the host-owned name through the additive Agent Name protocol. Embed reads it on mount, rename notifications, and every visible exposure. Older hosts retain the Today fallback until they implement agent.getName.

ClientCard chat references

A host that implements todayPage.openChat advertises today_page_chat_reference.v1 in its document-start capabilities. Web then emits this payload through the common bridge:

type TodayPageOpenChatPayload = {
  schemaVersion: 1
  text: ''
  replyTo:
    | {
        targetType: 'day_in_progress_event'
        quote: string
        payload: { eventId: string }
      }
    | {
        targetType: 'today_page_card'
        payload: {
          widgetId: string
          kind: 'health_sleep' | 'health_workout'
          summary: string
        }
      }
}

Native opens a chat draft and preserves the complete reference. It must not send a message automatically. The health reference's widgetId contains the ClientCard id, consistent with the shared card renderer's semantic target. Native hosts without this capability render chat CTAs disabled. When Embed is opened in a normal browser, the CTA serializes the same reference into the existing todayReply Web-chat route and opens a draft, matching the V2 Embed browser fallback. External URL actions and Widget actions retain the existing today:// routing. Advertising the native capability requires a native implementation; the browser fallback does not provide that native handler.

Offline and worker ownership

There are three independent persistence layers:

  1. /embed/sw.js, scoped to /embed/, precaches the V1, V3, and Live Widget static documents plus build JS/CSS and referenced fonts/host assets. Lazy ClientCard renderer chunks are included. The existing waiting-worker activation policy remains in place.
  2. The existing TCK resource cache stores downloaded hash-addressed Widget bundles/assets. V3 uses the same worker and the BFF fallback when no worker controls the page.
  3. V3 batches and validated, exact ClientCard revisions persist in a separate per-account IndexedDB snapshot. The snapshot expires after 24 hours and revalidates on opening. It is independent of the build's asset version.

API responses and authentication cookies are not part of shell precaching. Offline reopening requires a completed online warm-up, persisted data, and available WebView storage. Previously unseen Widget resources or revisions cannot be fetched offline; their cards show fallbacks. First use while offline and storage eviction are not covered. A host without Service Worker support can load online through the BFF, but cannot rely on this offline shell.

Local verification

Build and start Embed in two terminals, then run the browser checks:

pnpm --filter @todayai-labs/embed build
pnpm --filter @todayai-labs/embed exec next start -p 4078
# In another terminal; uses installed Chrome, or set EMBED_TEST_ORIGIN for another local port.
pnpm --filter @todayai-labs/embed exec tsx scripts/verify-v3-offline.mts

The script verifies mixed cards, native draft references and refresh/exposure messages, an offline reopen, the no-worker online BFF fallback, and an empty feed's offline shell. It uses fixture data and a simulated native bridge; real account authentication and native WebView storage support still require validation on the integrating client.

Chromeless Web V3 permalinks also gate semantic ClientCard chat CTAs on today_page_chat_reference.v1 for native hosts. Native hosts without that capability retain Widget draft navigation through the established today://chat/send scheme, but do not show unsupported semantic ClientCard chat actions. Normal browsers route both Embed and chromeless Web semantic actions to a Web chat draft. Web-link CTAs remain available. Browser and supported native drafts never auto-send.

On this page