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:
@@ -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 ✅
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user