feat(web): initial web support for Expo app#2364
Conversation
parseDate:false prevents Eden Treaty's JSON reviver from silently converting date-like strings to Date objects, which broke Zod z.string().datetime() validation on API responses.
Redirects platform-incompatible imports to no-op web shims via Metro resolveRequest, so the web bundle never loads native code: - react-native-maps → placeholder View - @react-native-ai/llama + llama.rn → null stubs - @react-native-ai/apple → null stub - @react-native-async-storage/async-storage → SSR-safe localStorage wrapper - react-native-blob-util → no-op fs/config stub - expo-sqlite/kv-store → localStorage-backed SQLiteStorage shim (sync + async API parity; Legend State requires getItemSync) Also: wasm in assetExts, conditionNames restricted to CJS to prevent Metro picking up Jotai's ESM .mjs build which contains import.meta.
output: 'static' triggers SSR/static generation which fails on window references and async storage during prerender. 'single' ships one index.html with client-side routing — no SSR.
- _layout: toggle html.classList 'dark' from isDarkColorScheme so NativeWind's darkMode:'class' config activates CSS dark variants - auth/index: set explicit style dimensions on logo Image — RN Web injects the PNG's native 1080×1080 dimensions as inline styles, overriding the h-8/w-8 NativeWind class - global.css: add @media screen overrides for NativeWind color utility classes using rgb(var()) syntax; NativeWind emits platformSelect() which browsers ignore, leaving all themed text black on dark bg
- Add style prop to all 5 auth screen logo Images so they don't render at native PNG size (1080×1080) on web - Make CatalogItemSchema review fields nullable/optional: scraped data uses camelCase (userName) but schema expected snake_case (user_name), causing ZodError on every catalog page load; also relax title/text/date to handle sparse external catalog data
- Add EXPO_UNSTABLE_WEB_MODAL=1 to start/web scripts so auth modals (login, create-account) render as real overlays on web (SDK 54 alpha) - Add unstable_settings anchor to auth layout for correct deep-link behavior with web modals
- Guard ImageCacheManager.clearCache() with Platform.OS !== 'web' to prevent expo-file-system crash on logout - Remove non-existent catalog/index Stack.Screen entry - Fix current-pack and weight-analysis Stack.Screen names to match actual dynamic route files ([id] suffix)
The SQLite sync API (getItemSync/setItemSync) requires SharedArrayBuffer, which needs COEP/COOP headers not present in Expo's web dev server. Without those headers, atomWithStorage's onMount fires getItemSync → throws → atom wiped to null → AI chat sends 'Bearer null' → 401. Fix: - authAtoms: use a module-level write-through cache on web so getItem() always returns the last value written in the session (sync, no SharedArrayBuffer needed). Native path is unchanged. - useAuthInit: after async token read succeeds on init, hydrate tokenAtom via store.set() so the cache is populated on page refresh too.
…ibraries Replaces global mocks with real web alternatives where possible: - lib/persist-plugin.ts/.web.ts: platform abstraction for Legend-State persistence. Native uses observablePersistSqlite (SQLite); web uses observablePersistAsyncStorage backed by our localStorage mock. All 9 store files updated to import from this abstraction. - mocks/react-native-keyboard-controller.tsx: proper web shim — KeyboardAwareScrollView → ScrollView, KeyboardStickyView → View, KeyboardProvider renders children, useReanimatedKeyboardAnimation returns a static shared value (keyboard doesn't overlay on web). - mocks/google-signin.ts: Google Sign-In is native-only; web users sign in with email/password. - mocks/datetimepicker.tsx: native <input type="date/time"> element for @react-native-community/datetimepicker on web. - metro.config.js: wire keyboard-controller, google-signin, and datetimepicker into WEB_STUBS.
- Replace gray MapView placeholder with react-leaflet (OpenStreetMap tiles, CDN-injected leaflet CSS, Marker with Popup support) - Add expo-file-system/legacy WEB_STUB — prevents UnavailabilityError on all FileSystem calls (getInfoAsync, downloadAsync, uploadAsync, etc.) - Add leaflet, react-leaflet, @types/leaflet dependencies - expo-blur and expo-dev-client left unstubbed — both have native .web.js shims
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughThis PR introduces comprehensive web platform support for the Expo app by migrating persistence layers to a shared plugin pattern with platform-specific implementations, adding web stubs for native modules, parameterizing modal routes, applying responsive styling for web, and configuring Metro for web bundling. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 44 minutes and 42 seconds.Comment |
Coverage Report for API Unit Tests Coverage (./packages/api)
File CoverageNo changed files found. |
There was a problem hiding this comment.
Pull request overview
Adds initial Expo Web support for the Expo app by switching the web build to SPA output and introducing web-safe shims/persistence paths for native-only dependencies (maps, storage, file system, keyboard utilities, AI modules).
Changes:
- Switch Expo web output to SPA mode and update Metro resolver to redirect native-only modules to web shims.
- Split Legend-State persistence plugin by platform (SQLite on native, AsyncStorage/localStorage on web) and harden auth token hydration for web.
- Replace
react-native-mapsusage on web with a Leaflet/react-leaflet implementation and add multiple web stubs to avoid native crashes.
Reviewed changes
Copilot reviewed 43 out of 44 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/api/src/schemas/catalog.ts | Loosens catalog review response schema (nullable/optional + mixed casing). |
| packages/api-client/src/index.ts | Disables Treaty's date reviver to keep API datetime fields as strings for Zod validation. |
| bun.lock | Adds Leaflet/react-leaflet (+ types) dependencies. |
| apps/expo/package.json | Enables unstable web modal env var for dev/web scripts; adds Leaflet/react-leaflet deps. |
| apps/expo/mocks/react-native-maps.tsx | Web shim for maps using react-leaflet with CDN CSS injection. |
| apps/expo/mocks/react-native-maps.ts | Additional (unused) placeholder maps shim. |
| apps/expo/mocks/react-native-keyboard-controller.tsx | Web stub for keyboard-controller components/hooks. |
| apps/expo/mocks/react-native-blob-util.ts | Web stub for blob-util filesystem/network helpers. |
| apps/expo/mocks/react-native-ai-llama.ts | Web stub for llama AI module. |
| apps/expo/mocks/react-native-ai-apple.ts | Web stub for Apple AI module. |
| apps/expo/mocks/google-signin.ts | Web stub for Google Sign-In (rejects sign-in/tokens). |
| apps/expo/mocks/expo-sqlite-kv-store.ts | Web shim for expo-sqlite/kv-store backed by localStorage (SSR-safe). |
| apps/expo/mocks/expo-file-system-legacy.ts | Web stub for expo-file-system/legacy to avoid UnavailabilityError crashes. |
| apps/expo/mocks/datetimepicker.tsx | Web shim for datetime picker using native <input> elements. |
| apps/expo/mocks/async-storage.ts | SSR-safe AsyncStorage shim backed by localStorage. |
| apps/expo/metro.config.js | Adds WEB_STUBS resolver redirection and tweaks resolver conditions/assetExts. |
| apps/expo/lib/utils/ImageCacheManager.ts | Guards cache clearing on web to avoid filesystem usage. |
| apps/expo/lib/persist-plugin.web.ts | Web persist plugin using AsyncStorage (localStorage-backed shim). |
| apps/expo/lib/persist-plugin.ts | Native persist plugin using SQLite kv-store. |
| apps/expo/global.css | Adds web overrides for NativeWind color classes that browsers can parse. |
| apps/expo/features/trips/store/trips.ts | Switches persistence plugin to platform-split persistPlugin. |
| apps/expo/features/trail-conditions/store/trailConditionReports.ts | Switches persistence plugin to platform-split persistPlugin. |
| apps/expo/features/packs/store/packs.ts | Switches persistence plugin to platform-split persistPlugin. |
| apps/expo/features/packs/store/packingMode.ts | Switches persistence plugin to platform-split persistPlugin. |
| apps/expo/features/packs/store/packWeightHistory.ts | Switches persistence plugin to platform-split persistPlugin. |
| apps/expo/features/packs/store/packItems.ts | Switches persistence plugin to platform-split persistPlugin. |
| apps/expo/features/pack-templates/store/packTemplates.ts | Switches persistence plugin to platform-split persistPlugin. |
| apps/expo/features/pack-templates/store/packTemplateItems.ts | Switches persistence plugin to platform-split persistPlugin. |
| apps/expo/features/catalog/screens/CatalogItemDetailScreen.tsx | Normalizes catalog item description rendering via helper. |
| apps/expo/features/catalog/lib/normalizeDescription.ts | Adds helper to normalize catalog descriptions that embed JSON arrays. |
| apps/expo/features/catalog/components/CatalogItemCard.tsx | Uses normalized description in catalog item cards. |
| apps/expo/features/auth/store/user.ts | Switches persistence plugin to platform-split persistPlugin. |
| apps/expo/features/auth/hooks/useAuthInit.ts | Hydrates tokenAtom from stored access token during init (web-safe). |
| apps/expo/features/auth/atoms/authAtoms.ts | Adds web-specific storage behavior to avoid sync SQLite issues overwriting token atom. |
| apps/expo/app/auth/index.tsx | Adjusts logo sizing with Platform.select for cross-platform consistency. |
| apps/expo/app/auth/_layout.tsx | Adds unstable_settings anchor for auth layout routing. |
| apps/expo/app/auth/(login)/reset-password.tsx | Adjusts logo sizing with Platform.select. |
| apps/expo/app/auth/(login)/index.tsx | Adjusts logo sizing with Platform.select. |
| apps/expo/app/auth/(login)/forgot-password.tsx | Adjusts logo sizing with Platform.select. |
| apps/expo/app/auth/(create-account)/index.tsx | Adjusts logo sizing with Platform.select. |
| apps/expo/app/auth/(create-account)/credentials.tsx | Adjusts logo sizing with Platform.select. |
| apps/expo/app/_layout.tsx | Syncs NativeWind dark mode class to <html> on web. |
| apps/expo/app/(app)/_layout.tsx | Fixes Stack.Screen names to match file paths; removes catalog list screen options entry. |
| apps/expo/app.config.ts | Switches Expo web output from static to single (SPA). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| mergeItem: (key, value) => { | ||
| if (!isClient) return Promise.resolve(); | ||
| const existing = window.localStorage.getItem(key); | ||
| const merged = existing | ||
| ? JSON.stringify({ ...JSON.parse(existing), ...JSON.parse(value) }) | ||
| : value; | ||
| window.localStorage.setItem(key, merged); | ||
| return Promise.resolve(); | ||
| }, |
| multiMerge: (pairs) => { | ||
| if (!isClient) return Promise.resolve(); | ||
| for (const [k, v] of pairs) { | ||
| const existing = window.localStorage.getItem(k); | ||
| const merged = existing ? JSON.stringify({ ...JSON.parse(existing), ...JSON.parse(v) }) : v; | ||
| window.localStorage.setItem(k, merged); | ||
| } | ||
| return Promise.resolve(); |
| ls: () => Promise.resolve([]), | ||
| }, | ||
| config: () => ({ | ||
| fetch: () => Promise.resolve(null), | ||
| }), | ||
| }; |
| user_name: z.string().nullable().optional(), | ||
| userName: z.string().nullable().optional(), | ||
| user_avatar: z.string().nullable().optional(), | ||
| userAvatar: z.string().nullable().optional(), | ||
| context: z.record(z.string(), z.string()).nullable().optional(), | ||
| recommends: z.boolean().nullable().optional(), | ||
| rating: z.number(), | ||
| title: z.string(), | ||
| text: z.string(), | ||
| date: z.string(), | ||
| title: z.string().nullable().optional(), | ||
| text: z.string().nullable().optional(), | ||
| date: z.string().nullable().optional(), |
| <input | ||
| type={inputType} | ||
| defaultValue={toInputValue(value, mode)} | ||
| onChange={handleChange} | ||
| style={{ padding: 8, borderRadius: 6, border: '1px solid #ccc', fontSize: 16 }} | ||
| /> |
| import React from 'react'; | ||
| import { View } from 'react-native'; | ||
|
|
||
| const MapView = ({ | ||
| children, | ||
| style, | ||
| ...props | ||
| }: { | ||
| children?: React.ReactNode; | ||
| style?: unknown; | ||
| [key: string]: unknown; | ||
| }) => | ||
| React.createElement( | ||
| View, | ||
| { style: [{ backgroundColor: '#e0e0e0', flex: 1 }, style], ...props }, | ||
| children, | ||
| ); | ||
|
|
||
| const Marker = () => null; | ||
|
|
||
| export default MapView; | ||
| export { Marker }; |
| "leaflet": "^1.9.4", | ||
| "llama.rn": "0.10.1", | ||
| "nanoid": "^5.1.9", | ||
| "nativewind": "^4.2.3", | ||
| "radash": "catalog:", | ||
| "react": "catalog:", | ||
| "react-dom": "catalog:", | ||
| "react-i18next": "^17.0.4", | ||
| "react-leaflet": "^4.2.1", | ||
| "react-native": "0.81.5", |
| export async function uploadAsync( | ||
| _url: string, | ||
| _fileUri: string, | ||
| _options?: object, | ||
| ): Promise<{ status: number; body: string; headers: Record<string, string> }> { | ||
| console.warn('FileSystem.uploadAsync is not supported on web'); | ||
| return { status: 200, body: '', headers: {} }; | ||
| } |
…ides - Drop async-storage from WEB_STUBS — library already uses window.localStorage on web - react-native-maps mock: use static imports instead of dynamic require (no more biome-ignores) - expo-file-system-legacy stub: drop unused _options params (Biome useMaxParams clean) - Auth screens: remove Platform.select style on logo Image — NativeWind ios: class prefix is sufficient
- `useReanimatedKeyboardAnimation` now returns both `height` and `progress`
SharedValues — the Alert component from @packrat-ai/nativewindui destructures
`height` and would crash on web without it
- Add `web:h-8 web:w-8` to logo Image classNames in all 6 auth screens so the
logo renders at a sensible size on web without disturbing the existing iOS
sizing; replaces the removed Platform.select({ ios: { height: 48 } }) style
- Remove orphaned mocks/react-native-maps.ts (Metro WEB_STUBS already points to .tsx; the .ts file was never loaded) - Wrap JSON.parse calls in mergeItem/multiMerge with try/catch — falls back to overwriting with new value if stored data is non-JSON - Fix react-native-blob-util config().fetch() stub to return a thenable with no-op .progress()/.cancel() methods, preventing crashes when localModelManager.ts chains those methods on the result - Bump react-leaflet from ^4.2.1 to ^5.0.0 (React 19 peer support)
…e files Native uses expo-sqlite/kv-store sync API, web uses AsyncStorage (which falls through to localStorage via @react-native-async-storage web support). Mirrors the existing persist-plugin / persist-plugin.web pattern.
Resolved conflicts: - package.json: keep react-leaflet ^5.0.0 + @types/leaflet, take RN 0.83.6 and @types/react ~19.2.10 from development - catalog.ts: keep nullable/optional title/text/date (real data fix) and userName camelCase alias; date stays z.string() since parseDate:false - bun.lock: regenerated after resolution
Deploying packrat-guides with
|
| Latest commit: |
4dcc57b
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://95e190aa.packrat-guides-6gq.pages.dev |
| Branch Preview URL: | https://feat-web-implementation.packrat-guides-6gq.pages.dev |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
packrat-admin | 4dcc57b | Commit Preview URL Branch Preview URL |
May 01 2026, 04:16 AM |
Deploying packrat-landing with
|
| Latest commit: |
4dcc57b
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://e81d5a79.packrat-landing.pages.dev |
| Branch Preview URL: | https://feat-web-implementation.packrat-landing.pages.dev |
There was a problem hiding this comment.
Pull request overview
Introduces initial Expo Web (SPA) support by switching web output to single-page mode, adding Metro web stubs for native-only modules, and adjusting persistence/mapping/auth flows to function in a browser environment.
Changes:
- Switch Expo web output to SPA mode and configure Metro to resolve native-only dependencies to web shims.
- Add web implementations/stubs for maps (Leaflet), file system, SQLite KV store, keyboard controller, date picker, Google Sign-In, and AI native modules.
- Split persistence and storage layers by platform (Legend-State + Jotai) and add web-specific UI tweaks (dark mode
<html>class sync, description normalization).
Reviewed changes
Copilot reviewed 44 out of 45 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/api/src/schemas/catalog.ts | Loosens catalog review field requirements and adds camelCase aliases in response schema. |
| packages/api-client/src/index.ts | Disables Eden Treaty date auto-revival to keep API datetime fields as strings. |
| bun.lock | Adds Leaflet / react-leaflet dependencies to lockfile. |
| apps/expo/package.json | Enables EXPO_UNSTABLE_WEB_MODAL for start/web; adds Leaflet deps. |
| apps/expo/mocks/react-native-maps.tsx | Web MapView shim backed by react-leaflet + OSM tiles. |
| apps/expo/mocks/react-native-keyboard-controller.tsx | Web stub for keyboard-controller APIs. |
| apps/expo/mocks/react-native-blob-util.ts | Web stub for RNBlobUtil used by local model manager. |
| apps/expo/mocks/react-native-ai-llama.ts | Web stub for @react-native-ai/llama. |
| apps/expo/mocks/react-native-ai-apple.ts | Web stub for @react-native-ai/apple. |
| apps/expo/mocks/google-signin.ts | Web stub for native Google Sign-In module. |
| apps/expo/mocks/expo-sqlite-kv-store.ts | Web shim for expo-sqlite/kv-store using localStorage/memory fallback. |
| apps/expo/mocks/expo-file-system-legacy.ts | Web stub for expo-file-system legacy APIs. |
| apps/expo/mocks/datetimepicker.tsx | Web implementation of datetimepicker using <input type="date/time">. |
| apps/expo/mocks/async-storage.ts | SSR-safe localStorage shim for AsyncStorage (currently not wired via Metro). |
| apps/expo/metro.config.js | Adds WEB_STUBS resolver table + resolver condition tweaks for web builds. |
| apps/expo/lib/utils/ImageCacheManager.ts | Guards clearCache() on web. |
| apps/expo/lib/persist-plugin.web.ts | Web persist plugin uses AsyncStorage-backed Legend-State plugin. |
| apps/expo/lib/persist-plugin.ts | Native persist plugin uses Legend-State sqlite persistence. |
| apps/expo/lib/kvStorage.web.ts | Web kvStorage uses AsyncStorage. |
| apps/expo/lib/kvStorage.ts | Native kvStorage uses sync expo-sqlite/kv-store. |
| apps/expo/global.css | Adds web overrides for NativeWind color classes. |
| apps/expo/features/trips/store/trips.ts | Switches Legend-State persistence to shared platform persistPlugin. |
| apps/expo/features/trail-conditions/store/trailConditionReports.ts | Switches Legend-State persistence to shared platform persistPlugin. |
| apps/expo/features/packs/store/packs.ts | Switches Legend-State persistence to shared platform persistPlugin. |
| apps/expo/features/packs/store/packingMode.ts | Switches persistence to shared platform persistPlugin. |
| apps/expo/features/packs/store/packWeightHistory.ts | Switches persistence to shared platform persistPlugin. |
| apps/expo/features/packs/store/packItems.ts | Switches persistence to shared platform persistPlugin. |
| apps/expo/features/pack-templates/store/packTemplates.ts | Switches persistence to shared platform persistPlugin. |
| apps/expo/features/pack-templates/store/packTemplateItems.ts | Switches persistence to shared platform persistPlugin. |
| apps/expo/features/catalog/screens/CatalogItemDetailScreen.tsx | Normalizes catalog description before rendering. |
| apps/expo/features/catalog/lib/normalizeDescription.ts | Adds description normalization helper for malformed “Details: […]” strings. |
| apps/expo/features/catalog/components/CatalogItemCard.tsx | Normalizes catalog description in list card. |
| apps/expo/features/auth/store/user.ts | Switches user store persistence to shared platform persistPlugin. |
| apps/expo/features/auth/hooks/useAuthInit.ts | Hydrates token atom during auth init for web compatibility. |
| apps/expo/features/auth/atoms/authAtoms.ts | Moves token atoms to platform kvStorage abstraction. |
| apps/expo/app/auth/index.tsx | Adjusts logo sizing classes for web. |
| apps/expo/app/auth/_layout.tsx | Adds expo-router unstable_settings.anchor. |
| apps/expo/app/auth/(login)/reset-password.tsx | Adjusts logo sizing classes for web. |
| apps/expo/app/auth/(login)/index.tsx | Adjusts logo sizing classes for web. |
| apps/expo/app/auth/(login)/forgot-password.tsx | Adjusts logo sizing classes for web. |
| apps/expo/app/auth/(create-account)/index.tsx | Adjusts logo sizing classes for web. |
| apps/expo/app/auth/(create-account)/credentials.tsx | Adjusts logo sizing classes for web. |
| apps/expo/app/_layout.tsx | Syncs NativeWind dark mode to <html> on web. |
| apps/expo/app/(app)/_layout.tsx | Fixes Stack.Screen route names and removes catalog list screen options. |
| apps/expo/app.config.ts | Switches Expo web output to single (SPA). |
Comments suppressed due to low confidence (1)
packages/api/src/schemas/catalog.ts:68
- CatalogItemSchema.reviews now allows
title/text/date(anduser_name) to be nullable/optional, but downstream consumers treat these as required strings (e.g., callingreview.text.length, usingreview.titleas a key). This schema change makes those consumers vulnerable to runtime crashes if the API ever returns null/missing fields. Consider keeping these fields required in the public response schema, or normalizing/coercing incoming data (e.g., defaulting to empty strings) so the API contract remains consistent.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| user_name: z.string().nullable().optional(), | ||
| userName: z.string().nullable().optional(), | ||
| user_avatar: z.string().nullable().optional(), | ||
| userAvatar: z.string().nullable().optional(), | ||
| context: z.record(z.string(), z.string()).nullable().optional(), |
| function LeafletMap({ style, initialRegion, region, children, ...props }: MapViewProps) { | ||
| const r = region ?? initialRegion; | ||
| const center: LatLngExpression = r ? [r.latitude, r.longitude] : [20, 0]; | ||
| const zoom = r ? Math.round(10 - Math.log2(r.latitudeDelta + 0.001)) : 5; | ||
|
|
||
| return ( | ||
| <MapContainer | ||
| center={center} | ||
| zoom={zoom} | ||
| style={{ flex: 1, height: '100%', ...(style as object) }} | ||
| {...(props as object)} |
| import AsyncStorage from '@react-native-async-storage/async-storage'; | ||
|
|
||
| export default { | ||
| getItem: (key: string) => AsyncStorage.getItem(key), | ||
| setItem: (key: string, value: string | null) => { | ||
| if (value === null) return AsyncStorage.removeItem(key); | ||
| return AsyncStorage.setItem(key, value); | ||
| }, | ||
| removeItem: (key: string) => AsyncStorage.removeItem(key), |
| // headers). Use the AsyncStorage plugin instead, which falls through to our | ||
| // localStorage-backed mock via the metro web stub. |
| export async function downloadAsync( | ||
| _uri: string, | ||
| _fileUri: string, | ||
| ): Promise<{ status: number; uri: string; headers: Record<string, string>; mimeType: string }> { | ||
| return { status: 200, uri: _fileUri, headers: {}, mimeType: '' }; | ||
| } | ||
|
|
||
| export async function uploadAsync( | ||
| _url: string, | ||
| _fileUri: string, | ||
| ): Promise<{ status: number; body: string; headers: Record<string, string> }> { | ||
| return { status: 200, body: '', headers: {} }; | ||
| } |
| public async clearCache(): Promise<void> { | ||
| if (Platform.OS === 'web') return; | ||
| const dirInfo = await FileSystem.getInfoAsync(this.cacheDirectory); | ||
| if (dirInfo.exists) { | ||
| await FileSystem.deleteAsync(this.cacheDirectory); |
| export type LlamaLanguageModel = never; | ||
|
|
||
| export const llama = { | ||
| languageModel: () => null, | ||
| }; |
…ble title/text/date
…eviews Only user_avatar (snake_case) is present in the DB JSONB type; userAvatar was a stale alias with no backing data.
There was a problem hiding this comment.
Actionable comments posted: 15
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/expo/features/catalog/lib/normalizeDescription.ts`:
- Around line 5-10: The code calls JSON.parse(match[1]) where match[1] is typed
string | undefined; add an explicit guard for the capture group before parsing
(e.g., check if match[1] is defined) inside the block that handles
DETAILS_ARRAY_RE so TypeScript narrows the type, and if undefined skip
parsing/return fallback; update the block around match, DETAILS_ARRAY_RE and the
try/catch that produces items to only call JSON.parse when match[1] is non-null
and handle the else path (e.g., continue to fallback return).
In `@apps/expo/global.css`:
- Around line 5-56: The new web-specific color rules (.text-foreground,
.bg-background, .bg-card, .border-border, etc.) rely on CSS variables but your
dark-mode variable values are only set via media queries, so toggling html.dark
won't change those --* tokens; update the CSS where you define the color
variables to also set the dark token values under a manual dark class (e.g.,
html.dark or :root.dark) in addition to any prefers-color-scheme media rules so
the .text-*/.bg-* overrides pick up manual dark mode toggles.
In `@apps/expo/metro.config.js`:
- Around line 19-35: The metro resolver's WEB_STUBS map is missing the
async-storage stub causing web builds to crash; add an entry to WEB_STUBS
mapping '@react-native-async-storage/async-storage' to the existing mock
'mocks/async-storage.ts' (e.g., add
"'@react-native-async-storage/async-storage': 'mocks/async-storage.ts',")
ensuring it's included alongside the other stubs in the
apps/expo/metro.config.js file and formatted consistently with surrounding
entries.
In `@apps/expo/mocks/datetimepicker.tsx`:
- Around line 26-31: In handleChange, empty input (raw === '' or only
whitespace) produces an Invalid Date; guard against this by checking raw.trim()
=== '' before constructing a Date and instead invoke onChange with a
dismissed/empty payload (e.g., call onChange({ type: 'dismissed' }, undefined)
or the equivalent expected by your mock) so you never pass an Invalid Date to
onChange; update the branch in function handleChange to use raw and mode to
decide between creating a Date or sending the dismissed callback.
In `@apps/expo/mocks/expo-file-system-legacy.ts`:
- Around line 45-57: The stub transfer functions downloadAsync and uploadAsync
currently return status 200 despite doing nothing; update both functions in
expo-file-system-legacy.ts so they return status 501 (Not Implemented) instead
of 200 to signal unsupported operations on web—keep the existing return shape ({
status, uri, headers, mimeType } for downloadAsync and { status, body, headers }
for uploadAsync) but change status to 501.
- Around line 59-66: createDownloadResumable is declared async but must be a
synchronous factory; remove the async keyword from the export function
declaration so it returns the DownloadResumable object directly (not a Promise)
and keep the returned object's methods (downloadAsync, pauseAsync, resumeAsync,
savable) as async functions where needed so callers can immediately call
.downloadAsync() on the returned value.
In `@apps/expo/mocks/expo-sqlite-kv-store.ts`:
- Around line 143-149: The mergeItem implementation calls deepMerge on any
parsed JSON, which can corrupt arrays/primitives; change mergeItem so it parses
existing and new value, checks both are plain objects (records) before calling
deepMerge, and only then compute merged and call rawSet; otherwise, fallback to
overwrite by calling rawSet with the new value; reference the mergeItem method,
getItemSync, deepMerge and rawSet when making the changes.
- Around line 10-30: Wrap all direct window.localStorage usages in rawGet,
rawSet, rawRemove, and rawKeys with try/catch so that any exception (e.g., in
restricted browser modes) falls back to memFallback; specifically, inside rawGet
catch errors from getItem and return memFallback.get(key) ?? null, inside rawSet
catch errors from setItem and call memFallback.set(key, value), inside rawRemove
catch errors from removeItem and use memFallback.delete(key) while still
returning whether the key existed, and inside rawKeys catch errors when
enumerating window.localStorage and instead return
Array.from(memFallback.keys()) after slicing PREFIX — keep isClient checks but
treat thrown errors as equivalent to not being able to use localStorage.
In `@apps/expo/mocks/google-signin.ts`:
- Around line 6-8: The web mock rejects from signIn and getTokens with plain
Error objects lacking a code property, which breaks error handling in
signInWithGoogle (useAuthActions.ts) that checks isErrorWithCode and error.code;
update the mocks (signIn and getTokens) to reject errors that include a code
property (e.g., via creating an Error and assigning error.code =
'ERR_GOOGLE_SIGNIN_NOT_SUPPORTED' or similar) so isErrorWithCode and the
error.code checks can correctly detect and handle web-specific rejections.
In `@apps/expo/mocks/react-native-ai-llama.ts`:
- Around line 1-5: The mock returns null which causes downstream calls like
_prepareLlamaModel() (await llamaModel.prepare()) and deleteLocalModel() (await
llamaModel.unload()) to crash; change the stub so llama.languageModel() returns
an object whose prepare and unload methods immediately throw a clear "Llama
model unsupported on web" error (and keep the exported type LlamaLanguageModel
aligned with that shape or use a generic/any type) so failures occur immediately
and with an explicit message.
In `@apps/expo/mocks/react-native-maps.tsx`:
- Around line 60-71: The LeafletMap mock currently ignores MapViewProps
callbacks so map events are no-ops; update the LeafletMap component to call
useMapEvents and wire map events to the incoming props (onPress -> map click/box
click, onRegionChange -> onmove, onRegionChangeComplete -> moveend) and pass
through relevant event data (center/latitudeDelta/longitudeDelta) to match
MapViewProps signatures; similarly update the Marker mock to supply an
eventHandlers prop (or onClick handler) that invokes MarkerProps.onPress with
the event and marker id/coordinate; ensure you reference the existing LeafletMap
function and the Marker mock component, keep props spread intact, and add
minimal type conversions to produce LatLng/region objects expected by consumers.
In `@apps/expo/package.json`:
- Line 33: The package.json npm scripts use POSIX-style env assignments which
fail on Windows; install cross-env as a devDependency and update each affected
script (android, ios, start, update:development, update:preview, web) to prefix
the environment assignments with cross-env (e.g., replace
"APP_VARIANT=development EXPO_UNSTABLE_WEB_MODAL=1 expo start" with a
cross-env-prefixed form) so the same env vars are set cross-platform; ensure
package.json scripts now use cross-env before the command and run npm install
--save-dev cross-env to add the dependency.
In `@packages/api-client/src/index.ts`:
- Around line 130-136: The global parseDate:false on treaty<App> breaks routes
that use z.date(); instead make parseDate configurable per-client: remove the
hardcoded parseDate:false in the treaty<App>(...) call and read an optional
parseDate boolean from the createApiClient/config parameter (default true), then
pass that value into treaty as parseDate; update call sites that need string
dates to explicitly pass parseDate:false. Reference symbols: treaty<App>,
parseDate, and the createApiClient/config used to instantiate the client; after
change, run the suggested rg checks to find remaining z.date() usages and adjust
call sites as needed.
In `@packages/api/src/schemas/catalog.ts`:
- Line 56: The schema in this file defines both user_avatar and userAvatar
(user_avatar vs userAvatar) which is a duplicate—decide on a single canonical
field name (prefer snake_case or camelCase consistent with the rest of your API)
and remove the redundant z.string().nullable().optional() entry; if you
intentionally need both shapes for different consumers, keep one schema field
and add an explicit transformation/mapper or document the intent (e.g., map
user_avatar -> userAvatar in your response normalization code) so the duplicate
field definitions (user_avatar and userAvatar) are not left ambiguous.
- Around line 60-62: The schema change made title, text, and date
nullable/optional which breaks ItemReviews: update either the schema or the
component. Option A: revert the fields in packages/api/src/schemas/catalog.ts to
required strings (remove .nullable().optional() from title, text, date) so
reviews always supply values. Option B: make ItemReviews.tsx resilient by adding
null guards and fallbacks around review.text, review.title, and review.date (use
safe length checks like review.text?.length ?? 0 when deciding truncation,
provide a stable key like review.title ?? `review-${index}`, and only call
formatDate when review.date is present). Pick one approach and apply
consistently to the symbols review.text, review.title, review.date and the
formatDate usage.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 15a2bf2d-792d-4cbd-82e7-e9fbff905bb0
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock,!bun.lock
📒 Files selected for processing (44)
apps/expo/app.config.tsapps/expo/app/(app)/_layout.tsxapps/expo/app/_layout.tsxapps/expo/app/auth/(create-account)/credentials.tsxapps/expo/app/auth/(create-account)/index.tsxapps/expo/app/auth/(login)/forgot-password.tsxapps/expo/app/auth/(login)/index.tsxapps/expo/app/auth/(login)/reset-password.tsxapps/expo/app/auth/_layout.tsxapps/expo/app/auth/index.tsxapps/expo/features/auth/atoms/authAtoms.tsapps/expo/features/auth/hooks/useAuthInit.tsapps/expo/features/auth/store/user.tsapps/expo/features/catalog/components/CatalogItemCard.tsxapps/expo/features/catalog/lib/normalizeDescription.tsapps/expo/features/catalog/screens/CatalogItemDetailScreen.tsxapps/expo/features/pack-templates/store/packTemplateItems.tsapps/expo/features/pack-templates/store/packTemplates.tsapps/expo/features/packs/store/packItems.tsapps/expo/features/packs/store/packWeightHistory.tsapps/expo/features/packs/store/packingMode.tsapps/expo/features/packs/store/packs.tsapps/expo/features/trail-conditions/store/trailConditionReports.tsapps/expo/features/trips/store/trips.tsapps/expo/global.cssapps/expo/lib/kvStorage.tsapps/expo/lib/kvStorage.web.tsapps/expo/lib/persist-plugin.tsapps/expo/lib/persist-plugin.web.tsapps/expo/lib/utils/ImageCacheManager.tsapps/expo/metro.config.jsapps/expo/mocks/async-storage.tsapps/expo/mocks/datetimepicker.tsxapps/expo/mocks/expo-file-system-legacy.tsapps/expo/mocks/expo-sqlite-kv-store.tsapps/expo/mocks/google-signin.tsapps/expo/mocks/react-native-ai-apple.tsapps/expo/mocks/react-native-ai-llama.tsapps/expo/mocks/react-native-blob-util.tsapps/expo/mocks/react-native-keyboard-controller.tsxapps/expo/mocks/react-native-maps.tsxapps/expo/package.jsonpackages/api-client/src/index.tspackages/api/src/schemas/catalog.ts
| /* Web: NativeWind emits platformSelect() for color classes which browsers can't parse. | ||
| These overrides use standard rgb(var()) syntax so web gets themed colors. */ | ||
| @media screen { | ||
| .text-foreground { | ||
| color: rgb(var(--foreground)); | ||
| } | ||
| .text-muted-foreground { | ||
| color: rgb(var(--muted-foreground)); | ||
| } | ||
| .text-card-foreground { | ||
| color: rgb(var(--card-foreground)); | ||
| } | ||
| .text-popover-foreground { | ||
| color: rgb(var(--popover-foreground)); | ||
| } | ||
| .text-primary-foreground { | ||
| color: rgb(var(--primary-foreground)); | ||
| } | ||
| .text-secondary-foreground { | ||
| color: rgb(var(--secondary-foreground)); | ||
| } | ||
| .text-accent-foreground { | ||
| color: rgb(var(--accent-foreground)); | ||
| } | ||
| .text-destructive-foreground { | ||
| color: rgb(var(--destructive-foreground)); | ||
| } | ||
| .bg-background { | ||
| background-color: rgb(var(--background)); | ||
| } | ||
| .bg-card { | ||
| background-color: rgb(var(--card)); | ||
| } | ||
| .bg-primary { | ||
| background-color: rgb(var(--primary)); | ||
| } | ||
| .bg-secondary { | ||
| background-color: rgb(var(--secondary)); | ||
| } | ||
| .bg-muted { | ||
| background-color: rgb(var(--muted)); | ||
| } | ||
| .bg-accent { | ||
| background-color: rgb(var(--accent)); | ||
| } | ||
| .bg-destructive { | ||
| background-color: rgb(var(--destructive)); | ||
| } | ||
| .border-border { | ||
| border-color: rgb(var(--border)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Web color overrides currently bypass manual dark-mode class.
These new .text-*/.bg-* web rules read --* tokens, but dark token assignment is still media-query based. If the app toggles html.dark while system theme is light, colors remain light and the UI theme becomes inconsistent.
💡 Suggested fix
`@layer` base {
:root {
/* light tokens */
}
+ :root.dark {
+ --background: 0 0 0;
+ --foreground: 255 255 255;
+ --card: 21 21 24;
+ --card-foreground: 255 255 255;
+ --popover: 40 40 42;
+ --popover-foreground: 255 255 255;
+ --primary: 3 133 255;
+ --primary-foreground: 255 255 255;
+ --secondary: 100 211 254;
+ --secondary-foreground: 255 255 255;
+ --muted: 70 70 73;
+ --muted-foreground: 142 142 147;
+ --accent: 255 52 95;
+ --accent-foreground: 255 255 255;
+ --destructive: 254 67 54;
+ --destructive-foreground: 255 255 255;
+ --border: 40 40 42;
+ --input: 55 55 57;
+ --ring: 40 40 42;
+ }
+
`@media` (prefers-color-scheme: dark) {
:root {
/* existing dark tokens */
}
}
}As per coding guidelines: "Use NativeWind (Tailwind for React Native) with CSS variable-based color system and manual dark mode toggle for mobile styling."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/expo/global.css` around lines 5 - 56, The new web-specific color rules
(.text-foreground, .bg-background, .bg-card, .border-border, etc.) rely on CSS
variables but your dark-mode variable values are only set via media queries, so
toggling html.dark won't change those --* tokens; update the CSS where you
define the color variables to also set the dark token values under a manual dark
class (e.g., html.dark or :root.dark) in addition to any prefers-color-scheme
media rules so the .text-*/.bg-* overrides pick up manual dark mode toggles.
| // Native-only packages that need web shims. | ||
| // Add new entries here when a package crashes on web. | ||
| const WEB_STUBS = { | ||
| 'react-native-maps': 'mocks/react-native-maps.tsx', | ||
| 'react-native-blob-util': 'mocks/react-native-blob-util.ts', | ||
| '@react-native-ai/llama': 'mocks/react-native-ai-llama.ts', | ||
| 'llama.rn': 'mocks/react-native-ai-llama.ts', | ||
| '@react-native-ai/apple': 'mocks/react-native-ai-apple.ts', | ||
| 'expo-sqlite/kv-store': 'mocks/expo-sqlite-kv-store.ts', | ||
| // Keyboard utilities — on web the software keyboard doesn't overlay content | ||
| 'react-native-keyboard-controller': 'mocks/react-native-keyboard-controller.tsx', | ||
| // Google Sign-In and date picker are native-only; web uses password auth | ||
| '@react-native-google-signin/google-signin': 'mocks/google-signin.ts', | ||
| '@react-native-community/datetimepicker': 'mocks/datetimepicker.tsx', | ||
| // expo-file-system throws UnavailabilityError on web; stub all ops as no-ops | ||
| 'expo-file-system/legacy': 'mocks/expo-file-system-legacy.ts', | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check all direct imports of `@react-native-async-storage/async-storage`
rg -n "from '@react-native-async-storage/async-storage'" apps/expo/Repository: PackRat-AI/PackRat
Length of output: 1572
🏁 Script executed:
# Check if the async-storage mock exists
fd -t f "async-storage.ts" apps/expo/mocks/ 2>/dev/null || echo "Mock not found"
# Also check the mock file content if it exists
if [ -f "apps/expo/mocks/async-storage.ts" ]; then
head -20 apps/expo/mocks/async-storage.ts
fiRepository: PackRat-AI/PackRat
Length of output: 819
Add @react-native-async-storage/async-storage to WEB_STUBS.
The codebase has 11 cross-platform files that directly import @react-native-async-storage/async-storage (including useAuthActions.ts, atomWithAsyncStorage.ts, and feature atoms). The mock exists at mocks/async-storage.ts but isn't wired up in the metro resolver, so these imports will crash on web.
const WEB_STUBS = {
'react-native-maps': 'mocks/react-native-maps.tsx',
'react-native-blob-util': 'mocks/react-native-blob-util.ts',
'@react-native-ai/llama': 'mocks/react-native-ai-llama.ts',
'llama.rn': 'mocks/react-native-ai-llama.ts',
'@react-native-ai/apple': 'mocks/react-native-ai-apple.ts',
'expo-sqlite/kv-store': 'mocks/expo-sqlite-kv-store.ts',
+ '@react-native-async-storage/async-storage': 'mocks/async-storage.ts',
// Keyboard utilities — on web the software keyboard doesn't overlay content
'react-native-keyboard-controller': 'mocks/react-native-keyboard-controller.tsx',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/expo/metro.config.js` around lines 19 - 35, The metro resolver's
WEB_STUBS map is missing the async-storage stub causing web builds to crash; add
an entry to WEB_STUBS mapping '@react-native-async-storage/async-storage' to the
existing mock 'mocks/async-storage.ts' (e.g., add
"'@react-native-async-storage/async-storage': 'mocks/async-storage.ts',")
ensuring it's included alongside the other stubs in the
apps/expo/metro.config.js file and formatted consistently with surrounding
entries.
| function handleChange(e: React.ChangeEvent<HTMLInputElement>) { | ||
| if (!onChange) return; | ||
| const raw = e.target.value; | ||
| const date = mode === 'time' ? new Date(`1970-01-01T${raw}`) : new Date(raw); | ||
| onChange({ type: 'set', nativeEvent: { timestamp: date.getTime() } }, date); | ||
| } |
There was a problem hiding this comment.
Edge case: empty input creates Invalid Date.
If the user clears the input field, raw is an empty string, and new Date('') or new Date('1970-01-01T') produces an Invalid Date that gets passed to onChange.
Proposed guard
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
if (!onChange) return;
const raw = e.target.value;
+ if (!raw) return; // Ignore empty/cleared input
const date = mode === 'time' ? new Date(`1970-01-01T${raw}`) : new Date(raw);
onChange({ type: 'set', nativeEvent: { timestamp: date.getTime() } }, date);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function handleChange(e: React.ChangeEvent<HTMLInputElement>) { | |
| if (!onChange) return; | |
| const raw = e.target.value; | |
| const date = mode === 'time' ? new Date(`1970-01-01T${raw}`) : new Date(raw); | |
| onChange({ type: 'set', nativeEvent: { timestamp: date.getTime() } }, date); | |
| } | |
| function handleChange(e: React.ChangeEvent<HTMLInputElement>) { | |
| if (!onChange) return; | |
| const raw = e.target.value; | |
| if (!raw) return; // Ignore empty/cleared input | |
| const date = mode === 'time' ? new Date(`1970-01-01T${raw}`) : new Date(raw); | |
| onChange({ type: 'set', nativeEvent: { timestamp: date.getTime() } }, date); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/expo/mocks/datetimepicker.tsx` around lines 26 - 31, In handleChange,
empty input (raw === '' or only whitespace) produces an Invalid Date; guard
against this by checking raw.trim() === '' before constructing a Date and
instead invoke onChange with a dismissed/empty payload (e.g., call onChange({
type: 'dismissed' }, undefined) or the equivalent expected by your mock) so you
never pass an Invalid Date to onChange; update the branch in function
handleChange to use raw and mode to decide between creating a Date or sending
the dismissed callback.
| export async function downloadAsync( | ||
| _uri: string, | ||
| _fileUri: string, | ||
| ): Promise<{ status: number; uri: string; headers: Record<string, string>; mimeType: string }> { | ||
| return { status: 200, uri: _fileUri, headers: {}, mimeType: '' }; | ||
| } | ||
|
|
||
| export async function uploadAsync( | ||
| _url: string, | ||
| _fileUri: string, | ||
| ): Promise<{ status: number; body: string; headers: Record<string, string> }> { | ||
| return { status: 200, body: '', headers: {} }; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, verify the file exists and examine the stub file
cat -n apps/expo/mocks/expo-file-system-legacy.tsRepository: PackRat-AI/PackRat
Length of output: 2424
🏁 Script executed:
# Check the ImageCacheManager.ts file, particularly around line 57
if [ -f apps/expo/lib/utils/ImageCacheManager.ts ]; then
cat -n apps/expo/lib/utils/ImageCacheManager.ts | head -70
fiRepository: PackRat-AI/PackRat
Length of output: 2671
🏁 Script executed:
# Search for usage of downloadAsync and uploadAsync to understand the pattern
rg "downloadAsync|uploadAsync" apps/expo --type ts --type tsx -B 2 -A 2Repository: PackRat-AI/PackRat
Length of output: 90
🏁 Script executed:
# Search for usages without the -t tsx flag that caused the error
rg "downloadAsync|uploadAsync" apps/expo -B 2 -A 2Repository: PackRat-AI/PackRat
Length of output: 4934
🏁 Script executed:
# Also search in the entire codebase to see if there are other usages
rg "downloadAsync|uploadAsync" --type ts -B 2 -A 2Repository: PackRat-AI/PackRat
Length of output: 4934
🏁 Script executed:
# Check if there are other mock patterns in the codebase
ls -la apps/expo/mocks/Repository: PackRat-AI/PackRat
Length of output: 915
🏁 Script executed:
# Check uploadImage.ts to see if it also checks status codes
cat -n apps/expo/features/packs/utils/uploadImage.ts | head -100Repository: PackRat-AI/PackRat
Length of output: 1797
Return 501 (Not Implemented) instead of 200 for stub transfer methods.
These web stubs currently report success while doing nothing. Both ImageCacheManager.ts line 61 and uploadImage.ts line 25 depend on status codes to determine success; returning 200 masks the fact that no actual transfer occurred on the web platform. Use 501 (standard HTTP code for unsupported operations) instead.
Proposed fix
export async function downloadAsync(
_uri: string,
_fileUri: string,
): Promise<{ status: number; uri: string; headers: Record<string, string>; mimeType: string }> {
- return { status: 200, uri: _fileUri, headers: {}, mimeType: '' };
+ return { status: 501, uri: _fileUri, headers: {}, mimeType: '' };
}
export async function uploadAsync(
_url: string,
_fileUri: string,
): Promise<{ status: number; body: string; headers: Record<string, string> }> {
- return { status: 200, body: '', headers: {} };
+ return { status: 501, body: '', headers: {} };
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/expo/mocks/expo-file-system-legacy.ts` around lines 45 - 57, The stub
transfer functions downloadAsync and uploadAsync currently return status 200
despite doing nothing; update both functions in expo-file-system-legacy.ts so
they return status 501 (Not Implemented) instead of 200 to signal unsupported
operations on web—keep the existing return shape ({ status, uri, headers,
mimeType } for downloadAsync and { status, body, headers } for uploadAsync) but
change status to 501.
| function LeafletMap({ style, initialRegion, region, children, ...props }: MapViewProps) { | ||
| const r = region ?? initialRegion; | ||
| const center: LatLngExpression = r ? [r.latitude, r.longitude] : [20, 0]; | ||
| const zoom = r ? Math.round(10 - Math.log2(r.latitudeDelta + 0.001)) : 5; | ||
|
|
||
| return ( | ||
| <MapContainer | ||
| center={center} | ||
| zoom={zoom} | ||
| style={{ flex: 1, height: '100%', ...(style as object) }} | ||
| {...(props as object)} | ||
| > |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/expo/mocks/react-native-maps.tsxRepository: PackRat-AI/PackRat
Length of output: 3961
🌐 Web query:
react-leaflet useMapEvents hook documentation
💡 Result:
The useMapEvents hook in React Leaflet (v5.x) attaches provided Leaflet event handlers to the map instance and returns the map instance. It must be used in a descendant of MapContainer. Usage: const map = useMapEvents({ click: => { map.locate }, locationfound: (e) => { // Handle location }, }) Example from official docs: function LocationMarker { const [position, setPosition] = useState(null) const map = useMapEvents({ click { map.locate }, locationfound(e) { setPosition(e.latlng) map.flyTo(e.latlng, map.getZoom) }, }) return position === null ? null : ( You are here ) } Import: import { useMapEvents } from 'react-leaflet/hooks' It supports LeafletEventHandlerFnMap as the argument. For single events, use useMapEvent instead. Official documentation: https://react-leaflet.js.org/docs/api-map/ (v5.x, current version).
Citations:
- 1: https://react-leaflet.js.org/docs/example-events
- 2: https://react-leaflet.js.org/docs/api-map/
- 3: https://react-leaflet.js.org/docs/example-events/
🌐 Web query:
react-leaflet Marker eventHandlers prop click event
💡 Result:
In react-leaflet (versions 3, 4, and 5), the Marker component uses the eventHandlers prop to attach Leaflet events like click, not a direct onClick prop (which was used in earlier versions but deprecated since v3). Use it as: import { Marker } from 'react-leaflet'; function MyComponent { const handleClick = (e) => { console.log('Marker clicked at', e.latlng); }; return ( <Marker position={[51.505, -0.09]} eventHandlers={{ click: handleClick }} /> ); } The eventHandlers prop is of type LeafletEventHandlerFnMap, where keys are Leaflet event names (e.g., 'click', 'dragend') and values are callback functions receiving the Leaflet event object. This is confirmed across official docs for v3/v4/v5 (child components API pages listing eventHandlers under Marker props with "Evented" behavior), examples like DraggableMarker using eventHandlers for dragend, Stack Overflow answers noting onClick stopped working in v3 and recommending eventHandlers, and the changelog showing no removal or change to this prop up to v5.0.0 (Dec 2024).
Citations:
- 1: https://stackoverflow.com/questions/40901539/arbitrary-function-on-react-leaflet-marker-click
- 2: https://stackoverflow.com/questions/65076608/react-leaflet-custom-event-onclick-for-marker-component/65077309
- 3: https://react-leaflet.js.org/docs/v3/example-draggable-marker/
- 4: https://react-leaflet.js.org/docs/core-api/
- 5: https://react-leaflet.js.org/docs/api-components/
- 6: https://react-leaflet.js.org/docs/v4/api-components/
- 7: http://react-leaflet.js.org/docs/api-components/
- 8: https://react-leaflet.js.org/docs/example-draggable-marker
🏁 Script executed:
rg -t tsx -t ts 'MapView|LeafletMap' apps/expo --max-count=20 -B 2 -A 5 | grep -A 8 'onPress\|onRegionChange'Repository: PackRat-AI/PackRat
Length of output: 90
🏁 Script executed:
rg 'MapView|LeafletMap' apps/expo -g '*.ts' -g '*.tsx' -B 2 -A 5 | grep -A 8 'onPress\|onRegionChange'Repository: PackRat-AI/PackRat
Length of output: 698
🏁 Script executed:
rg 'onPress|onRegionChange' apps/expo -g '*.ts' -g '*.tsx' | grep -v 'react-native-maps.tsx'Repository: PackRat-AI/PackRat
Length of output: 48977
🏁 Script executed:
rg 'MapView|MapContainer|LeafletMap' apps/expo -g '*.ts' -g '*.tsx' --max-count=30 -B 3 -A 10Repository: PackRat-AI/PackRat
Length of output: 13902
Wire map and marker callbacks to react-leaflet events.
onPress, onRegionChange, onRegionChangeComplete are declared in MapViewProps and onPress in MarkerProps, but they are not connected to Leaflet event handlers. Consumers get silent no-ops on web. This violates the API contract even though current usage doesn't expose it yet.
Extract and wire callbacks using useMapEvents for map events and eventHandlers for marker clicks:
Implementation sketch
-import { Marker as LeafletMarker, MapContainer, Popup, TileLayer } from 'react-leaflet';
+import { Marker as LeafletMarker, MapContainer, Popup, TileLayer, useMapEvents } from 'react-leaflet';
+function MapEventsBridge({
+ onPress,
+ onRegionChangeComplete,
+}: Pick<MapViewProps, 'onPress' | 'onRegionChangeComplete'>) {
+ useMapEvents({
+ click(e) {
+ onPress?.({ nativeEvent: { coordinate: { latitude: e.latlng.lat, longitude: e.latlng.lng } } });
+ },
+ moveend(e) {
+ const c = e.target.getCenter();
+ const b = e.target.getBounds();
+ onRegionChangeComplete?.({
+ latitude: c.lat,
+ longitude: c.lng,
+ latitudeDelta: Math.abs(b.getNorth() - b.getSouth()),
+ longitudeDelta: Math.abs(b.getEast() - b.getWest()),
+ });
+ },
+ });
+ return null;
+}
function LeafletMap({ style, initialRegion, region, children, ...props }: MapViewProps) {
+ const { onPress, onRegionChangeComplete, ...restProps } = props;
...
return (
<MapContainer
...
- {...(props as object)}
+ {...(restProps as object)}
>
+ <MapEventsBridge onPress={onPress} onRegionChangeComplete={onRegionChangeComplete} />
<TileLayer ... />
{children}
</MapContainer>
);
}
-export function Marker({ coordinate, title, children }: MarkerProps) {
+export function Marker({ coordinate, title, children, onPress }: MarkerProps) {
return (
- <LeafletMarker position={[coordinate.latitude, coordinate.longitude]}>
+ <LeafletMarker
+ position={[coordinate.latitude, coordinate.longitude]}
+ eventHandlers={onPress ? { click: onPress } : undefined}
+ >
{title ? <Popup>{title}</Popup> : children}
</LeafletMarker>
);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/expo/mocks/react-native-maps.tsx` around lines 60 - 71, The LeafletMap
mock currently ignores MapViewProps callbacks so map events are no-ops; update
the LeafletMap component to call useMapEvents and wire map events to the
incoming props (onPress -> map click/box click, onRegionChange -> onmove,
onRegionChangeComplete -> moveend) and pass through relevant event data
(center/latitudeDelta/longitudeDelta) to match MapViewProps signatures;
similarly update the Marker mock to supply an eventHandlers prop (or onClick
handler) that invokes MarkerProps.onPress with the event and marker
id/coordinate; ensure you reference the existing LeafletMap function and the
Marker mock component, keep props spread intact, and add minimal type
conversions to produce LatLng/region objects expected by consumers.
| "ios": "APP_VARIANT=development expo run:ios", | ||
| "lint": "biome check --write", | ||
| "start": "APP_VARIANT=development expo start", | ||
| "start": "APP_VARIANT=development EXPO_UNSTABLE_WEB_MODAL=1 expo start", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify scripts using inline env-var assignment (POSIX style).
jq -r '.scripts | to_entries[] | select(.value | test("(^|\\s)[A-Z_][A-Z0-9_]*=")) | "\(.key): \(.value)"' apps/expo/package.jsonRepository: PackRat-AI/PackRat
Length of output: 458
Use a cross-platform env setter for all npm scripts with env vars.
POSIX-style env assignment breaks npm run on Windows. Six scripts are affected: android, ios, start, update:development, update:preview, and web. Switch to cross-env for portability.
Affected scripts
"scripts": {
- "android": "APP_VARIANT=development expo run:android",
+ "android": "cross-env APP_VARIANT=development expo run:android",
- "ios": "APP_VARIANT=development expo run:ios",
+ "ios": "cross-env APP_VARIANT=development expo run:ios",
- "start": "APP_VARIANT=development EXPO_UNSTABLE_WEB_MODAL=1 expo start",
+ "start": "cross-env APP_VARIANT=development EXPO_UNSTABLE_WEB_MODAL=1 expo start",
- "update:development": "APP_VARIANT=development eas update --branch development --environment development",
+ "update:development": "cross-env APP_VARIANT=development eas update --branch development --environment development",
- "update:preview": "APP_VARIANT=preview eas update --branch preview --environment preview",
+ "update:preview": "cross-env APP_VARIANT=preview eas update --branch preview --environment preview",
- "web": "EXPO_UNSTABLE_WEB_MODAL=1 expo start --web",
+ "web": "cross-env EXPO_UNSTABLE_WEB_MODAL=1 expo start --web",
},
"devDependencies": {
+ "cross-env": "^7.0.3",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/expo/package.json` at line 33, The package.json npm scripts use
POSIX-style env assignments which fail on Windows; install cross-env as a
devDependency and update each affected script (android, ios, start,
update:development, update:preview, web) to prefix the environment assignments
with cross-env (e.g., replace "APP_VARIANT=development EXPO_UNSTABLE_WEB_MODAL=1
expo start" with a cross-env-prefixed form) so the same env vars are set
cross-platform; ensure package.json scripts now use cross-env before the command
and run npm install --save-dev cross-env to add the dependency.
| // parseDate:false disables Eden Treaty's JSON reviver that silently converts | ||
| // date-like strings (ISO 8601, "YYYY-MM-DD HH:MM") to Date objects. Without | ||
| // this, every Zod z.string().datetime() field in API response schemas fails. | ||
| return treaty<App>(config.baseUrl, { | ||
| fetcher: authFetcher as unknown as typeof fetch, | ||
| parseDate: false, | ||
| }).api; |
There was a problem hiding this comment.
Global parseDate: false changes the client contract for all date fields.
This switch affects every route, but some schemas still model dates as z.date() (for example, catalog createdAt/updatedAt). That creates a runtime/type mismatch unless all consumers and schemas are migrated together.
💡 Safer rollout pattern
export type ApiClientConfig = {
baseUrl: string;
auth: AuthHooks;
/** Optional fetch override (e.g. for tests or custom runtimes). */
fetcher?: typeof fetch;
+ /** Optional Eden date parsing behavior. Defaults to current behavior for compatibility. */
+ parseDate?: boolean;
};
// ...
return treaty<App>(config.baseUrl, {
fetcher: authFetcher as unknown as typeof fetch,
- parseDate: false,
+ parseDate: config.parseDate ?? true,
}).api;
}#!/bin/bash
set -euo pipefail
echo "1) Locate remaining z.date() usage in API schemas:"
rg -n --type ts '\bz\.date\(' packages/api/src/schemas
echo
echo "2) Locate createApiClient call sites and parseDate overrides:"
rg -n --type ts '\bcreateApiClient\s*\(' apps packages
rg -n --type ts '\bparseDate\b' apps packages
echo
echo "Expected verification outcome:"
echo "- If z.date() remains in response schemas while parseDate is globally false,"
echo " runtime data will be string-shaped unless transformed before use."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/api-client/src/index.ts` around lines 130 - 136, The global
parseDate:false on treaty<App> breaks routes that use z.date(); instead make
parseDate configurable per-client: remove the hardcoded parseDate:false in the
treaty<App>(...) call and read an optional parseDate boolean from the
createApiClient/config parameter (default true), then pass that value into
treaty as parseDate; update call sites that need string dates to explicitly pass
parseDate:false. Reference symbols: treaty<App>, parseDate, and the
createApiClient/config used to instantiate the client; after change, run the
suggested rg checks to find remaining z.date() usages and adjust call sites as
needed.
| title: z.string().nullable().optional(), | ||
| text: z.string().nullable().optional(), | ||
| date: z.string().nullable().optional(), |
There was a problem hiding this comment.
Schema relaxation breaks ItemReviews component.
Making title, text, and date nullable/optional will cause runtime crashes in ItemReviews.tsx:
review.text.length > 150→ TypeError whentextis null/undefinedkey={review.title}→ React warning for undefined keyformatDate(review.date)→ Invalid Date when null
Either:
- Keep these fields required in the schema (rejecting incomplete review data), or
- Update
ItemReviews.tsxto handle null/missing values with fallbacks
If the API genuinely returns incomplete reviews, the component needs null guards:
const shouldTruncate = (review.text?.length ?? 0) > 150;
// ...
<View key={review.title ?? `review-${index}`}>
// ...
{review.date && <Text>{formatDate(review.date)}</Text>}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/api/src/schemas/catalog.ts` around lines 60 - 62, The schema change
made title, text, and date nullable/optional which breaks ItemReviews: update
either the schema or the component. Option A: revert the fields in
packages/api/src/schemas/catalog.ts to required strings (remove
.nullable().optional() from title, text, date) so reviews always supply values.
Option B: make ItemReviews.tsx resilient by adding null guards and fallbacks
around review.text, review.title, and review.date (use safe length checks like
review.text?.length ?? 0 when deciding truncation, provide a stable key like
review.title ?? `review-${index}`, and only call formatDate when review.date is
present). Pick one approach and apply consistently to the symbols review.text,
review.title, review.date and the formatDate usage.
- normalizeDescription: guard match[1] explicitly so TypeScript is satisfied without non-null assertion - ImageCacheManager: replace Platform.OS check with typeof document check to remove the react-native import — react-native/index.js uses Flow syntax that Rollup can't parse in unit tests
Coverage Report for Expo Unit Tests Coverage (./apps/expo)
File Coverage
|
||||||||||||||||||||||||||||||||||||||
feat(web): initial web support for Expo app
Summary
metro.config.jsredirects native-only packages to web shims on buildwebCacheMap preventsatomWithStorage.onMountfrom overwriting the access token via the synchronous SQLite API (which requires SharedArrayBuffer on web); hydrated on page refresh viauseAuthInitlib/persist-plugin.ts/.web.tsplatform split usesobservablePersistAsyncStorageon web (backed by the existing localStorage mock) instead of the SQLite pluginreact-native-mapsreplaced with a react-leaflet implementation on web (OpenStreetMap tiles, CDN-injected leaflet CSS,MarkerwithPopup)react-native-keyboard-controller,@react-native-community/datetimepicker,@react-native-google-signin/google-signinUnavailabilityErrorcrashes fromImageCacheManageranduploadImageImageCacheManager.clearCache()guarded withPlatform.OS === 'web'early returnStack.Screennames that didn't match actual file paths (current-pack/[id],weight-analysis/[id])darkclass to<html>viauseEffectWhat still uses native paths on web
uploadImage.ts) — stub returns 200 but does no actual upload; web image upload would need a separatefetch + FormDataimplementationllama.rn,@react-native-ai/*) — stubs throw; AI chat uses the cloud API which works fineTest plan
<html>class switches correctlySummary by CodeRabbit
Release Notes
New Features
Bug Fixes
Improvements