Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
140 commits
Select commit Hold shift + click to select a range
536ea8a
Initial plan
Copilot Aug 20, 2025
1b9043b
Initial plan
Copilot Aug 20, 2025
4221777
Initial plan
Copilot Aug 20, 2025
ebfe3c5
Initial plan
Copilot Aug 20, 2025
227baef
Initial plan
Copilot Aug 22, 2025
f9b5f2f
Initial plan
Copilot Aug 22, 2025
e89b7bb
Initial plan
Copilot Aug 23, 2025
d8c8cdd
Initial plan
Copilot Aug 23, 2025
4321eb9
Initial plan
Copilot Aug 20, 2025
4a99c3b
Initial plan
Copilot Aug 23, 2025
2609e15
Initial plan
Copilot Aug 24, 2025
72f3fc5
Initial plan
Copilot Aug 23, 2025
865a7e5
Initial plan
Copilot Aug 23, 2025
4a0579d
Initial plan
Copilot Sep 9, 2025
2a85a13
Initial plan
Copilot Sep 11, 2025
792e268
Initial plan
Copilot Sep 10, 2025
aa13ebb
Initial plan
Copilot Sep 11, 2025
7d3e87c
Initial plan
Copilot Sep 12, 2025
d9b1dc3
Initial plan
Copilot Oct 4, 2025
fe6d04a
Initial plan
Copilot Oct 17, 2025
e944700
Initial plan
Copilot Oct 17, 2025
09d4b15
Initial plan
Copilot Oct 18, 2025
a440888
Initial plan
Copilot Oct 20, 2025
e902552
Initial plan
Copilot Oct 25, 2025
b89a1de
Initial plan
Copilot Oct 26, 2025
235b49b
Initial plan
Copilot Nov 18, 2025
f0ac70d
Initial plan
Copilot Nov 6, 2025
a91051f
Initial plan
Copilot Nov 6, 2025
f97d50c
Initial plan
Copilot Nov 8, 2025
e9d2b06
Initial plan
Copilot Nov 8, 2025
14c768b
Initial plan
Copilot Nov 9, 2025
6a4e774
Initial plan
Copilot Nov 9, 2025
77e4059
Initial plan
Copilot Nov 11, 2025
380858e
Initial plan
Copilot Nov 11, 2025
3972884
Initial plan
Copilot Nov 11, 2025
c2b30d8
Initial plan
Copilot Nov 11, 2025
fdeced1
Initial plan
Copilot Nov 18, 2025
a7a1c73
Initial plan
Copilot Nov 18, 2025
7c32e6e
Initial plan
Copilot Nov 18, 2025
5cb1155
Initial plan
Copilot Nov 18, 2025
32aed96
Initial plan
Copilot Nov 18, 2025
02d7013
Initial plan
Copilot Nov 18, 2025
02611a2
Initial plan
Copilot Nov 18, 2025
00db2d8
Initial plan
Copilot Nov 18, 2025
b5b028a
Initial plan
Copilot Nov 18, 2025
e282d3b
Initial plan
Copilot Nov 18, 2025
c506000
Initial plan
Copilot Nov 18, 2025
beff25c
Initial plan
Copilot Nov 18, 2025
343c0cd
Initial plan
Copilot Nov 18, 2025
b26f32d
Initial plan
Copilot Dec 2, 2025
8dc222f
Initial plan
Copilot Dec 13, 2025
e2e9409
Initial plan
Copilot Mar 9, 2026
b4a147e
Initial plan
Copilot Mar 9, 2026
edbb6d2
Initial plan
Copilot Mar 9, 2026
4f9092b
Initial plan
Copilot Mar 10, 2026
c0e7dd2
Initial plan
Copilot Mar 9, 2026
c0083ef
Initial plan
Copilot Mar 10, 2026
b0f7e4c
Initial plan
Copilot Feb 21, 2026
e3c4a62
Initial plan
Copilot Feb 27, 2026
e0233d1
Initial plan
Copilot Mar 10, 2026
414f2d2
Initial plan
Copilot Mar 7, 2026
14cd051
Initial plan
Copilot Mar 10, 2026
28f2ebd
Initial plan
Copilot Mar 11, 2026
00e19ec
Initial plan
Copilot Mar 9, 2026
9c99414
Initial plan
Copilot Mar 9, 2026
2e5396c
Initial plan
Copilot Mar 13, 2026
0f005f1
Initial plan
Copilot Mar 13, 2026
b6c9765
Initial plan
Copilot Mar 13, 2026
28872f6
Initial plan
Copilot Mar 13, 2026
32489fa
fix(expo/CreatePackItemForm): default quantity to 1 instead of 0
mikib0 Mar 13, 2026
a151463
Initial plan
Copilot Mar 13, 2026
94ada9d
Initial plan
Copilot Mar 16, 2026
65151ae
Initial plan
Copilot Mar 9, 2026
2a038db
Initial plan
Copilot Mar 13, 2026
6434404
Initial plan
Copilot Mar 13, 2026
9382211
Initial plan
Copilot Mar 9, 2026
25d7b58
Initial plan
Copilot Mar 9, 2026
cbded7a
Initial plan
Copilot Apr 1, 2026
6ed13f8
Initial plan
Copilot Mar 21, 2026
cae1103
Initial plan
Copilot Mar 9, 2026
1aebad7
Initial plan
Copilot Mar 9, 2026
142fecb
Initial plan
Copilot Mar 9, 2026
cea16a0
Initial plan
Copilot Mar 9, 2026
5677d4d
Initial plan
Copilot Mar 9, 2026
6e307d3
Initial plan
Copilot Mar 9, 2026
36450a4
Initial plan
Copilot Mar 9, 2026
1a68f2f
Initial plan
Copilot Mar 9, 2026
ebd601f
Initial plan
Copilot Mar 9, 2026
b646ff4
Initial plan
Copilot Mar 9, 2026
9a3e8d0
Initial plan
Copilot Aug 22, 2025
5eb59f7
Initial plan
Copilot Feb 27, 2026
2b7dde2
chore: reopen trigger (no-op commit to restore PR state)
andrew-bierman Apr 11, 2026
2744d3e
ci: trigger biome check
andrew-bierman Apr 11, 2026
4ddf88f
ci: retrigger CI after biome fixes
andrew-bierman Apr 11, 2026
8d28564
Initial plan
Copilot Mar 9, 2026
a216ec3
ci: trigger checks for dependabot merges
andrew-bierman Apr 13, 2026
30ab5cc
Initial plan
Copilot Apr 13, 2026
93dad92
ci: retrigger checks after copilot merge-conflict fix
andrew-bierman Apr 14, 2026
6180c2a
ci: trigger CI on Copilot bot's expo-symbols type fixes
andrew-bierman Apr 14, 2026
2d979d4
Initial plan
Copilot Apr 14, 2026
4e0a10d
trigger: retrigger CI after node_modules clean install verified vites…
andrew-bierman Apr 15, 2026
d3c575e
Initial plan
Copilot Apr 14, 2026
dcbfa26
trigger: retrigger CI after node_modules clean install verified vites…
andrew-bierman Apr 15, 2026
7228f91
ci: re-trigger checks after @types/react alignment fix
claude Apr 16, 2026
e5cb0d6
Initial plan
Copilot Apr 14, 2026
698f705
Initial plan
Copilot Apr 16, 2026
4efb2e5
ci: trigger CI run on updated branch
claude Apr 16, 2026
b964271
Initial plan
Copilot Sep 22, 2025
e75c31d
Initial plan
Copilot Sep 22, 2025
a41e127
ci: trigger workflows
andrew-bierman Apr 14, 2026
9a5c7d3
✨ feat(web): add platform shims and web layout adapters for MVP
andrew-bierman Apr 30, 2026
115b545
🐛 fix(web): resolve runtime errors blocking web MVP boot
andrew-bierman Apr 30, 2026
b95f078
🏷️ replace TestIds/T with unified testIds registry
andrew-bierman May 1, 2026
26bcb35
✅ use testIds in add-item test; drop fragile waitForResponse
andrew-bierman May 1, 2026
ff62acf
🐛 fix(trips): useDeleteTrip returns fn directly, not object
andrew-bierman May 1, 2026
246b4be
✅ add e2e specs for packs, trips, and profile CRUD
andrew-bierman May 1, 2026
7375556
✅ fix(e2e): await item POST before navigating to prevent request abort
andrew-bierman May 1, 2026
6368f42
🌐 fix(web): replace native Alert.alert no-ops + add missing testIDs f…
andrew-bierman May 1, 2026
5983036
✅ fix(e2e): harden trips + profile web tests for NativeWindUI and SPA…
andrew-bierman May 1, 2026
a8e5e00
🐛 fix(web): replace typeof === 'function' with isFunction guard
andrew-bierman May 1, 2026
1574c2d
🔧 chore(env-lint): exempt apps/expo/e2e-web from process.env check
andrew-bierman May 1, 2026
db33ed1
🔧 chore(checks): exempt e2e-web from check:casts:strict + annotate fi…
andrew-bierman May 1, 2026
40d398e
🔧 chore(web): add safe-cast annotations to web shims and mock files
andrew-bierman May 1, 2026
d652847
fix(ci): biome unused import + coverage threshold for web-only files
andrew-bierman May 1, 2026
0042f74
refactor(web): delete useAuthActions.web.ts; add google-signin + appl…
andrew-bierman May 1, 2026
491b6f2
refactor(auth): consolidate token atoms + Treaty client; delete .web.…
andrew-bierman May 1, 2026
3eade65
refactor(web): eliminate feature-layer .web.ts via lib/storage/persis…
andrew-bierman May 1, 2026
f18387f
fix: resolve all type errors surfaced by ignoreDeprecations 5.0
andrew-bierman May 1, 2026
c5873c1
ci: add Playwright web E2E workflow
andrew-bierman May 1, 2026
cb7684d
ci(web-e2e): follow iOS/Android E2E pattern for auth and secrets
andrew-bierman May 1, 2026
a951d82
ci: retrigger CI after suspected transient runner failure
claude Apr 26, 2026
ef5bd01
chore: remove orphaned lib/storage/persistPlugin files
andrew-bierman May 1, 2026
7118ddd
refactor: rename e2e-web/ → playwright/ for clarity
andrew-bierman May 1, 2026
ab5e306
refactor(web): replace Metro stubs with lib/ platform abstractions
andrew-bierman May 1, 2026
2e48375
fix: add safe-cast annotation in Picker.web.tsx
andrew-bierman May 1, 2026
4291d59
fix: use isValidElement<ItemProps> to avoid cast in Picker.web.tsx
andrew-bierman May 1, 2026
9a71fe4
refactor: move testID onto DataItem instead of hardcoding in render fn
andrew-bierman May 1, 2026
0ca2b49
fix(auth): remove unused onFocus from OTPField
andrew-bierman May 14, 2026
96126ea
fix(web): import RefObject/ReactNode instead of using React namespace
andrew-bierman May 14, 2026
305fdd6
fix(web): format globalSetup.ts and sort providers imports
andrew-bierman May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions .github/workflows/web-e2e-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: Web E2E Tests (Playwright)

