/** * 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. * * ShareProvider + Linking listener: when iOS opens the app via the share * sheet (CFBundleDocumentTypes) or Android via a SEND intent, the incoming * file:// / content:// URL is captured and forwarded to UploadScreen via * ShareContext. */ import * as Linking from "expo-linking"; 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"; import { ShareProvider, useShare } from "../src/context/ShareContext"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** Extract a display filename from a file:// or content:// URI. */ function filenameFromUri(uri: string): string { try { const decoded = decodeURIComponent(uri); // Take the last path segment and strip any query string const last = decoded.split("/").pop() ?? "shared_file"; return last.split("?")[0] || "shared_file"; } catch { return "shared_file"; } } /** * Build a Linking URL handler that forwards incoming file:// / content:// * URLs to ShareContext. Extracted as a module-level factory so the handler * itself is created once and can be easily unit-tested without a React context. */ function makeUrlHandler(addPendingFile: (f: { uri: string; filename: string }) => void) { return ({ url }: { url: string }) => { if (!url.startsWith("file://") && !url.startsWith("content://")) return; addPendingFile({ uri: url, filename: filenameFromUri(url) }); }; } // --------------------------------------------------------------------------- // Auth guard – redirects to the correct route group after auth state resolves // --------------------------------------------------------------------------- function AuthGuard() { const { isLoading, isAuthenticated } = useAuth(); const { addPendingFile } = useShare(); const segments = useSegments(); const router = useRouter(); // Listen for files shared from other apps (iOS Share Sheet / Android Intent). // Both cold-start (app was not running) and warm-start (app in background) // cases are handled. useEffect(() => { const handleIncomingUrl = makeUrlHandler(addPendingFile); // Cold start – app launched directly by a share action Linking.getInitialURL().then((url) => { if (url) handleIncomingUrl({ url }); }); // Warm start – app was already running when the share action occurred const subscription = Linking.addEventListener("url", handleIncomingUrl); return () => subscription.remove(); }, [addPendingFile]); 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, }, });