restore(mobile): restore mobile/ directory to pre-d2217531 state

Restored mobile/ from d22175310a711e7ebdd8062ae29a54f0136dc3f6^
(parent commit d94e9ca4bc).

Commit d22175310a (google-labs-jules[bot], 2026-03-23T14:45:22Z) introduced
an SSRF security fix for IMAP connections but unintentionally deleted or
truncated a large number of files across the repository, including 24 files
under mobile/.

This commit targets only the mobile/ directory and restores the following
files to their pre-d2217531 state:

- mobile/README.md
- mobile/app.json
- mobile/app/(tabs)/_layout.tsx
- mobile/app/(tabs)/file-detail.tsx  (re-added)
- mobile/app/+not-found.tsx          (re-added)
- mobile/app/_layout.tsx
- mobile/eslint.config.js            (re-added)
- mobile/package-lock.json
- mobile/package.json
- mobile/src/context/ShareContext.tsx
- mobile/src/i18n/de.json            (re-added)
- mobile/src/i18n/en.json            (re-added)
- mobile/src/i18n/es.json            (re-added)
- mobile/src/i18n/fr.json            (re-added)
- mobile/src/i18n/index.ts           (re-added)
- mobile/src/i18n/it.json            (re-added)
- mobile/src/screens/FileDetailScreen.tsx (re-added)
- mobile/src/screens/FilesScreen.tsx
- mobile/src/screens/LoginScreen.tsx
- mobile/src/screens/ProfileScreen.tsx
- mobile/src/screens/UploadScreen.tsx
- mobile/src/screens/WelcomeScreen.tsx
- mobile/src/services/api.ts
- mobile/src/utils/mimeTypes.ts      (re-added)
- mobile/src/utils/normalizeUri.ts   (re-added)