on:
push:
branches: [main, development]
paths:
- "apps/expo/**"
- ".github/workflows/web-e2e-tests.yml"
# Note: Using `pull_request` (not `pull_request_target`) so forked PRs get
# CI feedback on their own code. Secrets are unavailable for forks, so
# the job is skipped via the `if` condition on the job below.
pull_request:
branches: [main, development]
paths:
- "apps/expo/**"
- ".github/workflows/web-e2e-tests.yml"
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
web-e2e:
name: Web E2E Tests
runs-on: ubuntu-latest
timeout-minutes: 30
# Skip on forked PRs — secrets are not available in forks
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository

env:
# The E2E user is upserted into the dev DB by the seed step below,
# so both email and password are driven entirely by repo secrets.
TEST_EMAIL: ${{ secrets.E2E_TEST_EMAIL }}
TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }}

steps:
- name: Verify E2E secrets are configured
run: |
missing=()
[ -z "${TEST_EMAIL:-}" ] && missing+=("E2E_TEST_EMAIL")
[ -z "${TEST_PASSWORD:-}" ] && missing+=("E2E_TEST_PASSWORD")
[ -z "${NEON_DATABASE_URL:-}" ] && missing+=("NEON_DEV_DATABASE_URL")
[ -z "${EXPO_PUBLIC_API_URL:-}" ] && missing+=("EXPO_PUBLIC_API_URL")
if [ ${#missing[@]} -gt 0 ]; then
echo "::error::Required E2E secrets missing: ${missing[*]}"
echo "::error::Set them via: gh secret set <NAME> --repo PackRat-AI/PackRat"
exit 1
fi
env:
NEON_DATABASE_URL: ${{ secrets.NEON_DEV_DATABASE_URL }}
EXPO_PUBLIC_API_URL: ${{ secrets.EXPO_PUBLIC_API_URL }}

- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
cache: true

- name: Cache node_modules
uses: actions/cache@v4
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: |
node-modules-${{ runner.os }}-

- name: Install dependencies
env:
PACKRAT_NATIVEWIND_UI_GITHUB_TOKEN: ${{ secrets.PACKRAT_NATIVEWIND_UI_GITHUB_TOKEN }}
run: bun install --frozen-lockfile

- name: Install Playwright browsers
run: bunx playwright install chromium --with-deps
Comment on lines +58 to +80

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

Pin third-party GitHub Actions to commit SHAs.

Third-party actions are using mutable tags (@v6, @v2, @v4) instead of pinned commit SHAs. This creates supply-chain attack risk.

As per coding guidelines: "Pin third-party actions to a full commit SHA (not a mutable tag) to prevent supply-chain attacks."

Pin these actions to specific commit SHAs:

  • Line 58: actions/checkout@v6
  • Line 61: oven-sh/setup-bun@v2
  • Line 67: actions/cache@v4
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/web-e2e-tests.yml around lines 58 - 80, Replace the
mutable action tags with their pinned commit SHAs to eliminate supply-chain
risk: update the uses entries for actions/checkout (currently
"actions/checkout@v6"), oven-sh/setup-bun (currently "oven-sh/setup-bun@v2"),
and actions/cache (currently "actions/cache@v4") to use the corresponding full
commit SHAs instead of the version tags; locate and edit the uses lines for
those identifiers in the workflow, verify each SHA is the intended release
commit for that action, and keep the rest of the step configuration (inputs,
env, run commands) unchanged.


- name: Build Expo web app
working-directory: apps/expo
env:
EXPO_PUBLIC_API_URL: ${{ secrets.EXPO_PUBLIC_API_URL }}
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID: ${{ secrets.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID }}
EXPO_PUBLIC_R2_PUBLIC_URL: ${{ secrets.EXPO_PUBLIC_R2_PUBLIC_URL }}
EXPO_PUBLIC_SENTRY_DSN: ${{ secrets.EXPO_PUBLIC_SENTRY_DSN }}
EXPO_PUBLIC_GOOGLE_MAPS_API_KEY: ${{ secrets.EXPO_PUBLIC_GOOGLE_MAPS_API_KEY }}
run: bunx expo export -p web --output-dir dist

- name: Seed E2E test user in dev DB
run: bun run --filter @packrat/api db:seed:e2e-user
env:
NEON_DATABASE_URL: ${{ secrets.NEON_DEV_DATABASE_URL }}
E2E_TEST_EMAIL: ${{ env.TEST_EMAIL }}
E2E_TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }}

