mobile

React Native Quick Start Guide

Initial Setup

Expo is recommended because:

  • Easier setup and development
  • Built-in OTA updates
  • Managed workflow (less native code)
  • Great developer experience

2. Install Expo CLI

npm install -g expo-cli
# or
npm install -g @expo/cli

3. Create New App in Monorepo

cd apps
npx create-expo-app mobile --template blank-typescript

4. Update Root package.json

Add mobile scripts:

{
  "scripts": {
    "dev:mobile": "cd apps/mobile && npm start",
    "dev:mobile:ios": "cd apps/mobile && npm run ios",
    "dev:mobile:android": "cd apps/mobile && npm run android"
  }
}

5. Install Core Dependencies

cd apps/mobile
npm install @supabase/supabase-js @react-native-async-storage/async-storage
npm install @react-navigation/native @react-navigation/native-stack @react-navigation/bottom-tabs
npm install react-native-screens react-native-safe-area-context
npm install react-hook-form zod
npm install date-fns
npm install @tanstack/react-query

6. Install Development Dependencies

npm install --save-dev @types/react @types/react-native

Project Structure

apps/mobile/
├── src/
│   ├── screens/
│   │   ├── auth/
│   │   │   ├── SignInScreen.tsx
│   │   │   └── SignUpScreen.tsx
│   │   ├── dashboard/
│   │   │   ├── DashboardScreen.tsx
│   │   │   ├── EntryDetailScreen.tsx
│   │   │   └── EntryEditorScreen.tsx
│   │   └── settings/
│   │       └── SettingsScreen.tsx
│   ├── components/
│   │   ├── ui/
│   │   │   ├── Button.tsx
│   │   │   ├── Card.tsx
│   │   │   ├── Input.tsx
│   │   │   └── Textarea.tsx
│   │   └── EntryCard.tsx
│   ├── navigation/
│   │   ├── AppNavigator.tsx
│   │   ├── AuthNavigator.tsx
│   │   └── MainNavigator.tsx
│   ├── services/
│   │   ├── supabase.ts
│   │   └── api.ts
│   ├── hooks/
│   │   ├── useAuth.ts
│   │   └── useEntries.ts
│   ├── contexts/
│   │   └── AuthContext.tsx
│   ├── utils/
│   │   ├── dateUtils.ts
│   │   └── fonts.ts
│   └── types/
│       └── index.ts
├── app.json
├── package.json
└── tsconfig.json

Basic Implementation Examples

1. Supabase Client Setup

// src/services/supabase.ts
import { createClient } from '@supabase/supabase-js'
import AsyncStorage from '@react-native-async-storage/async-storage'

const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL!
const supabaseKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!

export const supabase = createClient(supabaseUrl, supabaseKey, {
  auth: {
    storage: AsyncStorage,
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: false,
  },
})

2. Auth Context

// src/contexts/AuthContext.tsx
import React, { createContext, useContext, useEffect, useState } from 'react'
import { Session, User } from '@supabase/supabase-js'
import { supabase } from '../services/supabase'

interface AuthContextType {
  user: User | null
  session: Session | null
  loading: boolean
  signOut: () => Promise<void>
}

const AuthContext = createContext<AuthContextType | undefined>(undefined)

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null)
  const [session, setSession] = useState<Session | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    // Get initial session
    supabase.auth.getSession().then(({ data: { session } }) => {
      setSession(session)
      setUser(session?.user ?? null)
      setLoading(false)
    })

    // Listen for auth changes
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (_event, session) => {
        setSession(session)
        setUser(session?.user ?? null)
      }
    )

    return () => subscription.unsubscribe()
  }, [])

  const signOut = async () => {
    await supabase.auth.signOut()
  }

  return (
    <AuthContext.Provider value={{ user, session, loading, signOut }}>
      {children}
    </AuthContext.Provider>
  )
}

export function useAuth() {
  const context = useContext(AuthContext)
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider')
  }
  return context
}

3. Navigation Setup

// src/navigation/AppNavigator.tsx
import React from 'react'
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { useAuth } from '../contexts/AuthContext'
import AuthNavigator from './AuthNavigator'
import MainNavigator from './MainNavigator'

const Stack = createNativeStackNavigator()

export default function AppNavigator() {
  const { user, loading } = useAuth()

  if (loading) {
    return <LoadingScreen />
  }

  return (
    <NavigationContainer>
      <Stack.Navigator screenOptions={{ headerShown: false }}>
        {user ? (
          <Stack.Screen name="Main" component={MainNavigator} />
        ) : (
          <Stack.Screen name="Auth" component={AuthNavigator} />
        )}
      </Stack.Navigator>
    </NavigationContainer>
  )
}

4. Dashboard Screen Example

// src/screens/dashboard/DashboardScreen.tsx
import React, { useState, useEffect } from 'react'
import { View, FlatList, StyleSheet } from 'react-native'
import { useQuery } from '@tanstack/react-query'
import { apiRequest } from '../../services/api'
import EntryCard from '../../components/EntryCard'
import { JournalEntry } from '../../types'

export default function DashboardScreen() {
  const [page, setPage] = useState(0)
  const pageSize = 20

  const { data, isLoading, refetch } = useQuery({
    queryKey: ['entries', page],
    queryFn: async () => {
      const response = await apiRequest<{ entries: JournalEntry[] }>(
        `/api/journal/entries?page=${page}&limit=${pageSize}`
      )
      return response.entries
    },
  })

  const loadMore = () => {
    if (!isLoading && data && data.length === pageSize) {
      setPage((p) => p + 1)
    }
  }

  return (
    <View style={styles.container}>
      <FlatList
        data={data || []}
        renderItem={({ item }) => <EntryCard entry={item} />}
        keyExtractor={(item) => item.id}
        onEndReached={loadMore}
        onEndReachedThreshold={0.5}
        refreshing={false}
        onRefresh={refetch}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
})

Environment Setup

1. Create .env file

# apps/mobile/.env
EXPO_PUBLIC_SUPABASE_URL=your_supabase_url
EXPO_PUBLIC_SUPABASE_ANON_KEY=your_anon_key
EXPO_PUBLIC_API_URL=https://your-backend.railway.app

2. Install dotenv

npm install dotenv

3. Update app.json

{
  "expo": {
    "name": "HiveJournal",
    "slug": "hivejournal",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "automatic",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.hivejournal.app"
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      },
      "package": "com.hivejournal.app"
    },
    "web": {
      "favicon": "./assets/favicon.png"
    }
  }
}

Running the App

Development

# Start Expo dev server
npm run dev:mobile

# Run on iOS simulator
npm run dev:mobile:ios

# Run on Android emulator
npm run dev:mobile:android

Building

# Install EAS CLI
npm install -g eas-cli

# Login to Expo
eas login

# Configure EAS
eas build:configure

# Build for iOS
eas build --platform ios

# Build for Android
eas build --platform android

Next Steps

  1. Set up the project structure
  2. Implement authentication
  3. Build dashboard screen
  4. Add entry detail and editor
  5. Implement remaining features

See REACT_NATIVE_PLAN.md for detailed feature breakdown and roadmap.

REACT NATIVE QUICKSTART — Docs | HiveJournal