React Native Setup and Configuration
Overview
React Native setup with modern development patterns, TypeScript integration, and cross-platform mobile development, following AzmX development standards for scalable mobile applications.
Core Dependencies
From typical React Native package.json:
{
"dependencies": {
"react": "18.3.1",
"react-native": "0.75.2",
"@react-navigation/native": "^6.1.18",
"@react-navigation/stack": "^6.4.1",
"@react-navigation/bottom-tabs": "^6.6.1",
"react-native-screens": "^3.34.0",
"react-native-safe-area-context": "^4.10.8",
"react-native-gesture-handler": "^2.18.1",
"@react-native-async-storage/async-storage": "^1.24.0",
"react-native-vector-icons": "^10.1.0",
"react-native-svg": "^15.6.0"
},
"devDependencies": {
"@types/react": "^18.3.5",
"@types/react-native": "^0.73.0",
"typescript": "^5.5.4",
"@react-native/eslint-config": "^0.75.2",
"@react-native/metro-config": "^0.75.2",
"@react-native/typescript-config": "^0.75.2",
"jest": "^29.7.0",
"@testing-library/react-native": "^12.6.1"
}
}
Project Structure
ReactNativeApp/
├── android/ # Android native code
├── ios/ # iOS native code
├── src/
│ ├── components/
│ │ ├── common/
│ │ │ ├── Button/
│ │ │ │ ├── Button.tsx
│ │ │ │ ├── Button.test.tsx
│ │ │ │ └── index.ts
│ │ │ ├── Input/
│ │ │ ├── Modal/
│ │ │ └── LoadingSpinner/
│ │ └── screens/
│ │ ├── Home/
│ │ ├── Profile/
│ │ └── Settings/
│ ├── screens/
│ │ ├── AuthScreens/
│ │ │ ├── LoginScreen.tsx
│ │ │ └── SignupScreen.tsx
│ │ ├── MainScreens/
│ │ │ ├── HomeScreen.tsx
│ │ │ └── ProfileScreen.tsx
│ │ └── index.ts
│ ├── navigation/
│ │ ├── AppNavigator.tsx
│ │ ├── AuthNavigator.tsx
│ │ └── TabNavigator.tsx
│ ├── services/
│ │ ├── api.ts
│ │ ├── auth.ts
│ │ ├── storage.ts
│ │ └── permissions.ts
│ ├── hooks/
│ │ ├── useApi.ts
│ │ ├── useAuth.ts
│ │ └── useStorage.ts
│ ├── types/
│ │ ├── navigation.ts
│ │ ├── api.ts
│ │ └── user.ts
│ ├── utils/
│ │ ├── constants.ts
│ │ ├── helpers.ts
│ │ └── validation.ts
│ ├── styles/
│ │ ├── colors.ts
│ │ ├── typography.ts
│ │ └── spacing.ts
│ └── App.tsx
├── __tests__/
├── metro.config.js
├── tsconfig.json
├── babel.config.js
└── package.json
TypeScript Configuration
TSConfig (tsconfig.json)
{
"extends": "@react-native/typescript-config/tsconfig.json",
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "./src",
"paths": {
"@/*": ["*"],
"@/components/*": ["components/*"],
"@/screens/*": ["screens/*"],
"@/navigation/*": ["navigation/*"],
"@/services/*": ["services/*"],
"@/hooks/*": ["hooks/*"],
"@/types/*": ["types/*"],
"@/utils/*": ["utils/*"],
"@/styles/*": ["styles/*"]
}
},
"include": [
"src/**/*",
"__tests__/**/*"
],
"exclude": [
"node_modules",
"android",
"ios"
]
}
Metro Configuration (metro.config.js)
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config')
/**
* Metro configuration
* https://facebook.github.io/metro/docs/configuration
*/
const config = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
resolver: {
alias: {
'@': './src',
},
},
}
module.exports = mergeConfig(getDefaultConfig(__dirname), config)
Type Definitions
Navigation Types (src/types/navigation.ts)
import { NavigatorScreenParams } from '@react-navigation/native'
export type RootStackParamList = {
Auth: NavigatorScreenParams<AuthStackParamList>
Main: NavigatorScreenParams<MainTabParamList>
Modal: {
title: string
content: string
}
}
export type AuthStackParamList = {
Login: undefined
Signup: undefined
ForgotPassword: undefined
}
export type MainTabParamList = {
Home: undefined
Search: undefined
Profile: {
userId?: string
}
Settings: undefined
}
export type HomeStackParamList = {
HomeScreen: undefined
Details: {
itemId: string
}
}
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}
API Types (src/types/api.ts)
// Base response structure
export interface ApiResponse<T = any> {
success: boolean
data?: T
message?: string
errors?: Record<string, string[]>
}
// Network request states
export type RequestState = 'idle' | 'loading' | 'success' | 'error'
export interface ApiError {
message: string
code?: string
statusCode?: number
}
export interface PaginationParams {
page: number
limit: number
sortBy?: string
sortOrder?: 'asc' | 'desc'
}
export interface PaginatedResponse<T> {
data: T[]
pagination: {
total: number
page: number
limit: number
totalPages: number
}
}
// Device and app info
export interface DeviceInfo {
platform: 'ios' | 'android'
version: string
buildNumber: string
deviceId: string
appVersion: string
}
User Types (src/types/user.ts)
export interface User {
id: string
email: string
firstName: string
lastName: string
avatar?: string
phoneNumber?: string
dateOfBirth?: string
role: UserRole
isActive: boolean
preferences: UserPreferences
createdAt: string
updatedAt: string
}
export type UserRole = 'user' | 'admin' | 'moderator'
export interface UserPreferences {
theme: 'light' | 'dark' | 'system'
language: string
notifications: NotificationPreferences
}
export interface NotificationPreferences {
push: boolean
email: boolean
sms: boolean
marketing: boolean
}
export interface AuthTokens {
accessToken: string
refreshToken: string
expiresAt: string
}
export interface LoginRequest {
email: string
password: string
deviceInfo: DeviceInfo
}
export interface SignupRequest {
email: string
password: string
firstName: string
lastName: string
phoneNumber?: string
deviceInfo: DeviceInfo
}
Styling System
Colors (src/styles/colors.ts)
export const colors = {
// Primary colors
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
},
// Neutral colors
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
},
// Semantic colors
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444',
info: '#3b82f6',
// Background colors
background: {
light: '#ffffff',
dark: '#000000',
},
// Text colors
text: {
primary: '#111827',
secondary: '#6b7280',
light: '#ffffff',
},
} as const
export type ColorKey = keyof typeof colors
Typography (src/styles/typography.ts)
import { TextStyle } from 'react-native'
export const typography = {
// Font families
fontFamily: {
regular: 'Inter-Regular',
medium: 'Inter-Medium',
semiBold: 'Inter-SemiBold',
bold: 'Inter-Bold',
},
// Font sizes
fontSize: {
xs: 12,
sm: 14,
base: 16,
lg: 18,
xl: 20,
'2xl': 24,
'3xl': 30,
'4xl': 36,
},
// Line heights
lineHeight: {
tight: 1.25,
snug: 1.375,
normal: 1.5,
relaxed: 1.625,
loose: 2,
},
// Text styles
styles: {
h1: {
fontSize: 30,
fontFamily: 'Inter-Bold',
lineHeight: 36,
},
h2: {
fontSize: 24,
fontFamily: 'Inter-SemiBold',
lineHeight: 30,
},
h3: {
fontSize: 20,
fontFamily: 'Inter-SemiBold',
lineHeight: 26,
},
body: {
fontSize: 16,
fontFamily: 'Inter-Regular',
lineHeight: 24,
},
caption: {
fontSize: 14,
fontFamily: 'Inter-Regular',
lineHeight: 20,
},
small: {
fontSize: 12,
fontFamily: 'Inter-Regular',
lineHeight: 16,
},
} as Record<string, TextStyle>,
} as const
Component Architecture
Button Component (src/components/common/Button/Button.tsx)
import React from 'react'
import {
TouchableOpacity,
Text,
ViewStyle,
TextStyle,
ActivityIndicator,
} from 'react-native'
import { colors, typography } from '@/styles'
export interface ButtonProps {
title: string
onPress: () => void
variant?: 'primary' | 'secondary' | 'outline' | 'ghost'
size?: 'small' | 'medium' | 'large'
disabled?: boolean
loading?: boolean
style?: ViewStyle
textStyle?: TextStyle
testID?: string
}
export const Button: React.FC<ButtonProps> = ({
title,
onPress,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
style,
textStyle,
testID,
}) => {
const getButtonStyle = (): ViewStyle => {
const baseStyle: ViewStyle = {
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
}
const sizeStyles = {
small: { paddingHorizontal: 12, paddingVertical: 6 },
medium: { paddingHorizontal: 16, paddingVertical: 10 },
large: { paddingHorizontal: 20, paddingVertical: 14 },
}
const variantStyles = {
primary: {
backgroundColor: colors.primary[500],
borderWidth: 0,
},
secondary: {
backgroundColor: colors.gray[500],
borderWidth: 0,
},
outline: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: colors.primary[500],
},
ghost: {
backgroundColor: 'transparent',
borderWidth: 0,
},
}
return {
...baseStyle,
...sizeStyles[size],
...variantStyles[variant],
...(disabled && { opacity: 0.5 }),
...style,
}
}
const getTextStyle = (): TextStyle => {
const sizeStyles = {
small: { fontSize: typography.fontSize.sm },
medium: { fontSize: typography.fontSize.base },
large: { fontSize: typography.fontSize.lg },
}
const variantStyles = {
primary: { color: colors.text.light },
secondary: { color: colors.text.light },
outline: { color: colors.primary[500] },
ghost: { color: colors.primary[500] },
}
return {
fontFamily: typography.fontFamily.semiBold,
...sizeStyles[size],
...variantStyles[variant],
...textStyle,
}
}
return (
<TouchableOpacity
style={getButtonStyle()}
onPress={onPress}
disabled={disabled || loading}
activeOpacity={0.8}
testID={testID}
>
{loading && (
<ActivityIndicator
size="small"
color={variant === 'primary' ? colors.text.light : colors.primary[500]}
style={{ marginRight: 8 }}
/>
)}
<Text style={getTextStyle()}>{title}</Text>
</TouchableOpacity>
)
}
export default Button
Navigation Setup
App Navigator (src/navigation/AppNavigator.tsx)
import React from 'react'
import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'
import { useAuth } from '@/hooks/useAuth'
import { AuthNavigator } from './AuthNavigator'
import { TabNavigator } from './TabNavigator'
import { RootStackParamList } from '@/types/navigation'
const Stack = createStackNavigator<RootStackParamList>()
export const AppNavigator: React.FC = () => {
const { isAuthenticated } = useAuth()
return (
<NavigationContainer>
<Stack.Navigator
screenOptions={{
headerShown: false,
}}
>
{isAuthenticated ? (
<Stack.Screen name="Main" component={TabNavigator} />
) : (
<Stack.Screen name="Auth" component={AuthNavigator} />
)}
</Stack.Navigator>
</NavigationContainer>
)
}
export default AppNavigator
Tab Navigator (src/navigation/TabNavigator.tsx)
import React from 'react'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import Icon from 'react-native-vector-icons/Feather'
import { colors } from '@/styles'
import { MainTabParamList } from '@/types/navigation'
import { HomeScreen } from '@/screens/MainScreens/HomeScreen'
import { ProfileScreen } from '@/screens/MainScreens/ProfileScreen'
import { SettingsScreen } from '@/screens/MainScreens/SettingsScreen'
const Tab = createBottomTabNavigator<MainTabParamList>()
export const TabNavigator: React.FC = () => {
return (
<Tab.Navigator
screenOptions={{
tabBarActiveTintColor: colors.primary[500],
tabBarInactiveTintColor: colors.gray[400],
tabBarLabelStyle: {
fontSize: 12,
fontFamily: 'Inter-Medium',
},
tabBarStyle: {
backgroundColor: colors.background.light,
borderTopColor: colors.gray[200],
},
headerShown: false,
}}
>
<Tab.Screen
name="Home"
component={HomeScreen}
options={{
tabBarIcon: ({ color, size }) => (
<Icon name="home" size={size} color={color} />
),
}}
/>
<Tab.Screen
name="Search"
component={HomeScreen}
options={{
tabBarIcon: ({ color, size }) => (
<Icon name="search" size={size} color={color} />
),
}}
/>
<Tab.Screen
name="Profile"
component={ProfileScreen}
options={{
tabBarIcon: ({ color, size }) => (
<Icon name="user" size={size} color={color} />
),
}}
/>
<Tab.Screen
name="Settings"
component={SettingsScreen}
options={{
tabBarIcon: ({ color, size }) => (
<Icon name="settings" size={size} color={color} />
),
}}
/>
</Tab.Navigator>
)
}
API Service Layer
API Service (src/services/api.ts)
import AsyncStorage from '@react-native-async-storage/async-storage'
import { ApiResponse, ApiError } from '@/types/api'
class ApiService {
private baseURL: string
private timeout: number
constructor() {
this.baseURL = __DEV__
? 'http://localhost:8000/api'
: 'https://api.azmx.sa/api'
this.timeout = 10000
}
private async getAuthToken(): Promise<string | null> {
try {
return await AsyncStorage.getItem('accessToken')
} catch (error) {
console.error('Failed to get auth token:', error)
return null
}
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
const token = await this.getAuthToken()
const config: RequestInit = {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
}
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), this.timeout)
try {
const response = await fetch(`${this.baseURL}${endpoint}`, {
...config,
signal: controller.signal,
})
clearTimeout(timeoutId)
if (!response.ok) {
throw new ApiError(
`HTTP error! status: ${response.status}`,
response.status.toString(),
response.status
)
}
const data = await response.json()
return data
} catch (error) {
clearTimeout(timeoutId)
if (error instanceof ApiError) {
throw error
}
throw new ApiError(
error instanceof Error ? error.message : 'Network error'
)
}
}
async get<T>(endpoint: string): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, { method: 'GET' })
}
async post<T>(endpoint: string, data?: any): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
})
}
async put<T>(endpoint: string, data?: any): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
})
}
async delete<T>(endpoint: string): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, { method: 'DELETE' })
}
}
export class ApiError extends Error {
constructor(
message: string,
public code?: string,
public statusCode?: number
) {
super(message)
this.name = 'ApiError'
}
}
export const apiService = new ApiService()
export default apiService
Custom Hooks
useAuth Hook (src/hooks/useAuth.ts)
import { useState, useEffect, useCallback } from 'react'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { User, AuthTokens, LoginRequest, SignupRequest } from '@/types/user'
import { apiService } from '@/services/api'
interface AuthState {
user: User | null
isAuthenticated: boolean
isLoading: boolean
error: string | null
}
export const useAuth = () => {
const [authState, setAuthState] = useState<AuthState>({
user: null,
isAuthenticated: false,
isLoading: true,
error: null,
})
// Initialize auth state
useEffect(() => {
initializeAuth()
}, [])
const initializeAuth = async () => {
try {
const token = await AsyncStorage.getItem('accessToken')
const userString = await AsyncStorage.getItem('user')
if (token && userString) {
const user = JSON.parse(userString)
setAuthState({
user,
isAuthenticated: true,
isLoading: false,
error: null,
})
} else {
setAuthState(prev => ({
...prev,
isLoading: false,
}))
}
} catch (error) {
setAuthState({
user: null,
isAuthenticated: false,
isLoading: false,
error: 'Failed to initialize authentication',
})
}
}
const login = useCallback(async (loginData: LoginRequest) => {
try {
setAuthState(prev => ({ ...prev, isLoading: true, error: null }))
const response = await apiService.post<{
user: User
tokens: AuthTokens
}>('/auth/login', loginData)
if (response.success && response.data) {
const { user, tokens } = response.data
// Store tokens and user data
await AsyncStorage.multiSet([
['accessToken', tokens.accessToken],
['refreshToken', tokens.refreshToken],
['user', JSON.stringify(user)],
])
setAuthState({
user,
isAuthenticated: true,
isLoading: false,
error: null,
})
}
} catch (error) {
setAuthState(prev => ({
...prev,
isLoading: false,
error: error instanceof Error ? error.message : 'Login failed',
}))
}
}, [])
const signup = useCallback(async (signupData: SignupRequest) => {
try {
setAuthState(prev => ({ ...prev, isLoading: true, error: null }))
const response = await apiService.post<{
user: User
tokens: AuthTokens
}>('/auth/signup', signupData)
if (response.success && response.data) {
const { user, tokens } = response.data
await AsyncStorage.multiSet([
['accessToken', tokens.accessToken],
['refreshToken', tokens.refreshToken],
['user', JSON.stringify(user)],
])
setAuthState({
user,
isAuthenticated: true,
isLoading: false,
error: null,
})
}
} catch (error) {
setAuthState(prev => ({
...prev,
isLoading: false,
error: error instanceof Error ? error.message : 'Signup failed',
}))
}
}, [])
const logout = useCallback(async () => {
try {
await AsyncStorage.multiRemove(['accessToken', 'refreshToken', 'user'])
setAuthState({
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
})
} catch (error) {
console.error('Logout error:', error)
}
}, [])
return {
...authState,
login,
signup,
logout,
}
}
Testing Configuration
Jest Configuration (jest.config.js)
module.exports = {
preset: 'react-native',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
transformIgnorePatterns: [
'node_modules/(?!(react-native|@react-native|react-navigation|@react-navigation|react-native-vector-icons)/)',
],
setupFilesAfterEnv: ['<rootDir>/__tests__/setup.ts'],
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1',
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/types/**/*',
],
}
Test Setup (tests/setup.ts)
import 'react-native-gesture-handler/jestSetup'
// Mock react-native-vector-icons
jest.mock('react-native-vector-icons/Feather', () => 'Icon')
// Mock AsyncStorage
jest.mock('@react-native-async-storage/async-storage', () =>
require('@react-native-async-storage/async-storage/jest/async-storage-mock')
)
// Mock react-navigation
jest.mock('@react-navigation/native', () => {
const actualNav = jest.requireActual('@react-navigation/native')
return {
...actualNav,
useNavigation: () => ({
navigate: jest.fn(),
goBack: jest.fn(),
}),
useRoute: () => ({
params: {},
}),
}
})
// Silence the warning: Animated: `useNativeDriver` is not supported
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper')
Build Configuration
Android Build (android/app/build.gradle)
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
applicationId "com.azmx.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
multiDexEnabled true
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Signing config for release builds
signingConfig signingConfigs.release
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
splits {
abi {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
}
}
iOS Build Configuration (ios/Podfile)
# Resolve react_native_pods.rb with node to allow for hoisting
require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
platform :ios, '13.0'
prepare_react_native_project!
target 'AzmXApp' do
config = use_native_modules!
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => true,
:fabric_enabled => flags[:fabric_enabled],
:flipper_configuration => FlipperConfiguration.enabled,
:app_clip => false
)
target 'AzmXAppTests' do
inherit! :complete
end
post_install do |installer|
react_native_post_install(
installer,
config[:reactNativePath],
:mac_catalyst_enabled => false
)
__apply_Xcode_12_5_M1_post_install_workaround(installer)
end
end
Performance Optimizations
Image Optimization
import React from 'react'
import { Image, ImageProps } from 'react-native'
import FastImage, { FastImageProps } from 'react-native-fast-image'
interface OptimizedImageProps extends Omit<FastImageProps, 'source'> {
source: string | { uri: string }
fallback?: ImageProps['source']
}
export const OptimizedImage: React.FC<OptimizedImageProps> = ({
source,
fallback,
...props
}) => {
const imageSource = typeof source === 'string' ? { uri: source } : source
return (
<FastImage
source={imageSource}
resizeMode={FastImage.resizeMode.cover}
fallback={fallback}
{...props}
/>
)
}
Memory Management
import { useEffect, useRef } from 'react'
import { AppState, AppStateStatus } from 'react-native'
export const useAppState = (callback: (state: AppStateStatus) => void) => {
const appStateRef = useRef(AppState.currentState)
useEffect(() => {
const handleAppStateChange = (nextAppState: AppStateStatus) => {
appStateRef.current = nextAppState
callback(nextAppState)
}
const subscription = AppState.addEventListener('change', handleAppStateChange)
return () => subscription?.remove()
}, [callback])
return appStateRef.current
}
Development Commands
Package.json Scripts
{
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"start": "react-native start",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"type-check": "tsc --noEmit",
"clean": "react-native clean",
"pod-install": "cd ios && pod install",
"build:android": "cd android && ./gradlew assembleRelease",
"build:ios": "react-native run-ios --configuration Release"
}
}
Common Development Tasks
# Start Metro bundler
npm start
# Run on Android
npm run android
# Run on iOS
npm run ios
# Install iOS dependencies
npm run pod-install
# Clean project
npm run clean
# Type checking
npm run type-check
# Run tests
npm test
# Build for Android
npm run build:android
# Build for iOS
npm run build:ios