- name: Serve web app (SPA mode, port 8081)
working-directory: apps/expo
# -s routes all 404s to index.html for client-side routing
run: npx serve -s dist -l 8081 &

- name: Wait for web server
run: |
for i in $(seq 1 30); do
curl -sf http://localhost:8081 && echo "Server ready" && break
echo "Waiting... ($i/30)"
sleep 2
done
Comment on lines +104 to +110

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

Fail fast if the web server never comes up.

The loop logs retries but does not exit non-zero after the final attempt, so failures surface later with noisier symptoms.

Suggested fix
       - name: Wait for web server
         run: |
+          ready=0
           for i in $(seq 1 30); do
-            curl -sf http://localhost:8081 && echo "Server ready" && break
+            if curl -sf http://localhost:8081; then
+              echo "Server ready"
+              ready=1
+              break
+            fi
             echo "Waiting... ($i/30)"
             sleep 2
           done
+          [ "$ready" -eq 1 ] || { echo "Server failed to start"; exit 1; }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/web-e2e-tests.yml around lines 104 - 110, The "Wait for
web server" step's retry loop never returns non-zero on ultimate failure; update
that step so it fails fast when the server never becomes reachable by detecting
the loop's unsuccessful completion and calling exit 1 (or use a curl check that
short-circuits with || exit 1). Locate the shell loop in the "Wait for web
server" step and either add a success flag and exit 1 if not set after the for
loop, or replace the final check with a conditional that exits non-zero when
curl never succeeds.


