Files
gh-christianlouis-docuelevate/mobile/app/+not-found.tsx
T
copilot-swe-agent[bot] e518bce922 fix: merge main branch and renumber migration 037→040
Resolve all merge conflicts between our automation feature branch and
current main (v0.163.0, 920 commits ahead).

Conflicts resolved:
- app/api/__init__.py: add automation_router alongside main's new routers
  (classification_rules, qr_auth, sessions, system_reset)
- app/config.py: add main's new settings (dropbox_use_global_credentials,
  factory_reset_on_startup, enable_factory_reset)
- app/models.py: add main's new models (ClassificationRuleModel, UserSession,
  QRLoginChallenge, SharePoint integration type)
- app/utils/settings_service.py: merge automation_hooks_enabled with main's
  new metadata entries
- docs/API.md: merge automation API docs with main's classification rules docs
- docs/ConfigurationGuide.md: add factory reset settings
- tests/conftest.py: import both AutomationHook and new main models

Migration renumbered:
- 037_add_automation_hooks → 040_add_automation_hooks
- down_revision: 039_add_classification_rules (was 036_add_document_translation_fields)
- Chain: 036 → 037 → 038 → 039 → 040 (automation hooks)

For all non-automation files with conflicts, main's version was taken since
our branch did not modify those files (conflicts were from a stale prior merge).

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
Agent-Logs-Url: https://github.com/christianlouis/DocuElevate/sessions/cb62f012-3b69-4415-835e-3857ce3e9f45
2026-03-20 23:54:04 +00:00

155 lines
6.0 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.
/**
* Catch-all "not found" route for expo-router.
*
* This screen intercepts two different situations:
*
* 1. **iOS "Open In…" / share sheet** — iOS delivers files to the app via a
* `docuelevate://<path>` URL. expo-router strips the custom scheme and
* tries to match the raw filesystem path (e.g.
* `/private/var/mobile/Library/…/file.pdf`) as an in-app route. Because
* no such route exists, expo-router previously threw "unmatched route
* docuelevate://…" and the upload never happened.
*
* This screen detects the filesystem-path pattern, adds the file directly
* to `ShareContext`, and redirects to the Upload tab. `UploadScreen`
* picks up the pending file and begins uploading automatically.
*
* The `Linking` listener in `_layout.tsx` may also fire for the same URL;
* `ShareContext.addPendingFile` deduplicates by URI so the file is only
* uploaded once.
*
* 2. **Any other unmatched in-app route** — redirect silently to the root so
* the user isn't left on a blank error page.
*/
import { usePathname, useRouter } from "expo-router";
import React, { useEffect } from "react";
import { ActivityIndicator, StyleSheet, View } from "react-native";
import { useShare } from "../src/context/ShareContext";
import { mimeTypeFromFilename } from "../src/utils/mimeTypes";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* First path-segment names that identify iOS/Android sandbox filesystem paths.
* These can never be expo-router route-group names, so their presence is a
* strong positive signal that the URL is a shared file rather than a route.
*
* iOS: /private/var/mobile/… → "private"
* /var/mobile/… → "var" (symlink to /private/var/mobile)
* /tmp/… → "tmp"
* Android: /data/user/0/… → "data"
* /storage/emulated/0/… → "storage"
*/
const FS_PATH_ROOTS = ["private", "var", "tmp", "data", "storage"];
/**
* Route-group / special-file prefixes that identify genuine in-app routes
* rather than filesystem path segments.
*
* ⚠️ Keep this list in sync with the top-level entries in the `app/`
* directory. Add an entry here if you add a new top-level route group
* that does **not** use the parentheses convention.
*/
const IN_APP_ROUTE_PREFIXES = [
"(auth)", // app/(auth)/
"(tabs)", // app/(tabs)/
"_", // expo-router special files (_layout, _sitemap, …)
"+", // expo-router special files (+not-found, …)
"--", // Expo Go development proxy prefix
];
/**
* Return `true` when `pathname` looks like a filesystem path delivered by iOS
* "Open In…" (e.g. `/private/var/mobile/Library/…/file.pdf`) rather than a
* legitimate in-app route.
*
* Detection strategy:
* 1. **Positive check** if the first path segment matches a known device
* filesystem root (see `FS_PATH_ROOTS`), it is definitely a file path.
* 2. **Fallback negative check** if the path does not start with any known
* in-app route prefix (see `IN_APP_ROUTE_PREFIXES`), treat it as a file
* path. This is a heuristic but safe because expo-router route groups
* always use parentheses (e.g. `(auth)`, `(tabs)`).
*/
function looksLikeFilePath(pathname: string): boolean {
const stripped = pathname.replace(/^\/+/, "");
if (stripped.length === 0) return false;
// Positive signal: path starts with a known device filesystem root segment.
const firstSegment = stripped.split("/")[0];
if (FS_PATH_ROOTS.includes(firstSegment)) return true;
// Fallback: paths that start with a known in-app route prefix are routes.
return !IN_APP_ROUTE_PREFIXES.some((prefix) => stripped.startsWith(prefix));
}
/**
* Extract a display filename from a filesystem path.
* Handles URL-encoded characters and strips query strings.
*/
function filenameFromPath(pathname: string): string {
try {
const decoded = decodeURIComponent(pathname);
const segments = decoded.split("/").filter(Boolean);
const last = segments[segments.length - 1] ?? "shared_file";
return last.split("?")[0] || "shared_file";
} catch {
return "shared_file";
}
}
// ---------------------------------------------------------------------------
// Screen component
// ---------------------------------------------------------------------------
export default function NotFoundScreen() {
const pathname = usePathname();
const router = useRouter();
const { addPendingFile } = useShare();
// Guard: track which pathname has been handled so the effect does not
// re-fire when `router` or `addPendingFile` change identity mid-navigation.
const handledRef = React.useRef<string | null>(null);
useEffect(() => {
if (handledRef.current === pathname) return; // already handled
handledRef.current = pathname;
if (looksLikeFilePath(pathname)) {
// Filesystem path from iOS "Open In…" add the file to ShareContext
// and redirect to the Upload tab. UploadScreen will pick up the
// pending file and begin uploading automatically.
//
// The pathname from expo-router is the raw filesystem path
// (e.g. "/private/var/mobile/Library/…/file.pdf"). Reconstruct a
// file:// URI so the upload logic can read the file.
const fileUri = `file://${pathname}`;
const filename = filenameFromPath(pathname);
addPendingFile({ uri: fileUri, filename, mimeType: mimeTypeFromFilename(filename) });
router.replace("/(tabs)/");
} else {
// Truly unknown in-app route fall back to the root redirect.
router.replace("/");
}
}, [pathname, router, addPendingFile]);
// Show a brief spinner while the redirect is in flight.
return (
<View style={styles.container}>
<ActivityIndicator size="large" color="#1e40af" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
backgroundColor: "#f9fafb",
},
});