Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
31a91f9
🐛 fix: disable Eden Treaty date reviver to preserve ISO strings
andrew-bierman Apr 30, 2026
0938f0f
🔧 feat(web): add Metro module stubs for native-only packages
andrew-bierman Apr 30, 2026
0ced8c1
🔧 config(web): switch Expo web output to SPA mode
andrew-bierman Apr 30, 2026
52294c9
🐛 fix(web): dark mode class, logo size, NativeWind color overrides
andrew-bierman Apr 30, 2026
9081fb4
♻️ refactor(web): replace per-module if-chains with WEB_STUBS lookup …
andrew-bierman Apr 30, 2026
18a3248
🐛 fix: auth logo oversizing and catalog schema review field mismatch
andrew-bierman Apr 30, 2026
2d3d2ae
✨ feat: enable EXPO_UNSTABLE_WEB_MODAL for web modal presentation
andrew-bierman Apr 30, 2026
25fea47
fix(catalog): normalize Details:[...] array description format from s…
andrew-bierman Apr 30, 2026
c982c54
fix: web compatibility — logout crash and route name mismatches
andrew-bierman Apr 30, 2026
bfede61
chore: remove unused getCatalogListOptions function
andrew-bierman Apr 30, 2026
bac123b
fix: web-safe token atom storage — prevent AI chat 401
andrew-bierman Apr 30, 2026
fbfda4b
fix: use isFunction guard in expo-sqlite-kv-store mock
andrew-bierman Apr 30, 2026
b466506
feat(web): proper platform-specific implementations for native-only l…
andrew-bierman May 1, 2026
cb80fa5
feat(web): react-leaflet maps + expo-file-system web stub
andrew-bierman May 1, 2026
593d502
chore: sort package.json deps alphabetically
andrew-bierman May 1, 2026
daf1541
refactor(web): remove redundant stubs and Platform.select style overr…
andrew-bierman May 1, 2026
ffeb9f6
fix: add height SharedValue to keyboard mock + web logo sizing
andrew-bierman May 1, 2026
3993535
fix(web): address PR review comments in web stubs
andrew-bierman May 1, 2026
f102dad
refactor(auth): replace makeKvStorage with platform-specific kvStorag…
andrew-bierman May 1, 2026
87cf6aa
Merge remote-tracking branch 'origin/main' into feat/web-implementation
andrew-bierman May 1, 2026
c09ad83
chore: merge development into feat/web-implementation
andrew-bierman May 1, 2026
4dcc57b
fix: bump @types/leaflet to ^1.9.21 to match apps/admin version
andrew-bierman May 1, 2026
a3b9d12
refactor(catalog): drop userName alias — not in DB schema, keep nulla…
andrew-bierman May 1, 2026
912906b
chore: remove camelCase userAvatar duplicate from CatalogItemSchema r…
andrew-bierman May 1, 2026
ea8ad30
fix(ci): fix TypeScript error and Vitest parse failure
andrew-bierman May 1, 2026
ad15222
fix: use 'in' operator for web detection instead of typeof
andrew-bierman May 1, 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
2 changes: 1 addition & 1 deletion apps/expo/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default (): ExpoConfig =>
scheme: 'packrat',
web: {
bundler: 'metro',
output: 'static',
output: 'single',
favicon: './assets/favicon.png',
},
plugins: [
Expand Down
11 changes: 2 additions & 9 deletions apps/expo/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ export default function AppLayout() {
options={getCatalogAddToPackItemDetailsOptions(t)}
/>
<Stack.Screen name="ai-chat" />
<Stack.Screen name="catalog/index" options={getCatalogListOptions(t)} />
<Stack.Screen name="catalog/[id]" options={getCatalogItemDetailOptions(t)} />
<Stack.Screen name="weather/index" options={{ headerShown: false }} />
<Stack.Screen
Expand Down Expand Up @@ -104,7 +103,7 @@ export default function AppLayout() {
/>

<Stack.Screen
name="current-pack"
name="current-pack/[id]"
options={{
headerShown: false,
presentation: 'modal',
Expand All @@ -128,7 +127,7 @@ export default function AppLayout() {
}}
/>
<Stack.Screen
name="weight-analysis"
name="weight-analysis/[id]"
options={{
headerShown: false,
presentation: 'modal',
Expand Down Expand Up @@ -351,12 +350,6 @@ const getItemEditOptions = (t: TranslationFunction) =>
title: t('common.edit'),
}) as const;

const getCatalogListOptions = (t: TranslationFunction) =>
({
title: t('catalog.itemsCatalog'),
headerLargeTitle: true,
}) as const;

const getCatalogItemDetailOptions = (t: TranslationFunction) =>
({
title: t('items.itemDetails'),
Expand Down
9 changes: 8 additions & 1 deletion apps/expo/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { userStore } from 'expo-app/features/auth/store';
import { useColorScheme, useInitialAndroidBarSync } from 'expo-app/lib/hooks/useColorScheme';
import { Providers } from 'expo-app/providers';
import { NAV_THEME } from 'expo-app/theme';
import { useRef } from 'react';
import { useEffect, useRef } from 'react';

Sentry.init({
dsn: clientEnvs.EXPO_PUBLIC_SENTRY_DSN,
Expand Down Expand Up @@ -41,6 +41,13 @@ function RootLayout() {

const { colorScheme, isDarkColorScheme } = useColorScheme();

// Sync NativeWind dark mode class to <html> on web (darkMode: 'class' requires it)
useEffect(() => {
if (typeof document !== 'undefined') {
document.documentElement.classList.toggle('dark', isDarkColorScheme);
}
}, [isDarkColorScheme]);

return (
<Providers>
<StatusBar
Expand Down
2 changes: 1 addition & 1 deletion apps/expo/app/auth/(create-account)/credentials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ export default function CredentialsScreen() {
<View className="items-center pb-1">
<Image
source={LOGO_SOURCE}
className="ios:h-12 ios:w-12 h-8 w-8 rounded-md"
className="ios:h-12 ios:w-12 web:h-8 web:w-8 h-8 w-8 rounded-md"
resizeMode="contain"
/>
<Text variant="title1" className="ios:font-bold pb-1 pt-4 text-center">
Expand Down
2 changes: 1 addition & 1 deletion apps/expo/app/auth/(create-account)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default function InfoScreen() {
<View className="items-center pb-1">
<Image
source={LOGO_SOURCE}
className="ios:h-12 ios:w-12 h-8 w-8 rounded-md"
className="ios:h-12 ios:w-12 web:h-8 web:w-8 h-8 w-8 rounded-md"
resizeMode="contain"
/>
<Text variant="title1" className="ios:font-bold pb-1 pt-4 text-center">
Expand Down
2 changes: 1 addition & 1 deletion apps/expo/app/auth/(login)/forgot-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default function ForgotPasswordScreen() {
<View className="items-center pb-1">
<Image
source={LOGO_SOURCE}
className="ios:h-12 ios:w-12 h-8 w-8 rounded-md"
className="ios:h-12 ios:w-12 web:h-8 web:w-8 h-8 w-8 rounded-md"
resizeMode="contain"
/>
<Text variant="title1" className="ios:font-bold pb-1 pt-4 text-center">
Expand Down
2 changes: 1 addition & 1 deletion apps/expo/app/auth/(login)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export default function LoginScreen() {
<View className="items-center pb-1">
<Image
source={LOGO_SOURCE}
className="ios:h-12 ios:w-12 h-8 w-8 rounded-md"
className="ios:h-12 ios:w-12 web:h-8 web:w-8 h-8 w-8 rounded-md"
resizeMode="contain"
/>
<Text variant="title1" className="ios:font-bold pb-1 pt-4 text-center">
Expand Down
2 changes: 1 addition & 1 deletion apps/expo/app/auth/(login)/reset-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export default function ResetPasswordScreen() {
<View className="items-center pb-1">
<Image
source={LOGO_SOURCE}
className="ios:h-12 ios:w-12 h-8 w-8 rounded-md"
className="ios:h-12 ios:w-12 web:h-8 web:w-8 h-8 w-8 rounded-md"
resizeMode="contain"
/>
<Text variant="title1" className="ios:font-bold pb-1 pt-4 text-center">
Expand Down
4 changes: 4 additions & 0 deletions apps/expo/app/auth/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { useTranslation } from 'expo-app/lib/hooks/useTranslation';
import { router, Stack } from 'expo-router';
import { Platform } from 'react-native';

export const unstable_settings = {
anchor: 'index',
};

export default function AuthLayout() {
const { t } = useTranslation();

Expand Down
2 changes: 1 addition & 1 deletion apps/expo/app/auth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default function AuthIndexScreen() {
<View className="items-center">
<Image
source={LOGO_SOURCE}
className="ios:h-12 ios:w-12 h-8 w-8 rounded-md"
className="ios:h-12 ios:w-12 web:h-8 web:w-8 h-8 w-8 rounded-md"
resizeMode="contain"
/>
</View>
Expand Down
20 changes: 3 additions & 17 deletions apps/expo/features/auth/atoms/authAtoms.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Storage from 'expo-sqlite/kv-store';
import kvStorage from 'expo-app/lib/kvStorage';
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';

Expand All @@ -12,23 +12,9 @@ export type User = {
};

// Token storage atom
export const tokenAtom = atomWithStorage<string | null>('access_token', null, {
getItem: (key) => Storage.getItemSync(key),
setItem: (key, value) => {
if (value === null) return Storage.removeItemSync(key);
return Storage.setItemSync(key, value);
},
removeItem: (key) => Storage.removeItemSync(key),
});
export const tokenAtom = atomWithStorage<string | null>('access_token', null, kvStorage);

export const refreshTokenAtom = atomWithStorage<string | null>('refresh_token', null, {
getItem: (key) => Storage.getItemSync(key),
setItem: (key, value) => {
if (value === null) return Storage.removeItemSync(key);
return Storage.setItemSync(key, value);
},
removeItem: (key) => Storage.removeItemSync(key),
});
export const refreshTokenAtom = atomWithStorage<string | null>('refresh_token', null, kvStorage);

// Loading state atom
export const isLoadingAtom = atom(false);
Expand Down
9 changes: 8 additions & 1 deletion apps/expo/features/auth/hooks/useAuthInit.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { clientEnvs } from '@packrat/env/expo-client';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { GoogleSignin } from '@react-native-google-signin/google-signin';
import { store } from 'expo-app/atoms/store';
import { router } from 'expo-router';
import Storage from 'expo-sqlite/kv-store';
import { useEffect, useState } from 'react';
import { Platform } from 'react-native';
import { tokenAtom } from '../atoms/authAtoms';
import { isAuthed } from '../store';

export function useAuthInit() {
Expand Down Expand Up @@ -37,7 +39,12 @@ export function useAuthInit() {

// If user has session or hasSkippedLogin before, continue to app
if (accessToken || hasSkippedLogin === 'true') {
if (accessToken) isAuthed.set(true);
if (accessToken) {
isAuthed.set(true);
// Hydrate tokenAtom so components (e.g. AI chat) get the correct
// token without relying on the sync SQLite read (unavailable on web).
store.set(tokenAtom, accessToken);
}
setIsLoading(false);
return;
} else {
Expand Down
5 changes: 2 additions & 3 deletions apps/expo/features/auth/store/user.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { observable, syncState } from '@legendapp/state';
import { observablePersistSqlite } from '@legendapp/state/persist-plugins/expo-sqlite';
import { syncObservable } from '@legendapp/state/sync';
import { syncedCrud } from '@legendapp/state/sync-plugins/crud';
import type { User } from 'expo-app/features/profile/types';
import Storage from 'expo-sqlite/kv-store';
import { persistPlugin } from 'expo-app/lib/persist-plugin';

export const userStore = observable<User | null>(null);

Expand All @@ -12,7 +11,7 @@ syncObservable(
syncedCrud({
persist: {
name: 'user',
plugin: observablePersistSqlite(Storage),
plugin: persistPlugin,
},
}),
);
Expand Down
3 changes: 2 additions & 1 deletion apps/expo/features/catalog/components/CatalogItemCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Icon } from 'expo-app/components/Icon';
import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme';
import { useTranslation } from 'expo-app/lib/hooks/useTranslation';
import { TouchableWithoutFeedback, View } from 'react-native';
import { normalizeDescription } from '../lib/normalizeDescription';
import type { CatalogItem } from '../types';
import { CatalogItemImage } from './CatalogItemImage';

Expand Down Expand Up @@ -52,7 +53,7 @@ export function CatalogItemCard({ item, onPress }: CatalogItemCardProps) {
{item.brand && <CardSubtitle className="text-xs">{item.brand}</CardSubtitle>}

<CardDescription className="mt-1 text-xs" numberOfLines={2}>
{item.description}
{normalizeDescription(item.description)}
</CardDescription>
</View>
</CardContent>
Expand Down
15 changes: 15 additions & 0 deletions apps/expo/features/catalog/lib/normalizeDescription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const DETAILS_ARRAY_RE = /^Details:\s*(\[[\s\S]*\])$/;

export function normalizeDescription(description: string | null | undefined): string | null {
if (!description) return null;
const match = description.match(DETAILS_ARRAY_RE);
if (match && match[1]) {
try {
const items = JSON.parse(match[1]) as string[];
return items.join('. ');
} catch {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// fall through
}
}
return description;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Linking, Text as RNText, ScrollView, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { CatalogItemImage } from '../components/CatalogItemImage';
import { useCatalogItemDetails } from '../hooks';
import { normalizeDescription } from '../lib/normalizeDescription';

export function CatalogItemDetailScreen() {
const router = useRouter();
Expand Down Expand Up @@ -109,7 +110,7 @@ export function CatalogItemDetailScreen() {
)}

<View className="mb-4">
<Text className="mb-2 text-foreground">{item.description}</Text>
<Text className="mb-2 text-foreground">{normalizeDescription(item.description)}</Text>
</View>

<View className="mb-4 flex-row flex-wrap gap-1">
Expand Down
5 changes: 2 additions & 3 deletions apps/expo/features/pack-templates/store/packTemplateItems.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { observable, syncState } from '@legendapp/state';
import { observablePersistSqlite } from '@legendapp/state/persist-plugins/expo-sqlite';
import { syncObservable } from '@legendapp/state/sync';
import { syncedCrud } from '@legendapp/state/sync-plugins/crud';
import {
Expand All @@ -8,7 +7,7 @@ import {
} from '@packrat/api/schemas/packTemplates';
import { isAuthed } from 'expo-app/features/auth/store';
import { apiClient } from 'expo-app/lib/api/packrat';
import Storage from 'expo-sqlite/kv-store';
import { persistPlugin } from 'expo-app/lib/persist-plugin';
import type { PackTemplateItem } from '../types';

const listAllPackTemplateItems = async (): Promise<PackTemplateItem[]> => {
Expand Down Expand Up @@ -84,7 +83,7 @@ syncObservable(
updatePartial: true,
mode: 'merge',
persist: {
plugin: observablePersistSqlite(Storage),
plugin: persistPlugin,
retrySync: true,
name: 'packTemplateItems',
},
Expand Down
5 changes: 2 additions & 3 deletions apps/expo/features/pack-templates/store/packTemplates.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { observable, syncState } from '@legendapp/state';
import { observablePersistSqlite } from '@legendapp/state/persist-plugins/expo-sqlite';
import { syncObservable } from '@legendapp/state/sync';
import { syncedCrud } from '@legendapp/state/sync-plugins/crud';
import {
Expand All @@ -8,7 +7,7 @@ import {
} from '@packrat/api/schemas/packTemplates';
import { isAuthed } from 'expo-app/features/auth/store';
import { apiClient } from 'expo-app/lib/api/packrat';
import Storage from 'expo-sqlite/kv-store';
import { persistPlugin } from 'expo-app/lib/persist-plugin';
import type { PackTemplate, PackTemplateInStore } from '../types';

const listPackTemplates = async (): Promise<PackTemplateInStore[] | null> => {
Expand Down Expand Up @@ -73,7 +72,7 @@ syncObservable(
fieldDeleted: 'deleted',
mode: 'merge',
persist: {
plugin: observablePersistSqlite(Storage),
plugin: persistPlugin,
retrySync: true,
name: 'packTemplates',
},
Expand Down
5 changes: 2 additions & 3 deletions apps/expo/features/packs/store/packItems.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { observable, syncState } from '@legendapp/state';
import { observablePersistSqlite } from '@legendapp/state/persist-plugins/expo-sqlite';
import { syncObservable } from '@legendapp/state/sync';
import { syncedCrud } from '@legendapp/state/sync-plugins/crud';
import { PackItemSchema, PackWithWeightsSchema } from '@packrat/api/schemas/packs';
import { isRemoteUrl } from '@packrat/guards';
import { isAuthed } from 'expo-app/features/auth/store';
import { apiClient } from 'expo-app/lib/api/packrat';
import { persistPlugin } from 'expo-app/lib/persist-plugin';
import ImageCacheManager from 'expo-app/lib/utils/ImageCacheManager';
import Storage from 'expo-sqlite/kv-store';
import type { PackItem } from '../types';
import { uploadImage } from '../utils';

Expand Down Expand Up @@ -55,7 +54,7 @@ syncObservable(
updatePartial: true,
mode: 'merge',
persist: {
plugin: observablePersistSqlite(Storage),
plugin: persistPlugin,
retrySync: true,
name: 'packItems',
},
Expand Down
5 changes: 2 additions & 3 deletions apps/expo/features/packs/store/packWeightHistory.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { observable, syncState } from '@legendapp/state';
import { observablePersistSqlite } from '@legendapp/state/persist-plugins/expo-sqlite';
import { syncObservable } from '@legendapp/state/sync';
import { syncedCrud } from '@legendapp/state/sync-plugins/crud';
import { PackWeightHistoryResponseSchema } from '@packrat/api/schemas/packs';
import { isAuthed } from 'expo-app/features/auth/store';
import { apiClient } from 'expo-app/lib/api/packrat';
import { persistPlugin } from 'expo-app/lib/persist-plugin';
import { obs } from 'expo-app/lib/store';
import Storage from 'expo-sqlite/kv-store';
import { nanoid } from 'nanoid';
import type { PackWeightHistoryEntry } from '../types';
import { computePackWeights } from '../utils';
Expand Down Expand Up @@ -40,7 +39,7 @@ syncObservable(
fieldCreatedAt: 'createdAt',
mode: 'merge',
persist: {
plugin: observablePersistSqlite(Storage),
plugin: persistPlugin,
retrySync: true,
name: 'packWeigthHistory',
},
Expand Down
5 changes: 2 additions & 3 deletions apps/expo/features/packs/store/packingMode.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { observable } from '@legendapp/state';
import { observablePersistSqlite } from '@legendapp/state/persist-plugins/expo-sqlite';
import { syncObservable } from '@legendapp/state/sync';
import Storage from 'expo-sqlite/kv-store';
import { persistPlugin } from 'expo-app/lib/persist-plugin';

export const packingModeStore = observable<Record<string, Record<string, boolean>>>({});

syncObservable(packingModeStore, {
persist: {
plugin: observablePersistSqlite(Storage),
plugin: persistPlugin,
retrySync: true,
name: 'packingMode',
},
Expand Down
5 changes: 2 additions & 3 deletions apps/expo/features/packs/store/packs.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { observable, syncState } from '@legendapp/state';
import { observablePersistSqlite } from '@legendapp/state/persist-plugins/expo-sqlite';
import { syncObservable } from '@legendapp/state/sync';
import { syncedCrud } from '@legendapp/state/sync-plugins/crud';
import { PackWithWeightsSchema } from '@packrat/api/schemas/packs';
import { isAuthed } from 'expo-app/features/auth/store';
import { apiClient } from 'expo-app/lib/api/packrat';
import Storage from 'expo-sqlite/kv-store';
import { persistPlugin } from 'expo-app/lib/persist-plugin';
import type { PackInStore } from '../types';

const listPacks = async (): Promise<PackInStore[] | null> => {
Expand Down Expand Up @@ -58,7 +57,7 @@ syncObservable(
fieldDeleted: 'deleted',
mode: 'merge',
persist: {
plugin: observablePersistSqlite(Storage),
plugin: persistPlugin,
retrySync: true,
name: 'packs',
},
Expand Down
Loading
Loading