Security fixes introduced by d2217531 that are unrelated to mobile/
(IMAP SSRF fix in app/utils/network.py and app/tasks/imap_tasks.py)
are preserved — this restore targets only files under mobile/.
This commit is contained in:
copilot-swe-agent[bot]
2026-03-24 12:12:05 +00:00
parent aca12858c1
commit 2f3c22000c
25 changed files with 2354 additions and 646 deletions
+222 -14
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,30 +11,84 @@ import {
Pressable,
ScrollView,
StyleSheet,
Switch,
Text,
View,
} from "react-native";
import { useAuth } from "../context/AuthContext";
import { useLocale, getSupportedLanguages, t } from "../i18n";
import api from "../services/api";
const DEFAULT_SERVER_URL = "https://app.docuelevate.org";
export default function ProfileScreen() {
const { user, signOut, baseUrl } = useAuth();
const { lang, setLang } = useLocale();
const effectiveBaseUrl = baseUrl || DEFAULT_SERVER_URL;
const appVersion = Constants.expoConfig?.version ?? "1.0.0";
const languages = getSupportedLanguages();
async function handleLanguageSelect(code: string) {
await setLang(code);
// Fire-and-forget: sync the choice to the server so it persists across
// platforms (desktop web will reflect this preference too).
api.setServerLanguage(code).catch(() => {
// Network errors are non-critical – the local change is already applied.
});
}
function handleSignOut() {
Alert.alert("Sign out", "Are you sure you want to sign out?", [
{ text: "Cancel", style: "cancel" },
Alert.alert(t("profile.sign_out_title"), t("profile.sign_out_msg"), [
{ text: t("common.cancel"), style: "cancel" },
{
text: "Sign out",
text: t("profile.sign_out"),
style: "destructive",
onPress: signOut,
},
]);
}
function handleDeleteAccount() {
Alert.alert(
t("profile.delete_account_title"),
t("profile.delete_account_msg"),
[
{ text: t("common.cancel"), style: "cancel" },
{
text: t("profile.delete_account"),
style: "destructive",
onPress: () => {
Linking.openURL(`${effectiveBaseUrl}/account/delete`).catch(() => {
Alert.alert(t("common.error"), t("profile.could_not_open", { page: t("profile.delete_account") }));
});
},
},
]
);
}
function openPrivacyPolicy() {
Linking.openURL(`${effectiveBaseUrl}/privacy`).catch(() => {
Alert.alert(t("common.error"), t("profile.could_not_open", { page: t("profile.privacy_policy") }));
});
}
function openTermsOfService() {
Linking.openURL(`${effectiveBaseUrl}/terms`).catch(() => {
Alert.alert(t("common.error"), t("profile.could_not_open", { page: t("profile.terms_of_service") }));
});
}
function openImprint() {
Linking.openURL(`${effectiveBaseUrl}/imprint`).catch(() => {
Alert.alert(t("common.error"), t("profile.could_not_open", { page: t("profile.imprint") }));
});
}
if (!user) {
return (
<View style={styles.center}>
<Text style={styles.emptyText}>Not signed in</Text>
<Text style={styles.emptyText}>{t("profile.not_signed_in")}</Text>
</View>
);
}
@@ -56,44 +112,121 @@ export default function ProfileScreen() {
)}
<Text style={styles.displayName}>{user.display_name ?? user.owner_id}</Text>
{user.email && <Text style={styles.email}>{user.email}</Text>}
{user.is_admin && <Text style={styles.adminBadge}>Admin</Text>}
{user.is_admin && <Text style={styles.adminBadge}>{t("profile.admin")}</Text>}
</View>
{/* Server info */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Connection</Text>
<Text style={styles.sectionTitle}>{t("profile.connection")}</Text>
<View style={styles.row}>
<Text style={styles.rowLabel}>Server</Text>
<Text style={styles.rowLabel}>{t("profile.server")}</Text>
<Text style={styles.rowValue} numberOfLines={1}>
{baseUrl || "–"}
{effectiveBaseUrl}
</Text>
</View>
<View style={styles.row}>
<Text style={styles.rowLabel}>User ID</Text>
<Text style={styles.rowLabel}>{t("profile.user_id")}</Text>
<Text style={styles.rowValue} numberOfLines={1}>
{user.owner_id}
</Text>
</View>
</View>
{/* Danger zone */}
{/* Settings */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>{t("profile.settings")}</Text>
<Text style={styles.settingLabel}>{t("profile.language")}</Text>
<View style={styles.languageGrid}>
{languages.map((l) => (
<Pressable
key={l.code}
style={[
styles.languageChip,
lang === l.code && styles.languageChipActive,
]}
onPress={() => handleLanguageSelect(l.code)}
accessibilityRole="button"
accessibilityLabel={`Set language to ${l.label}`}
accessibilityState={{ selected: lang === l.code }}
>
<Text
style={[
styles.languageChipText,
lang === l.code && styles.languageChipTextActive,
]}
>
{l.label}
</Text>
</Pressable>
))}
</View>
</View>
{/* Legal & Privacy */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>{t("profile.legal")}</Text>
<Pressable
style={styles.linkRow}
onPress={openPrivacyPolicy}
accessibilityRole="link"
accessibilityLabel={t("profile.privacy_policy")}
>
<Text style={styles.linkText}>{t("profile.privacy_policy")}</Text>
<Text style={styles.linkChevron}>›</Text>
</Pressable>
<Pressable
style={styles.linkRow}
onPress={openTermsOfService}
accessibilityRole="link"
accessibilityLabel={t("profile.terms_of_service")}
>
<Text style={styles.linkText}>{t("profile.terms_of_service")}</Text>
<Text style={styles.linkChevron}>›</Text>
</Pressable>
<Pressable
style={[styles.linkRow, styles.linkRowLast]}
onPress={openImprint}
accessibilityRole="link"
accessibilityLabel={t("profile.imprint")}
>
<Text style={styles.linkText}>{t("profile.imprint")}</Text>
<Text style={styles.linkChevron}>›</Text>
</Pressable>
</View>
{/* Sign out */}
<View style={styles.section}>
<Pressable
style={styles.signOutButton}
onPress={handleSignOut}
accessibilityRole="button"
accessibilityLabel="Sign out"
accessibilityLabel={t("profile.sign_out")}
>
<Text style={styles.signOutText}>Sign out</Text>
<Text style={styles.signOutText}>{t("profile.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={t("profile.delete_account")}
>
<Text style={styles.deleteAccountText}>{t("profile.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 +313,27 @@ const styles = StyleSheet.create({
maxWidth: "60%",
textAlign: "right",
},
linkRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: "#f3f4f6",
minHeight: 44,
},
linkRowLast: {
borderBottomWidth: 0,
},
linkText: {
fontSize: 15,
color: "#1e40af",
},
linkChevron: {
fontSize: 18,
color: "#9ca3af",
fontWeight: "600",
},
signOutButton: {
backgroundColor: "#fee2e2",
borderRadius: 10,
@@ -192,4 +346,58 @@ 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,
},
settingLabel: {
fontSize: 14,
color: "#374151",
fontWeight: "500",
marginBottom: 10,
},
languageGrid: {
flexDirection: "row",
flexWrap: "wrap",
gap: 8,
},
languageChip: {
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 20,
backgroundColor: "#f3f4f6",
borderWidth: 1,
borderColor: "#e5e7eb",
minHeight: 36,
justifyContent: "center",
},
languageChipActive: {
backgroundColor: "#dbeafe",
borderColor: "#1e40af",
},
languageChipText: {
fontSize: 13,
color: "#6b7280",
fontWeight: "500",
},
languageChipTextActive: {
color: "#1e40af",
fontWeight: "700",
},
});