Skip to content

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

TechnologyPurpose
React NativeCross-platform framework
ExpoDevelopment platform
Expo RouterFile-based navigation
TypeScriptType safety
React Native PaperMaterial Design UI
Redux ToolkitClient state
TanStack QueryServer state + offline
JestTesting

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.json

Core 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                       │
└─────────────────────────────────────────────────────┘

Tab Navigation (Main)

(tabs)/
├── index.tsx      → Dashboard
├── projects.tsx   → Projects list
├── leads.tsx      → CRM/Leads
└── profile.tsx    → User profile

Stack Navigation (Drill-down)

quote/
├── [id].tsx       → Quote details
└── create.tsx     → New quote wizard

project/
└── [id].tsx       → Project details

Auth Flow (Separate)

(auth)/
├── login.tsx
└── forgot-password.tsx

Feature Module Structure

features/leads/
├── components/
│   ├── LeadList.tsx
│   ├── LeadCard.tsx
│   └── LeadForm.tsx
├── hooks/
│   ├── useLeads.ts
│   └── useCreateLead.ts
├── api/
│   └── leadsApi.ts
├── types.ts
└── index.ts

Mobile-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 android

Git Hooks

HookAction
pre-commitESLint + Prettier on staged files
commit-msgcommitlint validation
pre-pushType 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

Built with VitePress | v1.1.0