Overview
PARKINMX uses Expo SDK ~54.0.27 with file-based routing via Expo Router. The app is configured to run on iOS, Android, and Web with platform-specific optimizations.
Expo SDK : ~54.0.27Expo Router : ~6.0.17React Native : 0.81.5
App Configuration
The main Expo configuration is in app.json:
{
"expo" : {
"name" : "parkinmx" ,
"slug" : "parkinmx" ,
"version" : "1.0.0" ,
"orientation" : "portrait" ,
"icon" : "./assets/images/icon.png" ,
"scheme" : "parkinmx" ,
"userInterfaceStyle" : "automatic" ,
"newArchEnabled" : true ,
"ios" : {
"supportsTablet" : true
},
"android" : {
"adaptiveIcon" : {
"backgroundColor" : "#E6F4FE" ,
"foregroundImage" : "./assets/images/android-icon-foreground.png" ,
"backgroundImage" : "./assets/images/android-icon-background.png" ,
"monochromeImage" : "./assets/images/android-icon-monochrome.png"
},
"edgeToEdgeEnabled" : true ,
"predictiveBackGestureEnabled" : false ,
"permissions" : [
"android.permission.USE_BIOMETRIC" ,
"android.permission.USE_FINGERPRINT"
]
},
"web" : {
"output" : "static" ,
"favicon" : "./assets/images/favicon.png"
},
"plugins" : [
"expo-router" ,
[
"expo-splash-screen" ,
{
"image" : "./assets/images/splash-icon.png" ,
"imageWidth" : 200 ,
"resizeMode" : "contain" ,
"backgroundColor" : "#ffffff" ,
"dark" : {
"backgroundColor" : "#000000"
}
}
],
"expo-web-browser" ,
"expo-secure-store" ,
"expo-local-authentication" ,
"expo-barcode-scanner"
],
"experiments" : {
"typedRoutes" : true ,
"reactCompiler" : true
},
"extra" : {
"router" : {},
"eas" : {
"projectId" : "932388e1-77d0-406c-8f32-ea382c964a89"
}
},
"owner" : "alejandromv03"
}
}
Configuration Breakdown
Basic App Info
App display name: parkinmx
URL-friendly identifier used in Expo Go and EAS
Current app version: 1.0.0
Locked to portrait mode for consistent UX
Deep linking scheme: parkinmx:// Enables opening the app via custom URLs: Linking . openURL ( 'parkinmx://reservar' );
automatic - Supports both light and dark mode based on system settings
React Native New Architecture
The new architecture enables:
JSI (JavaScript Interface) : Direct JS-to-native communication without the bridge
Fabric : New rendering system for better performance
TurboModules : Lazy-loaded native modules
Concurrent Rendering : Better UI responsiveness
iOS Configuration
"ios" : {
"supportsTablet" : true
}
iPhone Support Optimized for all iPhone models (SE to Pro Max)
iPad Support Full tablet support with responsive layouts
Additional iOS Setup (if building locally):
"ios" : {
"supportsTablet" : true ,
"bundleIdentifier" : "com.yourcompany.parkinmx" ,
"buildNumber" : "1.0.0" ,
"infoPlist" : {
"NSCameraUsageDescription" : "ParkInMX needs camera access to scan QR codes for parking check-in." ,
"NSFaceIDUsageDescription" : "ParkInMX uses Face ID to securely authenticate you." ,
"NSLocationWhenInUseUsageDescription" : "ParkInMX needs your location to show nearby parking lots."
}
}
Android Configuration
"android" : {
"adaptiveIcon" : {
"backgroundColor" : "#E6F4FE" ,
"foregroundImage" : "./assets/images/android-icon-foreground.png" ,
"backgroundImage" : "./assets/images/android-icon-background.png" ,
"monochromeImage" : "./assets/images/android-icon-monochrome.png"
},
"edgeToEdgeEnabled" : true ,
"predictiveBackGestureEnabled" : false ,
"permissions" : [
"android.permission.USE_BIOMETRIC" ,
"android.permission.USE_FINGERPRINT"
]
}
Adaptive Icons
The main icon graphic displayed on top of the background. Requirements :
Size: 108x108dp (432x432px @4x)
Format: PNG with transparency
Safe zone: 66dp diameter circle in center
Either a solid color (#E6F4FE) or an image. Used as the backdrop for the foreground icon.
Single-color version for themed icons (Android 13+). Allows the system to apply user-selected colors to the icon.
Android Features
true - Content draws behind system bars for immersive UIimport { useSafeAreaInsets } from 'react-native-safe-area-context' ;
const insets = useSafeAreaInsets ();
// Apply insets.top, insets.bottom to avoid system UI overlap
predictiveBackGestureEnabled
false - Disables Android 14+ predictive back animationsSet to false if your app has custom navigation transitions that conflict with the system gesture.
Requested Android permissions:
USE_BIOMETRIC - Fingerprint/face unlock
USE_FINGERPRINT - Legacy fingerprint API (pre-Android 9)
Additional Android Setup :
"android" : {
"package" : "com.yourcompany.parkinmx" ,
"versionCode" : 1 ,
"permissions" : [
"android.permission.USE_BIOMETRIC" ,
"android.permission.USE_FINGERPRINT" ,
"android.permission.CAMERA" ,
"android.permission.INTERNET"
]
}
Web Configuration
"web" : {
"output" : "static" ,
"favicon" : "./assets/images/favicon.png"
}
static - Generates static HTML files for hostingAlternative: "server" for SSR (not commonly used for Expo apps)
Web Build Command :
Outputs to dist/ directory, ready for deployment to:
Vercel
Netlify
GitHub Pages
Any static hosting service
Expo Plugins
Active Plugins
Enables file-based routing with support for:
Stack navigation
Tabs navigation
Modal routes
Dynamic routes
Deep linking
Entry Point : expo-router/entry (set in package.json main field)
Configures the app splash screen shown during initial load. [
"expo-splash-screen" ,
{
"image" : "./assets/images/splash-icon.png" ,
"imageWidth" : 200 ,
"resizeMode" : "contain" ,
"backgroundColor" : "#ffffff" ,
"dark" : {
"backgroundColor" : "#000000"
}
}
]
Usage in Code :import * as SplashScreen from 'expo-splash-screen' ;
SplashScreen . preventAutoHideAsync (); // Keep visible
// Later, after loading:
await SplashScreen . hideAsync ();
Opens URLs in an in-app browser (instead of external browser). import * as WebBrowser from 'expo-web-browser' ;
await WebBrowser . openBrowserAsync ( 'https://parkinmx.com/terms' );
Benefits :
OAuth flows (keep user in app)
Better UX for external links
Automatic credential autofill
Encrypted key-value storage for sensitive data. iOS : Keychain ServicesAndroid : EncryptedSharedPreferencesimport * as SecureStore from 'expo-secure-store' ;
// Store PIN securely
await SecureStore . setItemAsync ( 'userPIN' , hashedPIN );
// Retrieve
const storedPIN = await SecureStore . getItemAsync ( 'userPIN' );
expo-local-authentication
Biometric authentication (Face ID, Touch ID, fingerprint). import * as LocalAuthentication from 'expo-local-authentication' ;
// Check if biometrics available
const hasHardware = await LocalAuthentication . hasHardwareAsync ();
const isEnrolled = await LocalAuthentication . isEnrolledAsync ();
// Authenticate
const result = await LocalAuthentication . authenticateAsync ({
promptMessage: 'Authenticate to access ParkInMX' ,
fallbackLabel: 'Use PIN instead'
});
if ( result . success ) {
// Proceed to app
}
QR code and barcode scanning. components/ScannerModal.tsx
import { Camera } from 'expo-camera' ;
const [ hasPermission , setHasPermission ] = useState ( null );
useEffect (() => {
( async () => {
const { status } = await Camera . requestCameraPermissionsAsync ();
setHasPermission ( status === 'granted' );
})();
}, []);
< Camera
onBarCodeScanned = {({ data }) => {
console . log ( 'Scanned:' , data );
// Verify reservation code
}}
style = {StyleSheet. absoluteFillObject }
/>
Other Key Expo Modules
These don’t require explicit plugin configuration but are used throughout the app:
expo-notifications Push notifications (local & remote)
expo-image-picker Select photos from gallery or camera
expo-camera Direct camera access for QR scanning
expo-haptics Tactile feedback (vibrations)
expo-blur Native blur effects for iOS/Android
expo-print PDF generation and printing
expo-sharing Native share sheet
expo-constants Access app.json and device info
Experimental Features
"experiments" : {
"typedRoutes" : true ,
"reactCompiler" : true
}
Typed Routes
Enables TypeScript autocomplete and type checking for Expo Router navigation:
import { useRouter } from 'expo-router' ;
const router = useRouter ();
// ✅ TypeScript knows these routes exist
router . push ( '/reservar' );
router . push ( '/perfil' );
router . push ({ pathname: '/EditReservationScreen' , params: { id: '123' } });
// ❌ TypeScript error: route doesn't exist
router . push ( '/nonexistent' );
React Compiler
Experimental - May have bugs or performance issues
Automatically optimizes React components:
Memoizes components without React.memo
Optimizes hooks without useMemo/useCallback
Reduces unnecessary re-renders
How it works : The React Compiler analyzes your components at build time and inserts optimizations automatically.
Package.json Scripts
{
"scripts" : {
"start" : "expo start" ,
"android" : "expo start --android" ,
"ios" : "expo start --ios" ,
"web" : "expo start --web" ,
"lint" : "expo lint" ,
"reset-project" : "node ./scripts/reset-project.js"
}
}
Development
Production Build
Linting
# Start development server
npm start
# Start with specific platform
npm run android
npm run ios
npm run web
# Clear cache and restart
npm start -- --clear
# Build for iOS (requires macOS)
eas build --platform ios
# Build for Android
eas build --platform android
# Build APK for testing
eas build --platform android --profile preview
# Run ESLint
npm run lint
# Auto-fix issues
npm run lint -- --fix
EAS Configuration
"extra" : {
"eas" : {
"projectId" : "932388e1-77d0-406c-8f32-ea382c964a89"
}
}
EAS (Expo Application Services) provides:
Cloud builds (no Xcode/Android Studio required)
OTA updates
App submission to stores
EAS Build Profiles
Create eas.json in project root:
{
"cli" : {
"version" : ">= 5.2.0"
},
"build" : {
"development" : {
"developmentClient" : true ,
"distribution" : "internal" ,
"ios" : {
"simulator" : true
}
},
"preview" : {
"distribution" : "internal" ,
"android" : {
"buildType" : "apk"
}
},
"production" : {
"autoIncrement" : true ,
"env" : {
"EXPO_PUBLIC_ENV" : "production"
}
}
},
"submit" : {
"production" : {
"ios" : {
"appleId" : "[email protected] " ,
"ascAppId" : "1234567890" ,
"appleTeamId" : "ABCDEF1234"
},
"android" : {
"serviceAccountKeyPath" : "./google-service-account.json" ,
"track" : "production"
}
}
}
}
Build
# Production build
eas build --platform all --profile production
# Preview APK
eas build --platform android --profile preview
Deep Linking
Enables opening the app via custom URLs:
Universal Links (iOS) and App Links (Android) configuration:
"ios" : {
"associatedDomains" : [
"applinks:parkinmx.com"
]
},
"android" : {
"intentFilters" : [
{
"action" : "VIEW" ,
"autoVerify" : true ,
"data" : [
{
"scheme" : "https" ,
"host" : "parkinmx.com" ,
"pathPrefix" : "/"
}
],
"category" : [
"BROWSABLE" ,
"DEFAULT"
]
}
]
}
Usage :
import * as Linking from 'expo-linking' ;
// Open app from external URL
const url = await Linking . getInitialURL ();
if ( url ) {
const { path , queryParams } = Linking . parse ( url );
// Navigate based on path
}
// Listen for incoming links
Linking . addEventListener ( 'url' , ({ url }) => {
const { path } = Linking . parse ( url );
router . push ( path );
});
Asset Management
assets/
└── images/
├── icon.png # 1024x1024 app icon
├── splash-icon.png # Splash screen logo
├── favicon.png # Web favicon
├── android-icon-foreground.png # 432x432 adaptive icon fg
├── android-icon-background.png # 432x432 adaptive icon bg
└── android-icon-monochrome.png # 432x432 themed icon
Use expo-asset to preload critical images: import { Asset } from 'expo-asset' ;
await Asset . loadAsync ([
require ( './assets/images/icon.png' ),
require ( './assets/images/splash-icon.png' )
]);
Environment Variables
Expo supports environment variables with the EXPO_PUBLIC_ prefix:
EXPO_PUBLIC_API_URL = https://api.parkinmx.com
EXPO_PUBLIC_FIREBASE_API_KEY = your_key_here
EXPO_PUBLIC_GEMINI_API_KEY = your_gemini_key
Access in code :
import Constants from 'expo-constants' ;
const apiUrl = process . env . EXPO_PUBLIC_API_URL ;
// or
const firebaseKey = Constants . expoConfig ?. extra ?. firebaseApiKey ;
EXPO_PUBLIC_ variables are bundled into the app and visible to users. Never store secrets here!
Hermes is enabled by default in Expo SDK 54+ Benefits :
Faster app startup
Reduced memory usage
Smaller app size
Use expo-image instead of react-native Image: import { Image } from 'expo-image' ;
< Image
source = {{ uri : photoURL }}
placeholder = { blurhash }
contentFit = "cover"
transition = { 200 }
/>
Advantages :
Native image caching
Better performance
Blur hash placeholders
Use Reanimated for Animations
Animations run on native thread, never drop frames: import Animated , { useAnimatedStyle , withSpring } from 'react-native-reanimated' ;
const animatedStyle = useAnimatedStyle (() => ({
transform: [{ scale: withSpring ( scale . value ) }]
}));
< Animated . View style = { animatedStyle } />
Use dynamic imports for heavy screens: import { lazy , Suspense } from 'react' ;
const HeavyScreen = lazy (() => import ( './HeavyScreen' ));
< Suspense fallback = {<LoadingSpinner />} >
< HeavyScreen />
</ Suspense >
Troubleshooting
# Clear Metro bundler cache
npm start -- --clear
# Clear node_modules and reinstall
rm -rf node_modules
npm install
# Clear iOS build
cd ios && rm -rf build Pods Podfile.lock && cd ..
npx pod-install
# Clear Android build
cd android && ./gradlew clean && cd ..
Plugin Configuration Issues
After modifying plugins in app.json: # Prebuild native projects
npx expo prebuild --clean
This regenerates ios/ and android/ folders with updated plugin configurations.
# Kill all Metro processes
killall -9 node
# Restart with clear cache
npm start -- --reset-cache
Next Steps
Architecture Overview Learn about the overall app architecture and tech stack