Skip to content

feat(web): initial web support for Expo app#2364

Merged
andrew-bierman merged 26 commits into
developmentfrom
feat/web-implementation
May 1, 2026
Merged

feat(web): initial web support for Expo app#2364
andrew-bierman merged 26 commits into
developmentfrom
feat/web-implementation

Conversation

@andrew-bierman

@andrew-bierman andrew-bierman commented May 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • SPA mode — switched Expo web output to single-page app mode
  • Metro WEB_STUBS — central lookup table in metro.config.js redirects native-only packages to web shims on build
  • Token atom fix — module-level webCache Map prevents atomWithStorage.onMount from overwriting the access token via the synchronous SQLite API (which requires SharedArrayBuffer on web); hydrated on page refresh via useAuthInit
  • Legend-State persistencelib/persist-plugin.ts/.web.ts platform split uses observablePersistAsyncStorage on web (backed by the existing localStorage mock) instead of the SQLite plugin
  • Mapsreact-native-maps replaced with a react-leaflet implementation on web (OpenStreetMap tiles, CDN-injected leaflet CSS, Marker with Popup)
  • Keyboard controller, date picker, Google Sign-In — web stubs added for react-native-keyboard-controller, @react-native-community/datetimepicker, @react-native-google-signin/google-signin
  • expo-file-system/legacy — web stub with no-op implementations prevents UnavailabilityError crashes from ImageCacheManager and uploadImage
  • Logout crash fixImageCacheManager.clearCache() guarded with Platform.OS === 'web' early return
  • Route fixes — corrected Stack.Screen names that didn't match actual file paths (current-pack/[id], weight-analysis/[id])
  • Dark mode — syncs NativeWind dark class to <html> via useEffect

