Mobile Architecture
The Vulcan mobile app is built with React Native and Expo, following a feature-based architecture similar to the web frontend.
Technology Stack
| Technology | Purpose |
|---|---|
| React Native | Cross-platform framework |
| Expo | Development platform |
| Expo Router | File-based navigation |
| TypeScript | Type safety |
| React Native Paper | Material Design UI |
| Redux Toolkit | Client state |
| TanStack Query | Server state + offline |
| Jest | Testing |
Directory Structure
vulcan-mobile/
├── assets/ # Static assets (images, fonts)
├── src/
│ ├── app/ # Expo Router routes (thin layer)
│ │ ├── (tabs)/ # Tab navigation group
│ │ │ ├── index.tsx # Dashboard
│ │ │ ├── projects.tsx # Projects list
│ │ │ ├── leads.tsx # Leads/CRM
│ │ │ └── profile.tsx # User profile
│ │ ├── (auth)/ # Auth flow screens
│ │ │ ├── login.tsx
│ │ │ └── forgot-password.tsx
│ │ ├── quote/ # Quote flow
│ │ │ ├── [id].tsx
│ │ │ └── create.tsx
│ │ └── _layout.tsx # Root layout
│ ├── features/ # Feature modules
│ │ ├── dashboard/
│ │ ├── leads/
│ │ ├── quotation/
│ │ ├── projects/
│ │ ├── ai/ # Voice transcription
│ │ └── site-assessment/
│ ├── components/ # Shared UI components
│ ├── hooks/ # Shared custom hooks
│ ├── store/ # Redux store and slices
│ ├── lib/ # Pure utilities
│ ├── api/ # Generated API clients
│ ├── services/ # External services
│ ├── types/ # Shared TypeScript types
│ └── constants/ # App-wide constants
├── app.json # Expo configuration
├── eas.json # EAS Build configuration
├── package.json
└── tsconfig.jsonCore Principles
Mobile-First, Not Mobile-Reduced
The mobile app is a purpose-built tool for field workers, not a reduced version of the desktop app.
- Full functionality available offline
- Voice input as primary alternative to typing
- Large touch targets for use with gloves
- GPS and camera integration
Offline-First Architecture
┌─────────────────────────────────────────────────────┐
│ Mobile App │
│ ┌───────────────────────────────────────────────┐ │
│ │ TanStack Query │ │
│ │ ┌─────────────┐ ┌─────────────────────┐ │ │
│ │ │ Cache │ ←──→ │ Persistence Layer │ │ │
│ │ │ (memory) │ │ (AsyncStorage) │ │ │
│ │ └─────────────┘ └─────────────────────┘ │ │
│ └───────────────────────────────────────────────┘ │
│ ↕ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Mutation Queue │ │
│ │ [pending mutations when offline] │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
↕
[When Online]
↕
┌─────────────────────────────────────────────────────┐
│ Backend APIs │
└─────────────────────────────────────────────────────┘Navigation Structure
Tab Navigation (Main)
(tabs)/
├── index.tsx → Dashboard
├── projects.tsx → Projects list
├── leads.tsx → CRM/Leads
└── profile.tsx → User profileStack Navigation (Drill-down)
quote/
├── [id].tsx → Quote details
└── create.tsx → New quote wizard
project/
└── [id].tsx → Project detailsAuth Flow (Separate)
(auth)/
├── login.tsx
└── forgot-password.tsxFeature Module Structure
features/leads/
├── components/
│ ├── LeadList.tsx
│ ├── LeadCard.tsx
│ └── LeadForm.tsx
├── hooks/
│ ├── useLeads.ts
│ └── useCreateLead.ts
├── api/
│ └── leadsApi.ts
├── types.ts
└── index.tsMobile-Specific Features
Voice Transcription
typescript
// features/ai/hooks/useVoiceTranscription.ts
export function useVoiceTranscription() {
const [isRecording, setIsRecording] = useState(false);
const [transcript, setTranscript] = useState('');
const startRecording = async () => {
// Request microphone permission
// Start audio recording
// Send to Azure Speech Services
};
const stopRecording = async () => {
// Stop recording
// Get transcription result
};
return { isRecording, transcript, startRecording, stopRecording };
}Site Photos
typescript
// features/site-assessment/hooks/useSitePhotos.ts
export function useSitePhotos(projectId: string) {
const takePhoto = async () => {
const result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
quality: 0.8,
exif: true, // Include GPS data
});
// Upload and associate with project
};
return { photos, takePhoto, deletePhoto };
}Offline Support
typescript
// hooks/useOffline.ts
export function useOffline() {
const [isOnline, setIsOnline] = useState(true);
const [pendingMutations, setPendingMutations] = useState(0);
useEffect(() => {
const unsubscribe = NetInfo.addEventListener(state => {
setIsOnline(state.isConnected ?? false);
});
return unsubscribe;
}, []);
return { isOnline, pendingMutations };
}Commands
bash
# Development
npx expo start # Start dev server
npx expo run:ios # Run on iOS simulator
npx expo run:android # Run on Android emulator
# Testing
npm run test # Run Jest tests
npm run lint # Run ESLint
npm run type-check # TypeScript check
# Building
eas build --profile development --platform ios
eas build --profile preview --platform all
eas build --profile production --platform all
# Submitting
eas submit --platform ios
eas submit --platform androidGit Hooks
| Hook | Action |
|---|---|
pre-commit | ESLint + Prettier on staged files |
commit-msg | commitlint validation |
pre-push | Type check + test suite |
Gotchas
Important
- Use React Native Paper for all UI components
- Expo Router uses file-based routing - don't mix with React Navigation directly
- Test on both iOS and Android - behavior can differ
- Be careful with native modules - prefer Expo SDK when possible
- EAS Build is required for native code - can't use Expo Go for everything