Goal: Implement the complete watchlist screen per UX spec with all eight states: skeleton loading, cold-start server warming, live prices, cold cache (null prices), stale warning, disconnected, offline, and empty.
Scope:
- `src/hooks/useWatchlist.ts` — TanStack Query: GET /watchlist/prices with Bearer token; staleTime: 14_000, refetchInterval: 15_000, refetchIntervalInBackground: false; tracks fetchedAt: Date in queryFn on success; WatchlistPricesResponseSchema.parse() on response
- `src/components/WatchlistRow.tsx` — React.memo; symbol (16sp/600), company name (12sp/400 secondary), price (18sp/600), directional arrow + % change (14sp/600); em dash for null price; min height 60dp; android_ripple
- Banner components: StaleDataBanner, DisconnectedBanner, ColdStartBanner, OfflineBanner, SessionExpiredBanner
- `app/(tabs)/index.tsx` — FlatList with keyExtractor={item.id}, getItemLayout (fixed 60dp), ListHeaderComponent (zero-height View when no banner to prevent layout shift), ListEmptyComponent; cold-start timer useEffect (10s); stale summary interval (60s)
- Stale/disconnected detection against STALE_WARNING_MS / STALE_DISCONNECTED_MS constants
Branch: `feat/mobile-watchlist-screen`
Dependency: Tasks 5 + 11 (backend endpoint deployed; Google OAuth working)
Test boundary: useWatchlist fetches and sets fetchedAt; WatchlistRow renders all price states; StaleDataBanner appears when age > STALE_WARNING_MS; DisconnectedBanner Retry press invalidates query; cold-start banner after 10s mock timer; empty state renders
Risk: MEDIUM — 8 conditional banner states; ListHeaderComponent zero-height placeholder must not produce layout shift in production build
Goal: Implement the complete watchlist screen per UX spec with all eight states: skeleton loading, cold-start server warming, live prices, cold cache (null prices), stale warning, disconnected, offline, and empty.
Scope:
Branch: `feat/mobile-watchlist-screen`
Dependency: Tasks 5 + 11 (backend endpoint deployed; Google OAuth working)
Test boundary: useWatchlist fetches and sets fetchedAt; WatchlistRow renders all price states; StaleDataBanner appears when age > STALE_WARNING_MS; DisconnectedBanner Retry press invalidates query; cold-start banner after 10s mock timer; empty state renders
Risk: MEDIUM — 8 conditional banner states; ListHeaderComponent zero-height placeholder must not produce layout shift in production build