From 4213d7029ea506ec9db034097047d8cd7f57871c Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sat, 26 Sep 2026 11:19:17 +0200 Subject: [PATCH] feat(mobile): block pets and delete account in-app (App Store requirements) - Block (Guideline 1.2, UGC): useBlockPet hook (native confirm Alert, blocks.create). PostCard "..." on other pets' posts now offers Report / Block; pet profile gets a header "..." with Report pet / Block (hidden for any of the owner's own pets), navigating back after a block. Feed, comments and DMs already enforce blocks server-side; explore and search don't yet (server follow-up). - Delete account (Guideline 5.1.1(v)): new delete-account screen reached from a quiet link below Log out in Settings. Mirrors web's DeleteAccountDialog (warning list + type a pet name) and calls the same POST /api/account/delete with the Clerk bearer token, then clears the query cache and signs out. Co-Authored-By: Claude Opus 5.5 --- mobile/app/(app)/_layout.tsx | 1 + mobile/app/(app)/delete-account.tsx | 140 +++++++++++ mobile/app/(app)/pets/[petId]/index.tsx | 220 +++++++++++------- mobile/app/(app)/settings.tsx | 2 + mobile/src/components/PostCard.tsx | 14 +- .../settings/DeleteAccountSection.tsx | 33 +++ mobile/src/hooks/useBlockPet.ts | 55 +++++ mobile/src/i18n/locales/de.json | 35 ++- mobile/src/i18n/locales/en.json | 35 ++- 9 files changed, 442 insertions(+), 93 deletions(-) create mode 100644 mobile/app/(app)/delete-account.tsx create mode 100644 mobile/src/components/settings/DeleteAccountSection.tsx create mode 100644 mobile/src/hooks/useBlockPet.ts diff --git a/mobile/app/(app)/_layout.tsx b/mobile/app/(app)/_layout.tsx index 5cabd39..e338305 100644 --- a/mobile/app/(app)/_layout.tsx +++ b/mobile/app/(app)/_layout.tsx @@ -50,6 +50,7 @@ export default function AppLayout() { + makeStyles(colors), [colors]); + const { getToken, signOut } = useAuth(); + const { pets, clearActivePetId } = useActivePet(); + const queryClient = useQueryClient(); + const [petName, setPetName] = useState(""); + const [isDeleting, setIsDeleting] = useState(false); + const [error, setError] = useState(null); + + const isValidName = pets.some((pet) => pet.name.toLowerCase() === petName.trim().toLowerCase()); + + const deleteAccount = async () => { + setIsDeleting(true); + setError(null); + try { + const token = await getToken(); + const res = await fetch(`${env.apiUrl}/api/account/delete`, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(token ? { authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ petNameConfirmation: petName.trim() }), + }); + if (!res.ok) { + const data = (await res.json().catch(() => ({}))) as { error?: string }; + throw new Error(data.error ?? t("deleteAccount.genericError")); + } + } catch (err) { + Sentry.captureException(err); + setError(err instanceof Error ? err.message : t("deleteAccount.genericError")); + setIsDeleting(false); + return; + } + + // The Clerk user no longer exists server-side, so signOut() may reject — + // either way the local session is cleared and useProtectedRoute() sends + // the user back to sign-in. + queryClient.clear(); + clearActivePetId(); + Alert.alert(t("deleteAccount.doneTitle")); + await signOut().catch(() => undefined); + }; + + const confirmDelete = () => { + Alert.alert(t("deleteAccount.finalConfirmTitle"), t("deleteAccount.finalConfirmMessage"), [ + { text: t("common.cancel"), style: "cancel" }, + { text: t("deleteAccount.confirmDelete"), style: "destructive", onPress: () => void deleteAccount() }, + ]); + }; + + return ( + + + + + {t("deleteAccount.warningTitle")} + + {t("deleteAccount.warningIntro")} + + + {DELETED_ITEMS.map((item) => ( + + ✕ + {t(`deleteAccount.${item}`)} + + ))} + + + {t("deleteAccount.confirmIntro")} + + {error ? {error} : null} + +