fix(mobile): Apple App Store compliance fixes

- Remove unused `fetch` from UIBackgroundModes (Guideline 2.5.4)
- Add iOS privacy manifest configuration for required reason APIs
- Add Privacy Policy and Terms of Service links to ProfileScreen
- Add account deletion capability (Guideline 5.1.1(v))
- Remove unused Switch import from ProfileScreen
- Replace emoji icons with Ionicons in UploadScreen and FilesScreen
- Add app version display to ProfileScreen

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-19 11:38:48 +00:00
parent 0aab5bcbf7
commit 5c15a2395a
4 changed files with 164 additions and 31 deletions
+24 -2
View File
@@ -22,7 +22,7 @@
"NSCameraUsageDescription": "DocuElevate uses the camera to scan QR codes for login and to capture documents for upload.",
"NSPhotoLibraryUsageDescription": "DocuElevate accesses your photo library to select documents for upload.",
"NSPhotoLibraryAddUsageDescription": "DocuElevate saves scanned documents to your photo library.",
"UIBackgroundModes": ["fetch", "remote-notification"],
"UIBackgroundModes": ["remote-notification"],
"ITSAppUsesNonExemptEncryption": false,
"LSSupportsOpeningDocumentsInPlace": true,
"CFBundleDocumentTypes": [
@@ -86,7 +86,29 @@
"expo-build-properties",
{
"ios": {
"buildReactNativeFromSource": true
"buildReactNativeFromSource": true,
"privacyManifests": {
"NSPrivacyAccessedAPITypes": [
{
"NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryUserDefaults",
"NSPrivacyAccessedAPITypeReasons": ["CA92.1"]
},
{
"NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryFileTimestamp",
"NSPrivacyAccessedAPITypeReasons": ["C617.1"]
},
{
"NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryDiskSpace",
"NSPrivacyAccessedAPITypeReasons": ["E174.1"]
},
{
"NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategorySystemBootTime",
"NSPrivacyAccessedAPITypeReasons": ["35F9.1"]
}
],
"NSPrivacyCollectedDataTypes": [],
"NSPrivacyTracking": false
}
}
}
],
+13 -12
View File
@@ -2,6 +2,7 @@
* FilesScreen list of documents processed by DocuElevate.
*/
import { Ionicons } from "@expo/vector-icons";
import React, { useCallback, useEffect, useState } from "react";
import {
ActivityIndicator,
@@ -34,15 +35,15 @@ function formatDate(iso: string): string {
}
}
function statusEmoji(status: string): string {
const map: Record<string, string> = {
completed: "✅",
processing: "⚙️",
pending: "⏳",
failed: "❌",
duplicate: "🔁",
function statusIcon(status: string): { name: keyof typeof Ionicons.glyphMap; color: string } {
const map: Record<string, { name: keyof typeof Ionicons.glyphMap; color: string }> = {
completed: { name: "checkmark-circle", color: "#059669" },
processing: { name: "sync-circle", color: "#d97706" },
pending: { name: "time-outline", color: "#6b7280" },
failed: { name: "close-circle", color: "#dc2626" },
duplicate: { name: "copy-outline", color: "#6b7280" },
};
return map[status?.toLowerCase()] ?? "📄";
return map[status?.toLowerCase()] ?? { name: "document-outline", color: "#6b7280" };
}
export default function FilesScreen() {
@@ -126,7 +127,7 @@ export default function FilesScreen() {
onEndReachedThreshold={0.4}
ListEmptyComponent={
<View style={styles.emptyState}>
<Text style={styles.emptyEmoji}>📂</Text>
<Ionicons name="folder-open-outline" size={48} color="#9ca3af" style={{ marginBottom: 12 }} />
<Text style={styles.emptyText}>No documents yet.</Text>
<Text style={styles.emptyHint}>
Upload a document from the Upload tab to get started.
@@ -144,9 +145,10 @@ export default function FilesScreen() {
function FileRow({ file }: { file: FileRecord }) {
const status = file.processing_status?.status ?? "pending";
const icon = statusIcon(status);
return (
<View style={rowStyles.row}>
<Text style={rowStyles.icon}>{statusEmoji(status)}</Text>
<Ionicons name={icon.name} size={22} color={icon.color} style={rowStyles.icon} />
<View style={rowStyles.info}>
<Text style={rowStyles.filename} numberOfLines={1}>
{file.original_filename}
@@ -179,7 +181,6 @@ const styles = StyleSheet.create({
},
retryText: { color: "#fff", fontWeight: "600" },
emptyState: { alignItems: "center", paddingTop: 60 },
emptyEmoji: { fontSize: 48, marginBottom: 12 },
emptyText: { fontSize: 16, color: "#374151", marginBottom: 8 },
emptyHint: {
fontSize: 13,
@@ -203,7 +204,7 @@ const rowStyles = StyleSheet.create({
shadowRadius: 4,
elevation: 2,
},
icon: { fontSize: 22, marginRight: 12 },
icon: { marginRight: 12 },
info: { flex: 1 },
filename: {
fontSize: 14,
+113 -3
View File
@@ -2,6 +2,8 @@
* ProfileScreen authenticated user profile and settings.
*/
import Constants from "expo-constants";
import * as Linking from "expo-linking";
import React from "react";
import {
Alert,
@@ -9,7 +11,6 @@ import {
Pressable,
ScrollView,
StyleSheet,
Switch,
Text,
View,
} from "react-native";
@@ -18,6 +19,8 @@ import { useAuth } from "../context/AuthContext";
export default function ProfileScreen() {
const { user, signOut, baseUrl } = useAuth();
const appVersion = Constants.expoConfig?.version ?? "1.0.0";
function handleSignOut() {
Alert.alert("Sign out", "Are you sure you want to sign out?", [
{ text: "Cancel", style: "cancel" },
@@ -29,6 +32,37 @@ export default function ProfileScreen() {
]);
}
function handleDeleteAccount() {
Alert.alert(
"Delete Account",
"This will permanently delete your account and all associated data. This action cannot be undone.",
[
{ text: "Cancel", style: "cancel" },
{
text: "Delete Account",
style: "destructive",
onPress: () => {
if (baseUrl) {
Linking.openURL(`${baseUrl}/account/delete`);
}
},
},
]
);
}
function openPrivacyPolicy() {
if (baseUrl) {
Linking.openURL(`${baseUrl}/privacy`);
}
}
function openTermsOfService() {
if (baseUrl) {
Linking.openURL(`${baseUrl}/terms`);
}
}
if (!user) {
return (
<View style={styles.center}>
@@ -76,7 +110,30 @@ export default function ProfileScreen() {
</View>
</View>
{/* Danger zone */}
{/* Legal & Privacy */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Legal</Text>
<Pressable
style={styles.linkRow}
onPress={openPrivacyPolicy}
accessibilityRole="link"
accessibilityLabel="Privacy Policy"
>
<Text style={styles.linkText}>Privacy Policy</Text>
<Text style={styles.linkChevron}></Text>
</Pressable>
<Pressable
style={styles.linkRow}
onPress={openTermsOfService}
accessibilityRole="link"
accessibilityLabel="Terms of Service"
>
<Text style={styles.linkText}>Terms of Service</Text>
<Text style={styles.linkChevron}></Text>
</Pressable>
</View>
{/* Sign out */}
<View style={styles.section}>
<Pressable
style={styles.signOutButton}
@@ -87,13 +144,28 @@ export default function ProfileScreen() {
<Text style={styles.signOutText}>Sign out</Text>
</Pressable>
</View>
{/* Account deletion Apple Guideline 5.1.1(v) */}
<View style={styles.section}>
<Pressable
style={styles.deleteAccountButton}
onPress={handleDeleteAccount}
accessibilityRole="button"
accessibilityLabel="Delete account"
>
<Text style={styles.deleteAccountText}>Delete Account</Text>
</Pressable>
</View>
{/* App version */}
<Text style={styles.versionText}>DocuElevate v{appVersion}</Text>
</ScrollView>
);
}
const styles = StyleSheet.create({
scroll: { flex: 1, backgroundColor: "#f9fafb" },
content: { padding: 20 },
content: { padding: 20, paddingBottom: 40 },
center: {
flex: 1,
alignItems: "center",
@@ -180,6 +252,24 @@ const styles = StyleSheet.create({
maxWidth: "60%",
textAlign: "right",
},
linkRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: "#f3f4f6",
minHeight: 44,
},
linkText: {
fontSize: 15,
color: "#1e40af",
},
linkChevron: {
fontSize: 18,
color: "#9ca3af",
fontWeight: "600",
},
signOutButton: {
backgroundColor: "#fee2e2",
borderRadius: 10,
@@ -192,4 +282,24 @@ const styles = StyleSheet.create({
fontWeight: "700",
fontSize: 15,
},
deleteAccountButton: {
backgroundColor: "#ffffff",
borderRadius: 10,
borderWidth: 1,
borderColor: "#dc2626",
paddingVertical: 14,
alignItems: "center",
minHeight: 48,
},
deleteAccountText: {
color: "#dc2626",
fontWeight: "600",
fontSize: 14,
},
versionText: {
fontSize: 12,
color: "#9ca3af",
textAlign: "center",
marginTop: 8,
},
});
+14 -14
View File
@@ -12,6 +12,7 @@
* track the real-time processing status of each uploaded file.
*/
import { Ionicons } from "@expo/vector-icons";
import * as DocumentPicker from "expo-document-picker";
import * as ImagePicker from "expo-image-picker";
import React, { useCallback, useEffect, useRef, useState } from "react";
@@ -256,7 +257,7 @@ export default function UploadScreen() {
accessibilityRole="button"
accessibilityLabel="Capture document with camera"
>
<Text style={styles.actionIcon}>📷</Text>
<Ionicons name="camera-outline" size={28} color="#fff" style={styles.actionIcon} />
<Text style={styles.actionLabel}>Camera</Text>
</Pressable>
@@ -266,7 +267,7 @@ export default function UploadScreen() {
accessibilityRole="button"
accessibilityLabel="Select photo from library"
>
<Text style={styles.actionIcon}>🖼</Text>
<Ionicons name="images-outline" size={28} color="#fff" style={styles.actionIcon} />
<Text style={styles.actionLabel}>Photos</Text>
</Pressable>
@@ -276,7 +277,7 @@ export default function UploadScreen() {
accessibilityRole="button"
accessibilityLabel="Pick file from device"
>
<Text style={styles.actionIcon}>📄</Text>
<Ionicons name="document-outline" size={28} color="#fff" style={styles.actionIcon} />
<Text style={styles.actionLabel}>Files</Text>
</Pressable>
</View>
@@ -285,7 +286,7 @@ export default function UploadScreen() {
<ScrollView style={styles.list} contentContainerStyle={styles.listContent}>
{uploads.length === 0 ? (
<View style={styles.emptyState}>
<Text style={styles.emptyEmoji}></Text>
<Ionicons name="cloud-upload-outline" size={48} color="#9ca3af" style={{ marginBottom: 12 }} />
<Text style={styles.emptyText}>
Tap Camera, Photos, or Files to upload a document.
</Text>
@@ -304,11 +305,11 @@ export default function UploadScreen() {
}
function UploadRow({ item, onRetry }: { item: UploadItem; onRetry: (item: UploadItem) => void }) {
const uploadIcons: Record<UploadItem["status"], string> = {
pending: "⏳",
uploading: "⬆️",
done: "✅",
error: "❌",
const uploadIconProps: Record<UploadItem["status"], { name: keyof typeof Ionicons.glyphMap; color: string }> = {
pending: { name: "time-outline", color: "#6b7280" },
uploading: { name: "arrow-up-circle-outline", color: "#1e40af" },
done: { name: "checkmark-circle", color: "#059669" },
error: { name: "close-circle", color: "#dc2626" },
};
/** Human-readable label for the server-side processing status. */
@@ -316,7 +317,7 @@ function UploadRow({ item, onRetry }: { item: UploadItem; onRetry: (item: Upload
const labels: Record<string, string> = {
pending: "Queued for processing…",
processing: "Processing…",
completed: "Processed",
completed: "Processed",
failed: "Processing failed",
duplicate: "Duplicate already processed",
};
@@ -342,7 +343,7 @@ function UploadRow({ item, onRetry }: { item: UploadItem; onRetry: (item: Upload
accessibilityLabel={canRetry ? `Retry uploading ${item.filename}` : undefined}
accessibilityHint={canRetry ? "Tap or long-press to retry this upload" : undefined}
>
<Text style={rowStyles.icon}>{uploadIcons[item.status]}</Text>
<Ionicons name={uploadIconProps[item.status].name} size={22} color={uploadIconProps[item.status].color} style={rowStyles.icon} />
<View style={rowStyles.info}>
<Text style={rowStyles.filename} numberOfLines={1}>
{item.filename}
@@ -397,7 +398,7 @@ const styles = StyleSheet.create({
cameraButton: { backgroundColor: "#1e40af" },
photoLibraryButton: { backgroundColor: "#7c3aed" },
fileButton: { backgroundColor: "#059669" },
actionIcon: { fontSize: 28, marginBottom: 6 },
actionIcon: { marginBottom: 6 },
actionLabel: {
color: "#fff",
fontSize: 14,
@@ -409,7 +410,6 @@ const styles = StyleSheet.create({
alignItems: "center",
paddingTop: 60,
},
emptyEmoji: { fontSize: 48, marginBottom: 12 },
emptyText: {
fontSize: 16,
color: "#374151",
@@ -443,7 +443,7 @@ const rowStyles = StyleSheet.create({
shadowRadius: 4,
elevation: 2,
},
icon: { fontSize: 22, marginRight: 12 },
icon: { marginRight: 12 },
info: { flex: 1 },
filename: {
fontSize: 14,