Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
1a7924e
feat(web): web support MVP — platform shims, Playwright e2e, OTP fix
andrew-bierman May 1, 2026
fadfe11
chore(web): remove unnecessary web layout overrides
andrew-bierman May 1, 2026
3230625
fix(web-e2e): drive login via UI instead of direct API calls
andrew-bierman May 1, 2026
c486fe9
fix: use __DEV__ instead of process.env.NODE_ENV in _layout.tsx
andrew-bierman May 1, 2026
a3861d3
ci(web-e2e): drop DB seed step — E2E user is a permanent dev account
andrew-bierman May 1, 2026
27ebe83
fix(web-e2e): navigate directly to /auth in globalSetup, increase tim…
andrew-bierman May 1, 2026
30ab72b
debug(web-e2e): screenshot + console logs in globalSetup to diagnose …
andrew-bierman May 1, 2026
aa25db4
fix(web-e2e): navigate directly to /auth/(login) instead of clicking …
andrew-bierman May 1, 2026
54c5d56
debug(web-e2e): screenshot + testID dump to diagnose CI auth render
andrew-bierman May 1, 2026
65bb942
fix(web-e2e): fix env var validation crash in CI static export
andrew-bierman May 1, 2026
3d15337
debug(web-e2e): capture post-login screenshot and network log for aut…
andrew-bierman May 1, 2026
4808c3a
fix(web-e2e): replace stale EXPO_PUBLIC_API_URL secret with dev/prod …
andrew-bierman May 1, 2026
5fca3ff
fix(web-e2e): use E2E_EXPO_PUBLIC_API_URL secret (E2E_ prefix convent…
andrew-bierman May 1, 2026
b803cc8
fix(web): constrain auth logo Image size on web
andrew-bierman May 1, 2026
32f45ec
chore(e2e): remove debug instrumentation from globalSetup
andrew-bierman May 1, 2026
e35e18b
fix(web): add Platform.select style fallback to all Image components
andrew-bierman May 1, 2026
e7c1108
fix(web): proper BackHandler stub + UI auth flow in globalSetup
andrew-bierman May 1, 2026
bedf863
fix(web): drop ts-ignore from BackHandler patch — not needed in .js file
andrew-bierman May 1, 2026
03c45d4
refactor(web): convert polyfills.js → polyfills.ts
andrew-bierman May 1, 2026
09c7cdd
suppress RNW dev-mode text node false positives in polyfills
andrew-bierman May 1, 2026
7bf99ad
fix(e2e): add testIDs and fix all failing web E2E tests
andrew-bierman May 1, 2026
bfeb63f
fix(e2e): fix add-from-catalog, item delete, and web alert
andrew-bierman May 1, 2026
c30dd4b
fix(e2e): resolve remaining 9 failing web E2E tests
andrew-bierman May 1, 2026
333e207
fix(e2e): update packs:name-input → pack-name-input after testIds merge
andrew-bierman May 1, 2026
b807776
fix(types): add missing TestIds import to CatalogItemDetailScreen and…
andrew-bierman May 1, 2026
46ed531
fix(types): remove searchBar testID prop not in published nativewindu…
andrew-bierman May 1, 2026
060b46c
fix(types): restore catalog:search-btn testID via safe-cast pending n…
andrew-bierman May 1, 2026
93286d3
fix(e2e): fix waitForResponse timing, add missing test timeouts, patc…
andrew-bierman May 1, 2026
31999a4
fix: remove location guard blocking trip sync, fix test selectors and…
andrew-bierman May 1, 2026
1fb3e85
fix(e2e): fix Alert imperative API and Dates section selector for web…
andrew-bierman May 1, 2026
a5f7620
chore: remove Alert patch (fix moves to nativewindui PR #14)
andrew-bierman May 1, 2026
b5611db
fix(web-e2e): use Alert.alert (window.confirm on web) for item and tr…
andrew-bierman May 1, 2026
8f42c6f
chore: upgrade @packrat-ai/nativewindui to 2.0.6, remove 2.0.5 patch
andrew-bierman May 1, 2026
820374a
fix(web-e2e): use window.confirm on web for item and trip delete
andrew-bierman May 2, 2026
7db46f1
fix(web-e2e): register dialog handler before trip delete button click
andrew-bierman May 2, 2026
d81df36
fix(trips): add delete handler to syncedCrud and fix delete test
andrew-bierman May 2, 2026
ae5e815
fix(trips): soft-delete via PUT with deleted:true, intercept PUT in test
andrew-bierman May 2, 2026
9f9b8ac
fix(web-e2e): await PUT before page.goto for delete trip assertion
andrew-bierman May 2, 2026
6380dca
fix(web-e2e): scope delete PUT filter to specific tripId, check respo…
andrew-bierman May 2, 2026
beedbd1
fix(trips): hard-delete via DELETE endpoint in useDeleteTrip, interce…
andrew-bierman May 2, 2026
033246e
fix(web-e2e): navigate directly to trip detail before delete to avoid…
andrew-bierman May 2, 2026
02fe6fb
fix(trips): sort list newest-first and use replace when no back history
andrew-bierman May 2, 2026
37e3447
fix(trips): add safe-cast annotation for expo-router Href
andrew-bierman May 2, 2026
5edea59
fix(web-e2e): gate email-input wrapper testID to non-web to fix Playw…
andrew-bierman May 7, 2026
839b484
fix(web-e2e): restore pack-name-input/pack-description-input testIDs
andrew-bierman May 7, 2026
418817e
fix(web-e2e): make catalog search assertion resilient to live data
andrew-bierman May 7, 2026
aa534d5
fix(biome): remove unused imports and duplicate testID prop
andrew-bierman May 12, 2026
0648f97
fix(biome): organize imports in useAuthActions
andrew-bierman May 13, 2026
812fd90
fix(web-e2e): use waitForResponse + explicit goto to fix globalSetup …
andrew-bierman May 13, 2026
f358a58
fix(web-e2e): submit login via Enter on password field, not button click
andrew-bierman May 13, 2026
fa94f67
fix(web-e2e): target input element directly and use page.keyboard for…
andrew-bierman May 13, 2026
8fd42f9
fix(e2e): remove locator('input') wrapper and guard web button submit
andrew-bierman May 13, 2026
9eb14aa
:whale: chore(api): add local-neon-http-proxy on :4444 to test stack
andrew-bierman May 23, 2026
b732454
:bug: fix(api): wire local dev to db.localtest.me proxy + CORS the au…
andrew-bierman May 23, 2026
ddf005f
:closed_lock_with_key: fix(api/auth): trust localhost origins when AP…
andrew-bierman May 23, 2026
a490672
:seedling: fix(api/seed): create account row alongside users so Bette…
andrew-bierman May 23, 2026
e6fe8b0
:seedling: feat(api/seed): add catalog seed for E2E (10 hand-picked i…
andrew-bierman May 23, 2026
19808f1
:white_check_mark: test(e2e): chrome channel + headed-local + paralle…
andrew-bierman May 23, 2026
2b684d0
:adhesive_bandage: test(e2e): settings regex tolerates "(Dev)" build …
andrew-bierman May 23, 2026
a2079f8
:label: fix(nativewindui): forward searchBar.testID to the search-ico…
andrew-bierman May 23, 2026
acd5cb4
:bug: fix(expo/ai-chat): resolve auth token at send-time so first sen…
andrew-bierman May 28, 2026
194b1da
:closed_lock_with_key: fix(api/auth): use wildcard 'http://localhost:…
andrew-bierman May 28, 2026
b3696b2
:wrench: chore(api): make compose host ports overrideable via env vars
andrew-bierman May 28, 2026
44ec618
:lock: test(e2e): default to headless everywhere; opt into headed wit…
andrew-bierman May 28, 2026
96c3783
:wrench: chore(env): use typed env shims for OPENAI_API_KEY + NEON_LO…
andrew-bierman May 28, 2026
eb4a368
:label: fix(expo/ai-chat): widen headers function return to Record<st…
andrew-bierman May 30, 2026
cacb3f7
Merge pull request #2511 from PackRat-AI/test/playwright-web-pass
andrew-bierman May 30, 2026
48815c4
:arrow_up: chore(deps): consume @packrat-ai/nativewindui 2.0.6, drop …
andrew-bierman Jun 1, 2026
f2723d9
Merge pull request #2528 from PackRat-AI/chore/bump-nativewindui-2.0.6
andrew-bierman Jun 1, 2026
62e108d
Merge branch 'development' into feat/web-e2e-fix
andrew-bierman Jun 1, 2026
ab6328e
🐛 fix(web-e2e): unblock playwright suite after development merge
andrew-bierman Jun 1, 2026
006bcdd
🐛 fix(web-e2e): wire ActionSheet on web + restore trips:dates-section…
andrew-bierman Jun 1, 2026
522c805
🐛 fix(web-e2e): close the last two web e2e gaps — AI chat 401 + trip …
andrew-bierman Jun 1, 2026
db3c627
🔀 Merge origin/development into feat/web-e2e-fix
andrew-bierman Jun 1, 2026
d93a23e
🔥 chore: drop orphan @packrat-ai/nativewindui 2.0.3 patch
andrew-bierman Jun 1, 2026
ab29080
🐛 fix(web-e2e): restore the 4 tests development's merge regressed to …
andrew-bierman Jun 1, 2026
206e6ba
🚨 chore: clear pre-push lint blockers
andrew-bierman Jun 1, 2026
f877822
♻️ refactor(web-e2e): address pre-review audit feedback
andrew-bierman Jun 1, 2026
7b23e15
🚨 fix(ci): commit nativewindui lockfile bump + restore auth storage w…
andrew-bierman Jun 1, 2026
c17727c
🐛 fix(web-e2e): address Copilot + CodeRabbit review on #2537
andrew-bierman Jun 1, 2026
0b7732a
🔀 Merge origin/development into feat/web-e2e-fix
andrew-bierman Jun 1, 2026
c760b79
🐛 fix(ci/web-e2e): bust stale node_modules cache before frozen install
andrew-bierman Jun 1, 2026
51e183b
🐛 fix(ci/web-e2e): adopt #2541's local-stack workflow (+ keep cache k…
andrew-bierman Jun 1, 2026
30fc510
Merge remote-tracking branch 'origin/development' into feat/web-e2e-fix
andrew-bierman Jun 6, 2026
363bac7
🐛 fix(expo): restore typed e2e branch after development merge
andrew-bierman Jun 6, 2026
152d8f6
🐛 fix(ci): provide valid R2 URL for web e2e
andrew-bierman Jun 6, 2026
b909b88
🐛 fix(ci): use deterministic chat stub for web e2e
andrew-bierman Jun 6, 2026
eca6d2a
🐛 fix(web-e2e): align specs with current app surface
andrew-bierman Jun 6, 2026
4f282d5
🐛 fix(web-e2e): seed catalog and align trip selectors
andrew-bierman Jun 6, 2026
274175b
🐛 fix(web-e2e): stub catalog seed embeddings
andrew-bierman Jun 6, 2026
d91ee42
🐛 fix(web-e2e): stub pack item embeddings
andrew-bierman Jun 6, 2026
226bb1c
✅ test(api): cover e2e embedding stub
andrew-bierman Jun 6, 2026
0c3909c
🐛 fix(web-e2e): align pack delete assertions
andrew-bierman Jun 6, 2026
164256f
🐛 fix(web-e2e): use native confirm for pack delete
andrew-bierman Jun 6, 2026
39c2c20
🐛 fix(android-e2e): target pack form fields by test id
andrew-bierman Jun 6, 2026
751c5ef
🛡️ harden e2e seed and delete fallback
andrew-bierman Jun 6, 2026
9b11b51
🔀 merge development into web e2e fixes
andrew-bierman Jun 6, 2026
b3a5856
🐛 preserve AI chat request body in web e2e
andrew-bierman Jun 6, 2026
f7d5060
🐛 restore web action sheet provider
andrew-bierman Jun 6, 2026
e37e97e
🐛 reset Android app state before Maestro
andrew-bierman Jun 6, 2026
5a3006d
🐛 dedupe web session token lookups
andrew-bierman Jun 6, 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
5 changes: 4 additions & 1 deletion .github/scripts/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ const apiFileContent = envFileContent
.split('\n')
.map((line) => {
if (line.startsWith('ENVIRONMENT=')) {
return 'ENVIRONMENT=dev';
// apiEnvSchema enforces z.enum(['development', 'production']) and
// throws on parse failure — keep the canonical 'development' value
// rather than the short 'dev' the script previously hardcoded.
return 'ENVIRONMENT=development';
}
return line;
})
Expand Down
1 change: 1 addition & 0 deletions .github/scripts/run-android-maestro-ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ await waitForReadyDevice();
await run(['adb', 'install', '-r', '-d', 'apps/expo/build/PackRat.apk']);
await waitForReadyDevice();
await run(['adb', 'shell', 'pm', 'path', appId]);
await run(['adb', 'shell', 'pm', 'clear', appId]);
await run(['adb', 'shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1']);
await waitForReadyDevice();
await run(['adb', 'shell', 'input', 'keyevent', 'KEYCODE_BACK'], { allowFailure: true });
Expand Down
16 changes: 12 additions & 4 deletions .github/workflows/web-e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,16 @@ jobs:
- name: Cache node_modules
uses: actions/cache@v4
with:
# Hash apps/expo/package.json too: bun's --frozen-lockfile happily
# installs on top of a partial cache, so when only the workspace
# package.json changes (e.g. nativewindui pin), the lockfile hash
# alone misses the bust and partial restore-keys hands us a stale
# tree (most recent symptom: @babel/helper-compilation-targets'
# nested lru-cache@5 disappears, breaking expo export).
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: |
node-modules-${{ runner.os }}-
key: node-modules-${{ runner.os }}-${{ hashFiles('bun.lock', 'apps/expo/package.json', 'package.json') }}
# Drop the partial-prefix fallback. A miss should be a clean
# install, not a slow drift toward an inconsistent tree.

- name: Install dependencies
env:
Expand All @@ -115,14 +121,16 @@ jobs:
# Node-side scripts (migrate/seed) can't use the worker-only
# db.localtest.me neon-proxy routing, so they hit raw Postgres on :5433.
# The worker (.dev.vars below) uses db.localtest.me → proxy :4444.
- name: Run migrations + seed e2e user (local DB)
- name: Run migrations + seed e2e data (local DB)
env:
NEON_DATABASE_URL: postgres://test_user:test_password@localhost:5433/packrat_test
E2E_TEST_EMAIL: ${{ env.TEST_EMAIL }}
E2E_TEST_PASSWORD: ${{ env.TEST_PASSWORD }}
OPENAI_API_KEY: sk-e2e-stub-placeholder
run: |
bun run --filter @packrat/api db:migrate
bun run --filter @packrat/api db:seed:e2e-user
bun run --filter @packrat/api db:seed:e2e-catalog
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# ── Write the worker's .dev.vars: real URLs + dummy-but-valid stand-ins ──
# env-validation.ts requires ~25 keys to boot; sign-in only exercises the
Expand Down
7 changes: 5 additions & 2 deletions apps/expo/app/(app)/ai-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,10 @@ export default function AIChat() {
trigger,
messageId,
}) => {
const authToken = token ?? (await getStoredSessionToken());
// Pull the live web token at request time. useChat captures the
// transport at mount, so relying only on the hook token can go stale.
const { data } = await authClient.getSession();
const authToken = data?.session?.token ?? token ?? (await getStoredSessionToken());
return {
api,
credentials: credentials ?? 'include',
Expand All @@ -245,7 +248,7 @@ export default function AIChat() {
}),
transportKey: 'remote',
};
}, [aiMode, isLocalReady, modelStatus, token, tools, userId]);
}, [aiMode, isLocalReady, modelStatus, tools, userId]);

