Skip to content

πŸ›οΈ Architecture ​

Routing ​

The app uses Expo Router β€” the filesystem maps directly to screens. Route groups (parenthesized folders) never appear in URLs.

app/
β”œβ”€β”€ _layout.tsx              # Root Stack: Sentry init, providers
β”œβ”€β”€ (auth)/                  # Unauthenticated routes
β”‚   β”œβ”€β”€ _layout.tsx          # Redirects β†’ / if already authenticated
β”‚   β”œβ”€β”€ login.tsx            # Email/password login
β”‚   β”œβ”€β”€ register.tsx         # Email/password signup
β”‚   └── forgot-password.tsx  # Password reset
β”œβ”€β”€ (app)/                   # Protected routes (requires auth)
β”‚   β”œβ”€β”€ _layout.tsx          # Redirects β†’ /login if not authenticated
β”‚   β”œβ”€β”€ (tabs)/              # Bottom tab navigation
β”‚   β”‚   β”œβ”€β”€ _layout.tsx      # 3-tab layout: Map, Chat, Settings
β”‚   β”‚   β”œβ”€β”€ index.tsx        # Map tab
β”‚   β”‚   β”œβ”€β”€ chat.tsx         # LLM chat (streaming SSE)
β”‚   β”‚   └── settings.tsx     # Settings + sign-out
β”‚   └── modal.tsx            # Modal screen

Typed routes are enabled (experiments.typedRoutes in app.json) β€” use the href prop with string literals and TypeScript will catch invalid paths.

Authentication ​

Firebase Authentication via @react-native-firebase/auth (native modules β€” not the JS SDK).

auth/
β”œβ”€β”€ auth-context.tsx    # AuthProvider + useAuth() hook
β”œβ”€β”€ mutations.ts        # signIn, signUp, signOut, resetPassword
β”œβ”€β”€ map-auth-error.ts   # Firebase error codes β†’ user-friendly messages
└── index.ts            # Barrel re-export

Auth state drives the routing guards in (auth)/_layout.tsx and (app)/_layout.tsx:

tsx
// (app)/_layout.tsx β€” redirect to login if no user
const { user, isLoading } = useAuth();
if (!isLoading && !user) return <Redirect href="/login" />;

Firebase ID tokens are obtained via lib/firebase/auth.ts β†’ getFirebaseToken(), which is called automatically by the API client before every request.

Styling: Uniwind + CVA ​

All styling uses Uniwind β€” Tailwind CSS classes compiled to React Native StyleSheet at build time via Metro. There is no StyleSheet.create() in most components.

tsx
// Use className like on the web
<View className="flex-1 bg-background px-4">
  <Text className="text-foreground font-semibold text-lg">Hello</Text>
</View>

Design tokens (colors, spacing, radii, dark mode) are defined in packages/tailwind-config/global.css and re-exported in apps/mobile/global.css:

css
@import '@tellia-solutions/tailwind-config/global.css';
@source '../../packages/ui/src';

Merging Classes ​

Use cn() from @tellia-solutions/ui to conditionally merge classes (like clsx + tailwind-merge):

tsx
import { cn } from '@tellia-solutions/ui';

<View className={cn('p-4', isActive && 'bg-primary')} />;

Component Variants with CVA ​

For components with multiple visual states, use CVA (Class Variance Authority):

tsx
import { cva } from 'class-variance-authority';

const buttonVariants = cva('rounded-lg px-4 py-2', {
  variants: {
    variant: {
      primary: 'bg-primary text-primary-foreground',
      destructive: 'bg-destructive text-destructive-foreground',
    },
    size: {
      sm: 'text-sm',
      lg: 'text-lg',
    },
  },
  defaultVariants: { variant: 'primary', size: 'sm' },
});

See packages/ui/CLAUDE.md for the full CVA patterns used in the shared UI package.

Bottom Sheets ​

Uses react-native-actions-sheet. Sheets are registered in sheets/registry.ts and typed via module augmentation in sheets.d.ts.

Available Sheets ​

Sheet IDPurpose
confirm-sheetGeneric yes/no confirmation
create-note-sheetCreate a geo-located note
note-detail-sheetView/edit an existing note
field-action-sheetActions on a map field
field-save-action-sheetSave a drawn field
parcel-action-sheetActions on a parcel

Opening a Sheet ​

tsx
import SheetManager from 'react-native-actions-sheet';

await SheetManager.show('confirm-sheet', {
  payload: { title: 'Delete field?', onConfirm: handleDelete },
});

SheetProvider must wrap the app (it does, in the root _layout.tsx).

Path Alias ​

@/* maps to the apps/mobile/ root (set in tsconfig.json). Always use the alias in imports:

tsx
// βœ…
import { useAuth } from '@/auth/auth-context';

// ❌
import { useAuth } from '../../auth/auth-context';

Platform-Specific Files ​

Append .ios.tsx or .android.tsx to override a file per platform. Metro resolves these automatically:

components/
β”œβ”€β”€ icon-symbol.tsx          # Fallback
β”œβ”€β”€ icon-symbol.ios.tsx      # iOS-specific (SF Symbols)
└── icon-symbol.android.tsx  # Android-specific (if needed)