Merge pull request #765 from christianlouis/copilot/fix-ios-file-upload-processing
fix(mobile): add shared file to ShareContext directly in +not-found.tsx
This commit is contained in:
+1
-1
@@ -208,7 +208,7 @@ docuelevate://private/var/mobile/Library/Mobile Documents/…/Invoice.pdf
|
|||||||
|
|
||||||
expo-router strips the scheme and tries to match `/private/var/mobile/…` as an in-app route. Because no such route exists, it previously threw an **"unmatched route docuelevate://"** error and the upload never completed.
|
expo-router strips the scheme and tries to match `/private/var/mobile/…` as an in-app route. Because no such route exists, it previously threw an **"unmatched route docuelevate://"** error and the upload never completed.
|
||||||
|
|
||||||
The fix is a catch-all `+not-found.tsx` route (see `mobile/app/+not-found.tsx`). When expo-router cannot match the path, it renders this screen instead. The screen detects that the path is a filesystem path rather than a real in-app route and immediately redirects to the Upload tab. The `Linking` listener registered in the root layout has concurrently (or will shortly) added the file to `ShareContext`, so the upload proceeds normally once the user lands on the Upload tab.
|
The fix is a catch-all `+not-found.tsx` route (see `mobile/app/+not-found.tsx`). When expo-router cannot match the path, it renders this screen instead. The screen detects that the path is a filesystem path rather than a real in-app route, 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 the root layout may also fire for the same URL; `ShareContext.addPendingFile` deduplicates by URI so the file is only uploaded once.
|
||||||
|
|
||||||
##### iOS Action / Share Extension (future enhancement)
|
##### iOS Action / Share Extension (future enhancement)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -171,7 +171,7 @@ iOS sometimes delivers the file path under the `docuelevate://` scheme:
|
|||||||
docuelevate://private/var/mobile/Library/Mobile Documents/…/Invoice.pdf
|
docuelevate://private/var/mobile/Library/Mobile Documents/…/Invoice.pdf
|
||||||
```
|
```
|
||||||
|
|
||||||
expo-router strips the scheme and tries to match `/private/var/mobile/…` as an in-app route. The catch-all `app/+not-found.tsx` intercepts this, detects the filesystem-path pattern, and immediately redirects to the Upload tab. The file — already added to `ShareContext` by the `Linking` listener — is then uploaded automatically.
|
expo-router strips the scheme and tries to match `/private/var/mobile/…` as an in-app route. The catch-all `app/+not-found.tsx` intercepts this, 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 the root layout may also fire for the same URL; `ShareContext` deduplicates by URI to prevent double uploads.
|
||||||
|
|
||||||
**Supported iOS file types:** PDF, images (JPEG / PNG / GIF / BMP / TIFF / WebP), plain text, Word (`.docx`, `.doc`), Excel (`.xlsx`, `.xls`), PowerPoint (`.pptx`, `.ppt`), and any other file (`public.data`).
|
**Supported iOS file types:** PDF, images (JPEG / PNG / GIF / BMP / TIFF / WebP), plain text, Word (`.docx`, `.doc`), Excel (`.xlsx`, `.xls`), PowerPoint (`.pptx`, `.ppt`), and any other file (`public.data`).
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,13 @@
|
|||||||
* no such route exists, expo-router previously threw "unmatched route
|
* no such route exists, expo-router previously threw "unmatched route
|
||||||
* docuelevate://…" and the upload never happened.
|
* docuelevate://…" and the upload never happened.
|
||||||
*
|
*
|
||||||
* This screen detects the filesystem-path pattern and immediately redirects
|
* This screen detects the filesystem-path pattern, adds the file directly
|
||||||
* to the Upload tab. The `Linking` listener registered in `_layout.tsx`
|
* to `ShareContext`, and redirects to the Upload tab. `UploadScreen`
|
||||||
* runs concurrently and adds the file to `ShareContext`; `UploadScreen`
|
* picks up the pending file and begins uploading automatically.
|
||||||
* picks it up and begins uploading as soon as the redirect completes.
|
*
|
||||||
|
* 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
|
* 2. **Any other unmatched in-app route** — redirect silently to the root so
|
||||||
* the user isn't left on a blank error page.
|
* the user isn't left on a blank error page.
|
||||||
@@ -22,6 +25,7 @@
|
|||||||
import { usePathname, useRouter } from "expo-router";
|
import { usePathname, useRouter } from "expo-router";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
||||||
|
import { useShare } from "../src/context/ShareContext";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
@@ -81,6 +85,21 @@ function looksLikeFilePath(pathname: string): boolean {
|
|||||||
return !IN_APP_ROUTE_PREFIXES.some((prefix) => stripped.startsWith(prefix));
|
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
|
// Screen component
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -88,18 +107,26 @@ function looksLikeFilePath(pathname: string): boolean {
|
|||||||
export default function NotFoundScreen() {
|
export default function NotFoundScreen() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { addPendingFile } = useShare();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (looksLikeFilePath(pathname)) {
|
if (looksLikeFilePath(pathname)) {
|
||||||
// Filesystem path from iOS "Open In…" – redirect to Upload tab.
|
// Filesystem path from iOS "Open In…" – add the file to ShareContext
|
||||||
// The Linking listener in _layout.tsx has already (or will shortly)
|
// and redirect to the Upload tab. UploadScreen will pick up the
|
||||||
// added the file to ShareContext; UploadScreen will pick it up.
|
// 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 });
|
||||||
router.replace("/(tabs)/");
|
router.replace("/(tabs)/");
|
||||||
} else {
|
} else {
|
||||||
// Truly unknown in-app route – fall back to the root redirect.
|
// Truly unknown in-app route – fall back to the root redirect.
|
||||||
router.replace("/");
|
router.replace("/");
|
||||||
}
|
}
|
||||||
}, [pathname, router]);
|
}, [pathname, router, addPendingFile]);
|
||||||
|
|
||||||
// Show a brief spinner while the redirect is in flight.
|
// Show a brief spinner while the redirect is in flight.
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
*
|
*
|
||||||
* The companion `+not-found.tsx` handles the case where expo-router receives
|
* The companion `+not-found.tsx` handles the case where expo-router receives
|
||||||
* a `docuelevate://` URL with a filesystem path (from iOS "Open In…") and
|
* a `docuelevate://` URL with a filesystem path (from iOS "Open In…") and
|
||||||
* cannot match it to a route. It detects the pattern and redirects to the
|
* cannot match it to a route. It adds the file directly to ShareContext and
|
||||||
* Upload tab so the file — already in ShareContext — is uploaded transparently.
|
* redirects to the Upload tab so the file is uploaded transparently.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as Linking from "expo-linking";
|
import * as Linking from "expo-linking";
|
||||||
@@ -55,9 +55,9 @@ function filenameFromUri(uri: string): string {
|
|||||||
*
|
*
|
||||||
* Note: expo-router also receives the same URL and will attempt to match it as
|
* Note: expo-router also receives the same URL and will attempt to match it as
|
||||||
* an in-app route. When no route matches it renders `+not-found.tsx`, which
|
* an in-app route. When no route matches it renders `+not-found.tsx`, which
|
||||||
* redirects to the Upload tab. This handler and `+not-found.tsx` work in
|
* adds the file to ShareContext directly and redirects to the Upload tab.
|
||||||
* concert: this handler adds the file to ShareContext, and `+not-found.tsx`
|
* Both this handler and `+not-found.tsx` call `addPendingFile`;
|
||||||
* ensures the user lands on the Upload tab so the file is uploaded.
|
* `ShareContext` deduplicates by URI so the file is only uploaded once.
|
||||||
*/
|
*/
|
||||||
function makeUrlHandler(addPendingFile: (f: { uri: string; filename: string }) => void) {
|
function makeUrlHandler(addPendingFile: (f: { uri: string; filename: string }) => void) {
|
||||||
return ({ url }: { url: string }) => {
|
return ({ url }: { url: string }) => {
|
||||||
|
|||||||
@@ -32,7 +32,16 @@ export function ShareProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const [pendingFiles, setPendingFiles] = useState<SharedFile[]>([]);
|
const [pendingFiles, setPendingFiles] = useState<SharedFile[]>([]);
|
||||||
|
|
||||||
const addPendingFile = useCallback((file: SharedFile) => {
|
const addPendingFile = useCallback((file: SharedFile) => {
|
||||||
setPendingFiles((prev) => [...prev, file]);
|
setPendingFiles((prev) => {
|
||||||
|
// Deduplicate by normalised URI so the same file is not uploaded twice
|
||||||
|
// when both the Linking handler (_layout.tsx) and +not-found.tsx fire.
|
||||||
|
const normalize = (uri: string) => {
|
||||||
|
try { return decodeURIComponent(uri); } catch { return uri; }
|
||||||
|
};
|
||||||
|
const norm = normalize(file.uri);
|
||||||
|
if (prev.some((f) => normalize(f.uri) === norm)) return prev;
|
||||||
|
return [...prev, file];
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const clearPendingFiles = useCallback(() => {
|
const clearPendingFiles = useCallback(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user