Mobile App Module
The MobileApp module provides the complete mobile app experience — gating, push notifications via Firebase Cloud Messaging HTTP v1 API, device management, voice assistant, and full settings screens for profile, security, roles, activity logs, and more.
Overview
- Gate: Mobile app only works if this module is enabled on the tenant
- Push Notifications: FCM HTTP v1 API (production-ready, OAuth2 via service account)
- Voice Assistant: Long-press FAB to activate, live microphone with waveform visualization
- Settings Screens: Profile, appearance, notifications, login history, roles, activity logs, team
- App Version Control: Force update / soft update enforcement
- Mobile Dashboard: Single aggregated endpoint with card-based UI
- Dynamic Navigation: Customizable tab bar and FAB quick actions
Architecture
Tech Stack
| Layer |
Technology |
| Framework |
Expo SDK 55, React Native 0.83, React 19.2 |
| Router |
Expo Router v7 (file-based) |
| Language |
TypeScript |
| UI |
Custom shadcn-style RN components |
| State |
React Context (Auth, Theme, Modules, Notifications, Navigation) |
| Storage |
AsyncStorage (general), SecureStore (tokens) |
| Real-time |
Reverb WebSocket via pusher-js |
| Push |
Firebase Cloud Messaging HTTP v1 API |
Project Structure
app/
├── app/ # Expo Router file-based routes
│ ├── _layout.tsx # Root layout (providers)
│ ├── (auth)/
│ │ └── login.tsx # Login + 2FA verification
│ └── (app)/
│ ├── _layout.tsx # Module gate + Stack navigator
│ ├── notifications.tsx # Notifications screen
│ ├── orders/[id].tsx # Order detail
│ ├── m/[...slug].tsx # SDUI catch-all route (dynamic screens)
│ ├── (tabs)/
│ │ ├── _layout.tsx # Custom tab bar + FAB + voice assistant
│ │ ├── dashboard.tsx # Card-based dashboard
│ │ ├── orders.tsx # Order list
│ │ ├── customers.tsx # Customer list
│ │ ├── warehouse.tsx # WMS operations
│ │ └── settings.tsx # Settings hub
│ └── settings/
│ ├── profile.tsx # Profile management
│ ├── appearance.tsx # Theme + color mode
│ ├── notifications.tsx # Push preferences
│ ├── login-history.tsx # Login history
│ ├── roles.tsx # Roles & permissions (admin)
│ ├── activity-logs.tsx # Audit trail (admin)
│ ├── navigation.tsx # Tab/FAB customization
│ └── team.tsx # Team management
├── components/
│ ├── screen-header.tsx # Rounded header with actions
│ ├── voice-assistant.tsx # Full-screen voice overlay
│ └── ui/ # Base UI components
├── contexts/
│ ├── auth-context.tsx # Auth state + permissions
│ ├── theme-context.tsx # Theme + color mode
│ ├── modules-context.tsx # Module discovery
│ ├── notifications-context.tsx # Real-time notifications
│ └── navigation-context.tsx # Dynamic tab/FAB config
├── lib/
│ ├── api/client.ts # API client (mirrors web)
│ ├── sdui/ # Server-Driven UI engine
│ │ ├── screen-renderer.tsx # Core renderer
│ │ ├── section-registry.ts # Component registry
│ │ ├── action-handler.ts # Action execution
│ │ ├── field-renderer.tsx # Data field rendering
│ │ ├── cache.ts # Two-layer cache
│ │ ├── event-bus.ts # Cross-component events
│ │ └── sections/ # 14 section components
│ ├── notifications/push.ts # Push token + FCM registration
│ ├── echo.ts # Reverb WebSocket setup
│ └── storage/ # SecureStore + AsyncStorage
├── theme/
│ ├── shadcn/ # Default theme
│ └── modern/ # Vibrant indigo theme
└── google-services.json # Firebase config (Android)
Server-Driven UI (SDUI)
The mobile app supports dynamic module screens via Server-Driven UI. Any module can contribute mobile screens by defining them as JSON on the backend — no app rebuild required.
- Catch-all route:
/(app)/m/[...slug] renders any SDUI screen
- Sidebar integration: Dynamic screens appear in the app sidebar automatically
- 14 section types: stats-row, list, grid, detail-fields, form, chart, timeline, actions-bar, tabs, and more
- Search, filters, pagination: Built into the list section component
- Actions: navigate, API calls, share, copy, modals, sheets — all driven by JSON
See the full documentation:
SDUI API Endpoints
| Endpoint |
Description |
GET /mobile/screens |
List all registered screens |
GET /mobile/screens/manifest |
Versioned manifest for cache sync |
GET /mobile/screens/{id} |
Full SDUI JSON definition |
GET /mobile/screens/{id}/data/{section} |
Section data (paginated, filterable) |
POST /mobile/screens/{id}/action/{actionId} |
Execute server-side action |
How the Gate Works
- User logs in via
/api/v1/auth/login
- App calls
GET /api/v1/mobile/check (public, no auth required)
- If
installed: false → slide-up "Mobile Access Not Available" screen with setup instructions
- If
installed: true → proceeds to main app
- The gate also displays license notice (mobile access requires separate license)
Custom Tab Bar
The bottom tab bar is fully custom with:
- Rounded top corners (20px) with card background
- Center FAB that floats above the bar
- Dynamic tabs — configured via backend API, user-customizable
- FAB arc menu — tap opens animated arc fan of quick actions
- Voice assistant — long-press FAB (400ms) activates voice overlay
FAB Icon Animation
The FAB cycles through 3 icons with smooth crossfade:
- Plus (3s idle) → Mic (1.5s) → Sparkles (1.5s) → back to Plus
This hints at the dual functionality (menu + voice) without adding UI clutter.
Voice Assistant
Activated by long-pressing the center FAB button.
Flow
- Listening — live microphone input with animated waveform
- Processing — typing dots animation
- Response — animated text with mock response (AI integration ready)
Audio Processing
| Feature |
Implementation |
| Recording |
expo-av Audio.Recording with metering (dev build) |
| Noise gate |
-35dB threshold — ambient noise filtered |
| Smoothing |
Exponential moving average (factor 0.35) |
| Silence detection |
Below 0.05 normalized = zero |
| Sample rate |
16kHz mono (voice-optimized) |
| Fallback |
Simulated waveform in Expo Go |
Waveform Visualization
- 28 bars updated every 60ms from mic metering
- Bars shift left like a scrolling waveform
- Center bars slightly taller (natural wave shape)
- Per-bar jitter for organic feel
- Opacity scales with intensity
Push Notifications
Architecture
NotificationService::send()
├── Creates Notification record (tenant DB)
├── Broadcasts NotificationCreated event (WebSocket → real-time)
└── SendPushOnNotification listener (queued)
└── PushNotificationService::sendToUser()
└── ExpoPushClient::send()
├── ExponentPushToken → Expo Push API
└── FCM token → FCM HTTP v1 API
FCM HTTP v1 API (Production)
The module uses the modern FCM HTTP v1 API with OAuth2 — not the deprecated legacy server key API.
| Component |
Detail |
| Endpoint |
POST https://fcm.googleapis.com/v1/projects/{projectId}/messages:send |
| Auth |
OAuth2 Bearer token via service account JWT |
| Token cache |
1 hour (auto-refreshes via Laravel Cache) |
| JWT library |
firebase/php-jwt (bundled with Laravel Passport) |
| Service account |
storage/app/firebase-service-account.json |
Token Auto-Routing
ExpoPushClient::send() automatically detects the token format:
ExponentPushToken[...] → Expo Push API (https://exp.host/--/api/v2/push/send)
Raw FCM token → FCM v1 API (https://fcm.googleapis.com/v1/projects/.../messages:send)
Notification Preferences
Users can toggle push categories per-device:
| Category |
Type |
Description |
| Orders |
order_update |
New orders, status changes, cancellations |
| Inventory |
inventory_alert |
Low stock warnings, restock reminders |
| Team |
team_notification |
Member joins, role changes, invitations |
| Custom |
custom |
Admin broadcasts, system updates |
Auto-Push Triggers
Every notification created via NotificationService::send() automatically triggers a push via the SendPushOnNotification event listener. This covers:
- Order created/updated/shipped/delivered
- Low stock alerts
- Team member events
- Admin broadcasts
- Any custom notification
Firebase Setup (Android)
- Create project at Firebase Console
- Add Android app with package name
com.anonymous.autocom
- Download
google-services.json → place at app/google-services.json
- Generate service account key (Project Settings → Service accounts → Generate new private key)
- Place at
storage/app/firebase-service-account.json in the backend
Push Notification Log
All push attempts are logged in push_notification_log:
| Field |
Description |
| status |
pending, sent, failed |
| error |
Error message if failed |
| type |
order_update, inventory_alert, team_notification, custom |
Settings Screens
Profile (settings/profile)
- Avatar — tap to pick from gallery (expo-image-picker, 1:1 crop), uploads via
POST /profile/avatar
- Name — editable with save button
- Email — read-only display
- Password change — expandable section with current/new/confirm fields, show/hide toggles, match indicator
- 2FA status — shows enabled/disabled with backup codes count
- Active sessions — lists all OAuth tokens with device name, last active time, revoke per-session or all
Appearance (settings/appearance)
- Color mode — Light/Dark/System cards with icon, label, description, check indicator
- Theme picker — large cards per theme with:
- Mini UI mockup (tiny replica of the app using that theme's actual color tokens)
- Color swatches (Primary, Accent, BG, Card, Text, Border, Alert)
- Active pill badge + border highlight
Notifications (settings/notifications)
- Status card — Push (active/off), Realtime (connected/off), Unread count
- Push preferences — 4 toggle switches (Orders, Inventory, Team, Custom)
- Graceful fallbacks — info cards when push unavailable (Expo Go) or disabled (no permission)
- Mark all as read action
Login History (settings/login-history)
- Stats row — sign-ins, failed attempts, flagged, unique IPs (last 30 days)
- Filter pills — All / Success / Failed / Blocked
- Entry list — status dot, device icon, browser + platform, IP, failure reason, relative time
- Pagination — infinite scroll with pull-to-refresh
- API —
GET /login-history with status/date filters
Roles & Permissions (settings/roles) — Admin Only
Requires team.manage_roles permission.
- Role list — cards with shield icon, name, permission count, system badge
- View mode — role details, permission groups (collapsible), edit/delete actions
- Edit/Create mode — name/description inputs, permission toggles per group with Select All, sensitive permission warnings
- Constraints — system roles (owner, admin, manager, support, viewer) are read-only
Activity Logs (settings/activity-logs) — Admin Only
Requires admin.audit_log permission.
- 3 tabs — All / Security / Team (each hits dedicated endpoint)
- Entry list — colored action icon, action label, user name, IP, relative time
- 21 action types mapped with distinct icons/colors
- Pagination — infinite scroll
Team (settings/team)
- Team member management (view, invite, suspend, remove)
- Role assignment
API Client Methods
Profile & Auth
| Method |
Endpoint |
Description |
updateProfile({name, email}) |
PATCH /profile |
Update name |
updatePassword({...}) |
POST /auth/change-password |
Change password (revokes other sessions) |
uploadAvatar(uri) |
POST /profile/avatar |
FormData image upload |
getSessions() |
GET /profile/sessions |
List active OAuth tokens |
revokeSession(id) |
DELETE /profile/sessions/{id} |
End specific session |
revokeAllSessions() |
DELETE /profile/sessions |
End all other sessions |
2FA
| Method |
Endpoint |
get2FAStatus() |
GET /2fa/status |
enable2FA(password) |
POST /2fa/enable |
confirm2FA(code) |
POST /2fa/confirm |
disable2FA(password, code) |
POST /2fa/disable |
Login History
| Method |
Endpoint |
getLoginHistory({status, per_page, page}) |
GET /login-history |
getLoginHistoryStats() |
GET /login-history/stats |
Activity Logs
| Method |
Endpoint |
getActivityLogs({action, per_page, page}) |
GET /activity-logs |
getSecurityEvents({per_page, page}) |
GET /activity-logs/security |
getTeamEvents({per_page, page}) |
GET /activity-logs/team |
Roles & Permissions
| Method |
Endpoint |
getRoles() |
GET /team/roles |
getRole(id) |
GET /team/roles/{id} |
createRole({name, description, permissions}) |
POST /team/roles |
updateRole(id, {...}) |
PUT /team/roles/{id} |
deleteRole(id) |
DELETE /team/roles/{id} |
getPermissions() |
GET /team/permissions |
Notifications
| Method |
Endpoint |
getNotifications({...}) |
GET /notifications |
getUnreadNotificationCount() |
GET /notifications/unread-count |
markNotificationAsRead(id) |
PATCH /notifications/{id}/read |
markAllNotificationsAsRead() |
POST /notifications/mark-all-read |
Mobile-Specific
| Method |
Endpoint |
Description |
checkMobileModule() |
GET /mobile/check |
Public module check |
POST /mobile/devices |
Register device |
FCM token + device info |
DELETE /mobile/devices/{token} |
Unregister device |
Called on logout |
PATCH /mobile/devices/preferences |
Update push preferences |
Per-type toggles |
POST /mobile/devices/heartbeat |
Device heartbeat |
Periodic keep-alive |
GET /mobile/dashboard |
Aggregated dashboard |
Overview + stats + orders |
Configuration
Backend (config/mobile-app.php)
return [
'push' => ['enabled' => env('MOBILE_PUSH_ENABLED', true)],
'fcm_service_account' => env('FCM_SERVICE_ACCOUNT_PATH', storage_path('app/firebase-service-account.json')),
'version_check' => ['enabled' => env('MOBILE_VERSION_CHECK_ENABLED', true)],
'dashboard_cache_ttl' => env('MOBILE_DASHBOARD_CACHE_TTL', 300),
'max_devices_per_user' => env('MOBILE_MAX_DEVICES_PER_USER', 5),
'cleanup_inactive_days' => env('MOBILE_CLEANUP_INACTIVE_DAYS', 90),
'default_tabs' => ['dashboard', 'orders', 'warehouse', 'settings'],
'default_quick_actions' => [...],
'available_mobile_tabs' => [...],
];
Mobile App (app/.env)
EXPO_PUBLIC_API_URL=http://your-api:5350/api/v1
EXPO_PUBLIC_REVERB_APP_KEY=your_reverb_key
EXPO_PUBLIC_REVERB_HOST=your-api-host
EXPO_PUBLIC_REVERB_PORT=5352
EXPO_PUBLIC_REVERB_SCHEME=http
Android Permissions (app.json)
{
"android": {
"permissions": ["RECORD_AUDIO", "READ_MEDIA_IMAGES", "android.permission.CAMERA"],
"googleServicesFile": "./google-services.json"
},
"ios": {
"infoPlist": {
"NSMicrophoneUsageDescription": "AutoCom uses your microphone for the AI voice assistant.",
"NSPhotoLibraryUsageDescription": "AutoCom needs access to your photos to update your profile picture."
}
}
}
Backend Services
| Service |
Responsibility |
ExpoPushClient |
Push delivery — auto-routes Expo tokens vs FCM tokens, FCM v1 OAuth2 |
PushNotificationService |
Push orchestration, user targeting, preference filtering, stats |
SendPushOnNotification |
Event listener — bridges NotificationCreated → push automatically |
DeviceService |
Device registration, cleanup, max device enforcement |
MobileDashboardService |
Aggregated dashboard with caching |
NavigationService |
Dynamic tab bar and quick action configuration |
MobileScreenRegistry |
SDUI screen registration, definition serving, action execution |
AppVersionService |
Version CRUD, version comparison |
Building the App
Prerequisites
- Node.js 18+
- JDK 17 (Azul Zulu recommended:
brew install --cask zulu@17)
- Android SDK (platform-tools, build-tools 35+, SDK platform 35+)
ANDROID_HOME and JAVA_HOME environment variables set
Development Build (Android)
cd app
npm install
npx expo prebuild --platform android
npx expo run:android # builds native + starts Metro
Development Build (iOS)
cd app
npm install
npx expo prebuild --platform ios
npx expo run:ios
Dev Server Only (after first build)
cd app
npx expo start --dev-client
Important Notes
- First build takes 10-15 minutes (downloads NDK, compiles native modules)
- Subsequent builds are fast (~30s incremental)
android/ and ios/ directories are gitignored — regenerated via npx expo prebuild
google-services.json must be in app/ root (Expo copies it during prebuild)
- Firebase service account JSON must be in backend
storage/app/ (not committed — deploy secret)
Artisan Commands
| Command |
Description |
mobile:cleanup-devices --days=90 |
Deactivate devices inactive for N days |
Deployment Checklist
- Firebase project created with Android app registered
google-services.json in app/ root
- Firebase service account JSON in backend
storage/app/firebase-service-account.json
MOBILE_PUSH_ENABLED=true in backend .env
- Queue worker running (
php artisan horizon or queue:work) for push job processing
- Reverb WebSocket server running for real-time notifications
- Mobile App module enabled on tenant via admin panel