Files
gh-christianlouis-docuelevate/mobile/app/_layout.tsx
T
copilot-swe-agent[bot] 4de6b439ce fix(mobile): iOS share sheet, empty files tab, and stuck upload status
- app.json: add CFBundleDocumentTypes to iOS infoPlist so the app
  appears in the iOS Share Sheet; add ACTION_SEND/SEND_MULTIPLE
  intentFilters for Android share intent support

- src/context/ShareContext.tsx (new): React context that queues files
  received from the share sheet and delivers them to UploadScreen

- app/_layout.tsx: wrap in ShareProvider; add Linking handler
  (makeUrlHandler factory + getInitialURL cold-start + addEventListener
  warm-start) to capture file:// and content:// URLs

- src/services/api.ts: fix FileRecord interface (original_filename,
  nested ProcessingStatus, mime_type); fix UploadResponse interface;
  fix listFiles() (per_page param, unwrap data.files); add
  getFileStatus(fileId) for single-file status polling

- src/screens/FilesScreen.tsx: use file.original_filename and
  file.processing_status.status; fix statusEmoji to use actual backend
  status values (completed/pending/duplicate)

- src/screens/UploadScreen.tsx: consume ShareContext for auto-upload of
  shared files; add 5-second polling loop (search by filename → file_id
  → getFileStatus) to show real-time server processing status;
  uploadFile wrapped in useCallback; proper effect dependency arrays

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-03-16 15:52:54 +00:00

136 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 (
<View style={styles.loading}>
<ActivityIndicator size="large" color="#1e40af" />
<Text style={styles.loadingText}>Loading</Text>
</View>
);
}
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(auth)" />
<Stack.Screen name="(tabs)" />
</Stack>
);
}
// ---------------------------------------------------------------------------
// Root layout export
// ---------------------------------------------------------------------------
export default function RootLayout() {
return (
<SafeAreaProvider>
<ShareProvider>
<AuthProvider>
<AuthGuard />
</AuthProvider>
</ShareProvider>
</SafeAreaProvider>
);
}
const styles = StyleSheet.create({
loading: {
flex: 1,
alignItems: "center",
justifyContent: "center",
backgroundColor: "#f9fafb",
gap: 12,
},
loadingText: {
color: "#6b7280",
fontSize: 15,
},
});