diff --git a/.github/scripts/env.ts b/.github/scripts/env.ts index bc8bd337f9..b41b019507 100644 --- a/.github/scripts/env.ts +++ b/.github/scripts/env.ts @@ -36,6 +36,7 @@ const expoFileContent = envFileContent } else if (line.startsWith('EXPO_PUBLIC_')) { return line; } + return undefined; }) .join('\n'); const expoNoTelemetry = 'EXPO_NO_TELEMETRY=true'; diff --git a/apps/expo/app/(app)/upcoming-trips.tsx b/apps/expo/app/(app)/upcoming-trips.tsx index 575072a78c..7ddb57a3a4 100644 --- a/apps/expo/app/(app)/upcoming-trips.tsx +++ b/apps/expo/app/(app)/upcoming-trips.tsx @@ -142,83 +142,79 @@ export default function UpcomingTripsScreen() { const selectedPack = selectedTrip ? packs.find((p) => p.id === selectedTrip.packId) : undefined; return ( - <> - - - - {t('trips.plannedAdventures')} - - + + + + {t('trips.plannedAdventures')} + + + + {/* Trip List */} + ({ + id: trip.id, + trip, + title: trip.name, + subTitle: `${trip.location?.name ?? t('trips.unknown')} • ${formatDate( + trip.startDate, + )} to ${formatDate(trip.endDate)}`, + }))} + extraData={selectedTripId} + keyExtractor={(item) => item.id} + renderItem={(info) => { + const { trip } = info.item; + const { status, completion } = getTripStatus(trip, t); + + return ( + } + rightView={ + + + + } + onPress={() => setSelectedTripId(trip.id)} + className={ + selectedTripId === trip.id ? 'bg-muted/50 dark:bg-slate-950' : 'dark:bg-transparent' + } + /> + ); + }} + /> + + {/* Trip Summary */} + {selectedTrip && ( + + + + {selectedTrip.name} + + + {selectedTrip.location?.name ?? 'No location'} + + - {/* Trip List */} - ({ - id: trip.id, - trip, - title: trip.name, - subTitle: `${trip.location?.name ?? t('trips.unknown')} • ${formatDate( - trip.startDate, - )} to ${formatDate(trip.endDate)}`, - }))} - extraData={selectedTripId} - keyExtractor={(item) => item.id} - renderItem={(info) => { - const { trip } = info.item; - const { status, completion } = getTripStatus(trip, t); - - return ( - } - rightView={ - - - - } - onPress={() => setSelectedTripId(trip.id)} - className={ - selectedTripId === trip.id - ? 'bg-muted/50 dark:bg-slate-950' - : 'dark:bg-transparent' - } - /> - ); - }} - /> - - {/* Trip Summary */} - {selectedTrip && ( - - - - {selectedTrip.name} + + + + DATES - - {selectedTrip.location?.name ?? 'No location'} + + {formatDate(selectedTrip.startDate)} - {formatDate(selectedTrip.endDate)} - - - - - DATES - - - {formatDate(selectedTrip.startDate)} - {formatDate(selectedTrip.endDate)} - - - - - PACK - - - {selectedPack ? `${selectedPack.items.length} items` : 'No pack assigned'} - - + + + PACK + + + {selectedPack ? `${selectedPack.items.length} items` : 'No pack assigned'} + - )} - - + + )} + ); } diff --git a/apps/expo/app/(app)/weight-analysis/[id].tsx b/apps/expo/app/(app)/weight-analysis/[id].tsx index fd679989b7..cb89ce9dda 100644 --- a/apps/expo/app/(app)/weight-analysis/[id].tsx +++ b/apps/expo/app/(app)/weight-analysis/[id].tsx @@ -90,7 +90,7 @@ export default function WeightAnalysisScreen() { - {data.categories.map((category, categoryIndex) => ( + {data.categories.map((category, _categoryIndex) => ( {/* Category Header */} @@ -110,7 +110,7 @@ export default function WeightAnalysisScreen() { .filter((item) => item.category.trim() === category.name.trim()) .map((item, itemIndex) => ( 0 ? 'border-border/25 dark:border-border/80 border-t' : '', diff --git a/apps/expo/app/auth/(create-account)/credentials.tsx b/apps/expo/app/auth/(create-account)/credentials.tsx index 137c8f1d00..8bfd77e8a7 100644 --- a/apps/expo/app/auth/(create-account)/credentials.tsx +++ b/apps/expo/app/auth/(create-account)/credentials.tsx @@ -140,7 +140,12 @@ export default function CredentialsScreen() { }; // Call signup function with all user data - await signUp(userData.email, userData.password, userData.firstName, userData.lastName); + await signUp({ + email: userData.email, + password: userData.password, + firstName: userData.firstName, + lastName: userData.lastName, + }); // Navigate to verification code screen router.push({ diff --git a/apps/expo/features/ai-packs/hooks/useGeneratedPacks.ts b/apps/expo/features/ai-packs/hooks/useGeneratedPacks.ts index ff74f58560..882f98fe07 100644 --- a/apps/expo/features/ai-packs/hooks/useGeneratedPacks.ts +++ b/apps/expo/features/ai-packs/hooks/useGeneratedPacks.ts @@ -3,6 +3,7 @@ import { useMutation } from '@tanstack/react-query'; import type { Pack } from 'expo-app/features/packs'; import { packsStore } from 'expo-app/features/packs/store'; import axiosInstance, { handleApiError } from 'expo-app/lib/api/client'; +import { obs } from 'expo-app/lib/store'; import type { GenerationRequest } from '../types'; const generatePacks = async (request: GenerationRequest): Promise => { @@ -25,10 +26,7 @@ export function useGeneratePacks() { const generatedPacksFromStore = use$(() => { if (mutation.data) { - return mutation.data.map((pack) => - // @ts-ignore: Safe because Legend-State uses Proxy - packsStore[pack.id].get(), - ); + return mutation.data.map((pack) => obs(packsStore, pack.id).get()); } return []; }); diff --git a/apps/expo/features/ai-packs/screens/AIPacksScreen.tsx b/apps/expo/features/ai-packs/screens/AIPacksScreen.tsx index 2c4a2db2fd..d174c6bff3 100644 --- a/apps/expo/features/ai-packs/screens/AIPacksScreen.tsx +++ b/apps/expo/features/ai-packs/screens/AIPacksScreen.tsx @@ -94,7 +94,7 @@ export function AIPacksScreen() { field.handleChange(Number.parseInt(text) || 1)} + onChangeText={(text) => field.handleChange(Number.parseInt(text, 10) || 1)} keyboardType="numeric" placeholder={t('ai.enterCount')} /> diff --git a/apps/expo/features/ai/atoms/chatStorageAtoms.ts b/apps/expo/features/ai/atoms/chatStorageAtoms.ts index 31ccb5a956..6c9c7c3a2b 100644 --- a/apps/expo/features/ai/atoms/chatStorageAtoms.ts +++ b/apps/expo/features/ai/atoms/chatStorageAtoms.ts @@ -82,6 +82,7 @@ export async function loadChatMessages(context: ChatContext): Promise { try { const key = getChatStorageKey(context); diff --git a/apps/expo/features/ai/components/ChatBubble.tsx b/apps/expo/features/ai/components/ChatBubble.tsx index ef915cba68..676c05439f 100644 --- a/apps/expo/features/ai/components/ChatBubble.tsx +++ b/apps/expo/features/ai/components/ChatBubble.tsx @@ -148,6 +148,8 @@ export const ChatBubble = React.memo(function ChatBubble({ /> ); + + return null; })} {/* */} diff --git a/apps/expo/features/auth/hooks/useAuthActions.ts b/apps/expo/features/auth/hooks/useAuthActions.ts index 37662a8fec..18f1764d0d 100644 --- a/apps/expo/features/auth/hooks/useAuthActions.ts +++ b/apps/expo/features/auth/hooks/useAuthActions.ts @@ -191,7 +191,17 @@ export function useAuthActions() { } }; - const signUp = async (email: string, password: string, firstName?: string, lastName?: string) => { + const signUp = async ({ + email, + password, + firstName, + lastName, + }: { + email: string; + password: string; + firstName?: string; + lastName?: string; + }) => { setIsLoading(true); try { const response = await fetch(`${clientEnvs.EXPO_PUBLIC_API_URL}/api/auth/register`, { diff --git a/apps/expo/features/guides/components/GuideCard.tsx b/apps/expo/features/guides/components/GuideCard.tsx index 13e4b00e2f..48b695be1e 100644 --- a/apps/expo/features/guides/components/GuideCard.tsx +++ b/apps/expo/features/guides/components/GuideCard.tsx @@ -1,5 +1,4 @@ import { Card, CardContent, CardTitle, Text } from '@packrat/ui/nativewindui'; -import { Icon } from '@roninoss/icons'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { TouchableOpacity, View } from 'react-native'; import type { Guide } from '../types'; diff --git a/apps/expo/features/pack-templates/components/PackTemplateItemCard.tsx b/apps/expo/features/pack-templates/components/PackTemplateItemCard.tsx index 36cd1e05b3..ec29aed987 100644 --- a/apps/expo/features/pack-templates/components/PackTemplateItemCard.tsx +++ b/apps/expo/features/pack-templates/components/PackTemplateItemCard.tsx @@ -98,54 +98,50 @@ export function PackTemplateItemCard({ }; return ( - <> - onPress(item)}> - - {/* Image */} - + onPress(item)}> + + {/* Image */} + - {/* Content */} - - - - {item.name} - - - - - - {item.category} + {/* Content */} + + + + {item.name} + + + + + + {item.category} - - {item.consumable && ( - - - {t('packTemplates.consumable')} - - - )} + + {item.consumable && ( + + + {t('packTemplates.consumable')} + + + )} - {item.worn && ( - - - {t('packTemplates.worn')} - - - )} - - - - {item.weight} - {item.weightUnit} - + {item.worn && ( + + {t('packTemplates.worn')} + + )} + + + + {item.weight} + {item.weightUnit} + - - {item.quantity} {t('packTemplates.qty')} - - + + {item.quantity} {t('packTemplates.qty')} + - - + + ); } diff --git a/apps/expo/features/pack-templates/hooks/useCreatePackTemplate.ts b/apps/expo/features/pack-templates/hooks/useCreatePackTemplate.ts index 83d5150a57..e3176d4046 100644 --- a/apps/expo/features/pack-templates/hooks/useCreatePackTemplate.ts +++ b/apps/expo/features/pack-templates/hooks/useCreatePackTemplate.ts @@ -1,3 +1,4 @@ +import { obs } from 'expo-app/lib/store'; import { nanoid } from 'nanoid/non-secure'; import { useCallback } from 'react'; import { packTemplatesStore } from '../store/packTemplates'; @@ -16,8 +17,7 @@ export function useCreatePackTemplate() { deleted: false, }; - // @ts-ignore: Safe because Legend-State uses Proxy - packTemplatesStore[id].set(newTemplate); + obs(packTemplatesStore, id).set(newTemplate); }, []); return createPackTemplate; diff --git a/apps/expo/features/pack-templates/hooks/useCreatePackTemplateItem.ts b/apps/expo/features/pack-templates/hooks/useCreatePackTemplateItem.ts index e8e78ce92d..519833a574 100644 --- a/apps/expo/features/pack-templates/hooks/useCreatePackTemplateItem.ts +++ b/apps/expo/features/pack-templates/hooks/useCreatePackTemplateItem.ts @@ -1,5 +1,6 @@ // useCreatePackTemplateItem.ts +import { obs } from 'expo-app/lib/store'; import { nanoid } from 'nanoid/non-secure'; import { useCallback } from 'react'; import { packTemplateItemsStore } from '../store/packTemplateItems'; @@ -18,10 +19,8 @@ export function useCreatePackTemplateItem() { deleted: false, }; - // @ts-ignore: Safe because Legend-State uses Proxy - packTemplateItemsStore[id].set(newItem); - // @ts-ignore: Safe because Legend-State uses Proxy - packTemplatesStore[packTemplateId].localUpdatedAt.set(new Date().toISOString()); + obs(packTemplateItemsStore, id).set(newItem); + obs(packTemplatesStore, packTemplateId).localUpdatedAt.set(new Date().toISOString()); }, [], ); diff --git a/apps/expo/features/pack-templates/hooks/useDeletePackTemplate.ts b/apps/expo/features/pack-templates/hooks/useDeletePackTemplate.ts index 04c0a89f63..10c9f1f2ba 100644 --- a/apps/expo/features/pack-templates/hooks/useDeletePackTemplate.ts +++ b/apps/expo/features/pack-templates/hooks/useDeletePackTemplate.ts @@ -1,10 +1,10 @@ +import { obs } from 'expo-app/lib/store'; import { useCallback } from 'react'; import { packTemplatesStore } from '../store/packTemplates'; export function useDeletePackTemplate() { const del = useCallback((id: string) => { - // @ts-ignore: Safe because Legend-State uses Proxy - packTemplatesStore[id].deleted.set(true); + obs(packTemplatesStore, id).deleted.set(true); }, []); return del; diff --git a/apps/expo/features/pack-templates/hooks/useDeletePackTemplateItem.ts b/apps/expo/features/pack-templates/hooks/useDeletePackTemplateItem.ts index dd9a7daeaf..8c8d4c0b2c 100644 --- a/apps/expo/features/pack-templates/hooks/useDeletePackTemplateItem.ts +++ b/apps/expo/features/pack-templates/hooks/useDeletePackTemplateItem.ts @@ -1,11 +1,11 @@ +import { obs } from 'expo-app/lib/store'; import { useCallback } from 'react'; import { packTemplateItemsStore } from '../store/packTemplateItems'; export function useDeletePackTemplateItem() { const deletePackTemplateItem = useCallback((id: string) => { // Soft delete by setting deleted flag - // @ts-ignore: Safe because Legend-State uses Proxy - packTemplateItemsStore[id].deleted.set(true); + obs(packTemplateItemsStore, id).deleted.set(true); return Promise.resolve({ id }); }, []); diff --git a/apps/expo/features/pack-templates/hooks/useGenerateTemplateFromOnlineContent.ts b/apps/expo/features/pack-templates/hooks/useGenerateTemplateFromOnlineContent.ts index 04131fc8a6..3cddc4065b 100644 --- a/apps/expo/features/pack-templates/hooks/useGenerateTemplateFromOnlineContent.ts +++ b/apps/expo/features/pack-templates/hooks/useGenerateTemplateFromOnlineContent.ts @@ -1,8 +1,13 @@ import { useMutation } from '@tanstack/react-query'; import axiosInstance, { handleApiError } from 'expo-app/lib/api/client'; +import { obs } from 'expo-app/lib/store'; import { packTemplateItemsStore } from '../store/packTemplateItems'; import { packTemplatesStore } from '../store/packTemplates'; -import type { PackTemplateInStore } from '../types'; +import type { PackTemplateInStore, PackTemplateItem, WeightUnit } from '../types'; + +const WEIGHT_UNITS: ReadonlySet = new Set(['g', 'kg', 'oz', 'lb']); +const isWeightUnit = (value: string): value is WeightUnit => + (WEIGHT_UNITS as ReadonlySet).has(value); export interface GenerateFromOnlineContentInput { contentUrl: string; @@ -83,11 +88,31 @@ export function useGenerateTemplateFromOnlineContent() { }, onSuccess: (data) => { const { items, ...template } = data; - // @ts-ignore: Safe because Legend-State uses Proxy - packTemplatesStore[template.id].set(template); + obs(packTemplatesStore, template.id).set(template); for (const item of items) { - // @ts-ignore: Safe because Legend-State uses Proxy - packTemplateItemsStore[item.id].set(item); + if (!isWeightUnit(item.weightUnit)) { + throw new Error(`Unsupported weightUnit "${item.weightUnit}" for item ${item.id}`); + } + const storeItem: PackTemplateItem = { + id: item.id, + packTemplateId: item.packTemplateId, + name: item.name, + description: item.description ?? undefined, + weight: item.weight, + weightUnit: item.weightUnit, + quantity: item.quantity, + category: item.category ?? '', + consumable: item.consumable, + worn: item.worn, + image: item.image ?? undefined, + notes: item.notes ?? undefined, + catalogItemId: item.catalogItemId ?? undefined, + userId: item.userId, + deleted: item.deleted, + createdAt: item.createdAt, + updatedAt: item.updatedAt, + }; + obs(packTemplateItemsStore, item.id).set(storeItem); } }, }); diff --git a/apps/expo/features/pack-templates/hooks/usePackTemplatesDetails.ts b/apps/expo/features/pack-templates/hooks/usePackTemplatesDetails.ts index 980cbd66f5..df62273388 100644 --- a/apps/expo/features/pack-templates/hooks/usePackTemplatesDetails.ts +++ b/apps/expo/features/pack-templates/hooks/usePackTemplatesDetails.ts @@ -1,4 +1,5 @@ import { use$ } from '@legendapp/state/react'; +import { obs } from 'expo-app/lib/store'; import { getTemplateItems } from '../store/packTemplateItems'; import { packTemplatesStore } from '../store/packTemplates'; import { computePackTemplateWeights } from '../utils/computePacktemplateWeight'; @@ -6,8 +7,7 @@ import { computePackTemplateWeights } from '../utils/computePacktemplateWeight'; // Hook to get a single pack template export function usePackTemplateDetails(id: string) { const template = use$(() => { - // @ts-ignore: Safe because Legend-State uses Proxy - const template = packTemplatesStore[id].get(); + const template = obs(packTemplatesStore, id).get(); const items = getTemplateItems(id); return { ...template, diff --git a/apps/expo/features/pack-templates/hooks/useUpdatePackTemplateItem.ts b/apps/expo/features/pack-templates/hooks/useUpdatePackTemplateItem.ts index 84a57a1dd7..993ad63919 100644 --- a/apps/expo/features/pack-templates/hooks/useUpdatePackTemplateItem.ts +++ b/apps/expo/features/pack-templates/hooks/useUpdatePackTemplateItem.ts @@ -1,15 +1,14 @@ // useUpdatePackTemplateItem.ts +import { obs } from 'expo-app/lib/store'; import { useCallback } from 'react'; import { packTemplateItemsStore, packTemplatesStore } from '../store'; import type { PackTemplateItem } from '../types'; export function useUpdatePackTemplateItem() { const updatePackTemplateItem = useCallback((item: PackTemplateItem) => { - // @ts-ignore: Safe because Legend-State uses Proxy - packTemplateItemsStore[item.id].set(item); - // @ts-ignore: Safe because Legend-State uses Proxy - packTemplatesStore[item.packTemplateId].localUpdatedAt.set(new Date().toISOString()); + obs(packTemplateItemsStore, item.id).set(item); + obs(packTemplatesStore, item.packTemplateId).localUpdatedAt.set(new Date().toISOString()); }, []); return updatePackTemplateItem; diff --git a/apps/expo/features/pack-templates/hooks/useUpdatePacktemplate.ts b/apps/expo/features/pack-templates/hooks/useUpdatePacktemplate.ts index 4bc7503da1..7ac5376c59 100644 --- a/apps/expo/features/pack-templates/hooks/useUpdatePacktemplate.ts +++ b/apps/expo/features/pack-templates/hooks/useUpdatePacktemplate.ts @@ -1,11 +1,11 @@ +import { obs } from 'expo-app/lib/store'; import { useCallback } from 'react'; import { packTemplatesStore } from '../store/packTemplates'; import type { PackTemplate } from '../types'; export function useUpdatePackTemplate() { const update = useCallback((template: PackTemplate) => { - // @ts-ignore: Safe because Legend-State uses Proxy - packTemplatesStore[template.id].set({ + obs(packTemplatesStore, template.id).set({ ...template, localUpdatedAt: new Date().toISOString(), }); diff --git a/apps/expo/features/pack-templates/hooks/useWritePermissionCheck.ts b/apps/expo/features/pack-templates/hooks/useWritePermissionCheck.ts index 11f5f41e00..1e265ed56c 100644 --- a/apps/expo/features/pack-templates/hooks/useWritePermissionCheck.ts +++ b/apps/expo/features/pack-templates/hooks/useWritePermissionCheck.ts @@ -1,9 +1,9 @@ import { useUser } from 'expo-app/features/auth/hooks/useUser'; +import { obs } from 'expo-app/lib/store'; import { packTemplatesStore } from '../store'; export function useWritePermissionCheck(id: string) { - // @ts-ignore: Safe because Legend-State uses Proxy - const packTemplate = packTemplatesStore[id].get(); + const packTemplate = obs(packTemplatesStore, id).get(); const user = useUser(); return packTemplate.isAppTemplate ? user?.role === 'ADMIN' : true; diff --git a/apps/expo/features/packs/components/GapAnalysisModal.tsx b/apps/expo/features/packs/components/GapAnalysisModal.tsx index ad17bdcd1c..9a255b3fb4 100644 --- a/apps/expo/features/packs/components/GapAnalysisModal.tsx +++ b/apps/expo/features/packs/components/GapAnalysisModal.tsx @@ -30,98 +30,92 @@ export function GapAnalysisModal({ const { isDarkColorScheme, colors } = useColorScheme(); return ( - <> - - - - - - {t('packs.gapAnalysis')} - - {pack.name} - - - - {pack.category} - - {location && ( - <> - - - - {location} - - - )} + + + + + + {t('packs.gapAnalysis')} + + {pack.name} + + + + {pack.category} + {location && ( + <> + + + + {location} + + + )} - - - + + + + - {/* Content */} - - {isLoading ? ( - - - {t('packs.analyzing')} - - ) : analysis ? ( - - {analysis.gaps.length > 0 ? ( - analysis.gaps.map((gap) => ( - - )) - ) : ( - - - - {t('packs.packLooksComplete')} - - - {t('packs.noSignificantGaps')} - - - )} - - ) : ( - - - + {/* Content */} + + {isLoading ? ( + + + {t('packs.analyzing')} + + ) : analysis ? ( + + {analysis.gaps.length > 0 ? ( + analysis.gaps.map((gap) => ( + + )) + ) : ( + + + + {t('packs.packLooksComplete')} + + + {t('packs.noSignificantGaps')} + - - {t('packs.unableToAnalyzePack')} - - - {t('packs.pleaseTryAgain')} - - + )} + + ) : ( + + + - )} - - - - + + {t('packs.unableToAnalyzePack')} + + + {t('packs.pleaseTryAgain')} + + + + )} + + + ); } diff --git a/apps/expo/features/packs/components/PackItemCard.tsx b/apps/expo/features/packs/components/PackItemCard.tsx index bf5d3e21e0..f39ac7b150 100644 --- a/apps/expo/features/packs/components/PackItemCard.tsx +++ b/apps/expo/features/packs/components/PackItemCard.tsx @@ -104,77 +104,75 @@ export function PackItemCard({ }; return ( - <> - (isSelectable ? restProps.onSelect(item) : onPress?.(item))} + (isSelectable ? restProps.onSelect(item) : onPress?.(item))} + > + - - {/* Image */} - + {/* Image */} + - {/* Content */} - - - - {item.name} - - {!isSelectable && ( - - - - )} - - {item.category} - - - {item.consumable && ( - - Consumable - - )} + {/* Content */} + + + + {item.name} + + {!isSelectable && ( + + + + )} + + {item.category} - {item.worn && ( - - Worn - - )} - - - - {item.weight} - {item.weightUnit} - + + {item.consumable && ( + + Consumable + + )} - {item.quantity} qty - + {item.worn && ( + + Worn + + )} + + + {item.weight} + {item.weightUnit} + - {/* Selection indicator */} - {isSelectable && ( - - {restProps.selected ? ( - - ) : ( - - )} - - )} + {item.quantity} qty + - - + + {/* Selection indicator */} + {isSelectable && ( + + {restProps.selected ? ( + + ) : ( + + )} + + )} + + ); } diff --git a/apps/expo/features/packs/hooks/useCreatePack.ts b/apps/expo/features/packs/hooks/useCreatePack.ts index ce9e24f205..dd519145bf 100644 --- a/apps/expo/features/packs/hooks/useCreatePack.ts +++ b/apps/expo/features/packs/hooks/useCreatePack.ts @@ -1,4 +1,5 @@ import { packsStore } from 'expo-app/features/packs/store'; +import { obs } from 'expo-app/lib/store'; import { nanoid } from 'nanoid/non-secure'; import { useCallback } from 'react'; import type { PackInput, PackInStore } from '../types'; @@ -17,8 +18,7 @@ export function useCreatePack() { deleted: false, }; - // @ts-ignore: Safe because Legend-State uses Proxy - packsStore[id].set(newPack); + obs(packsStore, id).set(newPack); return id; }, []); diff --git a/apps/expo/features/packs/hooks/useCreatePackItem.ts b/apps/expo/features/packs/hooks/useCreatePackItem.ts index 391ad62c38..e41682038e 100644 --- a/apps/expo/features/packs/hooks/useCreatePackItem.ts +++ b/apps/expo/features/packs/hooks/useCreatePackItem.ts @@ -1,4 +1,5 @@ import { packItemsStore, packsStore } from 'expo-app/features/packs/store'; +import { obs } from 'expo-app/lib/store'; import { nanoid } from 'nanoid/non-secure'; import { useCallback } from 'react'; import { recordPackWeight } from '../store/packWeightHistory'; @@ -27,10 +28,8 @@ export function useCreatePackItem() { deleted: false, }; - // @ts-ignore: Safe because Legend-State uses Proxy - packItemsStore[id].set(newItem); - // @ts-ignore: Safe because Legend-State uses Proxy - packsStore[packId].localUpdatedAt.set(new Date().toISOString()); + obs(packItemsStore, id).set(newItem); + obs(packsStore, packId).localUpdatedAt.set(new Date().toISOString()); recordPackWeight(packId); }, [], diff --git a/apps/expo/features/packs/hooks/useDeletePack.ts b/apps/expo/features/packs/hooks/useDeletePack.ts index 27615b3c7f..9c69d93d26 100644 --- a/apps/expo/features/packs/hooks/useDeletePack.ts +++ b/apps/expo/features/packs/hooks/useDeletePack.ts @@ -1,13 +1,14 @@ import { getPackItems, packItemsStore, packsStore } from 'expo-app/features/packs/store'; +import { obs } from 'expo-app/lib/store'; import { useCallback } from 'react'; export function useDeletePack() { const deletePack = useCallback((id: string) => { // Soft delete by setting deleted flag - // @ts-ignore: Safe because Legend-State uses Proxy - getPackItems(id).forEach((item) => packItemsStore[item.id].deleted.set(true)); - // @ts-ignore: Safe because Legend-State uses Proxy - packsStore[id].deleted.set(true); + for (const item of getPackItems(id)) { + obs(packItemsStore, item.id).deleted.set(true); + } + obs(packsStore, id).deleted.set(true); }, []); return deletePack; diff --git a/apps/expo/features/packs/hooks/useDeletePackItem.ts b/apps/expo/features/packs/hooks/useDeletePackItem.ts index 0f48fe24c4..9e8a35c557 100644 --- a/apps/expo/features/packs/hooks/useDeletePackItem.ts +++ b/apps/expo/features/packs/hooks/useDeletePackItem.ts @@ -1,11 +1,11 @@ import { packItemsStore } from 'expo-app/features/packs/store'; +import { obs } from 'expo-app/lib/store'; import { useCallback } from 'react'; export function useDeletePackItem() { const deletePackItem = useCallback((id: string) => { // Soft delete by setting deleted flag - // @ts-ignore: Safe because Legend-State uses Proxy - packItemsStore[id].deleted.set(true); + obs(packItemsStore, id).deleted.set(true); return Promise.resolve({ id }); }, []); diff --git a/apps/expo/features/packs/hooks/usePackDetailsFromStore.ts b/apps/expo/features/packs/hooks/usePackDetailsFromStore.ts index 68e6506395..9bdb7bc106 100644 --- a/apps/expo/features/packs/hooks/usePackDetailsFromStore.ts +++ b/apps/expo/features/packs/hooks/usePackDetailsFromStore.ts @@ -1,6 +1,7 @@ import { use$ } from '@legendapp/state/react'; import { getPackItems, packsStore } from 'expo-app/features/packs/store'; +import { obs } from 'expo-app/lib/store'; import { computePackWeights } from '../utils/computePackWeights'; /** @@ -13,8 +14,7 @@ import { computePackWeights } from '../utils/computePackWeights'; */ export function usePackDetailsFromStore(id: string) { const pack = use$(() => { - // @ts-ignore: Safe because Legend-State uses Proxy - const pack_ = packsStore[id].get(); + const pack_ = obs(packsStore, id).get(); const items = getPackItems(id); const packWithWeights = computePackWeights({ ...pack_, items }); return packWithWeights; diff --git a/apps/expo/features/packs/hooks/usePackItemOwnershipCheck.ts b/apps/expo/features/packs/hooks/usePackItemOwnershipCheck.ts index 0dd9f4d194..563e42f3ce 100644 --- a/apps/expo/features/packs/hooks/usePackItemOwnershipCheck.ts +++ b/apps/expo/features/packs/hooks/usePackItemOwnershipCheck.ts @@ -1,8 +1,8 @@ +import { obs } from 'expo-app/lib/store'; import { packItemsStore } from '../store'; export function usePackItemOwnershipCheck(id: string) { - // @ts-ignore: Safe because Legend-State uses Proxy - const packItem = packItemsStore[id].peek(); + const packItem = obs(packItemsStore, id).peek(); return !!packItem; } diff --git a/apps/expo/features/packs/hooks/usePackOwnershipCheck.ts b/apps/expo/features/packs/hooks/usePackOwnershipCheck.ts index 184c2b8745..a681ce4edf 100644 --- a/apps/expo/features/packs/hooks/usePackOwnershipCheck.ts +++ b/apps/expo/features/packs/hooks/usePackOwnershipCheck.ts @@ -1,8 +1,8 @@ +import { obs } from 'expo-app/lib/store'; import { packsStore } from '../store'; export function usePackOwnershipCheck(id: string) { - // @ts-ignore: Safe because Legend-State uses Proxy - const pack = packsStore[id].peek(); + const pack = obs(packsStore, id).peek(); return !!pack; } diff --git a/apps/expo/features/packs/hooks/useUpdatePack.ts b/apps/expo/features/packs/hooks/useUpdatePack.ts index f774e944c9..638504bc70 100644 --- a/apps/expo/features/packs/hooks/useUpdatePack.ts +++ b/apps/expo/features/packs/hooks/useUpdatePack.ts @@ -1,11 +1,11 @@ import { packsStore } from 'expo-app/features/packs/store'; +import { obs } from 'expo-app/lib/store'; import { useCallback } from 'react'; import type { Pack } from '../types'; export function useUpdatePack() { const updatePack = useCallback((pack: Pack) => { - // @ts-ignore: Safe because Legend-State uses Proxy - packsStore[pack.id].set({ + obs(packsStore, pack.id).set({ ...pack, localUpdatedAt: new Date().toISOString(), }); diff --git a/apps/expo/features/packs/screens/PackDetailScreen.tsx b/apps/expo/features/packs/screens/PackDetailScreen.tsx index 8c168a21c5..1897b0b84e 100644 --- a/apps/expo/features/packs/screens/PackDetailScreen.tsx +++ b/apps/expo/features/packs/screens/PackDetailScreen.tsx @@ -14,6 +14,7 @@ import { cn } from 'expo-app/lib/cn'; import { useBottomSheetAction } from 'expo-app/lib/hooks/useBottomSheetAction'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; +import { obs } from 'expo-app/lib/store'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { useMemo, useState } from 'react'; import { Image, ScrollView, TouchableOpacity, View } from 'react-native'; @@ -35,8 +36,7 @@ export function PackDetailScreen() { const [activeTab, setActiveTab] = useState(DEFAULT_TAB); const [isPackingMode, setIsPackingMode] = useState(false); const [packedItems, setPackedItems] = useState>( - // @ts-expect-error: Safe because Legend-State uses Proxy - packingModeStore[id as string].get() || {}, + obs(packingModeStore, id as string).get() || {}, ); const [isLocationPickerOpen, setIsLocationPickerOpen] = useState(false); @@ -95,8 +95,7 @@ export function PackDetailScreen() { }; const handleSavePackingMode = () => { - // @ts-expect-error: Safe because Legend-State uses Proxy - packingModeStore[id as string].set({ ...packedItems }); + obs(packingModeStore, id as string).set({ ...packedItems }); setIsPackingMode(false); setActiveTab(DEFAULT_TAB); // Reset tab when toggling mode Burnt.toast({ @@ -109,12 +108,10 @@ export function PackDetailScreen() { const exitPackingMode = () => { setIsPackingMode(!isPackingMode); setActiveTab(DEFAULT_TAB); // Reset tab when toggling mode - // @ts-expect-error: Safe because Legend-State uses Proxy - setPackedItems(packingModeStore[id as string].get() || {}); + setPackedItems(obs(packingModeStore, id as string).get() || {}); }; - // @ts-expect-error: Safe because Legend-State uses Proxy - const packingState = packingModeStore[id as string].get() || {}; + const packingState = obs(packingModeStore, id as string).get() || {}; if ( Object.entries(packedItems).every(([key, val]) => diff --git a/apps/expo/features/packs/store/packWeightHistory.ts b/apps/expo/features/packs/store/packWeightHistory.ts index 11c15a5bb5..5cc626a419 100644 --- a/apps/expo/features/packs/store/packWeightHistory.ts +++ b/apps/expo/features/packs/store/packWeightHistory.ts @@ -4,6 +4,7 @@ import { syncObservable } from '@legendapp/state/sync'; import { syncedCrud } from '@legendapp/state/sync-plugins/crud'; import { isAuthed } from 'expo-app/features/auth/store'; import axiosInstance, { handleApiError } from 'expo-app/lib/api/client'; +import { obs } from 'expo-app/lib/store'; import Storage from 'expo-sqlite/kv-store'; import { nanoid } from 'nanoid/non-secure'; import type { PackWeightHistoryEntry } from '../types'; @@ -68,16 +69,14 @@ syncObservable( ); export function recordPackWeight(packId: string) { - // @ts-ignore: Safe because Legend-State uses Proxy - const pack = packsStore[packId].peek(); + const pack = obs(packsStore, packId).peek(); const packItems = Object.values(packItemsStore.peek()).filter( (item) => item.packId === packId && !item.deleted, ); const { totalWeight } = computePackWeights({ ...pack, items: packItems }); const id = nanoid(); - // @ts-ignore: Safe because Legend-State uses Proxy - packWeigthHistoryStore[id].set({ + obs(packWeigthHistoryStore, id).set({ id, packId, weight: totalWeight, diff --git a/apps/expo/features/trips/hooks/useCreateTrip.ts b/apps/expo/features/trips/hooks/useCreateTrip.ts index 670763c714..c47c03d4f8 100644 --- a/apps/expo/features/trips/hooks/useCreateTrip.ts +++ b/apps/expo/features/trips/hooks/useCreateTrip.ts @@ -1,4 +1,5 @@ import { tripsStore } from 'expo-app/features/trips/store/trips'; +import { obs } from 'expo-app/lib/store'; import { nanoid } from 'nanoid/non-secure'; import { useCallback } from 'react'; import type { TripInput, TripInStore } from '../types'; @@ -16,8 +17,7 @@ export function useCreateTrip() { localUpdatedAt: timestamp, deleted: false, }; - // @ts-ignore: Safe because Legend-State uses Proxy - tripsStore[id].set(newTrip); + obs(tripsStore, id).set(newTrip); return id; }, []); diff --git a/apps/expo/features/trips/hooks/useDeleteTrip.ts b/apps/expo/features/trips/hooks/useDeleteTrip.ts index e06c5aed53..b85b73c0af 100644 --- a/apps/expo/features/trips/hooks/useDeleteTrip.ts +++ b/apps/expo/features/trips/hooks/useDeleteTrip.ts @@ -1,12 +1,13 @@ import { tripsStore } from 'expo-app/features/trips/store/trips'; +import { obs } from 'expo-app/lib/store'; import { useCallback } from 'react'; export function useDeleteTrip() { const deleteTrip = useCallback((id: string) => { // Soft delete by setting deleted flag - // @ts-ignore: Safe because Legend-State uses Proxy - if (tripsStore[id]) { - tripsStore[id].deleted.set(true); + const tripObs = obs(tripsStore, id); + if (tripObs) { + tripObs.deleted.set(true); } }, []); diff --git a/apps/expo/features/trips/hooks/useTripDetailsFromStore.ts b/apps/expo/features/trips/hooks/useTripDetailsFromStore.ts index 26245ce0f1..8c3909b4b7 100644 --- a/apps/expo/features/trips/hooks/useTripDetailsFromStore.ts +++ b/apps/expo/features/trips/hooks/useTripDetailsFromStore.ts @@ -1,5 +1,6 @@ import { use$ } from '@legendapp/state/react'; import { tripsStore } from 'expo-app/features/trips/store/trips'; +import { obs } from 'expo-app/lib/store'; /** * Retrieves a trip from the store. @@ -10,8 +11,7 @@ import { tripsStore } from 'expo-app/features/trips/store/trips'; */ export function useTripDetailsFromStore(id: string) { const trip = use$(() => { - // @ts-ignore: Safe because Legend-State uses Proxy - const trip_ = tripsStore[id].get(); + const trip_ = obs(tripsStore, id).get(); return trip_; }); diff --git a/apps/expo/features/trips/hooks/useUpdateTrip.ts b/apps/expo/features/trips/hooks/useUpdateTrip.ts index 66c13ff658..47c8d8b3e8 100644 --- a/apps/expo/features/trips/hooks/useUpdateTrip.ts +++ b/apps/expo/features/trips/hooks/useUpdateTrip.ts @@ -1,11 +1,11 @@ import { tripsStore } from 'expo-app/features/trips/store/trips'; +import { obs } from 'expo-app/lib/store'; import { useCallback } from 'react'; import type { Trip } from '../types'; export function useUpdateTrip() { const updateTrip = useCallback((trip: Trip) => { - // @ts-ignore: Safe because Legend-State uses Proxy - tripsStore[trip.id].set({ + obs(tripsStore, trip.id).set({ ...trip, localUpdatedAt: new Date().toISOString(), }); diff --git a/apps/expo/features/weather/lib/weatherIcons.ts b/apps/expo/features/weather/lib/weatherIcons.ts index b6b4f53233..70e491e8ff 100644 --- a/apps/expo/features/weather/lib/weatherIcons.ts +++ b/apps/expo/features/weather/lib/weatherIcons.ts @@ -6,6 +6,7 @@ import type { MaterialIconName } from '@roninoss/icons'; * @param isDay Whether it's daytime (true) or nighttime (false) * @returns The icon name to use */ + export function getWeatherIconName(code: number, isDay = 1): MaterialIconName { // Clear conditions if (code === 1000) { @@ -81,6 +82,7 @@ export function getWeatherIconName(code: number, isDay = 1): MaterialIconName { * @param isDay Whether it's daytime (true) or nighttime (false) * @returns The icon name to use */ + export function getWeatherIconByCondition(condition: string, isDay = 1): MaterialIconName { const conditionLower = condition.toLowerCase(); @@ -148,6 +150,7 @@ export function getWeatherIconByCondition(condition: string, isDay = 1): Materia /** * Get background gradient colors based on weather condition */ + export function getWeatherBackgroundColors(code: number, isNight: boolean): string[] { if (isNight) { // Night gradients diff --git a/apps/expo/features/weather/lib/weatherService.ts b/apps/expo/features/weather/lib/weatherService.ts index 054c436e2f..8694199372 100644 --- a/apps/expo/features/weather/lib/weatherService.ts +++ b/apps/expo/features/weather/lib/weatherService.ts @@ -26,6 +26,7 @@ export async function searchLocations(query: string): Promise`. This utility performs + * the single necessary cast in one documented place, allowing callers to work + * with the correctly-typed observable without per-call suppressions. + * + * @example + * // Instead of: + * // @ts-expect-error: Safe because Legend-State uses Proxy + * packItemsStore[id].deleted.set(true); + * + * // Use: + * obs(packItemsStore, id).deleted.set(true); + */ +export function obs(store: Observable>, id: string): Observable { + const observable = (store as unknown as Record>)[id]; + assertDefined(observable); + return observable; +} diff --git a/apps/expo/lib/utils/ImageCacheManager.ts b/apps/expo/lib/utils/ImageCacheManager.ts index 003cfc5967..b37dd0691a 100644 --- a/apps/expo/lib/utils/ImageCacheManager.ts +++ b/apps/expo/lib/utils/ImageCacheManager.ts @@ -39,6 +39,7 @@ export class ImageCacheManager { /** * Download and cache an image */ + public async cacheRemoteImage(fileName: string, remoteUrl: string): Promise { await this.initCacheDirectory(); diff --git a/apps/expo/lib/utils/__tests__/compute-pack.test.ts b/apps/expo/lib/utils/__tests__/compute-pack.test.ts index b67f30d0a6..90825eac36 100644 --- a/apps/expo/lib/utils/__tests__/compute-pack.test.ts +++ b/apps/expo/lib/utils/__tests__/compute-pack.test.ts @@ -145,7 +145,7 @@ describe('computePacksWeights', () => { makePack([makePackItem({ weight: 1000, weightUnit: 'g' })], { id: 'p2' }), ]; const results = computePacksWeights(packs); - expect(results[0]!.totalWeight).toBe(500); - expect(results[1]!.totalWeight).toBe(1000); + expect(results[0]?.totalWeight).toBe(500); + expect(results[1]?.totalWeight).toBe(1000); }); }); diff --git a/apps/expo/lib/utils/imageUtils.ts b/apps/expo/lib/utils/imageUtils.ts index ed722e3450..f20393cbd5 100644 --- a/apps/expo/lib/utils/imageUtils.ts +++ b/apps/expo/lib/utils/imageUtils.ts @@ -124,6 +124,7 @@ export const fetchImageExtension = async (url: string): Promise = * @param defaultExt The default extension to use if we can't determine one * @returns A promise resolving to the extension */ + export const getImageExtension = async (url: string, defaultExt = 'jpg'): Promise => { // First check if URL already has an extension const inferredExt = inferImageExtension(url); diff --git a/apps/expo/package.json b/apps/expo/package.json index cf4d162c06..09fddd11c1 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -130,7 +130,7 @@ }, "devDependencies": { "@babel/core": "^7.20.0", - "@biomejs/biome": "2.0.4", + "@biomejs/biome": "2.4.6", "@types/lodash.debounce": "^4.0.9", "@types/react": "~19.0.10", "@types/ungap__structured-clone": "^1.2.0", diff --git a/apps/expo/utils/polyfills.ts b/apps/expo/utils/polyfills.ts index 26fc5b6edf..1281f1668a 100644 --- a/apps/expo/utils/polyfills.ts +++ b/apps/expo/utils/polyfills.ts @@ -3,8 +3,9 @@ import { Platform } from 'react-native'; if (Platform.OS !== 'web') { const setupPolyfills = async () => { - // @ts-ignore - const { polyfillGlobal } = await import('react-native/Libraries/Utilities/PolyfillFunctions'); + const { polyfillGlobal } = (await import( + 'react-native/Libraries/Utilities/PolyfillFunctions' + )) as { polyfillGlobal: (name: string, getValue: () => unknown) => void }; const { TextEncoderStream, TextDecoderStream } = await import( '@stardazed/streams-text-encoding' diff --git a/apps/guides/app/api/dev/generate-batch/route.ts b/apps/guides/app/api/dev/generate-batch/route.ts index c9fe995643..40998bd2e5 100644 --- a/apps/guides/app/api/dev/generate-batch/route.ts +++ b/apps/guides/app/api/dev/generate-batch/route.ts @@ -22,7 +22,7 @@ export async function POST(request: Request) { const requestData = await request.json(); // Validate request - const count = Number.parseInt(requestData.count) || 5; + const count = Number.parseInt(requestData.count, 10) || 5; if (count <= 0 || count > 20) { return NextResponse.json( diff --git a/apps/guides/app/dev/generate/page.tsx b/apps/guides/app/dev/generate/page.tsx index f9b159f132..035dd28aae 100644 --- a/apps/guides/app/dev/generate/page.tsx +++ b/apps/guides/app/dev/generate/page.tsx @@ -261,7 +261,6 @@ export default function GeneratePage() {
- {/** biome-ignore lint/nursery/useUniqueElementIds: ignore */} - {/** biome-ignore lint/nursery/useUniqueElementIds: ignore */}