- name: Run Playwright E2E tests
working-directory: apps/expo
env:
BASE_URL: http://localhost:8081
API_URL: ${{ secrets.EXPO_PUBLIC_API_URL }}
NEON_DATABASE_URL: ${{ secrets.NEON_DEV_DATABASE_URL }}
CI: "true"
run: bun test:web

- name: Upload Playwright report on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: playwright-report
path: apps/expo/playwright-report/
retention-days: 7

- name: Upload Playwright traces on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: playwright-traces
path: apps/expo/test-results/
retention-days: 7
Comment on lines +121 to +135

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

Pin artifact upload actions to commit SHAs.

The actions/upload-artifact@v7 actions (lines 123, 131) use mutable tags. Pin them to commit SHAs per the security guideline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/web-e2e-tests.yml around lines 121 - 135, The workflow
uses mutable tags for the artifact upload steps ("Upload Playwright report on
failure" and "Upload Playwright traces on failure") via uses:
actions/upload-artifact@v7; replace those mutable tags with the corresponding
pinned commit SHAs (e.g., actions/upload-artifact@<commit-sha>) so both uses
entries reference an exact commit SHA instead of `@v7`, keeping the step names and
inputs (name, path, retention-days) unchanged.

7 changes: 7 additions & 0 deletions apps/expo/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,10 @@ android
.env
.env.*
!.env.example

# Playwright E2E — cached auth tokens (written by globalSetup, contain real credentials)
playwright/.auth-tokens.json
playwright/playwright-report/
playwright/test-results/
playwright-report/
test-results/
33 changes: 33 additions & 0 deletions apps/expo/app/(app)/(tabs)/_layout.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { featureFlags } from 'expo-app/config';
import { useTranslation } from 'expo-app/lib/hooks/useTranslation';
import { Tabs } from 'expo-router';

/**
* Web version of the tabs layout.
* Replaces NativeTabs (expo-router/unstable-native-tabs) with standard Expo Router Tabs.
* NativeTabs uses native UITabBarController and cannot run on web.
* Metro automatically picks this file over _layout.tsx for web builds.
*/
export default function TabLayout() {
const { t } = useTranslation();

return (
<Tabs screenOptions={{ headerShown: false }}>
<Tabs.Screen name="(home)" options={{ title: t('navigation.dashboard') }} />
<Tabs.Screen name="packs" options={{ title: t('navigation.packs') }} />
<Tabs.Screen
name="feed"
options={{ title: t('navigation.feed'), href: featureFlags.enableFeed ? undefined : null }}
/>
<Tabs.Screen
name="trips"
options={{
title: t('navigation.trips'),
href: featureFlags.enableTrips ? undefined : null,
}}
/>
<Tabs.Screen name="catalog" options={{ title: t('navigation.catalog') }} />
<Tabs.Screen name="profile" options={{ title: t('navigation.profile') }} />
</Tabs>
);
}
3 changes: 3 additions & 0 deletions apps/expo/app/(app)/(tabs)/profile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ function Profile() {
{
id: 'name',
title: t('common.name'),
testID: testIds.profile.nameEditBtn,
onPress: () => router.push('/(app)/(tabs)/profile/name'),
...(Platform.OS === 'ios' ? { value: displayName } : { subTitle: displayName }),
},
Expand Down Expand Up @@ -146,6 +147,7 @@ function Item({ info }: { info: ListRenderItemInfo<DataItem> }) {
return (
<ListItem
titleClassName="text-lg"
testID={info.item.testID}
onPress={info.item.onPress}
rightView={
<View className="flex-1 flex-row items-center gap-0.5 px-2">
Expand Down Expand Up @@ -331,4 +333,5 @@ type DataItem =
value?: string;
subTitle?: string;
onPress?: () => void;
testID?: string;
};
2 changes: 1 addition & 1 deletion apps/expo/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import '../polyfills';

import { ThemeProvider as NavThemeProvider } from '@react-navigation/native';
import 'expo-dev-client';
import 'expo-app/lib/devClient';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import '../global.css';
Expand Down
55 changes: 55 additions & 0 deletions apps/expo/app/_layout.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import '../polyfills';

import { ThemeProvider as NavThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import '../global.css';

import { Alert, type AlertMethods } from '@packrat-ai/nativewindui';

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

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

This file imports NativeWindUI directly from @packrat-ai/nativewindui, while the rest of the app uses the wrapper re-export @packrat/ui/nativewindui. For consistency (and to keep the option of swapping/re-wrapping the UI lib in one place), prefer importing from @packrat/ui/nativewindui here too.

Suggested change
import { Alert, type AlertMethods } from '@packrat-ai/nativewindui';
import { Alert, type AlertMethods } from '@packrat/ui/nativewindui';

Copilot uses AI. Check for mistakes.
import { useColorScheme, useInitialAndroidBarSync } from 'expo-app/lib/hooks/useColorScheme';
import { Providers } from 'expo-app/providers';
import { NAV_THEME } from 'expo-app/theme';
import { type RefObject, useRef } from 'react';

/**
* Web version of the root layout.
* Removes native-only imports:
* - expo-dev-client (not needed on web)
* - @sentry/react-native (use @sentry/nextjs / @sentry/browser for web instead)
* Metro automatically picks this file over _layout.tsx for web builds.
*/

export { ErrorBoundary } from 'expo-router';

export let appAlert: RefObject<AlertMethods | null>;

function RootLayout() {
useInitialAndroidBarSync();

appAlert = useRef<AlertMethods>(null);

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

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

React.RefObject is used but React isn’t imported, which will break TS compilation. Prefer importing the type (import type { RefObject } from 'react') and declaring export let appAlert: RefObject<AlertMethods | null>; (or import type React if you want to keep React.RefObject).

Copilot uses AI. Check for mistakes.

const { colorScheme, isDarkColorScheme } = useColorScheme();

return (
<Providers>
<StatusBar
key={`root-status-bar-${isDarkColorScheme ? 'light' : 'dark'}`}
style={isDarkColorScheme ? 'light' : 'dark'}
/>
<NavThemeProvider value={NAV_THEME[colorScheme]}>
<Stack screenOptions={SCREEN_OPTIONS}>
<Stack.Screen name="(app)" />
<Stack.Screen name="auth" />
</Stack>
<Alert title="" buttons={[]} ref={appAlert} />
</NavThemeProvider>
</Providers>
);
}

export default RootLayout;

const SCREEN_OPTIONS = {
headerShown: false,
animation: 'ios_from_right',
} as const;
36 changes: 36 additions & 0 deletions apps/expo/atoms/atomWithSecureStorage.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { isFunction } from '@packrat/guards';
import { atom } from 'jotai';

/**
* Web (localStorage) equivalent of atomWithSecureStorage.
* Note: localStorage is NOT cryptographically secure. This is a functional
* fallback for web; sensitive flows should use server-side sessions on web.
* Metro automatically picks this file over atomWithSecureStorage.ts for web builds.
*/
export const atomWithSecureStorage = <T>(key: string, initialValue: T) => {
const baseAtom = atom(initialValue);

baseAtom.onMount = (setValue) => {
try {
const item = localStorage.getItem(key);
setValue(item !== null ? JSON.parse(item) : initialValue);
} catch {
setValue(initialValue);
}
};

const derivedAtom = atom(
(get) => get(baseAtom),
(get, set, update: T | ((prev: T) => T)) => {
const nextValue = isFunction(update) ? (update as (prev: T) => T)(get(baseAtom)) : update;
set(baseAtom, nextValue);
try {
localStorage.setItem(key, JSON.stringify(nextValue));
} catch {
// Ignore storage errors
}
},
);

return derivedAtom;
};
25 changes: 25 additions & 0 deletions apps/expo/features/ai/lib/localModelManager.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Web no-op stub for localModelManager.
* On-device AI models (llama.rn, @react-native-ai/apple) are native-only.
* Metro automatically picks this file over localModelManager.ts for web builds.
*/

export function isAppleIntelligenceAvailable(): boolean {
return false;
}

export function getLocalModel(): null {
return null;
}
Comment on lines +11 to +13

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find and compare native localModelManager implementation

# Search for the native implementation file
fd -e ts -e tsx 'localModelManager' apps/expo/features/ai/lib/ --exec cat {}

Repository: PackRat-AI/PackRat

Length of output: 9743


Fix getLocalModel() return type to match native implementation.

The web stub declares null as the return type, but the native implementation returns LlamaLanguageModel | null. Change the web stub to:

import { type LlamaLanguageModel } from '@react-native-ai/llama';

export function getLocalModel(): LlamaLanguageModel | null {
  return null;
}

This ensures type safety across platform-specific builds—consuming code will expect the same type on web and native.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/expo/features/ai/lib/localModelManager.web.ts` around lines 11 - 13,
Update the web stub getLocalModel to match the native signature: import the
LlamaLanguageModel type from '@react-native-ai/llama' and change the function
return type from null to "LlamaLanguageModel | null" while still returning null
(i.e., keep the implementation but adjust the type declaration for
getLocalModel).


export async function isLlamaModelDownloaded(): Promise<boolean> {
return false;
}

export async function initLocalModel(): Promise<void> {}

export async function downloadLocalModel(): Promise<void> {}

export async function cancelLocalModelDownload(): Promise<void> {}

export async function deleteLocalModel(): Promise<void> {}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function HorizontalCatalogItemCard({ item, ...restProps }: HorizontalCata
onPress={isSelectable ? () => restProps.onSelect(item) : restProps.onPress}
>
<View
testID={`catalog-item-card-${item.id}`}
className={`rounded-lg flex-row gap-3 border p-4 bg-red
Comment on lines +39 to 40

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

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

This testID string duplicates the centralized testIds.items.catalogCard(id) helper added in lib/testIds.ts. Using the helper keeps IDs consistent across components/tests and avoids hand-interpolated strings drifting over time.

Copilot uses AI. Check for mistakes.
${
isSelectable && restProps.selected
Expand Down
5 changes: 2 additions & 3 deletions apps/expo/features/packs/hooks/usePackOwnershipCheck.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { use$ } from '@legendapp/state/react';
import { obs } from 'expo-app/lib/store';
import { packsStore } from '../store';

export function usePackOwnershipCheck(id: string) {
const pack = obs(packsStore, id).peek();

return !!pack;
return use$(() => !!obs(packsStore, id).get());
}
Loading
Loading