From 0ea652fa5332b2d1a4a5dddf09edeb1ea3cbfd3f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 15 Mar 2026 15:31:32 +0000
Subject: [PATCH 1/5] Initial plan
From b4b50e834088045510b3c235464862789d31c646 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 15 Mar 2026 15:34:12 +0000
Subject: [PATCH 2/5] fix(mobile): update Node.js minimum requirement to
20.19.4
metro-config@0.83.3 uses Array.prototype.toReversed() which was added
in Node.js 20.0.0. The README incorrectly stated "Node.js 18+" causing
users to run `npx expo start` with Node 18 and hit:
TypeError: configs.toReversed is not a function
- Update README.md to say "Node.js 20.19.4+" with nvm hint
- Update package.json engines from >=20.16.0 to >=20.19.4 to match
metro-config's exact minimum (as declared in package-lock.json)
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
---
mobile/README.md | 2 +-
mobile/package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/mobile/README.md b/mobile/README.md
index 4a7cf7f8..17ac7d81 100644
--- a/mobile/README.md
+++ b/mobile/README.md
@@ -14,7 +14,7 @@ Native mobile application for DocuElevate, built with **React Native** and **Exp
## 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:
diff --git a/mobile/package.json b/mobile/package.json
index cfdeab07..fa028286 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -55,7 +55,7 @@
"typescript": "^5.3.0"
},
"engines": {
- "node": ">=20.16.0"
+ "node": ">=20.19.4"
},
"private": true,
"expo": {
From 9d5b1e25e24fd2582dc5609ab2406498616ae3b2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 15 Mar 2026 15:49:32 +0000
Subject: [PATCH 3/5] wip: plan
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
---
mobile/README.md | 7 ++++++-
mobile/app.json | 3 +--
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/mobile/README.md b/mobile/README.md
index 17ac7d81..215ce5ee 100644
--- a/mobile/README.md
+++ b/mobile/README.md
@@ -66,7 +66,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
diff --git a/mobile/app.json b/mobile/app.json
index 7cc083c7..c3996187 100644
--- a/mobile/app.json
+++ b/mobile/app.json
@@ -38,8 +38,7 @@
"RECEIVE_BOOT_COMPLETED",
"VIBRATE"
],
- "versionCode": 1,
- "googleServicesFile": "./google-services.json"
+ "versionCode": 1
},
"web": {
"favicon": "./assets/favicon.png"
From a2de72ab729acf7514f73b0b5b4d3ddad15ebccf Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 15 Mar 2026 15:54:00 +0000
Subject: [PATCH 4/5] wip: tracking plan
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
---
mobile/App.tsx | 19 ++-
mobile/src/screens/LoginScreen.tsx | 29 +++-
mobile/src/screens/WelcomeScreen.tsx | 217 +++++++++++++++++++++++++++
3 files changed, 260 insertions(+), 5 deletions(-)
create mode 100644 mobile/src/screens/WelcomeScreen.tsx
diff --git a/mobile/App.tsx b/mobile/App.tsx
index 89509120..b0e8ce34 100644
--- a/mobile/App.tsx
+++ b/mobile/App.tsx
@@ -1,13 +1,15 @@
/**
* App.tsx – root component for the DocuElevate mobile app.
*
- * Wraps the entire app in the AuthProvider and renders either the login
- * screen (unauthenticated) or the main tab navigator (authenticated).
+ * Wraps the entire app in the AuthProvider and renders either the
+ * authenticated tab navigator or an unauthenticated auth stack that takes
+ * the user through WelcomeScreen → LoginScreen.
* Push notification registration is handled by the usePushNotifications hook.
*/
import { NavigationContainer } from "@react-navigation/native";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
+import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
@@ -18,8 +20,19 @@ import FilesScreen from "./src/screens/FilesScreen";
import LoginScreen from "./src/screens/LoginScreen";
import ProfileScreen from "./src/screens/ProfileScreen";
import UploadScreen from "./src/screens/UploadScreen";
+import WelcomeScreen, { type AuthStackParamList } from "./src/screens/WelcomeScreen";
const Tab = createBottomTabNavigator();
+const AuthStack = createNativeStackNavigator();
+
+function AuthNavigator() {
+ return (
+
+
+
+
+ );
+}
function TabNavigator() {
const { isAuthenticated } = useAuth();
@@ -97,7 +110,7 @@ function AppContent() {
return (
- {isAuthenticated ? : }
+ {isAuthenticated ? : }
);
}
diff --git a/mobile/src/screens/LoginScreen.tsx b/mobile/src/screens/LoginScreen.tsx
index 35c11a4f..80a29107 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 type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import React, { useState } from "react";
import {
ActivityIndicator,
@@ -20,8 +21,13 @@ import {
View,
} from "react-native";
import { useAuth } from "../context/AuthContext";
+import type { AuthStackParamList } from "./WelcomeScreen";
-export default function LoginScreen() {
+type LoginScreenProps = {
+ navigation: NativeStackNavigationProp;
+};
+
+export default function LoginScreen({ navigation }: LoginScreenProps) {
const { signIn } = useAuth();
const [serverUrl, setServerUrl] = useState("");
const [loading, setLoading] = useState(false);
@@ -97,6 +103,15 @@ export default function LoginScreen() {
You will be redirected to your organisation's sign-in page.
+
+ navigation.navigate("Welcome")}
+ accessibilityRole="button"
+ accessibilityLabel="Back to welcome screen"
+ style={styles.backLink}
+ >
+ ← Back
+
);
@@ -179,4 +194,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..34425947
--- /dev/null
+++ b/mobile/src/screens/WelcomeScreen.tsx
@@ -0,0 +1,217 @@
+/**
+ * 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 type { NativeStackNavigationProp } from "@react-navigation/native-stack";
+import React from "react";
+import {
+ Image,
+ Pressable,
+ SafeAreaView,
+ ScrollView,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+
+export type AuthStackParamList = {
+ Welcome: undefined;
+ Login: undefined;
+};
+
+type WelcomeScreenProps = {
+ navigation: NativeStackNavigationProp;
+};
+
+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({ navigation }: WelcomeScreenProps) {
+ 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={() => navigation.navigate("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",
+ },
+});
From 82d90b48e24320734f6b602044d2845efbbead05 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 15 Mar 2026 16:02:02 +0000
Subject: [PATCH 5/5] fix(mobile): correct asset logo path depth and add
expo-router app/ routes for web support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Fix fatal crash: require('../../../assets/logo.png') in LoginScreen and
WelcomeScreen resolved 3 levels above mobile/src/screens/ — outside the
mobile/ directory. Changed to ../../assets/logo.png which correctly
resolves to the existing mobile/assets/logo.png.
- Add expo-router app/ directory (root cause of missing welcome screen and
web support): app/_layout.tsx, (auth)/, (tabs)/ with all route files
- Add WelcomeScreen.tsx: branded intro screen with feature highlights
- Update LoginScreen/WelcomeScreen to use useRouter() (expo-router style)
- Add react-native-web ~0.20.0 and react-dom 19.2.4 for web channel
- Add expo-device ~7.0.3 (was imported but missing from package.json)
- Remove android.googleServicesFile from app.json (file is gitignored;
README documents how to restore it for Android FCM builds)
- Add web.bundler: metro and web.output: single to app.json
- Fix aria-hidden to explicit boolean value in WelcomeScreen
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
---
mobile/App.tsx | 143 +----------------
mobile/README.md | 42 +++--
mobile/app.json | 4 +-
mobile/app/(auth)/_layout.tsx | 17 ++
mobile/app/(auth)/index.tsx | 4 +
mobile/app/(auth)/login.tsx | 4 +
mobile/app/(tabs)/_layout.tsx | 72 +++++++++
mobile/app/(tabs)/files.tsx | 4 +
mobile/app/(tabs)/index.tsx | 4 +
mobile/app/(tabs)/profile.tsx | 4 +
mobile/app/_layout.tsx | 81 ++++++++++
mobile/package-lock.json | 230 ++++++++++++++++++++++++++-
mobile/package.json | 3 +
mobile/src/screens/LoginScreen.tsx | 14 +-
mobile/src/screens/WelcomeScreen.tsx | 20 +--
15 files changed, 467 insertions(+), 179 deletions(-)
create mode 100644 mobile/app/(auth)/_layout.tsx
create mode 100644 mobile/app/(auth)/index.tsx
create mode 100644 mobile/app/(auth)/login.tsx
create mode 100644 mobile/app/(tabs)/_layout.tsx
create mode 100644 mobile/app/(tabs)/files.tsx
create mode 100644 mobile/app/(tabs)/index.tsx
create mode 100644 mobile/app/(tabs)/profile.tsx
create mode 100644 mobile/app/_layout.tsx
diff --git a/mobile/App.tsx b/mobile/App.tsx
index b0e8ce34..642e3d00 100644
--- a/mobile/App.tsx
+++ b/mobile/App.tsx
@@ -1,140 +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
- * authenticated tab navigator or an unauthenticated auth stack that takes
- * the user through WelcomeScreen → LoginScreen.
- * 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 { createNativeStackNavigator } from "@react-navigation/native-stack";
-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";
-import WelcomeScreen, { type AuthStackParamList } from "./src/screens/WelcomeScreen";
-
-const Tab = createBottomTabNavigator();
-const AuthStack = createNativeStackNavigator();
-
-function AuthNavigator() {
- return (
-
-
-
-
- );
-}
-
-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 215ce5ee..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,6 +11,7 @@ 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
@@ -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
@@ -97,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 c3996187..38c986fb 100644
--- a/mobile/app.json
+++ b/mobile/app.json
@@ -41,7 +41,9 @@
"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 fa028286..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"
},
diff --git a/mobile/src/screens/LoginScreen.tsx b/mobile/src/screens/LoginScreen.tsx
index 80a29107..f278b46e 100644
--- a/mobile/src/screens/LoginScreen.tsx
+++ b/mobile/src/screens/LoginScreen.tsx
@@ -6,7 +6,7 @@
* AuthContext stores the API token and navigates to the main app.
*/
-import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
+import { useRouter } from "expo-router";
import React, { useState } from "react";
import {
ActivityIndicator,
@@ -21,14 +21,10 @@ import {
View,
} from "react-native";
import { useAuth } from "../context/AuthContext";
-import type { AuthStackParamList } from "./WelcomeScreen";
-type LoginScreenProps = {
- navigation: NativeStackNavigationProp;
-};
-
-export default function LoginScreen({ navigation }: LoginScreenProps) {
+export default function LoginScreen() {
const { signIn } = useAuth();
+ const router = useRouter();
const [serverUrl, setServerUrl] = useState("");
const [loading, setLoading] = useState(false);
@@ -62,7 +58,7 @@ export default function LoginScreen({ navigation }: LoginScreenProps) {
navigation.navigate("Welcome")}
+ onPress={() => router.back()}
accessibilityRole="button"
accessibilityLabel="Back to welcome screen"
style={styles.backLink}
diff --git a/mobile/src/screens/WelcomeScreen.tsx b/mobile/src/screens/WelcomeScreen.tsx
index 34425947..5923f9a3 100644
--- a/mobile/src/screens/WelcomeScreen.tsx
+++ b/mobile/src/screens/WelcomeScreen.tsx
@@ -5,7 +5,7 @@
* and a "Get Started" button that navigates to the LoginScreen.
*/
-import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
+import { useRouter } from "expo-router";
import React from "react";
import {
Image,
@@ -17,15 +17,6 @@ import {
View,
} from "react-native";
-export type AuthStackParamList = {
- Welcome: undefined;
- Login: undefined;
-};
-
-type WelcomeScreenProps = {
- navigation: NativeStackNavigationProp;
-};
-
const FEATURES: { icon: string; title: string; description: string }[] = [
{
icon: "🔍",
@@ -44,7 +35,8 @@ const FEATURES: { icon: string; title: string; description: string }[] = [
},
];
-export default function WelcomeScreen({ navigation }: WelcomeScreenProps) {
+export default function WelcomeScreen() {
+ const router = useRouter();
return (
{FEATURES.map((feature) => (
- {feature.icon}
+ {feature.icon}
{feature.title}
{feature.description}
@@ -85,7 +77,7 @@ export default function WelcomeScreen({ navigation }: WelcomeScreenProps) {
{/* CTA */}
[styles.button, pressed && styles.buttonPressed]}
- onPress={() => navigation.navigate("Login")}
+ onPress={() => router.push("/(auth)/login")}
accessibilityRole="button"
accessibilityLabel="Get started — connect to your DocuElevate server"
>