refactor(mobile): extract shared MIME type utility and improve error handling

- Extract EXT_TO_MIME map and mimeTypeFromFilename to shared module
  at mobile/src/utils/mimeTypes.ts (used by _layout.tsx and +not-found.tsx)
- Add error logging to ensureLocalUri catch block for debugging
- Add error handling to Linking.openURL calls in ProfileScreen
- Fix incorrect LSSupportsOpeningDocumentsInPlace docs in audit report

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-19 12:05:47 +00:00
parent f549505bfd
commit cfe83d7efa
6 changed files with 59 additions and 81 deletions
+2 -1
View File
@@ -185,8 +185,9 @@ The privacy manifest is configured via `expo-build-properties` plugin in `app.js
### 4.4 Document Handling ✅ ### 4.4 Document Handling ✅
- `CFBundleDocumentTypes` properly declares supported file types. - `CFBundleDocumentTypes` properly declares supported file types.
- `LSSupportsOpeningDocumentsInPlace: true` enables direct file access from the Files app. - `LSSupportsOpeningDocumentsInPlace: false` ensures iOS copies shared files to the app's accessible Inbox directory, avoiding security-scoped URL issues.
- The `+not-found.tsx` handler correctly intercepts iOS "Open In…" file paths. - The `+not-found.tsx` handler correctly intercepts iOS "Open In…" file paths.
- `UploadScreen` uses `expo-file-system` to copy external files to cache before uploading for reliable file access.
### 4.5 Crash Resistance ✅ ### 4.5 Crash Resistance ✅
+1 -38
View File
@@ -26,49 +26,12 @@ 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"; import { useShare } from "../src/context/ShareContext";
import { mimeTypeFromFilename } from "../src/utils/mimeTypes";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers // Helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/**
* Common MIME type mappings for file extensions.
* Used to infer the MIME type of files shared via iOS "Open In…" so the
* server receives a correct Content-Type instead of application/octet-stream.
*/
const EXT_TO_MIME: Record<string, string> = {
pdf: "application/pdf",
jpg: "image/jpeg",
jpeg: "image/jpeg",
png: "image/png",
gif: "image/gif",
bmp: "image/bmp",
tiff: "image/tiff",
tif: "image/tiff",
webp: "image/webp",
heic: "image/heic",
heif: "image/heif",
txt: "text/plain",
csv: "text/csv",
doc: "application/msword",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
xls: "application/vnd.ms-excel",
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
ppt: "application/vnd.ms-powerpoint",
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
rtf: "application/rtf",
html: "text/html",
xml: "application/xml",
json: "application/json",
zip: "application/zip",
};
/** Infer MIME type from a filename's extension, or undefined if unknown. */
function mimeTypeFromFilename(filename: string): string | undefined {
const ext = filename.split(".").pop()?.toLowerCase();
return ext ? EXT_TO_MIME[ext] : undefined;
}
/** /**
* First path-segment names that identify iOS/Android sandbox filesystem paths. * 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 * These can never be expo-router route-group names, so their presence is a
+1 -38
View File
@@ -23,6 +23,7 @@ import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
import { SafeAreaProvider } from "react-native-safe-area-context"; import { SafeAreaProvider } from "react-native-safe-area-context";
import { AuthProvider, useAuth } from "../src/context/AuthContext"; import { AuthProvider, useAuth } from "../src/context/AuthContext";
import { ShareProvider, useShare } from "../src/context/ShareContext"; import { ShareProvider, useShare } from "../src/context/ShareContext";
import { mimeTypeFromFilename } from "../src/utils/mimeTypes";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers // Helpers
@@ -50,44 +51,6 @@ function filenameFromUri(uri: string): string {
} }
} }
/**
* Common MIME type mappings for file extensions.
* Used to infer the MIME type of files shared via the Share Sheet / "Open In…"
* so the server receives a correct Content-Type instead of application/octet-stream.
*/
const EXT_TO_MIME: Record<string, string> = {
pdf: "application/pdf",
jpg: "image/jpeg",
jpeg: "image/jpeg",
png: "image/png",
gif: "image/gif",
bmp: "image/bmp",
tiff: "image/tiff",
tif: "image/tiff",
webp: "image/webp",
heic: "image/heic",
heif: "image/heif",
txt: "text/plain",
csv: "text/csv",
doc: "application/msword",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
xls: "application/vnd.ms-excel",
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
ppt: "application/vnd.ms-powerpoint",
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
rtf: "application/rtf",
html: "text/html",
xml: "application/xml",
json: "application/json",
zip: "application/zip",
};
/** Infer MIME type from a filename's extension, or undefined if unknown. */
function mimeTypeFromFilename(filename: string): string | undefined {
const ext = filename.split(".").pop()?.toLowerCase();
return ext ? EXT_TO_MIME[ext] : undefined;
}
/** /**
* Build a Linking URL handler that forwards incoming file:// / content:// * Build a Linking URL handler that forwards incoming file:// / content://
* URLs to ShareContext. Extracted as a module-level factory so the handler * URLs to ShareContext. Extracted as a module-level factory so the handler
+9 -3
View File
@@ -45,7 +45,9 @@ export default function ProfileScreen() {
text: "Delete Account", text: "Delete Account",
style: "destructive", style: "destructive",
onPress: () => { onPress: () => {
Linking.openURL(`${effectiveBaseUrl}/account/delete`); Linking.openURL(`${effectiveBaseUrl}/account/delete`).catch(() => {
Alert.alert("Error", "Could not open the account deletion page. Please try again.");
});
}, },
}, },
] ]
@@ -53,11 +55,15 @@ export default function ProfileScreen() {
} }
function openPrivacyPolicy() { function openPrivacyPolicy() {
Linking.openURL(`${effectiveBaseUrl}/privacy`); Linking.openURL(`${effectiveBaseUrl}/privacy`).catch(() => {
Alert.alert("Error", "Could not open the privacy policy. Please try again.");
});
} }
function openTermsOfService() { function openTermsOfService() {
Linking.openURL(`${effectiveBaseUrl}/terms`); Linking.openURL(`${effectiveBaseUrl}/terms`).catch(() => {
Alert.alert("Error", "Could not open the terms of service. Please try again.");
});
} }
if (!user) { if (!user) {
+2 -1
View File
@@ -95,8 +95,9 @@ export default function UploadScreen() {
try { try {
await FileSystem.copyAsync({ from: uri, to: destUri }); await FileSystem.copyAsync({ from: uri, to: destUri });
return destUri; return destUri;
} catch { } catch (copyErr) {
// Copy failed fall back to the original URI (might work for some paths). // Copy failed fall back to the original URI (might work for some paths).
console.warn("[ensureLocalUri] copyAsync failed:", { from: uri, to: destUri, error: copyErr });
return uri; return uri;
} }
}, []); }, []);
+44
View File
@@ -0,0 +1,44 @@
/**
* Shared MIME type utilities for the DocuElevate mobile app.
*
* Used by the Linking handler in _layout.tsx, the catch-all +not-found.tsx,
* and any other code that needs to infer a MIME type from a file extension.
*/
/**
* Common MIME type mappings for file extensions.
* Used to infer the MIME type of files shared via the Share Sheet / "Open In…"
* so the server receives a correct Content-Type instead of application/octet-stream.
*/
export const EXT_TO_MIME: Record<string, string> = {
pdf: "application/pdf",
jpg: "image/jpeg",
jpeg: "image/jpeg",
png: "image/png",
gif: "image/gif",
bmp: "image/bmp",
tiff: "image/tiff",
tif: "image/tiff",
webp: "image/webp",
heic: "image/heic",
heif: "image/heif",
txt: "text/plain",
csv: "text/csv",
doc: "application/msword",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
xls: "application/vnd.ms-excel",
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
ppt: "application/vnd.ms-powerpoint",
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
rtf: "application/rtf",
html: "text/html",
xml: "application/xml",
json: "application/json",
zip: "application/zip",
};
/** Infer MIME type from a filename's extension, or undefined if unknown. */
export function mimeTypeFromFilename(filename: string): string | undefined {
const ext = filename.split(".").pop()?.toLowerCase();
return ext ? EXT_TO_MIME[ext] : undefined;
}