diff --git a/mobile/App.tsx b/mobile/App.tsx
index 89509120..642e3d00 100644
--- a/mobile/App.tsx
+++ b/mobile/App.tsx
@@ -1,127 +1,9 @@
/**
- * App.tsx β root component for the DocuElevate mobile app.
+ * App.tsx β legacy root component (not the active entry point).
*
- * Wraps the entire app in the AuthProvider and renders either the login
- * screen (unauthenticated) or the main tab navigator (authenticated).
- * Push notification registration is handled by the usePushNotifications hook.
+ * The app uses expo-router as its entry point ("main": "expo-router/entry"
+ * in package.json). Routing, authentication, and navigation are all handled
+ * by the file-based routes in the `app/` directory.
+ *
+ * This file is retained for reference only and is not executed at runtime.
*/
-
-import { NavigationContainer } from "@react-navigation/native";
-import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
-import { Ionicons } from "@expo/vector-icons";
-import React from "react";
-import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
-import { SafeAreaProvider } from "react-native-safe-area-context";
-import { AuthProvider, useAuth } from "./src/context/AuthContext";
-import { usePushNotifications } from "./src/hooks/usePushNotifications";
-import FilesScreen from "./src/screens/FilesScreen";
-import LoginScreen from "./src/screens/LoginScreen";
-import ProfileScreen from "./src/screens/ProfileScreen";
-import UploadScreen from "./src/screens/UploadScreen";
-
-const Tab = createBottomTabNavigator();
-
-function TabNavigator() {
- const { isAuthenticated } = useAuth();
- usePushNotifications(isAuthenticated);
-
- return (
-
- (
-
- ),
- headerTitle: "DocuElevate",
- }}
- />
- (
-
- ),
- headerTitle: "My Documents",
- }}
- />
- (
-
- ),
- headerTitle: "Profile",
- }}
- />
-
- );
-}
-
-function AppContent() {
- const { isLoading, isAuthenticated } = useAuth();
-
- if (isLoading) {
- return (
-
-
- Loadingβ¦
-
- );
- }
-
- return (
-
- {isAuthenticated ? : }
-
- );
-}
-
-export default function App() {
- return (
-
-
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- loading: {
- flex: 1,
- alignItems: "center",
- justifyContent: "center",
- backgroundColor: "#f9fafb",
- gap: 12,
- },
- loadingText: {
- color: "#6b7280",
- fontSize: 15,
- },
-});
diff --git a/mobile/README.md b/mobile/README.md
index 4a7cf7f8..30acf511 100644
--- a/mobile/README.md
+++ b/mobile/README.md
@@ -1,6 +1,6 @@
# DocuElevate Mobile App
-Native mobile application for DocuElevate, built with **React Native** and **Expo** for both iOS (primary) and Android.
+Native mobile application for DocuElevate, built with **React Native** and **Expo** for iOS, Android, and Web.
## Features
@@ -11,10 +11,11 @@ Native mobile application for DocuElevate, built with **React Native** and **Exp
- π **Push Notifications** β receive real-time push notifications when documents finish processing (via Expo push notifications)
- π **Document List** β browse and search your processed documents
- π€ **Profile** β view account details and sign out
+- π **Web** β run directly in the browser via Expo web (Metro bundler)
## Requirements
-- Node.js 18+
+- Node.js 20.19.4+ (use [nvm](https://github.com/nvm-sh/nvm): `nvm use` in this directory)
- Expo CLI (`npm install -g @expo/cli`)
- Expo Go app on device (for development) **or** Expo Application Services (EAS) for production builds
- An Expo account:
@@ -26,11 +27,14 @@ Native mobile application for DocuElevate, built with **React Native** and **Exp
cd mobile
npm install
-# 2. Start the development server
-npx expo start
+# 2. Start the development server (choose a platform)
+npx expo start # interactive menu (iOS / Android / Web)
+npx expo start --ios # open directly in iOS Simulator
+npx expo start --android # open in Android Emulator
+npx expo start --web # open in the browser
```
-Scan the QR code with **Expo Go** on your iOS or Android device.
+Scan the QR code with **Expo Go** on your iOS or Android device, or press `w` in the interactive menu to open the web build.
## Building
@@ -66,7 +70,12 @@ eas build --platform all
### Android-specific
-- Add a `google-services.json` file (from Firebase Console) to the `mobile/` directory for push notification support
+- **Android push notifications** require a `google-services.json` file from Firebase Console. This file is intentionally excluded from the repository (`.gitignore`). To enable FCM push notifications in your Android builds:
+ 1. Create a Firebase project at
+ 2. Add an Android app with the package name `org.docuelevate.mobile`
+ 3. Download `google-services.json` and place it in the `mobile/` directory
+ 4. Add `"googleServicesFile": "./google-services.json"` back to the `android` section of `app.json` before building
+- The app runs and bundles correctly without `google-services.json`; only Android push notifications will be unavailable
- For Play Store submission: create a service account in Google Play Console, download the JSON key as `google-play-service-account.json`, and update `eas.json`
## Configuration
@@ -92,23 +101,35 @@ The Expo push token is sent to the backend after login via `POST /api/mobile/reg
```
mobile/
-βββ App.tsx # Root component
-βββ app.json # Expo configuration
-βββ eas.json # EAS Build configuration
+βββ app/ # expo-router file-based routes
+β βββ _layout.tsx # Root layout (AuthProvider + auth guard)
+β βββ (auth)/ # Unauthenticated route group
+β β βββ _layout.tsx # Auth stack (no header)
+β β βββ index.tsx # Welcome screen
+β β βββ login.tsx # Login screen
+β βββ (tabs)/ # Authenticated route group
+β βββ _layout.tsx # Tab navigator (Upload / Files / Profile)
+β βββ index.tsx # Upload tab
+β βββ files.tsx # Files tab
+β βββ profile.tsx # Profile tab
+βββ App.tsx # Legacy file (not the entry point; see app/)
+βββ app.json # Expo configuration
+βββ eas.json # EAS Build configuration
βββ package.json
βββ tsconfig.json
βββ src/
βββ context/
- β βββ AuthContext.tsx # Authentication state management
+ β βββ AuthContext.tsx # Authentication state management
βββ hooks/
β βββ usePushNotifications.ts # Push notification registration
βββ screens/
- β βββ LoginScreen.tsx # SSO login
- β βββ UploadScreen.tsx # Camera capture + file picker
- β βββ FilesScreen.tsx # Document list
- β βββ ProfileScreen.tsx # User profile + sign out
+ β βββ WelcomeScreen.tsx # Branded intro / onboarding
+ β βββ LoginScreen.tsx # SSO login
+ β βββ UploadScreen.tsx # Camera capture + file picker
+ β βββ FilesScreen.tsx # Document list
+ β βββ ProfileScreen.tsx # User profile + sign out
βββ services/
- βββ api.ts # DocuElevate API client
+ βββ api.ts # DocuElevate API client
```
## Share Extension (iOS)
diff --git a/mobile/app.json b/mobile/app.json
index 7cc083c7..38c986fb 100644
--- a/mobile/app.json
+++ b/mobile/app.json
@@ -38,11 +38,12 @@
"RECEIVE_BOOT_COMPLETED",
"VIBRATE"
],
- "versionCode": 1,
- "googleServicesFile": "./google-services.json"
+ "versionCode": 1
},
"web": {
- "favicon": "./assets/favicon.png"
+ "favicon": "./assets/favicon.png",
+ "bundler": "metro",
+ "output": "single"
},
"plugins": [
"expo-router",
diff --git a/mobile/app/(auth)/_layout.tsx b/mobile/app/(auth)/_layout.tsx
new file mode 100644
index 00000000..7040faec
--- /dev/null
+++ b/mobile/app/(auth)/_layout.tsx
@@ -0,0 +1,17 @@
+/**
+ * Layout for the unauthenticated route group: Welcome β Login.
+ *
+ * Uses a headerless native stack so the screens animate naturally.
+ */
+
+import { Stack } from "expo-router";
+import React from "react";
+
+export default function AuthLayout() {
+ return (
+
+
+
+
+ );
+}
diff --git a/mobile/app/(auth)/index.tsx b/mobile/app/(auth)/index.tsx
new file mode 100644
index 00000000..be80c4c1
--- /dev/null
+++ b/mobile/app/(auth)/index.tsx
@@ -0,0 +1,4 @@
+/**
+ * Welcome screen route β first screen shown to unauthenticated users.
+ */
+export { default } from "../../src/screens/WelcomeScreen";
diff --git a/mobile/app/(auth)/login.tsx b/mobile/app/(auth)/login.tsx
new file mode 100644
index 00000000..33382568
--- /dev/null
+++ b/mobile/app/(auth)/login.tsx
@@ -0,0 +1,4 @@
+/**
+ * Login screen route β server URL entry and SSO sign-in.
+ */
+export { default } from "../../src/screens/LoginScreen";
diff --git a/mobile/app/(tabs)/_layout.tsx b/mobile/app/(tabs)/_layout.tsx
new file mode 100644
index 00000000..c115f7de
--- /dev/null
+++ b/mobile/app/(tabs)/_layout.tsx
@@ -0,0 +1,72 @@
+/**
+ * Tab navigator layout for authenticated users.
+ *
+ * Registers three tabs: Upload (default), Files, and Profile.
+ * Push notifications are initialised here so they activate as soon as the
+ * user enters the authenticated area.
+ */
+
+import { Tabs } from "expo-router";
+import { Ionicons } from "@expo/vector-icons";
+import React from "react";
+import { usePushNotifications } from "../../src/hooks/usePushNotifications";
+import { useAuth } from "../../src/context/AuthContext";
+
+export default function TabLayout() {
+ const { isAuthenticated } = useAuth();
+ usePushNotifications(isAuthenticated);
+
+ return (
+
+ (
+
+ ),
+ headerTitle: "DocuElevate",
+ }}
+ />
+ (
+
+ ),
+ headerTitle: "My Documents",
+ }}
+ />
+ (
+
+ ),
+ headerTitle: "Profile",
+ }}
+ />
+
+ );
+}
diff --git a/mobile/app/(tabs)/files.tsx b/mobile/app/(tabs)/files.tsx
new file mode 100644
index 00000000..c882d1d9
--- /dev/null
+++ b/mobile/app/(tabs)/files.tsx
@@ -0,0 +1,4 @@
+/**
+ * Files tab route.
+ */
+export { default } from "../../src/screens/FilesScreen";
diff --git a/mobile/app/(tabs)/index.tsx b/mobile/app/(tabs)/index.tsx
new file mode 100644
index 00000000..741646fe
--- /dev/null
+++ b/mobile/app/(tabs)/index.tsx
@@ -0,0 +1,4 @@
+/**
+ * Upload tab route (default tab for authenticated users).
+ */
+export { default } from "../../src/screens/UploadScreen";
diff --git a/mobile/app/(tabs)/profile.tsx b/mobile/app/(tabs)/profile.tsx
new file mode 100644
index 00000000..9007d030
--- /dev/null
+++ b/mobile/app/(tabs)/profile.tsx
@@ -0,0 +1,4 @@
+/**
+ * Profile tab route.
+ */
+export { default } from "../../src/screens/ProfileScreen";
diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx
new file mode 100644
index 00000000..2c3e02e8
--- /dev/null
+++ b/mobile/app/_layout.tsx
@@ -0,0 +1,81 @@
+/**
+ * Root layout for the DocuElevate mobile app.
+ *
+ * Wraps the entire app in AuthProvider and SafeAreaProvider, then uses the
+ * AuthGuard component to redirect between the unauthenticated (auth) route
+ * group and the authenticated (tabs) route group based on session state.
+ */
+
+import { Stack, useRouter, useSegments } from "expo-router";
+import React, { useEffect } from "react";
+import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
+import { SafeAreaProvider } from "react-native-safe-area-context";
+import { AuthProvider, useAuth } from "../src/context/AuthContext";
+
+// ---------------------------------------------------------------------------
+// Auth guard β redirects to the correct route group after auth state resolves
+// ---------------------------------------------------------------------------
+
+function AuthGuard() {
+ const { isLoading, isAuthenticated } = useAuth();
+ const segments = useSegments();
+ const router = useRouter();
+
+ useEffect(() => {
+ if (isLoading) return;
+
+ const inAuthGroup = segments[0] === "(auth)";
+
+ if (!isAuthenticated && !inAuthGroup) {
+ // Unauthenticated visitor outside the auth group β send to welcome
+ router.replace("/(auth)/");
+ } else if (isAuthenticated && inAuthGroup) {
+ // Authenticated user on auth screens β send to main app
+ router.replace("/(tabs)/");
+ }
+ }, [isAuthenticated, isLoading, segments, router]);
+
+ if (isLoading) {
+ return (
+
+
+ Loadingβ¦
+
+ );
+ }
+
+ return (
+
+
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Root layout export
+// ---------------------------------------------------------------------------
+
+export default function RootLayout() {
+ return (
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ loading: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "#f9fafb",
+ gap: 12,
+ },
+ loadingText: {
+ color: "#6b7280",
+ fontSize: 15,
+ },
+});
diff --git a/mobile/package-lock.json b/mobile/package-lock.json
index 2c16b730..8db516c6 100644
--- a/mobile/package-lock.json
+++ b/mobile/package-lock.json
@@ -20,6 +20,7 @@
"expo-constants": "~18.0.13",
"expo-crypto": "~15.0.8",
"expo-dev-client": "~6.0.20",
+ "expo-device": "~7.0.3",
"expo-document-picker": "~14.0.8",
"expo-file-system": "~19.0.21",
"expo-font": "~14.0.0",
@@ -34,9 +35,11 @@
"expo-status-bar": "~3.0.9",
"expo-web-browser": "~15.0.10",
"react": "19.2.4",
+ "react-dom": "19.2.4",
"react-native": "0.81.5",
"react-native-safe-area-context": "5.6.0",
- "react-native-screens": "4.16.0"
+ "react-native-screens": "4.16.0",
+ "react-native-web": "~0.20.0"
},
"devDependencies": {
"@babel/core": "^7.24.0",
@@ -46,7 +49,7 @@
"typescript": "^5.3.0"
},
"engines": {
- "node": ">=20.16.0"
+ "node": ">=20.19.4"
}
},
"node_modules/@0no-co/graphql.web": {
@@ -5530,6 +5533,15 @@
"url": "https://opencollective.com/core-js"
}
},
+ "node_modules/cross-fetch": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
+ "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "node-fetch": "^2.7.0"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -5544,6 +5556,15 @@
"node": ">= 8"
}
},
+ "node_modules/css-in-js-utils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
+ "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
+ "license": "MIT",
+ "dependencies": {
+ "hyphenate-style-name": "^1.0.3"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -6949,6 +6970,18 @@
"expo": "*"
}
},
+ "node_modules/expo-device": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/expo-device/-/expo-device-7.0.3.tgz",
+ "integrity": "sha512-uNGhDYmpDj/3GySWZmRiYSt52Phdim11p0pXfgpCq/nMks0+UPZwl3D0vin5N8/gpVe5yzb13GYuFxiVoDyniw==",
+ "license": "MIT",
+ "dependencies": {
+ "ua-parser-js": "^0.7.33"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-document-picker": {
"version": "14.0.8",
"resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-14.0.8.tgz",
@@ -7321,6 +7354,62 @@
"bser": "2.1.1"
}
},
+ "node_modules/fbjs": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz",
+ "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-fetch": "^3.1.5",
+ "fbjs-css-vars": "^1.0.0",
+ "loose-envify": "^1.0.0",
+ "object-assign": "^4.1.0",
+ "promise": "^7.1.1",
+ "setimmediate": "^1.0.5",
+ "ua-parser-js": "^1.0.35"
+ }
+ },
+ "node_modules/fbjs-css-vars": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
+ "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==",
+ "license": "MIT"
+ },
+ "node_modules/fbjs/node_modules/promise": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
+ "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
+ "license": "MIT",
+ "dependencies": {
+ "asap": "~2.0.3"
+ }
+ },
+ "node_modules/fbjs/node_modules/ua-parser-js": {
+ "version": "1.0.41",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz",
+ "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "ua-parser-js": "script/cli.js"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -7959,6 +8048,12 @@
"node": ">= 14"
}
},
+ "node_modules/hyphenate-style-name": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
+ "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -8062,6 +8157,15 @@
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
+ "node_modules/inline-style-prefixer": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
+ "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==",
+ "license": "MIT",
+ "dependencies": {
+ "css-in-js-utils": "^3.1.0"
+ }
+ },
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -9971,6 +10075,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
"node_modules/node-forge": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz",
@@ -10604,6 +10728,12 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "license": "MIT"
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -10860,7 +10990,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -10984,6 +11113,38 @@
"react-native": "*"
}
},
+ "node_modules/react-native-web": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.20.0.tgz",
+ "integrity": "sha512-OOSgrw+aON6R3hRosCau/xVxdLzbjEcsLysYedka0ZON4ZZe6n9xgeN9ZkoejhARM36oTlUgHIQqxGutEJ9Wxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.6",
+ "@react-native/normalize-colors": "^0.74.1",
+ "fbjs": "^3.0.4",
+ "inline-style-prefixer": "^7.0.1",
+ "memoize-one": "^6.0.0",
+ "nullthrows": "^1.1.1",
+ "postcss-value-parser": "^4.2.0",
+ "styleq": "^0.1.3"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/react-native-web/node_modules/@react-native/normalize-colors": {
+ "version": "0.74.89",
+ "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.89.tgz",
+ "integrity": "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==",
+ "license": "MIT"
+ },
+ "node_modules/react-native-web/node_modules/memoize-one": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+ "license": "MIT"
+ },
"node_modules/react-native/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@@ -11488,8 +11649,7 @@
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/semver": {
"version": "6.3.1",
@@ -11656,6 +11816,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+ "license": "MIT"
+ },
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -12149,6 +12315,12 @@
"integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==",
"license": "MIT"
},
+ "node_modules/styleq": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.1.3.tgz",
+ "integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==",
+ "license": "MIT"
+ },
"node_modules/sucrase": {
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
@@ -12406,6 +12578,12 @@
"node": ">=0.6"
}
},
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
"node_modules/ts-api-utils": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
@@ -12584,6 +12762,32 @@
"node": ">=14.17"
}
},
+ "node_modules/ua-parser-js": {
+ "version": "0.7.41",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz",
+ "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "ua-parser-js": "script/cli.js"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@@ -12910,6 +13114,16 @@
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
"license": "MIT"
},
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"node_modules/whatwg-url-without-unicode": {
"version": "8.0.0-3",
"resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz",
@@ -12924,6 +13138,12 @@
"node": ">=10"
}
},
+ "node_modules/whatwg-url/node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/mobile/package.json b/mobile/package.json
index cfdeab07..d04b5d1d 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -30,6 +30,7 @@
"expo-camera": "~17.0.10",
"expo-constants": "~18.0.13",
"expo-crypto": "~15.0.8",
+ "expo-device": "~7.0.3",
"expo-document-picker": "~14.0.8",
"expo-file-system": "~19.0.21",
"expo-image-manipulator": "~14.0.8",
@@ -43,7 +44,9 @@
"expo-status-bar": "~3.0.9",
"expo-web-browser": "~15.0.10",
"react": "19.2.4",
+ "react-dom": "19.2.4",
"react-native": "0.81.5",
+ "react-native-web": "~0.20.0",
"react-native-safe-area-context": "5.6.0",
"react-native-screens": "4.16.0"
},
@@ -55,7 +58,7 @@
"typescript": "^5.3.0"
},
"engines": {
- "node": ">=20.16.0"
+ "node": ">=20.19.4"
},
"private": true,
"expo": {
diff --git a/mobile/src/screens/LoginScreen.tsx b/mobile/src/screens/LoginScreen.tsx
index 35c11a4f..f278b46e 100644
--- a/mobile/src/screens/LoginScreen.tsx
+++ b/mobile/src/screens/LoginScreen.tsx
@@ -1,11 +1,12 @@
/**
- * LoginScreen β entry point for unauthenticated users.
+ * LoginScreen β server URL entry and SSO sign-in.
*
* Renders a server URL input and a "Sign in with SSO" button that opens the
* DocuElevate web login page in the system browser. On success the
* AuthContext stores the API token and navigates to the main app.
*/
+import { useRouter } from "expo-router";
import React, { useState } from "react";
import {
ActivityIndicator,
@@ -23,6 +24,7 @@ import { useAuth } from "../context/AuthContext";
export default function LoginScreen() {
const { signIn } = useAuth();
+ const router = useRouter();
const [serverUrl, setServerUrl] = useState("");
const [loading, setLoading] = useState(false);
@@ -56,7 +58,7 @@ export default function LoginScreen() {
You will be redirected to your organisation's sign-in page.
+
+ router.back()}
+ accessibilityRole="button"
+ accessibilityLabel="Back to welcome screen"
+ style={styles.backLink}
+ >
+ β Back
+
);
@@ -179,4 +190,14 @@ const styles = StyleSheet.create({
color: "#9ca3af",
textAlign: "center",
},
+ backLink: {
+ marginTop: 20,
+ alignItems: "center",
+ minHeight: 44,
+ justifyContent: "center",
+ },
+ backLinkText: {
+ fontSize: 13,
+ color: "#6b7280",
+ },
});
diff --git a/mobile/src/screens/WelcomeScreen.tsx b/mobile/src/screens/WelcomeScreen.tsx
new file mode 100644
index 00000000..5923f9a3
--- /dev/null
+++ b/mobile/src/screens/WelcomeScreen.tsx
@@ -0,0 +1,209 @@
+/**
+ * WelcomeScreen β first screen shown to unauthenticated users on first launch.
+ *
+ * Presents the DocuElevate brand, a short description of what the app does,
+ * and a "Get Started" button that navigates to the LoginScreen.
+ */
+
+import { useRouter } from "expo-router";
+import React from "react";
+import {
+ Image,
+ Pressable,
+ SafeAreaView,
+ ScrollView,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+
+const FEATURES: { icon: string; title: string; description: string }[] = [
+ {
+ icon: "π",
+ title: "OCR & Text Extraction",
+ description: "Convert scanned PDFs and images into fully searchable text automatically.",
+ },
+ {
+ icon: "π€",
+ title: "AI Metadata Extraction",
+ description: "AI classifies documents and pulls out key fields like dates, amounts, and subjects.",
+ },
+ {
+ icon: "βοΈ",
+ title: "Multi-Cloud Storage",
+ description: "Route processed files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more.",
+ },
+];
+
+export default function WelcomeScreen() {
+ const router = useRouter();
+ return (
+
+
+ {/* Hero */}
+
+
+
+
+ DocuElevate
+ Intelligent Document Processing
+
+ Ingest documents, run OCR, extract metadata with AI, and route files
+ to your cloud storage β all in one seamless pipeline.
+
+
+
+ {/* Feature highlights */}
+
+ {FEATURES.map((feature) => (
+
+ {feature.icon}
+
+ {feature.title}
+ {feature.description}
+
+
+ ))}
+
+
+ {/* CTA */}
+ [styles.button, pressed && styles.buttonPressed]}
+ onPress={() => router.push("/(auth)/login")}
+ accessibilityRole="button"
+ accessibilityLabel="Get started β connect to your DocuElevate server"
+ >
+ Get Started
+
+
+
+ Connect to your self-hosted or cloud DocuElevate server.
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safe: {
+ flex: 1,
+ backgroundColor: "#1e40af",
+ },
+ scroll: {
+ flexGrow: 1,
+ paddingHorizontal: 28,
+ paddingTop: 48,
+ paddingBottom: 40,
+ },
+ hero: {
+ alignItems: "center",
+ marginBottom: 40,
+ },
+ logoContainer: {
+ width: 96,
+ height: 96,
+ borderRadius: 24,
+ backgroundColor: "rgba(255,255,255,0.15)",
+ alignItems: "center",
+ justifyContent: "center",
+ marginBottom: 16,
+ },
+ logoImage: {
+ width: 72,
+ height: 72,
+ },
+ appName: {
+ fontSize: 36,
+ fontWeight: "800",
+ color: "#ffffff",
+ textAlign: "center",
+ marginBottom: 6,
+ letterSpacing: 0.5,
+ },
+ tagline: {
+ fontSize: 14,
+ fontWeight: "600",
+ color: "rgba(255,255,255,0.75)",
+ textTransform: "uppercase",
+ letterSpacing: 1.5,
+ textAlign: "center",
+ marginBottom: 16,
+ },
+ heroDescription: {
+ fontSize: 16,
+ color: "rgba(255,255,255,0.85)",
+ textAlign: "center",
+ lineHeight: 24,
+ maxWidth: 320,
+ },
+ features: {
+ backgroundColor: "rgba(255,255,255,0.1)",
+ borderRadius: 16,
+ paddingVertical: 8,
+ paddingHorizontal: 16,
+ marginBottom: 36,
+ },
+ featureRow: {
+ flexDirection: "row",
+ alignItems: "flex-start",
+ paddingVertical: 14,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: "rgba(255,255,255,0.2)",
+ },
+ featureIcon: {
+ fontSize: 24,
+ marginRight: 14,
+ marginTop: 1,
+ },
+ featureText: {
+ flex: 1,
+ },
+ featureTitle: {
+ fontSize: 15,
+ fontWeight: "600",
+ color: "#ffffff",
+ marginBottom: 2,
+ },
+ featureDescription: {
+ fontSize: 13,
+ color: "rgba(255,255,255,0.75)",
+ lineHeight: 18,
+ },
+ button: {
+ backgroundColor: "#ffffff",
+ borderRadius: 12,
+ paddingVertical: 16,
+ alignItems: "center",
+ justifyContent: "center",
+ minHeight: 52,
+ shadowColor: "#000",
+ shadowOpacity: 0.15,
+ shadowOffset: { width: 0, height: 4 },
+ shadowRadius: 8,
+ elevation: 4,
+ marginBottom: 16,
+ },
+ buttonPressed: {
+ opacity: 0.9,
+ transform: [{ scale: 0.98 }],
+ },
+ buttonText: {
+ color: "#1e40af",
+ fontSize: 17,
+ fontWeight: "700",
+ letterSpacing: 0.3,
+ },
+ hint: {
+ fontSize: 12,
+ color: "rgba(255,255,255,0.55)",
+ textAlign: "center",
+ },
+});