// transportKey forces useChat to remount when the transport type switches,
// since useChat captures the transport reference on mount and won't update it.
Expand Down
2 changes: 1 addition & 1 deletion apps/expo/app/(app)/trail-conditions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default function TrailConditionsScreen() {
const filteredReports = useMemo(() => {
if (!reports) return [];
if (selectedSurface === 'all') return reports;
return reports.filter((r) => r.surface === selectedSurface);
return reports.filter((r: TrailConditionReport) => r.surface === selectedSurface);
}, [reports, selectedSurface]);

if (!featureFlags.enableTrailConditions) return null;
Expand Down
2 changes: 2 additions & 0 deletions apps/expo/features/ai/hooks/useReportedContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type ReportedContentResponse = {
}>;
};

export type ReportedContentItem = ReportedContentResponse['reportedItems'][number];

type ReportedContentCount = {
count: number;
total: number;
Expand Down
4 changes: 2 additions & 2 deletions apps/expo/features/ai/screens/ReportedContentScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme';
import { useTranslation } from 'expo-app/lib/hooks/useTranslation';
import { useState } from 'react';
import { ActivityIndicator, FlatList, TouchableOpacity, View } from 'react-native';
import { useReportedContent } from '../hooks/useReportedContent';
import { type ReportedContentItem, useReportedContent } from '../hooks/useReportedContent';
import { useUpdateReportStatus } from '../hooks/useUpdateReportStatus';
import { reportReasonTranslationKeys } from '../lib/reportReasons';

Expand All @@ -21,7 +21,7 @@ export default function ReportedContentScreen() {
const { data, isLoading, error } = useReportedContent();
const updateMutation = useUpdateReportStatus();

const filteredData = data?.filter((item) => {
const filteredData = data?.filter((item: ReportedContentItem) => {
if (selectedFilter === 'all') return true;
return item.status === selectedFilter;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export function CatalogItemDetailScreen() {
{t('catalog.categoriesLabel')}
</Text>
<View className="flex-row flex-wrap gap-2">
{item.categories.map((category) => (
{item.categories.map((category: string) => (
<Chip key={category} textClassName="text-xs" variant="outline">
<Text> {decodeHtmlEntities(category)}</Text>
</Chip>
Expand Down Expand Up @@ -212,7 +212,7 @@ export function CatalogItemDetailScreen() {
{Object.entries(item.techs).map(([key, value]) => (
<View key={key} className="gap-1">
<Text className="text-xs text-muted-foreground uppercase">{key}</Text>
<Text className="font-medium text-foreground">{value}</Text>
<Text className="font-medium text-foreground">{String(value)}</Text>
</View>
))}
</View>
Expand Down
8 changes: 7 additions & 1 deletion apps/expo/features/packs/screens/PackListScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ type FilterOption = {
value: PackCategory | 'all';
};

type PackListRow = {
id: string;
name: string;
category: PackCategory | null;
};

function CreatePackIconButton() {
const { colors } = useColorScheme();
const { t } = useTranslation();
Expand Down Expand Up @@ -90,7 +96,7 @@ export function PackListScreen() {

const packs = selectedTypeIndex === USER_PACKS_INDEX ? userPacks : allPacksQuery.data;

const filteredPacks = packs?.filter((pack) => {
const filteredPacks = packs?.filter((pack: PackListRow) => {
const matchesSearch = pack.name.toLowerCase().includes(searchValue.toLowerCase());
const matchesCategory = activeFilter === 'all' || pack.category === activeFilter;
return matchesSearch && matchesCategory;
Expand Down
16 changes: 9 additions & 7 deletions apps/expo/features/packs/utils/getPackDetailOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ export function getPackDetailOptions(id: string) {

if (!isOwner) return null;

const confirmDelete = () => {
const deleteAndNavigate = () => {
deletePack(id);
if (router.canGoBack()) {
router.back();
}
};
const deleteAndNavigate = () => {
deletePack(id);
if (router.canGoBack()) {
router.back();
} else {
router.replace('/packs');
}
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const confirmDelete = () => {
if (Platform.OS === 'web') {
if (globalThis.confirm(t('packs.deletePackConfirm'))) {
deleteAndNavigate();
Expand Down
37 changes: 25 additions & 12 deletions apps/expo/features/trips/hooks/useDeleteTrip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,33 @@ import { useCallback } from 'react';

export function useDeleteTrip() {
const deleteTrip = useCallback(async (id: string) => {
// Optimistically mark as deleted in the local store so the UI updates immediately
// Optimistic local flip so the UI hides the trip immediately.
const tripObs = obs({ store: tripsStore, id });
if (tripObs) {
tripObs.deleted.set(true);
}
// Hard-delete on the server so the list GET won't return the trip on any subsequent reload
const { error } = await apiClient.trips({ tripId: id }).delete();
if (error) {
const err = new Error(String(error.value ?? 'Failed to delete trip'));
Sentry.captureException(err, {
tags: { feature: 'trips', action: 'deleteTrip' },
extra: { tripId: id, apiError: error.value, httpStatus: error.status },
if (tripObs) tripObs.deleted.set(true);

Sentry.addBreadcrumb({
category: 'trips',
message: 'Deleting trip',
level: 'info',
data: { tripId: id },
});

// Two failure modes: a transport rejection (network drop, fetch abort)
// and a non-2xx response (server still has the trip). In both we need
// to undo the optimistic flip, capture, and rethrow so the caller can
// surface it. 404 is treated as success since the trip is already gone.
try {
const response = await apiClient.trips({ tripId: id }).delete();
if (response.error && response.status !== 404) {
throw new Error(`Trip delete failed (${response.status})`);
}
Comment on lines +26 to +28

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

Don't wrap the API error in a new Error before capturing to Sentry.

Line 27 throws new Error(...) with only the status code, discarding the original response.error object. The catch block then sends this wrapper to Sentry, losing the original stack trace and error details.

As per coding guidelines: "Never wrap the original error in new Error(...) before passing to Sentry's captureException — this loses the original stack and context."

🛡️ Proposed fix
     try {
       const response = await apiClient.trips({ tripId: id }).delete();
       if (response.error && response.status !== 404) {
-        throw new Error(`Trip delete failed (${response.status})`);
+        throw response.error;
       }
     } catch (error) {
📝 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
if (response.error && response.status !== 404) {
throw new Error(`Trip delete failed (${response.status})`);
}
if (response.error && response.status !== 404) {
throw response.error;
}
🤖 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/trips/hooks/useDeleteTrip.ts` around lines 26 - 28, The
current code in the useDeleteTrip hook throws a new Error(`Trip delete failed
(${response.status})`) which discards the original response.error and its stack;
instead, when response.error exists rethrow the original error object (throw
response.error) so the catch block and Sentry.captureException receive the
original error and stack; if response.error is falsy but you still need to
signal failure, throw a new Error that includes status and any relevant response
info—ensure the catch block calls Sentry.captureException on the original error
object (or the new Error only when no original error exists).

} catch (error) {
if (tripObs) tripObs.deleted.set(false);
Sentry.captureException(error, {
tags: { feature: 'trips', action: 'delete' },
extra: { tripId: id },
});
throw err;
throw error;
}
}, []);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down
3 changes: 2 additions & 1 deletion apps/expo/features/trips/screens/TripDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { featureFlags } from 'expo-app/config';
import { SubmitConditionReportForm } from 'expo-app/features/trail-conditions/components/SubmitConditionReportForm';
import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme';
import { useTranslation } from 'expo-app/lib/hooks/useTranslation';
import { testIds } from 'expo-app/lib/testIds';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { useMemo, useState } from 'react';
import { Modal, ScrollView, View } from 'react-native';
Expand Down Expand Up @@ -74,7 +75,7 @@ export function TripDetailScreen() {
<Text className="mb-3 text-3xl font-bold text-foreground">{trip.name}</Text>

{/* Dates */}
<View className="mb-6">
<View className="mb-6" testID={testIds.trips.datesSection}>
<Text className="text-lg font-semibold text-foreground mb-2">{t('trips.dates')}</Text>
<View className="rounded-xl bg-card border border-border">
<View className="p-3 flex-row justify-between">
Expand Down
43 changes: 38 additions & 5 deletions apps/expo/lib/api/packrat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,53 @@ import { createApiClient } from '@packrat/api-client';
import { store } from 'expo-app/atoms/store';
import { needsReauthAtom } from 'expo-app/features/auth/atoms/authAtoms';
import { getApiBaseUrl } from 'expo-app/lib/api/getBaseUrl';
import { authClient, getStoredSessionToken } from 'expo-app/lib/auth-client';
import { authClient, parseSessionToken } from 'expo-app/lib/auth-client';
import * as SecureStore from 'expo-app/lib/secureStore';
import { Platform } from 'react-native';

const COOKIE_STORE_KEY = 'packrat_cookie';

// On web expoClient short-circuits and expo-secure-store is an empty stub, so
// we fall back to authClient.getSession(). Cache the token for 30s to keep
// apiClient's per-request token lookup from tripping Better Auth's prod
// rate limit. Invalidated by onNeedsReauth.
const WEB_TOKEN_CACHE_MS = 30_000;
let cachedToken: string | null = null;
let cachedTokenExpiresAt = 0;
let pendingTokenRequest: Promise<string | null> | null = null;

export const apiClient = createApiClient({
baseUrl: getApiBaseUrl(),
auth: {
// Read the token from SecureStore — no network call on every API request.
getAccessToken: getStoredSessionToken,
getAccessToken: async () => {
if (Platform.OS === 'web') {
const now = Date.now();
if (cachedToken && now < cachedTokenExpiresAt) return cachedToken;
pendingTokenRequest ??= authClient
.getSession()
.then(({ data }) => {
cachedToken = data?.session?.token ?? null;
cachedTokenExpiresAt = Date.now() + WEB_TOKEN_CACHE_MS;
return cachedToken;
})
.finally(() => {
pendingTokenRequest = null;
});
return pendingTokenRequest;
}
const cookieStr = await SecureStore.getItemAsync(COOKIE_STORE_KEY);
return parseSessionToken(cookieStr);
},
// Better Auth has no separate refresh-token endpoint; the 7-day session
// token is the only credential. Returning null here is intentional.
getRefreshToken: () => null,
onAccessTokenRefreshed: () => {},
onNeedsReauth: async () => {
// A 401 can be transient (e.g. the server briefly returned an error).
// Verify the session is actually gone before alarming the user.
cachedToken = null;
cachedTokenExpiresAt = 0;
pendingTokenRequest = null;
// 401 can be transient; verify the session is really gone before
// bouncing the user.
const { data } = await authClient.getSession();
if (data?.session) return;
store.set(needsReauthAtom, true);
Expand Down
1 change: 1 addition & 0 deletions apps/expo/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@
"viewDetails": "View Details",
"deleteTrip": "Delete trip?",
"deleteTripConfirmation": "Are you sure you want to delete this trip? This action cannot be undone.",
"deleteTripFailed": "Could not delete this trip. Please try again.",
"noTripsYetTitle": "No Trips Yet",
"createTripsToSee": "Create trips to start seeing your upcoming adventures!",
"gotIt": "Got it",
Expand Down
6 changes: 6 additions & 0 deletions apps/expo/mocks/react-native-community-datetimepicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ type Props = {
minimumDate?: Date;
maximumDate?: Date;
style?: unknown;
// RN's `testID` doesn't auto-flow into a plain <input> the way it does for
// react-native-web's <Text>/<View>. Forward it explicitly so Playwright's
// getByTestId can target the rendered date input.
testID?: string;
};

function toInputValue(date: Date, mode: Props['mode']): string {
Expand All @@ -25,6 +29,7 @@ export default function DateTimePicker({
onChange,
minimumDate,
maximumDate,
testID,
}: Props) {
const inputType = mode === 'time' ? 'time' : mode === 'datetime' ? 'datetime-local' : 'date';

Expand All @@ -39,6 +44,7 @@ export default function DateTimePicker({
return (
<input
type={inputType}
data-testid={testID}
defaultValue={toInputValue(value, mode)}
min={minimumDate ? toInputValue(minimumDate, mode) : undefined}
max={maximumDate ? toInputValue(maximumDate, mode) : undefined}
Expand Down
16 changes: 3 additions & 13 deletions apps/expo/playwright/tests/packs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,21 +119,11 @@ test.describe('Pack CRUD', () => {

page.on('dialog', (dialog) => dialog.accept());

const deleteSyncPromise = page.waitForResponse(
(r) =>
r.url().includes(`/api/packs/${packId}`) &&
(r.request().method() === 'PUT' || r.request().method() === 'PATCH'),
{ timeout: 20_000 },
);

const deleteButton = page.getByTestId(testIds.packs.deleteBtn);
await deleteButton.waitFor({ timeout: 15_000 });
await deleteButton.click();

const response = await deleteSyncPromise;
expect(response.ok()).toBeTruthy();

// After deletion has synced, go to list and confirm pack is gone.
// Pack deletion is a local soft-delete in the synced store; confirm the UI result.
await page.goto(`${BASE_URL}/packs`);
await expect(page.getByText(packName)).not.toBeVisible({ timeout: 10_000 });
});
Expand Down Expand Up @@ -213,8 +203,8 @@ test.describe('Item CRUD within a pack', () => {
if (await moreActionsButton.isVisible()) {
await moreActionsButton.click();
const deleteOption = page
.getByText(/delete/i)
.or(page.getByRole('menuitem', { name: /delete/i }))
.getByRole('menuitem', { name: /^Delete$/ })
.or(page.getByRole('button', { name: /^Delete$/ }))
.first();
await deleteOption.waitFor({ timeout: 5_000 });
await deleteOption.click();
Expand Down
Loading
Loading