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", + }, +});