Mobile Guidelines
Mobile Guidelines
Section titled “Mobile Guidelines”- React Native with Expo
- React Navigation (native stack)
- NativeWind (Tailwind CSS for React Native)
- Zustand (state management)
- TanStack Query (data fetching)
- Better Auth (authentication)
- Lucide React Native (icons)
- Jest + React Native Testing Library (testing)
- EAS Build (builds and deployment)
Getting Started
Section titled “Getting Started”Development
Section titled “Development”# Start development serverpnpm --filter=@lonestone/mobile dev
# Start on specific platformpnpm --filter=@lonestone/mobile iospnpm --filter=@lonestone/mobile android
# Run in web browser (for quick testing)pnpm --filter=@lonestone/mobile webBuilding
Section titled “Building”# Development build (requires Expo Go or dev client)pnpm --filter=@lonestone/mobile build:dev
# Preview build (internal distribution)pnpm --filter=@lonestone/mobile build:preview
# Production buildpnpm --filter=@lonestone/mobile build:productionTesting
Section titled “Testing”# Run testspnpm --filter=@lonestone/mobile test
# Run tests in watch modepnpm --filter=@lonestone/mobile test:watchProject Structure
Section titled “Project Structure”apps/mobile/├── src/│ ├── navigation/ # Navigation configuration│ │ ├── types.ts # Navigation types│ │ └── root-navigator.tsx│ └── screens/ # Global screens├── features/ # Feature modules│ ├── common/│ │ ├── hooks/│ │ └── utils/│ └── auth/│ ├── components/ # Auth screens and components│ ├── hooks/│ └── utils/├── components/ # Shared components├── lib/ # External library configs│ ├── auth-client.ts│ └── query-client.tsx├── store/ # Zustand stores│ ├── auth-store.ts│ ├── theme-store.ts│ └── index.ts├── config/ # App configuration│ └── env.config.ts # Environment variables├── assets/ # Images, fonts, etc.├── App.tsx # App entry point└── global.css # Global stylesNavigation
Section titled “Navigation”Defining Routes
Section titled “Defining Routes”Routes are defined in app/navigation/types.ts:
export type RootStackParamList = { Login: undefined Register: undefined Home: undefined Profile: undefined}Using Navigation
Section titled “Using Navigation”import { useNavigation } from '@react-navigation/native'import type { RootNavigationProp } from '@/src/navigation/types'
function MyScreen() { const navigation = useNavigation<RootNavigationProp>()
const handleNavigate = () => { navigation.navigate('Profile') }
return <Button onPress={handleNavigate}>Go to Profile</Button>}Conditional Navigation
Section titled “Conditional Navigation”The app uses conditional navigation based on authentication state. See src/navigation/root-navigator.tsx for the implementation.
State Management
Section titled “State Management”Using Zustand Stores
Section titled “Using Zustand Stores”import { useAuthStore } from '@/store'
function MyComponent() { const user = useAuthStore((state) => state.user) const setUser = useAuthStore((state) => state.setUser)
return <Text>{user?.name}</Text>}Creating New Stores
Section titled “Creating New Stores”import { create } from 'zustand'
interface MyState { count: number increment: () => void}
export const useMyStore = create<MyState>((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })),}))Data Fetching
Section titled “Data Fetching”Using TanStack Query
Section titled “Using TanStack Query”Structure query options in features/*/utils/*-queries.ts:
import { apiClient } from '@lonestone/openapi-generator'
export function fetchUserQueryOptions(userId: string) { return { queryKey: ['users', userId], queryFn: () => apiClient.usersControllerFindOne({ path: { id: userId } }), }}
// In componentimport { useQuery } from '@tanstack/react-query'
function UserProfile({ userId }: { userId: string }) { const { data: user, isLoading } = useQuery(fetchUserQueryOptions(userId))
if (isLoading) return <ActivityIndicator /> return <Text>{user?.name}</Text>}Authentication
Section titled “Authentication”Using Better Auth
Section titled “Using Better Auth”The mobile app uses the same Better Auth setup as the web apps:
import { authClient } from '@/lib/auth-client'import { useAuthStore } from '@/store'
// Sign inconst { data, error } = await authClient.signIn.email({ email, password,})
if (data?.user) { setUser(data.user)}
// Sign outawait authClient.signOut()logout()Deep Linking
Section titled “Deep Linking”Configuration
Section titled “Configuration”Deep links are configured using a custom scheme (e.g., lonestone://):
export default { scheme: process.env.EXPO_PUBLIC_SCHEME || 'lonestone', // ...}Handling Deep Links
Section titled “Handling Deep Links”Use the useLinking hook for deep link navigation:
import { useLinking } from '@/navigation/use-linking'import { useNavigation } from '@react-navigation/native'
function MyComponent() { const navigation = useNavigation()
// Handle deep link on mount useEffect(() => { const handleDeepLink = async (url: string) => { // Parse URL and extract params const { hostname, queryParams } = Linking.parse(url)
if (hostname === 'reset-password' && queryParams?.token) { navigation.navigate('ResetPassword', { token: queryParams.token }) } }
// Get initial URL Linking.getInitialURL().then((url) => { if (url) handleDeepLink(url) })
// Listen for incoming links const subscription = Linking.addEventListener('url', (event) => { handleDeepLink(event.url) })
return () => subscription.remove() }, [])}Testing Deep Links
Section titled “Testing Deep Links”Test with:
# iOS Simulatorxcrun simctl openurl booted "lonestone://reset-password?token=YOUR_TOKEN"
# Or use uri-scheme utilitynpx uri-scheme open "lonestone://reset-password?token=YOUR_TOKEN" --iosReplace lonestone:// with your project’s custom scheme.
Universal Links (Production)
Section titled “Universal Links (Production)”For production apps, use Universal Links (iOS) and App Links (Android) instead of custom schemes:
- Configure
apple-app-site-associationandassetlinks.jsonon your domain - Use the utility script:
pnpm create:universallinks - Update iOS entitlements and Android manifest
- Configure API to send universal URLs instead of custom scheme URLs
Styling with NativeWind
Section titled “Styling with NativeWind”Using Tailwind Classes
Section titled “Using Tailwind Classes”import { View, Text } from 'react-native'
function MyComponent() { return ( <View className="flex-1 justify-center items-center bg-white dark:bg-gray-900"> <Text className="text-2xl font-bold text-gray-900 dark:text-white"> Hello World </Text> </View> )}Dark Mode
Section titled “Dark Mode”Use the useTheme hook for dark mode:
import { useTheme } from '@/features/common/hooks/use-theme'
function MyComponent() { const { colorScheme, toggleColorScheme, isDark } = useTheme()
return ( <Button onPress={toggleColorScheme}> {isDark ? 'Light Mode' : 'Dark Mode'} </Button> )}Environment Variables
Section titled “Environment Variables”Configuration
Section titled “Configuration”Environment variables are validated with Zod in config/env.config.ts:
import { z } from 'zod'import { EXPO_PUBLIC_API_URL } from 'react-native-dotenv'
export const configValidationSchema = z.object({ EXPO_PUBLIC_API_URL: z.string().url(),})
export const config = { apiUrl: EXPO_PUBLIC_API_URL,} as constPublic environment variables must be prefixed with EXPO_PUBLIC_:
EXPO_PUBLIC_API_URL=http://localhost:3000Import from the config object, not directly from react-native-dotenv:
import { config } from '@/config/env.config'
const apiUrl = config.apiUrlComponents
Section titled “Components”Creating Reusable Components
Section titled “Creating Reusable Components”import React from 'react'import { TouchableOpacity, Text, ActivityIndicator } from 'react-native'
interface ButtonProps { children: React.ReactNode onPress: () => void isLoading?: boolean variant?: 'primary' | 'secondary'}
export function Button({ children, onPress, isLoading = false, variant = 'primary',}: ButtonProps) { return ( <TouchableOpacity onPress={onPress} disabled={isLoading} className={`px-6 py-3 rounded-lg ${ variant === 'primary' ? 'bg-blue-600' : 'bg-gray-600' }`} > {isLoading ? ( <ActivityIndicator color="white" /> ) : ( <Text className="text-white font-semibold">{children}</Text> )} </TouchableOpacity> )}Testing
Section titled “Testing”Writing Tests
Section titled “Writing Tests”import { render, fireEvent } from '@testing-library/react-native'import { Button } from '@/components/button'
describe('Button', () => { it('should render correctly', () => { const { getByText } = render(<Button onPress={() => {}}>Click me</Button>) expect(getByText('Click me')).toBeTruthy() })
it('should call onPress when clicked', () => { const mockOnPress = jest.fn() const { getByText } = render( <Button onPress={mockOnPress}>Click me</Button>, )
fireEvent.press(getByText('Click me')) expect(mockOnPress).toHaveBeenCalledTimes(1) })})EAS Build
Section titled “EAS Build”Build Profiles
Section titled “Build Profiles”The app has three build profiles defined in eas.json:
- development: For development with Expo Go or dev client
- preview: Internal distribution (APK for Android)
- production: Store distribution (AAB for Android, IPA for iOS)
Building
Section titled “Building”# Development buildeas build --profile development --platform ios
# Preview buildeas build --profile preview --platform all
# Production buildeas build --profile production --platform allSwitching from Expo Go to Dev Client
Section titled “Switching from Expo Go to Dev Client”When you need features that Expo Go doesn’t support:
-
Create a development build:
Terminal window eas build --profile development --platform ios -
Install the dev client on your device
-
Start the dev server:
Terminal window pnpm --filter=@lonestone/mobile dev -
Open the app using the dev client instead of Expo Go
Use Lucide React Native icons only:
import { User, Settings } from 'lucide-react-native'
function MyComponent() { return ( <View> <User size={24} color="black" /> <Settings size={24} color="gray" /> </View> )}Best Practices
Section titled “Best Practices”- Follow TypeScript best practices: No
anytypes, use proper interfaces - Use functional components: Avoid class components
- Keep components small: Single responsibility principle
- Use custom hooks: Extract complex logic into hooks
- Test important functionality: Write tests for critical features
- Follow the project structure: Maintain consistency with web apps where possible
- Use error boundaries: Handle errors gracefully
- Optimize images: Use appropriate image formats and sizes
- Handle loading states: Show loaders for async operations
- Handle offline scenarios: Consider network connectivity