What still uses native paths on web

  • Image uploads (uploadImage.ts) — stub returns 200 but does no actual upload; web image upload would need a separate fetch + FormData implementation
  • On-device AI (llama.rn, @react-native-ai/*) — stubs throw; AI chat uses the cloud API which works fine

Test plan

  • Navigate to app in browser — verify login/logout flow
  • AI chat — verify token auth works (no 401)
  • Trip detail — verify react-leaflet map renders with OpenStreetMap tiles
  • Trip location search — verify map + marker work
  • Dark mode toggle — verify <html> class switches correctly
  • Pack/item CRUD — verify no file-system crashes

Summary by CodeRabbit

Release Notes

  • New Features

    • Added map support for web platform.
    • Improved dark mode handling across all platforms.
  • Bug Fixes

    • Fixed logo sizing inconsistencies on web.
    • Fixed catalog item description display formatting.
    • Fixed dark mode synchronization on web.
  • Improvements

    • Enhanced web platform stability and compatibility.
    • Improved data storage and synchronization mechanisms.

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
Copilot AI review requested due to automatic review settings May 1, 2026 03:34
@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@andrew-bierman has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 44 minutes and 42 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0c6f6c90-ba5e-40b1-a563-29483a06ddb9

📥 Commits

Reviewing files that changed from the base of the PR and between a3b9d12 and ad15222.

📒 Files selected for processing (3)
  • apps/expo/features/catalog/lib/normalizeDescription.ts
  • apps/expo/lib/utils/ImageCacheManager.ts
  • packages/api/src/schemas/catalog.ts

Walkthrough

This 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

Cohort / File(s) Summary
Web Build & Runtime Configuration
apps/expo/app.config.ts, apps/expo/metro.config.js, apps/expo/package.json
Updated Expo web output to 'single' mode, augmented Metro resolver to support .wasm assets and stubbed native modules for web builds, added EXPO_UNSTABLE_WEB_MODAL=1 and leaflet dependencies.
Navigation & Layout Structures
apps/expo/app/(app)/_layout.tsx, apps/expo/app/_layout.tsx, apps/expo/app/auth/_layout.tsx
Removed catalog/index route; parameterized modal routes (current-pack/[id], weight-analysis/[id]); exported unstable_settings anchor for auth layout; added web dark mode synchronization via useEffect on RootLayout.
Responsive Logo Styling
apps/expo/app/auth/(create-account)/*, apps/expo/app/auth/(login)/*, apps/expo/app/auth/index.tsx
Applied explicit web:h-8 web:w-8 sizing classes to logo images across all auth screens alongside existing iOS and base sizing.
Persistence Layer Abstraction
apps/expo/lib/kvStorage.ts, apps/expo/lib/kvStorage.web.ts, apps/expo/lib/persist-plugin.ts, apps/expo/lib/persist-plugin.web.ts
Created shared key-value storage interface with platform-specific implementations (Expo SQLite on native, AsyncStorage on web) and centralized persist plugin using observablePersistSqlite or observablePersistAsyncStorage.
Store Persistence Migrations
apps/expo/features/auth/atoms/authAtoms.ts, apps/expo/features/packs/store/*, apps/expo/features/pack-templates/store/*, apps/expo/features/trail-conditions/store/*, apps/expo/features/trips/store/*
Switched all Legend state stores from inline observablePersistSqlite(Storage) to shared persistPlugin from expo-app/lib/persist-plugin for cleaner abstraction.
Auth Initialization & Schema
apps/expo/features/auth/hooks/useAuthInit.ts, packages/api/src/schemas/catalog.ts
Extended auth init to hydrate tokenAtom on session load; relaxed catalog review schema to accept optional/nullable userAvatar, title, text, and date fields (date now a string).
API Client Configuration
packages/api-client/src/index.ts
Added parseDate: false to Eden Treaty client config to prevent automatic date parsing, aligning with downstream Zod schema expectations.
Catalog Description Normalization
apps/expo/features/catalog/lib/normalizeDescription.ts, apps/expo/features/catalog/components/CatalogItemCard.tsx, apps/expo/features/catalog/screens/CatalogItemDetailScreen.tsx
Introduced normalizeDescription utility to parse and join Details: [...] JSON arrays; applied preprocessing to catalog item descriptions before rendering.
Global CSS & Utility Updates
apps/expo/global.css, apps/expo/lib/utils/ImageCacheManager.ts
Added web-only CSS overrides for themed color utilities; guarded image cache clearing with web platform check.
Web Mocks & Stubs
apps/expo/mocks/async-storage.ts, apps/expo/mocks/datetimepicker.tsx, apps/expo/mocks/expo-file-system-legacy.ts, apps/expo/mocks/expo-sqlite-kv-store.ts, apps/expo/mocks/google-signin.ts, apps/expo/mocks/react-native-ai-*.ts, apps/expo/mocks/react-native-blob-util.ts, apps/expo/mocks/react-native-keyboard-controller.tsx, apps/expo/mocks/react-native-maps.tsx
Added comprehensive web-compatible stubs/mocks for native and platform-specific modules, including AsyncStorage via localStorage, file system no-ops, input-based date picker, and leaflet-based maps.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

web

Suggested reviewers

  • Isthisanmol
  • mikib0
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(web): initial web support for Expo app' accurately and concisely summarizes the main change—adding web platform support to the Expo app.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web-implementation

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.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 44 minutes and 42 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added dependencies Pull requests that update a dependency file api mobile labels May 1, 2026
@github-actions

github-actions Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for API Unit Tests Coverage (./packages/api)

Status Category Percentage Covered / Total
🔵 Lines 72.93% 609 / 835
🔵 Statements 72.93% (🎯 65%) 609 / 835
🔵 Functions 96% 48 / 50
🔵 Branches 88.27% 271 / 307
File CoverageNo changed files found.
Generated in workflow #971 for commit ad15222 by the Vitest Coverage Report Action

@andrew-bierman andrew-bierman changed the base branch from main to development May 1, 2026 03:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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-maps usage 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.

Comment on lines +19 to +27
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();
},
Comment on lines +51 to +58
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();
Comment on lines +24 to +29
ls: () => Promise.resolve([]),
},
config: () => ({
fetch: () => Promise.resolve(null),
}),
};
Comment on lines +54 to +63
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(),
Comment on lines +34 to +39
<input
type={inputType}
defaultValue={toInputValue(value, mode)}
onChange={handleChange}
style={{ padding: 8, borderRadius: 6, border: '1px solid #ccc', fontSize: 16 }}
/>
Comment thread apps/expo/mocks/react-native-maps.ts Outdated
Comment on lines +1 to +22
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 };
Comment thread apps/expo/package.json Outdated
Comment on lines 121 to 130
"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",
Comment on lines +55 to +62
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.
@andrew-bierman andrew-bierman requested a review from Copilot May 1, 2026 04:10
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
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Deploying packrat-guides with  Cloudflare Pages  Cloudflare Pages

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

View logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

@cloudflare-workers-and-pages

Copy link
Copy Markdown
Contributor

Deploying packrat-landing with  Cloudflare Pages  Cloudflare Pages

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

View logs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 (and user_name) to be nullable/optional, but downstream consumers treat these as required strings (e.g., calling review.text.length, using review.title as 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.

Comment on lines 54 to 58
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(),
Comment on lines +60 to +70
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)}
Comment on lines +1 to +9
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),
Comment on lines +5 to +6
// headers). Use the AsyncStorage plugin instead, which falls through to our
// localStorage-backed mock via the metro web stub.
Comment on lines +45 to +57
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: {} };
}
Comment on lines 95 to 99
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);
Comment on lines +1 to +5
export type LlamaLanguageModel = never;

export const llama = {
languageModel: () => null,
};
…eviews

Only user_avatar (snake_case) is present in the DB JSONB type; userAvatar was a stale alias with no backing data.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8d1423b and a3b9d12.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock, !bun.lock
📒 Files selected for processing (44)
  • apps/expo/app.config.ts
  • apps/expo/app/(app)/_layout.tsx
  • apps/expo/app/_layout.tsx
  • apps/expo/app/auth/(create-account)/credentials.tsx
  • apps/expo/app/auth/(create-account)/index.tsx
  • apps/expo/app/auth/(login)/forgot-password.tsx
  • apps/expo/app/auth/(login)/index.tsx
  • apps/expo/app/auth/(login)/reset-password.tsx
  • apps/expo/app/auth/_layout.tsx
  • apps/expo/app/auth/index.tsx
  • apps/expo/features/auth/atoms/authAtoms.ts
  • apps/expo/features/auth/hooks/useAuthInit.ts
  • apps/expo/features/auth/store/user.ts
  • apps/expo/features/catalog/components/CatalogItemCard.tsx
  • apps/expo/features/catalog/lib/normalizeDescription.ts
  • apps/expo/features/catalog/screens/CatalogItemDetailScreen.tsx
  • apps/expo/features/pack-templates/store/packTemplateItems.ts
  • apps/expo/features/pack-templates/store/packTemplates.ts
  • apps/expo/features/packs/store/packItems.ts
  • apps/expo/features/packs/store/packWeightHistory.ts
  • apps/expo/features/packs/store/packingMode.ts
  • apps/expo/features/packs/store/packs.ts
  • apps/expo/features/trail-conditions/store/trailConditionReports.ts
  • apps/expo/features/trips/store/trips.ts
  • apps/expo/global.css
  • apps/expo/lib/kvStorage.ts
  • apps/expo/lib/kvStorage.web.ts
  • apps/expo/lib/persist-plugin.ts
  • apps/expo/lib/persist-plugin.web.ts
  • apps/expo/lib/utils/ImageCacheManager.ts
  • apps/expo/metro.config.js
  • apps/expo/mocks/async-storage.ts
  • apps/expo/mocks/datetimepicker.tsx
  • apps/expo/mocks/expo-file-system-legacy.ts
  • apps/expo/mocks/expo-sqlite-kv-store.ts
  • apps/expo/mocks/google-signin.ts
  • apps/expo/mocks/react-native-ai-apple.ts
  • apps/expo/mocks/react-native-ai-llama.ts
  • apps/expo/mocks/react-native-blob-util.ts
  • apps/expo/mocks/react-native-keyboard-controller.tsx
  • apps/expo/mocks/react-native-maps.tsx
  • apps/expo/package.json
  • packages/api-client/src/index.ts
  • packages/api/src/schemas/catalog.ts

Comment thread apps/expo/features/catalog/lib/normalizeDescription.ts
Comment thread apps/expo/global.css
Comment on lines +5 to +56
/* 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));
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment thread apps/expo/metro.config.js
Comment on lines +19 to +35
// 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',
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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
fi

Repository: 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.

Comment on lines +26 to +31
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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +45 to +57
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: {} };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, verify the file exists and examine the stub file
cat -n apps/expo/mocks/expo-file-system-legacy.ts

Repository: 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
fi

Repository: 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 2

Repository: 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 2

Repository: 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 2

Repository: 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 -100

Repository: 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.

Comment on lines +60 to +71
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)}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/expo/mocks/react-native-maps.tsx

Repository: 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:


🌐 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:


🏁 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 10

Repository: 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.

Comment thread apps/expo/package.json
"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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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.json

Repository: 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.

Comment on lines +130 to +136
// 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

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.

Comment thread packages/api/src/schemas/catalog.ts Outdated
Comment on lines +60 to +62
title: z.string().nullable().optional(),
text: z.string().nullable().optional(),
date: z.string().nullable().optional(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Schema relaxation breaks ItemReviews component.

Making title, text, and date nullable/optional will cause runtime crashes in ItemReviews.tsx:

  • review.text.length > 150 → TypeError when text is null/undefined
  • key={review.title} → React warning for undefined key
  • formatDate(review.date) → Invalid Date when null

Either:

  1. Keep these fields required in the schema (rejecting incomplete review data), or
  2. Update ItemReviews.tsx to 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
@github-actions

github-actions Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Expo Unit Tests Coverage (./apps/expo)

Status Category Percentage Covered / Total
🔵 Lines 81.67% 535 / 655
🔵 Statements 81.67% (🎯 75%) 535 / 655
🔵 Functions 92.98% 53 / 57
🔵 Branches 89.73% 201 / 224
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
apps/expo/lib/utils/ImageCacheManager.ts 100% 96.15% 100% 100%
Generated in workflow #971 for commit ad15222 by the Vitest Coverage Report Action

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

Labels

api dependencies Pull requests that update a dependency file mobile

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants