From a3f19ff70fc1b6ed326ec851d7ad7ac09dfd5538 Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sun, 6 Sep 2026 09:50:07 +0200 Subject: [PATCH] feat(mobile): add health tracking, milestones, messaging, and push registration Health: consent gate (Art. 9 DSGVO) plus weight/vet-visit/vaccine log sections, gated behind health.getConsentStatus like the web app. Milestones: view a pet's milestone posts and create the 5 user-initiated types with an optional photo. Messaging: chat detail/compose screen (messages/[conversationId].tsx) polling while focused, reachable from the conversations list and from a pet profile's new "Nachricht" entry point. Push: usePushNotifications requests permission and registers the Expo push token with owner.registerPushToken; real delivery still needs an EAS project id (`eas init`), tracked in the README. All four verified live on a real Android emulator: health CRUD (add/list/ delete a weight log), Health/Milestones/Nachricht entry points gated correctly by own-pet vs. other-pet, and full chat history loading via an existing conversation. Co-Authored-By: Claude Sonnet 5 --- mobile/README.md | 47 +++-- mobile/app.json | 9 +- mobile/app/(app)/_layout.tsx | 12 ++ .../app/(app)/messages/[conversationId].tsx | 135 ++++++++++++++ mobile/app/(app)/messages/index.tsx | 20 +- mobile/app/(app)/milestone-new.tsx | 167 +++++++++++++++++ .../app/(app)/pets/[petId]/health/index.tsx | 31 ++++ mobile/app/(app)/pets/[petId]/index.tsx | 45 ++++- mobile/app/(app)/pets/[petId]/milestones.tsx | 99 ++++++++++ mobile/app/_layout.tsx | 12 +- mobile/package-lock.json | 34 ++++ mobile/package.json | 1 + mobile/src/api/types.ts | 4 +- .../components/health/HealthConsentGate.tsx | 172 ++++++++++++++++++ .../src/components/health/VaccineSection.tsx | 113 ++++++++++++ .../src/components/health/VetVisitSection.tsx | 111 +++++++++++ .../components/health/WeightLogSection.tsx | 99 ++++++++++ mobile/src/hooks/usePushNotifications.ts | 73 ++++++++ mobile/src/lib/milestone-meta.ts | 55 ++++++ 19 files changed, 1208 insertions(+), 31 deletions(-) create mode 100644 mobile/app/(app)/messages/[conversationId].tsx create mode 100644 mobile/app/(app)/milestone-new.tsx create mode 100644 mobile/app/(app)/pets/[petId]/health/index.tsx create mode 100644 mobile/app/(app)/pets/[petId]/milestones.tsx create mode 100644 mobile/src/components/health/HealthConsentGate.tsx create mode 100644 mobile/src/components/health/VaccineSection.tsx create mode 100644 mobile/src/components/health/VetVisitSection.tsx create mode 100644 mobile/src/components/health/WeightLogSection.tsx create mode 100644 mobile/src/hooks/usePushNotifications.ts create mode 100644 mobile/src/lib/milestone-meta.ts diff --git a/mobile/README.md b/mobile/README.md index 9c6362f..1d80929 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -14,12 +14,13 @@ architecture-decision thread in this session's handoff. **Status:** Phase 1 — real Feed/Explore/Notifications/Pets tabs against the live backend, not just a connectivity scaffold anymore. Post creation, comments, repost, pet profile detail (view/follow/edit/followers-following), -stories (tray/creation/viewer), video playback, and search are done and -verified end-to-end against the live backend. Health tracking, milestones, -and message sending are still not built — see "What's not built yet" below. -Android is the priority platform; iOS builds need a Mac + Apple Developer -account and come later (same codebase, no separate source tree — see -"Android-first, then iOS" below). +stories (tray/creation/viewer), video playback, search, health tracking +(consent gate + weight/vet/vaccine logs), milestones, and message +sending/receiving are done and verified end-to-end against the live backend. +Push notifications are wired end-to-end but not yet deliverable — see "Push +notifications" below. Android is the priority platform; iOS builds need a +Mac + Apple Developer account and come later (same codebase, no separate +source tree — see "Android-first, then iOS" below). ## Architecture @@ -163,10 +164,14 @@ mobile/ notifications.tsx # notifications list pets.tsx # owner's pets + active-pet switcher messages/index.tsx # conversations list + messages/[conversationId].tsx # chat detail/compose, polls messages.list while focused (no realtime) search.tsx # pets/people/hashtags search, three tabs over one debounced query post-new.tsx # create a photo post (pick/upload → posts.create) story-new.tsx # create a photo story (pick/upload → stories.create) story-viewer.tsx # full-screen story playback (tap zones, progress bars, view tracking) + milestone-new.tsx # create a milestone post (type picker + optional photo → milestones.create) + pets/[petId]/health/index.tsx # weight/vet/vaccine logs, gated by HealthConsentGate + pets/[petId]/milestones.tsx # a pet's milestone posts (milestones.listByPet) posts/[postId]/comments.tsx # read/write/delete comments for a post image-viewer.tsx # full-screen tapped-image lightbox src/ @@ -174,6 +179,7 @@ mobile/ api/types.ts # inferRouterOutputs shortcuts (Pet, FeedPost, ...) context/ActivePetContext.tsx # MMKV-backed active-pet state hooks/useDebounce.ts # generic debounce hook (search input) + hooks/usePushNotifications.ts # permission + Expo push token registration (owner.registerPushToken) lib/env.ts # typed EXPO_PUBLIC_* access lib/tokenCache.ts # Clerk session persistence via expo-secure-store lib/storage.ts # MMKV instance + TanStack Query sync persister @@ -181,9 +187,11 @@ mobile/ lib/format.ts # pet display name + relative-time formatting lib/sponsor.ts # active-sponsor check lib/upload.ts # direct PUT of a picked image to a presigned Supabase URL + lib/milestone-meta.ts # MilestoneType → German label/icon map theme/tokens.ts # design tokens mirroring the web app's DESIGN.md components/ # Button, TextField, Card, PostCard, PawButton, PetAvatar, # PetIdentityBadges, FollowButton, StoryTray, PawRing, VideoPlayer + components/health/ # HealthConsentGate, WeightLogSection, VetVisitSection, VaccineSection ``` ## Android-first, then iOS @@ -210,11 +218,28 @@ switching, persisted cache for fast reopen) plus a full pet profile repost ("Paw-Back") creation, stories (tray/creation/viewer via `expo-video`-adjacent `expo-image`, plus `PawRing` seen/unseen indicator), video playback (`expo-video` against Mux HLS, polling while `PROCESSING`), -and search (pets/people/hashtags, `search.pets`/`byOwner`/`byHashtag`) are -done and verified end-to-end against the live backend on a real Android +search (pets/people/hashtags, `search.pets`/`byOwner`/`byHashtag`), health +tracking (consent gate + weight/vet visit/vaccine logs — feeding info, +emergency vet, and the shareable Health Card are web-only for now), +milestones (view + create, restricted to the 5 user-initiated types), and +messaging (conversation list + chat detail/compose, polling while the +screen is focused since messages.ts has no realtime mechanism) are done +and verified end-to-end against the live backend on a real Android emulator. Still missing, roughly in the order they'd unblock the most: -- Health tracking, milestones -- Sending messages (conversations list works; no chat detail/compose screen) -- Push notifications - No i18n — the whole app is hardcoded German text, matching the sign-in screen + +### Push notifications — wired but not yet deliverable + +`usePushNotifications` (in `app/_layout.tsx`) requests notification +permission and registers the resulting Expo push token with the backend +(`owner.registerPushToken`); `src/lib/notify.ts` on the web side sends a +best-effort push through Expo's push API alongside every in-app +Notification it creates (follow, reaction, comment, message, ...). Minting +a real token needs an **EAS project id** (`app.json`'s +`extra.eas.projectId`, set once via `eas init` — free, no EAS Build +required, but does need an Expo account login). Until that's done, the +permission prompt shows and is granted, but `getExpoPushTokenAsync` has +nothing to attach to and the hook no-ops with a console warning +(`[push] No EAS project id configured...`) — confirmed via `adb logcat` on +2026-09-06. No Owner row has an `expoPushToken` set until this is run. diff --git a/mobile/app.json b/mobile/app.json index 96cfa86..d25f4f6 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -39,7 +39,14 @@ "cameraPermission": "Erlaube PawFeed den Zugriff auf die Kamera, um ein Foto für deinen Beitrag aufzunehmen." } ], - "expo-video" + "expo-video", + [ + "expo-notifications", + { + "icon": "./assets/android-icon-monochrome.png", + "color": "#f97316" + } + ] ] } } diff --git a/mobile/app/(app)/_layout.tsx b/mobile/app/(app)/_layout.tsx index 62d892a..0e8623f 100644 --- a/mobile/app/(app)/_layout.tsx +++ b/mobile/app/(app)/_layout.tsx @@ -27,7 +27,10 @@ export default function AppLayout() { + + + , }} /> + , + }} + /> (); + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const { activePetId } = useActivePet(); + const listRef = useRef>(null); + + const [body, setBody] = useState(""); + + const { data, isLoading } = useQuery({ + ...trpc.messages.list.queryOptions({ conversationId, petId: activePetId ?? "" }), + enabled: !!activePetId, + refetchInterval: POLL_INTERVAL_MS, + }); + const messages = data?.messages ?? []; + + const invalidate = () => + queryClient.invalidateQueries({ + queryKey: trpc.messages.list.queryOptions({ conversationId, petId: activePetId ?? "" }).queryKey, + }); + + const sendMutation = useMutation( + trpc.messages.send.mutationOptions({ + onSuccess: async () => { + setBody(""); + await invalidate(); + await queryClient.invalidateQueries(trpc.conversations.pathFilter()); + setTimeout(() => listRef.current?.scrollToEnd({ animated: true }), 100); + }, + }) + ); + + // Mark received messages read as soon as the list loads (messages.list already + // does this server-side on read, but the inbox badge also needs a fresh count). + useEffect(() => { + if (activePetId) queryClient.invalidateQueries(trpc.conversations.pathFilter()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [messages.length]); + + const handleSubmit = useCallback(() => { + if (!body.trim() || !activePetId) return; + sendMutation.mutate({ conversationId, senderPetId: activePetId, body: body.trim() }); + }, [body, activePetId, conversationId, sendMutation]); + + return ( + + {!isLoading && ( + item.id} + contentContainerStyle={styles.list} + onContentSizeChange={() => listRef.current?.scrollToEnd({ animated: false })} + renderItem={({ item }) => } + ListEmptyComponent={ + + Noch keine Nachrichten. Schreib die erste! + + } + /> + )} + + + +