diff --git a/mobile/app/(app)/pets/[petId]/edit.tsx b/mobile/app/(app)/pets/[petId]/edit.tsx
index db925b6..933f900 100644
--- a/mobile/app/(app)/pets/[petId]/edit.tsx
+++ b/mobile/app/(app)/pets/[petId]/edit.tsx
@@ -11,6 +11,7 @@ import { TextField } from "../../../../src/components/TextField";
import { DateField } from "../../../../src/components/DateField";
import { Button } from "../../../../src/components/Button";
import { BreedPicker } from "../../../../src/components/pet/BreedPicker";
+import { PrivacySettings } from "../../../../src/components/pet/PrivacySettings";
import { radii, getTypography, type ColorTokens, type Typography } from "../../../../src/theme/tokens";
import { displayDateToIso, isoToDisplayDate } from "../../../../src/lib/date-format";
import { useBottomSafePadding } from "../../../../src/hooks/useBottomSafePadding";
@@ -18,7 +19,8 @@ import { useBottomSafePadding } from "../../../../src/hooks/useBottomSafePadding
/**
* Edit form (name, nickname, breed, bio, adoption story, birthday, adoption
* date) — a subset of web's src/app/(app)/pets/[petId]/edit/page.tsx.
- * DM-policy/follower-visibility/avatar upload aren't editable here yet.
+ * Privacy (DM policy, follower visibility) lives in PrivacySettings and saves
+ * on its own; avatar upload isn't editable here yet.
* Species itself stays fixed (only its breed is re-selectable) — found.md
* asked specifically for breed correction, not a full species change.
*/
@@ -168,6 +170,13 @@ export default function EditPetScreen() {
+
+
{t("editPet.deletePet")}
diff --git a/mobile/src/components/pet/PrivacySettings.tsx b/mobile/src/components/pet/PrivacySettings.tsx
new file mode 100644
index 0000000..ec6a01f
--- /dev/null
+++ b/mobile/src/components/pet/PrivacySettings.tsx
@@ -0,0 +1,142 @@
+import { useMemo } from "react";
+import { View, Text, Pressable, Alert, StyleSheet } from "react-native";
+import { useTranslation } from "react-i18next";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { Check } from "lucide-react-native";
+import { useTRPC } from "../../api/trpc";
+import { useTheme } from "../../context/ThemeContext";
+import { radii, getTypography, type ColorTokens, type Typography } from "../../theme/tokens";
+
+type DmPolicy = "EVERYONE" | "FOLLOWERS_ONLY";
+type FollowerVisibility = "EVERYONE" | "FOLLOWERS_ONLY" | "NOBODY";
+
+type Props = {
+ petId: string;
+ petName: string;
+ dmPolicy: DmPolicy;
+ followerVisibility: FollowerVisibility;
+};
+
+/**
+ * Port of the "Privatsphäre" block from web's src/app/(app)/pets/[petId]/edit/page.tsx
+ * (found.md: only editable on web so far). Each choice is saved immediately via
+ * pets.update — no separate save button, so it can't get lost in the long form.
+ */
+export function PrivacySettings({ petId, petName, dmPolicy, followerVisibility }: Props) {
+ const { t } = useTranslation();
+ const trpc = useTRPC();
+ const queryClient = useQueryClient();
+ const { colors } = useTheme();
+ const styles = useMemo(() => makeStyles(colors), [colors]);
+
+ const update = useMutation(
+ trpc.pets.update.mutationOptions({
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: trpc.pets.byId.queryOptions({ petId }).queryKey }),
+ onError: () => Alert.alert(t("privacy.saveFailed")),
+ })
+ );
+
+ const dmOptions: { value: DmPolicy; label: string }[] = [
+ { value: "EVERYONE", label: t("privacy.everyone") },
+ { value: "FOLLOWERS_ONLY", label: t("privacy.onlyFollowedPets") },
+ ];
+ const followerOptions: { value: FollowerVisibility; label: string }[] = [
+ { value: "EVERYONE", label: t("privacy.everyone") },
+ { value: "FOLLOWERS_ONLY", label: t("privacy.onlyMyFollowers") },
+ { value: "NOBODY", label: t("privacy.onlyMe") },
+ ];
+ const followerHint = {
+ EVERYONE: t("privacy.followerEveryoneHint", { name: petName }),
+ FOLLOWERS_ONLY: t("privacy.followerFollowersOnlyHint", { name: petName }),
+ NOBODY: t("privacy.followerNobodyHint", { name: petName }),
+ }[followerVisibility];
+
+ return (
+
+ {t("privacy.heading")}
+
+ {t("privacy.whoCanMessage", { name: petName })}
+ update.mutate({ petId, dmPolicy: value })}
+ styles={styles}
+ colors={colors}
+ />
+
+ {dmPolicy === "FOLLOWERS_ONLY" ? t("privacy.dmFollowersOnlyHint") : t("privacy.dmEveryoneHint")}
+
+
+ {t("privacy.whoCanSeeFollowers", { name: petName })}
+ update.mutate({ petId, followerVisibility: value })}
+ styles={styles}
+ colors={colors}
+ />
+ {followerHint}
+
+ );
+}
+
+function OptionGroup({
+ options,
+ value,
+ disabled,
+ onChange,
+ styles,
+ colors,
+}: {
+ options: { value: T; label: string }[];
+ value: T;
+ disabled: boolean;
+ onChange: (value: T) => void;
+ styles: ReturnType;
+ colors: ColorTokens;
+}) {
+ return (
+
+ {options.map((option, index) => {
+ const isSelected = option.value === value;
+ return (
+ 0 && styles.optionDivider]}
+ onPress={() => !isSelected && onChange(option.value)}
+ disabled={disabled}
+ accessibilityRole="radio"
+ accessibilityState={{ checked: isSelected, disabled }}
+ >
+ {option.label}
+ {isSelected && }
+
+ );
+ })}
+
+ );
+}
+
+function makeStyles(colors: ColorTokens) {
+ const typography: Typography = getTypography(colors);
+ return StyleSheet.create({
+ section: { marginTop: 24, gap: 8 },
+ heading: { ...typography.title, fontSize: 16 },
+ question: { ...typography.body, fontFamily: typography.title.fontFamily, marginTop: 8 },
+ group: { borderWidth: 1, borderColor: colors.hairlineGray, borderRadius: radii.lg, overflow: "hidden" },
+ option: {
+ minHeight: 48,
+ paddingHorizontal: 14,
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ backgroundColor: colors.pureWhite,
+ },
+ optionDivider: { borderTopWidth: 1, borderTopColor: colors.hairlineGray },
+ optionText: { ...typography.body },
+ optionTextSelected: { fontFamily: typography.title.fontFamily, color: colors.marigoldOrange },
+ hint: { ...typography.label, textTransform: "none", letterSpacing: 0 },
+ });
+}
diff --git a/mobile/src/i18n/locales/de.json b/mobile/src/i18n/locales/de.json
index ccde2a8..2123a41 100644
--- a/mobile/src/i18n/locales/de.json
+++ b/mobile/src/i18n/locales/de.json
@@ -438,5 +438,20 @@
"foundingMember": {
"label": "Gründungsmitglied",
"tooltip": "Gründungsmitglied – eines der ersten 250 Tiere auf PawFeed"
+ },
+ "privacy": {
+ "heading": "Privatsphäre",
+ "whoCanMessage": "Wer kann {{name}} Nachrichten schreiben?",
+ "everyone": "Alle",
+ "onlyFollowedPets": "Nur Tiere, denen ich folge",
+ "dmFollowersOnlyHint": "Nur Tiere, denen du folgst, können dir Direktnachrichten schreiben.",
+ "dmEveryoneHint": "Jedes Tier auf PawFeed kann dir Direktnachrichten schreiben.",
+ "whoCanSeeFollowers": "Wer darf die Follower von {{name}} sehen?",
+ "onlyMyFollowers": "Nur meine Follower",
+ "onlyMe": "Nur ich",
+ "followerEveryoneHint": "Jeder kann die Follower- und Folgt-Listen von {{name}} sehen.",
+ "followerFollowersOnlyHint": "Nur Tiere, die {{name}} folgen, können die Follower- und Folgt-Listen sehen.",
+ "followerNobodyHint": "Nur du kannst die Follower- und Folgt-Listen von {{name}} sehen.",
+ "saveFailed": "Privatsphäre-Einstellung konnte nicht gespeichert werden"
}
}
diff --git a/mobile/src/i18n/locales/en.json b/mobile/src/i18n/locales/en.json
index f557dbc..8abd824 100644
--- a/mobile/src/i18n/locales/en.json
+++ b/mobile/src/i18n/locales/en.json
@@ -438,5 +438,20 @@
"foundingMember": {
"label": "Founding member",
"tooltip": "Founding member – one of the first 250 pets on PawFeed"
+ },
+ "privacy": {
+ "heading": "Privacy",
+ "whoCanMessage": "Who can message {{name}}?",
+ "everyone": "Everyone",
+ "onlyFollowedPets": "Only pets I follow",
+ "dmFollowersOnlyHint": "Only pets that you follow can send you direct messages.",
+ "dmEveryoneHint": "Any pet on PawFeed can send you direct messages.",
+ "whoCanSeeFollowers": "Who can see {{name}}'s followers?",
+ "onlyMyFollowers": "Only my followers",
+ "onlyMe": "Only me",
+ "followerEveryoneHint": "Anyone can see {{name}}'s followers and following lists.",
+ "followerFollowersOnlyHint": "Only pets that follow {{name}} can see the followers and following lists.",
+ "followerNobodyHint": "Only you can see {{name}}'s followers and following lists.",
+ "saveFailed": "Failed to save privacy setting"
}
}