diff --git a/docs/AppleAppStoreCompliance.md b/docs/AppleAppStoreCompliance.md index e8d6716f..2df05a2a 100644 --- a/docs/AppleAppStoreCompliance.md +++ b/docs/AppleAppStoreCompliance.md @@ -185,8 +185,9 @@ The privacy manifest is configured via `expo-build-properties` plugin in `app.js ### 4.4 Document Handling ✅ - `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. +- `UploadScreen` uses `expo-file-system` to copy external files to cache before uploading for reliable file access. ### 4.5 Crash Resistance ✅ diff --git a/mobile/app/+not-found.tsx b/mobile/app/+not-found.tsx index 6f3d3604..86b7b430 100644 --- a/mobile/app/+not-found.tsx +++ b/mobile/app/+not-found.tsx @@ -26,49 +26,12 @@ 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 // --------------------------------------------------------------------------- -/** - * 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 = { - 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. * These can never be expo-router route-group names, so their presence is a diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index ad0274cf..d2396c07 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -23,6 +23,7 @@ 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"; +import { mimeTypeFromFilename } from "../src/utils/mimeTypes"; // --------------------------------------------------------------------------- // 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 = { - 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:// * URLs to ShareContext. Extracted as a module-level factory so the handler diff --git a/mobile/src/screens/ProfileScreen.tsx b/mobile/src/screens/ProfileScreen.tsx index 6b1d22fa..48146fc3 100644 --- a/mobile/src/screens/ProfileScreen.tsx +++ b/mobile/src/screens/ProfileScreen.tsx @@ -45,7 +45,9 @@ export default function ProfileScreen() { text: "Delete Account", style: "destructive", 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() { - Linking.openURL(`${effectiveBaseUrl}/privacy`); + Linking.openURL(`${effectiveBaseUrl}/privacy`).catch(() => { + Alert.alert("Error", "Could not open the privacy policy. Please try again."); + }); } 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) { diff --git a/mobile/src/screens/UploadScreen.tsx b/mobile/src/screens/UploadScreen.tsx index d883dd5c..38d4fb90 100644 --- a/mobile/src/screens/UploadScreen.tsx +++ b/mobile/src/screens/UploadScreen.tsx @@ -95,8 +95,9 @@ export default function UploadScreen() { try { await FileSystem.copyAsync({ from: uri, to: destUri }); return destUri; - } catch { + } catch (copyErr) { // 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; } }, []); diff --git a/mobile/src/utils/mimeTypes.ts b/mobile/src/utils/mimeTypes.ts new file mode 100644 index 00000000..82f6de91 --- /dev/null +++ b/mobile/src/utils/mimeTypes.ts @@ -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 = { + 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; +}