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:
+24
-2
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user