Mobile App Development Guide
You are working on the 100cims mobile app (packages/app ), an Expo React Native application for mountaineering.
Key Files
File Purpose
app/_layout.tsx
Root layout with providers
app.config.ts
Expo configuration, plugins, EAS project ID
lib/api-client.ts
openapi-fetch client with auth
types/api.ts
Generated OpenAPI types (DO NOT EDIT)
domains/*/
Feature-specific business logic
components/ui/
Reusable UI components
translations/*.json
i18n message files
Stack
-
Expo SDK 54 with React Native 0.81 (new architecture enabled)
-
expo-router 6 for file-based navigation
-
NativeWind 4 for styling (Tailwind CSS)
-
React Query 5 for server state
-
React Intl for i18n (en, ca, es)
Directory Structure
-
/app/ : expo-router screens (file-based routing)
-
/domains/ : Feature-specific business logic
-
/hooks/ : Shared React hooks
-
/lib/ : Utility functions (api-client, auth, dates, images, location, validation)
-
/components/ : Reusable UI components
-
/translations/ : i18n message files
-
/types/ : Shared TypeScript types (api.ts auto-generated, mountain.ts, etc.)
Domain Pattern
Each domain folder typically contains:
-
*.api.ts : React Query hooks for API calls
-
*.utils.ts : Domain-specific utilities
-
*.types.ts : TypeScript types (optional)
Example: /domains/user/user.api.ts exports useUser() , useUpdateUser() , etc.
Shared Hooks
Hook Location Purpose
useImagePicker
hooks/use-image-picker.ts
Pick and optimize images with state management
useMountainSelection
hooks/use-mountain-selection.ts
Manage mountain selection in challenge screens
useLocation
hooks/use-location.ts
Get user's current location
useIsKeyboardVisible
hooks/use-is-keyboard-visible.ts
Detect keyboard visibility
useDistanceToTarget
hooks/use-distance-to-target.ts
Calculate distance to coordinates
Shared Utilities
Utility Location Purpose
validateMountainForm
lib/mountain-validation.ts
Validate mountain form fields
getImageOptimized
lib/images.ts
Optimize images for upload
cleanText
lib/strings.ts
Normalize text for search
getDistanceInKm
lib/location.ts
Calculate distance between coordinates
Shared Types
Type Location Purpose
MountainData
types/mountain.ts
Base mountain data from API
MountainWithChallengeCount
types/mountain.ts
Mountain with challenge count
NewMountainData
types/mountain.ts
New mountain for local state
MountainInfo
types/mountain.ts
Minimal mountain info for display
Key Patterns
Using Shared Hooks
// Image picker with optimization import { useImagePicker } from '@/hooks/use-image-picker';
const { imageUri, imageBase64, hasChanged, pickImage, reset } = useImagePicker({ initialUri: existingImageUrl, });
// Mountain selection state management import { useMountainSelection } from '@/hooks/use-mountain-selection';
const { selectedMountainIds, newMountains, selectedMountainsForDisplay, totalMountainCount, handleSelectionChange, handleAddNewMountain, handleRemoveNewMountain, } = useMountainSelection({ allMountains });
Form Validation
import { validateMountainForm } from '@/lib/mountain-validation';
const validation = validateMountainForm({ name, location, height, latitude, longitude }, intl); if (!validation.valid) { return Alert.alert(validation.error); } // Use validation.data.name, validation.data.height (parsed numbers), etc.
API Calls
// Use openapi-fetch client from lib/api-client.ts import apiClient from '@/lib/api-client'; import { useQuery } from '@tanstack/react-query';
export const useUser = (userId: string) => { return useQuery({ queryKey: ['user', userId], queryFn: async () => { const { data, error } = await apiClient.GET('/api/user/{id}', { params: { path: { id: userId } } }); if (error) throw error; return data; } }); };
Infinite Scroll with Pagination
import { useInfiniteQuery } from '@tanstack/react-query';
export const useItemsList = () => { return useInfiniteQuery({ queryKey: ['items', 'list'], initialPageParam: 1, queryFn: async ({ pageParam }) => { const { data, error } = await apiClient.GET('/api/items', { params: { query: { page: pageParam, limit: 50 } }, }); if (error) throw error; return data.message; }, getNextPageParam: (lastPage) => { if (lastPage?.pagination?.hasMore) { return lastPage.pagination.page + 1; } return undefined; }, }); };
// In component: const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useItemsList(); const items = data?.pages?.flatMap((p) => p?.items ?? []) ?? [];
<FlatList data={items} onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()} onEndReachedThreshold={0.5} ListFooterComponent={isFetchingNextPage ? <ActivityIndicator /> : null} />
Authentication
-
JWT stored in AsyncStorage
-
setAuthToken(token) in lib/api-client.ts sets Authorization header
-
Protected routes check auth state in _layout.tsx
Styling with NativeWind
<View className="flex-1 bg-white dark:bg-gray-900"> <Text className="text-lg font-bold text-gray-900">Title</Text> </View>
Navigation
import { router } from 'expo-router';
// Navigate router.push('/user/123');
// With params router.push({ pathname: '/mountain/[id]', params: { id: '456' } });
Translations (CRITICAL)
All user-facing strings MUST use intl.formatMessage
- never hardcode text.
import { useIntl } from 'react-intl';
const { formatMessage } = useIntl();
// Simple string const title = formatMessage({ defaultMessage: 'Settings' });
// In JSX <ThemedText>{intl.formatMessage({ defaultMessage: "Save changes" })}</ThemedText>
// With FormattedMessage component import { FormattedMessage } from 'react-intl'; <FormattedMessage defaultMessage="Welcome back" />
Translation Workflow:
-
Add strings in code using defaultMessage (English text)
-
Run extraction: yarn translations:extract
-
Run compilation: yarn translations:generate
-
English (translations/en.json ) is auto-generated
-
Manually copy new keys to translations/ca.json (Catalan) and translations/es.json (Spanish)
-
Translate the values in ca.json and es.json
Translation Files:
-
translations/raw-en.json
-
Extracted messages (intermediate)
-
translations/en.json
-
Compiled English (used by app)
-
translations/ca.json
-
Catalan translations (manual)
-
translations/es.json
-
Spanish translations (manual)
Common Tasks
Add New Screen
-
Create file in /app/ (e.g., /app/settings.tsx )
-
Export default React component
-
File name becomes route path
Add API Integration
-
Ensure backend types are current: yarn generate-api-types
-
Create domain API file or add to existing
-
Use React Query for data fetching
-
Handle loading/error states
Add/Update Translations
-
Use intl.formatMessage({ defaultMessage: "English text" }) in code
-
Run yarn translations:extract to extract all messages
-
Run yarn translations:generate to compile English
-
Copy new keys from en.json to ca.json and es.json
-
Translate the values in ca.json and es.json
Add Native Module
npx expo install <package-name>
May need to rebuild: npx expo prebuild --clean
Environment Variables
Must prefix with EXPO_PUBLIC_ to access in app:
-
EXPO_PUBLIC_API_URL : Backend URL
-
EXPO_PUBLIC_REACT_ANALYTICS_KEY : Analytics key
-
OAuth client IDs for Google/Apple
Debugging
Common Issues
-
Type errors: Regenerate API types with yarn generate-api-types
-
Navigation issues: Check expo-router file structure
-
Styling not applying: Verify NativeWind babel config, global.css import
-
OAuth not working: Check google-services.json, Apple config in app.config.ts
Helpful Commands
npx expo start --clear # Clear cache npx expo-doctor # Diagnose issues npx uri-scheme list # Check deep linking
Build & Deploy
eas build --profile development # Dev build with expo-dev-client eas build --profile preview # Internal preview eas build --profile production # Production release
See eas.json for build profiles.