Mobile App
DocuElevate includes a native mobile application for iOS and Android built with React Native and Expo. The app allows users to capture documents with the device camera, pick files from the device storage, and receive push notifications when documents finish processing.
Features
| Feature | iOS | Android |
|---|---|---|
| SSO login (OAuth2) | ✅ | ✅ |
| Local / basic auth login | ✅ | ✅ |
| QR code login (scan from web) | ✅ | ✅ |
| Auto-generated API token | ✅ | ✅ |
| Camera capture → upload | ✅ | ✅ |
| File picker upload | ✅ | ✅ |
| Multi-image selection from library | ✅ | ✅ |
| Share Sheet / Share Intent | ✅ | ✅ |
| Push notifications | ✅ | ✅ |
| Document list with search | ✅ | ✅ |
| File detail view with processing logs | ✅ | ✅ |
| Pre-login legal pages (GDPR) | ✅ | ✅ |
| Localization (EN, DE, ES, FR, IT) | ✅ | ✅ |
| Language selection | ✅ | ✅ |
| Dark mode | ✅ | ✅ |
Getting Started (Development)
Prerequisites
- Node.js 20.19.4 or later (use nvm:
nvm installinsidemobile/reads.nvmrcautomatically) - Expo CLI:
npm install -g @expo/cli - Expo Go app on your iOS or Android device (for development)
- A running DocuElevate server reachable from your device
Run in development mode
cd mobile
npm install
npx expo start
Scan the QR code with Expo Go on your device. On iOS you can also use the Camera app.
Building for Production
DocuElevate uses Expo Application Services (EAS) to produce App Store / Play Store binaries.
# Install EAS CLI globally
npm install -g eas-cli
# Authenticate with Expo
eas login
# Build for iOS (requires Apple Developer account)
eas build --platform ios
# Build for Android
eas build --platform android
Note: The mobile app uses
expo-build-propertieswithbuildReactNativeFromSource: truefor iOS builds. This is required for Expo SDK 54 (React Native 0.81) compatibility — some native modules still use legacy bridge APIs (RCTBridge,RCTViewManager, etc.) that are no longer included in the default precompiled XCFrameworks. Building React Native from source makes these headers available, at the cost of slightly longer iOS build times.
See the EAS Build documentation for full setup instructions.
Automated CI/CD
An EAS Cloud Workflow (mobile/.eas/workflows/create-builds.yml) automates production builds and iOS submission:
- Path filtering: The workflow only triggers on pushes to
mainthat include changes inside themobile/directory. Backend-only or documentation-only changes do not trigger a mobile build. - Build: Both iOS and Android production builds run in parallel on EAS Build.
- Auto-submit (iOS): After a successful iOS build, the workflow automatically submits the binary to App Store Connect using the credentials configured in
eas.json(submit.production.ios). The build then appears in TestFlight for internal testing and can be promoted to the App Store from App Store Connect.
Prerequisite: An App Store Connect API Key must be configured in EAS for non-interactive submission. See Troubleshooting → "Session expired" below for setup instructions.
Version Management
Build numbers (iOS buildNumber / Android versionCode) are managed remotely by EAS. The eas.json configuration uses:
{
"cli": { "appVersionSource": "remote" },
"build": { "production": { "autoIncrement": true } }
}
appVersionSource: "remote"— EAS stores the current build number on its servers instead of reading it fromapp.json. This ensures every CI build gets a unique, ever-increasing number without needing to commit version bumps back to the repository.autoIncrement: true— EAS automatically increments the build number before each production build.
The ios.buildNumber and android.versionCode values in app.json serve as the initial seed when the remote version is first created; after that they are informational only. Do not rely on them for the actual version submitted to the stores.
Tip: To check or manually set the remote version, use
eas build:version:getandeas build:version:set.
Authentication
SSO Login Flow
The mobile app uses the server's existing OAuth2/SSO setup:
- User enters the DocuElevate server URL on the login screen.
- The app opens
<server>/login?mobile=1&redirect_uri=docuelevate://callbackin the system browser (Safari / Chrome Custom Tabs). - The server stores
docuelevate://callbackin the browser session and presents the login page. - The user authenticates via SSO or local credentials.
- After successful authentication the server mints a long-lived API token and redirects the browser to
docuelevate://callback?token=<token>. WebBrowser.openAuthSessionAsyncintercepts thedocuelevate://deep link and returns the URL to the app.- The app extracts the token from the URL and stores it securely in the device's keychain (
expo-secure-store).
Security note: The
redirect_uriis validated server-side; only URIs with thedocuelevate://custom scheme (production) or theexp://scheme (Expo Go development) are accepted, preventing open-redirect attacks.
Testing in Expo Go
When developing with Expo Go the app does not have the docuelevate:// custom URL scheme registered. The auth flow adapts automatically:
Linking.createURL('callback')returns anexp://URI pointing at the local dev server (e.g.exp://192.168.1.5:8081/--/callback).- This URI is sent to the server as
redirect_uri; the server accepts it alongside the productiondocuelevate://scheme. - After successful authentication the server redirects back to the
exp://URI. WebBrowser.openAuthSessionAsyncintercepts the deep link and the Expo Go app receives the token.
No extra configuration is needed — just run npx expo start and scan the QR code with the Expo Go app.
QR Code Login Flow
As an alternative to SSO, users can log in by scanning a QR code displayed in the web UI:
- The authenticated web user navigates to Profile → Security & Sessions → Log in on mobile via QR code.
- A QR code is displayed containing a deep link:
docuelevate://qr-login?token=<challenge_token>&server=<server_url>. - In the mobile app, the user taps Scan QR Code to Login, which opens the device camera.
- The app scans the QR code, extracts both the server URL and the challenge token, and calls
POST /api/qr-auth/claim. - An API token is issued and stored securely — no need to enter the server URL manually.
Note: The QR code already contains the server URL, so users do not need to type it in when using QR login.
Auto-generated Mobile Token
When the mobile app completes login it automatically creates a named API token ("Mobile App – <device name>") via POST /api/mobile/generate-token. This token:
- Works identically to tokens created manually in the web UI.
- Is shown in the API Tokens page (
/api-tokens) and can be revoked there. - Is stored in the device's secure keychain, never in plain storage.
Push Notifications
Push notifications are delivered via the Expo Push Notification service, which routes through Apple Push Notification service (APNs) for iOS and Firebase Cloud Messaging (FCM) for Android.
No server-side APNs/FCM credentials are required – Expo's servers handle the provider integration.
How it works
- After login, the app requests notification permission from the operating system.
- If granted, the app obtains an Expo Push Token (
ExponentPushToken[…]). - The token is registered with the backend via
POST /api/mobile/register-device. - When a document finishes processing, the server sends a push notification to all registered devices for that user.
Managing registered devices
Users can see and remove their registered devices from the Profile tab in the app, or via the API:
# List registered devices
curl -H "Authorization: Bearer <token>" https://your-server/api/mobile/devices
# Remove a device
curl -X DELETE -H "Authorization: Bearer <token>" https://your-server/api/mobile/devices/<id>
Uploading Documents
Camera Capture
- Open the Upload tab.
- Tap Camera.
- Point the camera at the document and take a photo.
- The image is immediately uploaded and queued for processing.
Photo Library
- Open the Upload tab.
- Tap Photos.
- Select one or more photos from the device's photo library (multi-selection is supported).
- All selected images are uploaded and queued for processing.
File Picker
- Open the Upload tab.
- Tap Files.
- Browse to and select one or more files (PDF, DOCX, images, etc.).
- Files are uploaded and queued for processing.
Share Sheet (iOS) / Share Intent (Android)
The app registers itself as a share target so any file can be sent directly to DocuElevate from another app:
- Open a file in Files, Mail, Safari, or any other app.
- Tap the Share button (iOS) or Share (Android).
- Find and tap DocuElevate in the share sheet.
- The file is immediately uploaded and queued for processing.
Note: The app must be installed on the device for it to appear in the share sheet.
iOS implementation
app.json declares CFBundleDocumentTypes (with LSHandlerRank: Alternate) inside the iOS infoPlist. This tells iOS that DocuElevate can open common document types, making it visible in the share sheet without overriding system defaults. When the user selects DocuElevate, iOS opens the app with a URL via application:openURL:options:.
The URL may arrive as a standard file:// path or under the app's custom docuelevate:// scheme (e.g. docuelevate://private/var/mobile/Library/…/file.pdf). The root layout detects the custom-scheme form and rewrites it to a file:// URL before forwarding it to the Upload screen through ShareContext.
Handling "unmatched route" errors from "Open In…"
iOS sometimes delivers the file path under the docuelevate:// scheme, e.g.:
docuelevate://private/var/mobile/Library/Mobile Documents/…/Invoice.pdf
expo-router strips the scheme and tries to match /private/var/mobile/… as an in-app route. Because no such route exists, it previously threw an "unmatched route docuelevate://" error and the upload never completed.
The fix is a catch-all +not-found.tsx route (see mobile/app/+not-found.tsx). When expo-router cannot match the path, it renders this screen instead. The screen detects that the path is a filesystem path rather than a real in-app route, adds the file directly to ShareContext, and redirects to the Upload tab. UploadScreen picks up the pending file and begins uploading automatically. The Linking listener in the root layout may also fire for the same URL; ShareContext.addPendingFile deduplicates by URI so the file is only uploaded once.
File accessibility and local caching
Shared files may reference paths outside the app's sandbox or use security-scoped URLs that React Native's fetch cannot read directly. To guarantee reliable uploads:
LSSupportsOpeningDocumentsInPlaceis set tofalseinapp.json, which tells iOS to copy shared files into the app'sDocuments/Inboxdirectory before handing them to the app.UploadScreenusesexpo-file-system(FileSystem.copyAsync) to copy anyfile://URI that is outside the app's cache/documents directory to a local cache path before uploading. This ensures the file is readable regardless of its origin.- MIME type inference: Both
+not-found.tsxand theLinkinghandler in_layout.tsxinfer the MIME type from the file extension (e.g..pdf→application/pdf) so the server receives a correctContent-Typeinstead ofapplication/octet-stream.
iOS Action / Share Extension (future enhancement)
Apps like DeepL ("Translate in DeepL") and Microsoft Word ("Convert to Word") appear as Action Extensions in the iOS share sheet — a system-level feature that requires a separate Xcode target built with Swift or Objective-C. A proper Action Extension runs in its own process and must share authentication credentials with the main app via an iOS App Group (shared keychain / shared container).
This level of iOS-native integration is a planned future enhancement. Until it is available, the recommended workflow is the current one: tap Share → DocuElevate (the app appears in the "Open With" row of the share sheet via CFBundleDocumentTypes).
Android implementation
app.json declares ACTION_SEND and ACTION_SEND_MULTIPLE intent filters for mimeType: "*/*" in the android.intentFilters section. Incoming content URIs are received the same way as on iOS.
Upload status polling
After a file is uploaded the app polls /api/files?search=<filename> every 5 seconds to find the corresponding FileRecord, then polls /api/files/{id} to track the processing status in real time. Polling stops automatically once the status reaches a terminal state (completed, failed, or duplicate).
Retrying failed uploads
If a file upload fails (e.g. due to network issues or a server error), the failed item stays visible in the upload list with an error message and a "Tap to retry" hint. Users can retry the upload in two ways:
- Tap the failed item to immediately retry the upload.
- Long-press the failed item to see a confirmation dialog with a Retry option.
The retry re-uses the original file URI so no re-selection is needed.
Document Search
The Files tab includes a search bar at the top that lets users search through their processed documents by filename. Searches are debounced (400ms) to avoid excessive API calls. Clear the search with the ✕ button to return to the full list.
File Detail View
Tapping any document in the Files tab opens a detail view showing:
- File metadata: filename, file size, MIME type, upload date, and file hash
- Processing status: current status with a colour-coded icon
- Processing log: chronological list of processing steps with individual status indicators and timestamps
Pull-to-refresh updates the detail view. This replicates the web interface at /files/{id} and /files/{id}/detail in a mobile-friendly layout.
Legal & Compliance
GDPR & Apple App Store Compliance
Privacy Policy, Terms of Service, and Imprint links are accessible before login from both the Welcome Screen and the Login Screen. This ensures compliance with:
- GDPR (General Data Protection Regulation) – users must be able to review the privacy policy before providing personal data
- Apple App Store Review Guidelines – apps must provide accessible privacy information before account creation
Post-login, the same links are available in the Profile tab under the "Legal" section.
Localization (i18n)
The mobile app supports five languages with automatic device-locale detection:
| Language | Code | Status |
|---|---|---|
| English | en |
✅ Complete |
| German (Deutsch) | de |
✅ Complete |
| Spanish (Español) | es |
✅ Complete |
| French (Français) | fr |
✅ Complete |
| Italian (Italiano) | it |
✅ Complete |
How it works
Language priority (highest to lowest):
- Server preference —
preferred_languagereturned byGET /api/mobile/whoamion login or app resume. Allows a language set on the desktop web interface to propagate to mobile automatically. - AsyncStorage — the last language explicitly selected on the device, used as an offline fallback when the server is unreachable.
- Device locale — detected via
expo-localizationon first launch. - English — final fallback when none of the above match a supported locale.
When a user selects a language on mobile the choice is:
- Applied immediately to all screens (via LocaleContext)
- Persisted locally to AsyncStorage
- Synced to the server via POST /api/i18n/language (fire-and-forget), so the next desktop login reflects the same preference.
Note: If the server's preferred language is not supported by the mobile app (e.g. a locale added to the web frontend but not yet translated for mobile), the mobile app falls back to the next priority in the list above.
Adding a new language
- Create a new translation file in
mobile/src/i18n/(e.g.pt.jsonfor Portuguese) - Copy the structure from
en.jsonand translate all values - Import the new file in
mobile/src/i18n/index.ts - Add it to the
translationsobject andgetSupportedLanguages()array
User Settings
The Profile tab includes a Settings section where users can:
- Change language: Select from the supported languages (English, German, Spanish, French, Italian)
- View server connection details
- Access legal documents (Privacy Policy, Terms of Service, Imprint)
- Sign out or delete their account
Mobile API Endpoints
The backend exposes a dedicated /api/mobile/ namespace:
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/mobile/generate-token |
Session | Exchange SSO session for API token |
POST |
/api/mobile/register-device |
Bearer | Register Expo push token |
GET |
/api/mobile/devices |
Bearer | List registered devices |
DELETE |
/api/mobile/devices/{id} |
Bearer | Deactivate a device |
GET |
/api/mobile/whoami |
Bearer | Get current user profile (includes preferred_language) |
POST |
/api/i18n/language |
Bearer | Sync language preference to server |
All other API endpoints (file upload, file listing, etc.) work with Bearer token authentication.
POST /api/mobile/generate-token
Exchanges an active web session (cookie) for a permanent API token suitable for use in the mobile app.
Request:
{ "device_name": "John's iPhone" }
Response (201):
{
"token": "de_AbCdEfGhIjKl...",
"token_id": 42,
"name": "Mobile App – John's iPhone",
"created_at": "2026-03-10T09:30:00Z"
}
⚠️ The
tokenvalue is returned once only. Store it in the device's secure keychain immediately.
POST /api/mobile/register-device
Registers an Expo push token for the authenticated user.
Request:
{
"push_token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
"device_name": "John's iPhone",
"platform": "ios"
}
Supported platforms: ios, android, web.
Re-registering the same token is safe (idempotent).
GET /api/mobile/whoami
Returns the current user's profile, including the server-stored language preference.
Response (200):
{
"owner_id": "john@example.com",
"display_name": "John Doe",
"email": "john@example.com",
"avatar_url": "https://www.gravatar.com/avatar/...",
"is_admin": false,
"preferred_language": "de"
}
preferred_language is null when no preference has been saved. The mobile
app applies this value on login / app resume, falling back to AsyncStorage and
then the device locale when it is null or unsupported.
Configuration
No server-side configuration is required to enable the mobile app. The Expo push notification routing does not need FCM or APNs credentials on the server.
If you wish to use direct FCM/APNs without Expo's relay, replace the send_expo_push_notification function in app/utils/push_notification.py with your own implementation.
Project Structure (mobile/)
mobile/
├── App.tsx # Root component (legacy, not used at runtime)
├── app/ # Expo Router file-based routes
│ ├── _layout.tsx # Root layout (AuthGuard + providers)
│ ├── index.tsx # Root redirect → /(auth)/
│ ├── (auth)/ # Unauthenticated route group
│ │ ├── _layout.tsx # Stack navigator (headerless)
│ │ ├── index.tsx # Welcome screen
│ │ ├── login.tsx # Login screen
│ │ └── qr-scanner.tsx # QR code scanner screen
│ └── (tabs)/ # Authenticated route group
│ ├── _layout.tsx # Tab navigator
│ ├── index.tsx # Upload screen (default tab)
│ ├── files.tsx # Files screen
│ └── profile.tsx # Profile screen
├── app.json # Expo/EAS configuration
├── eas.json # EAS Build profiles
├── package.json
├── tsconfig.json
└── src/
├── context/
│ ├── AuthContext.tsx # Auth state + SSO login flow
│ └── ShareContext.tsx # Shared-file queue (iOS Share Sheet / Android Intent)
├── hooks/
│ └── usePushNotifications.ts # Push token registration
├── screens/
│ ├── LoginScreen.tsx # Server URL + SSO button + QR code scanner
│ ├── QRScannerScreen.tsx # Camera-based QR code scanner for login
│ ├── UploadScreen.tsx # Camera capture + photo library + file picker
│ ├── FilesScreen.tsx # Processed document list with search
│ ├── FileDetailScreen.tsx # File detail view with processing logs
│ ├── ProfileScreen.tsx # User profile + settings + sign out
│ └── WelcomeScreen.tsx # Pre-login welcome with legal links
├── i18n/ # Localization (i18n)
│ ├── index.ts # i18n module (locale detection, t() function)
│ ├── en.json # English translations
│ ├── de.json # German translations
│ ├── es.json # Spanish translations
│ ├── fr.json # French translations
│ └── it.json # Italian translations
├── utils/
│ ├── mimeTypes.ts # MIME type mapping for file extensions
│ └── normalizeUri.ts # URI normalization for deduplication
└── services/
└── api.ts # DocuElevate REST API client
Troubleshooting
App shows "Hello World" / default Expo page after update
If the iOS or Android app shows a generic "Hello World – This is the first page of your app" screen instead of the DocuElevate UI, it means a stale default index.tsx file (generated by Expo CLI scaffolding) is being picked up in the mobile/app/ directory.
To fix:
- Delete any leftover default
mobile/app/index.tsxthat is not the repository version (the repo version contains a<Redirect>to/(auth)/). - Clear the Metro bundler cache and rebuild:
bash cd mobile npx expo start --clear - For production builds, run a clean EAS build:
bash eas build --platform ios --clear-cache
The repository includes a root app/index.tsx that immediately redirects to the authentication flow, so this issue should not recur once the correct file is present.
"Session expired Local session" during iOS build
EAS stores an Apple ID session locally (in ~/.expo/) to manage code-signing certificates and provisioning profiles. This session expires after a few weeks.
To fix:
- Refresh the session by running
eas credentialsand re-authenticating with your Apple ID. - Recommended for automation: Replace the Apple ID session with an App Store Connect API Key. API keys do not expire and work fully non-interactively:
- Create a key at appstoreconnect.apple.com → Users → Integrations → Keys
- Download the
.p8file and note the Key ID and Issuer ID - Run
eas credentials→ iOS → Add an App Store Connect API key - Upload the
.p8file when prompted
Once an API key is configured in EAS, automated builds (including CI and EAS Cloud Workflows) will no longer prompt for a password.
Node.js deprecation warning [DEP0169] during EAS build
(node:XXXXX) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized…
This warning is emitted by EAS CLI (an external tool) when it runs on Node.js 22 or later, which deprecates url.parse(). It does not indicate a problem in the DocuElevate mobile app itself and will not cause a build failure on its own.
The eas.json build profiles already include "NODE_NO_WARNINGS": "1" in their env sections to suppress this warning during EAS Cloud builds. For local builds with a system Node.js ≥ 22, suppress it by running:
NODE_NO_WARNINGS=1 eas build --platform ios
or by activating the project's pinned Node.js version first:
cd mobile
nvm use # reads .nvmrc → Node 20.19.4 (no deprecation warning)
eas build --platform ios
"Authentication was cancelled or failed"
- Ensure the server URL is correct (including
https://). - Verify the server is reachable from your device's network.
- Confirm that
AUTH_ENABLED=Trueon the server.
Push notifications not arriving
- Check that the app has notification permission (Settings → DocuElevate → Notifications).
- Verify the device is registered:
GET /api/mobile/devices. - Ensure the server can reach
https://exp.host(outbound HTTPS on port 443). - On Android, add
google-services.jsonto themobile/directory if you are building your own binary.
"Connection refused" or timeout
- Verify that the DocuElevate server is running and accessible.
- Ensure the server's
EXTERNAL_HOSTNAMEor reverse proxy is configured correctly. - Check that the server accepts CORS requests from
docuelevate://.