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 <noreply@anthropic.com>
246 lines
14 KiB
Markdown
246 lines
14 KiB
Markdown
# PawFeed Mobile
|
|
|
|
Native Android/iOS client for PawFeed, built with **React Native + Expo (Expo
|
|
Router)**. It talks to the exact same backend the web app uses — the
|
|
self-hosted Docker deployment described in `../docker/SCALING-ROADMAP.md` —
|
|
over the existing tRPC API. **No backend/Docker changes were needed to make
|
|
this possible**, see "Why no backend changes" below.
|
|
|
|
Decided over Capacitor (see the now-superseded `../mobil.md`) because a
|
|
native RN app gives better performance/feel and can still share real logic
|
|
with a future web-shared package, at the cost of more upfront work — see the
|
|
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, 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
|
|
|
|
- **Routing:** Expo Router (file-based, `app/`). `(auth)` and `(app)` are
|
|
route groups; `app/_layout.tsx` redirects between them based on Clerk's
|
|
session state (`useProtectedRoute`), mirroring Clerk's official
|
|
expo-router integration pattern.
|
|
- **Auth:** `@clerk/expo` (the current SDK — `@clerk/clerk-expo` is
|
|
deprecated, don't reinstall it). Session JWT persists in the OS keystore
|
|
via `expo-secure-store` (`src/lib/tokenCache.ts`) instead of Clerk's
|
|
in-memory default.
|
|
- **API:** `src/api/trpc.ts` creates a `@trpc/client` pointed at
|
|
`${EXPO_PUBLIC_API_URL}/api/trpc`, with the Clerk session token attached
|
|
as `Authorization: Bearer <token>` on every request (`useAuth().getToken`,
|
|
called per-request so a refreshed token is always used).
|
|
- **Type safety:** `AppRouter` is imported as a **type-only** import
|
|
directly from the web app's `../src/trpc/routers/_app.ts` — no code
|
|
duplication, no publishing a package. This works because `mobile/` is
|
|
nested inside the same repo: Node's (and TypeScript's) module resolution
|
|
walks up to the root `node_modules` for `@trpc/server`, `zod`, etc., so
|
|
the type import resolves without installing those packages a second time
|
|
here. If a future refactor turns this repo into an npm workspace, revisit
|
|
this — it wasn't necessary for the initial scaffold. `src/api/types.ts`
|
|
centralizes the `inferRouterOutputs<AppRouter>` types every screen uses
|
|
(`Pet`, `FeedPost`, etc.) instead of repeating the long relative import.
|
|
- **Design system:** `src/theme/tokens.ts` mirrors the web app's
|
|
`DESIGN.md` ("Spotlight Porch" — marigold-orange accent, porch-white
|
|
canvas, Inter font, hairline rings instead of shadows). `src/components/`
|
|
has the shared building blocks (`Button`, `TextField`, `Card`) plus
|
|
feed-specific ports of the web equivalents (`PostCard`, `PawButton`,
|
|
`PetAvatar`, `PetIdentityBadges`).
|
|
- **Active pet:** every pet-scoped procedure (`feed.getFeed`,
|
|
`reactions.toggle`, `notifications.list`, ...) takes an explicit `petId`
|
|
and asserts ownership server-side — there's no server-side "current pet".
|
|
`src/context/ActivePetContext.tsx` is mobile's equivalent of web's
|
|
`ActivePetContext` (MMKV-backed instead of `localStorage`), defaulting to
|
|
the owner's first pet; the Pets tab doubles as the pet switcher.
|
|
- **Fast reopen:** the TanStack Query cache is persisted to `react-native-mmkv`
|
|
(`src/lib/storage.ts`, wired via `PersistQueryClientProvider` in
|
|
`app/_layout.tsx`) so the last-known feed/pets/notifications render
|
|
immediately on relaunch while a background refetch reconciles them — no
|
|
blank screen while waiting on the network. Images go through `expo-image`
|
|
for the same reason (disk-cached, unlike RN's built-in `Image`).
|
|
- **Navigation:** `expo-router`'s built-in `<Tabs>`
|
|
(`app/(app)/(tabs)/_layout.tsx`) — Feed/Explore/Notifications/Pets,
|
|
mirroring the web app's actual mobile bottom nav
|
|
(`src/components/layout/MobileNav.tsx`), not an invented IA. Messages is
|
|
a header icon button pushing `app/(app)/messages/index.tsx`, matching
|
|
web (there it's a floating corner button, not a tab).
|
|
|
|
### Why no backend changes were needed
|
|
|
|
Two things had to be true for the app to reach the Docker-hosted backend
|
|
as-is, and both already were:
|
|
|
|
1. **CORS doesn't apply.** CORS is a browser same-origin mechanism; a native
|
|
app's `fetch` isn't a browser context, so the "just needs to reach it"
|
|
requirement was already satisfied by the API being reachable over HTTPS
|
|
at `pawfeed.org`.
|
|
2. **Clerk already accepts bearer tokens.** `src/proxy.ts` already marks
|
|
`/api/trpc/(.*)` as a public route at the middleware level (tRPC does its
|
|
own per-procedure auth — see the comment there), and `auth()` in
|
|
`src/trpc/init.ts` recognizes both the browser's session cookie AND an
|
|
`Authorization: Bearer` JWT out of the box. Native/Expo apps sending a
|
|
bearer token is Clerk's documented mobile auth flow, not a special case
|
|
that needed enabling server-side.
|
|
|
|
## Setup
|
|
|
|
**Runs as a custom dev client, not Expo Go.** `@clerk/expo`'s Device Trust
|
|
flow (see below) and `react-native-mmkv` both need native code Expo Go
|
|
doesn't ship, so this app builds and installs its own native shell locally
|
|
— no EAS account/login needed, everything runs on-device via the Android
|
|
SDK already required for the emulator.
|
|
|
|
```bash
|
|
cd mobile
|
|
npm install
|
|
cp .env.example .env # already done locally with the live Clerk publishable key
|
|
|
|
# First time only (or after adding a native dependency / editing app.json):
|
|
npx expo prebuild --platform android
|
|
JAVA_HOME="<path to a JDK 17 install>" npx expo run:android
|
|
```
|
|
|
|
`JAVA_HOME` matters: RN 0.86's Gradle build needs **JDK 17**, and a JDK
|
|
that's too new (e.g. a bundled Android Studio JBR on JDK 21+) fails native
|
|
module CMake configuration with a cryptic "restricted method... System"
|
|
error. Eclipse Temurin 17 works
|
|
(`winget install EclipseAdoptium.Temurin.17.JDK` on Windows).
|
|
|
|
Once the native app is installed, day-to-day iteration doesn't need another
|
|
`expo run:android` — just `npm start` (→ `expo start --dev-client`) and
|
|
reopen the already-installed app; that command only bundles/serves JS.
|
|
|
|
Requires an Android emulator (Android Studio) or a physical device. See
|
|
https://docs.expo.dev/versions/v57.0.0/ for current Expo APIs — SDK 57
|
|
changed things vs. older Expo docs/training data, per `AGENTS.md` in this
|
|
folder.
|
|
|
|
### Clerk dashboard: Native Applications API
|
|
|
|
Clerk's "Device Trust" feature (tied to the dashboard's Native Applications
|
|
API) makes `signIn.status` come back as `needs_client_trust` on a sign-in
|
|
from a new device — resolved via `signIn.mfa.sendEmailCode()` /
|
|
`verifyEmailCode()` in `app/(auth)/sign-in.tsx`, a one-time email-code check
|
|
per device, independent of any account-level MFA setting. If sign-in hangs
|
|
at "Clerk lädt…" forever with no error, or `signIn.status` never reaches
|
|
`complete`, check that the Native Applications API is enabled for this
|
|
Clerk instance.
|
|
|
|
### `.env`
|
|
|
|
| Var | Purpose |
|
|
|---|---|
|
|
| `EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY` | Same Clerk instance as the web app — copy from root `.env.local`'s `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`. Safe to embed client-side (that's what "publishable" means). |
|
|
| `EXPO_PUBLIC_API_URL` | Backend origin. Defaults to `https://pawfeed.org` (the live deployment) — see the LAN-IP / Android-emulator alternatives in `.env.example` for local dev against `npm run dev` instead. |
|
|
| `EXPO_PUBLIC_SUPABASE_URL` | Same Supabase project as the web app — copy from root `.env.local`'s `NEXT_PUBLIC_SUPABASE_URL`. Used to build public media URLs (`src/lib/media-url.ts`); safe client-side, it's the public storage origin, not a service key. |
|
|
|
|
**Caution:** the default `.env` points at the **live production** backend
|
|
and database — the same one real users are on ([[project_prod_launch_2026-08-10]]).
|
|
Reading (public stats, browsing) is harmless. Anything that writes data
|
|
while iterating on mobile features should point `EXPO_PUBLIC_API_URL` at a
|
|
local `npm run dev` instead (`http://10.0.2.2:3000` from the Android
|
|
emulator, or the dev machine's LAN IP from a physical device).
|
|
|
|
## Folder structure
|
|
|
|
```
|
|
mobile/
|
|
app/ # expo-router routes
|
|
_layout.tsx # ClerkProvider + persisted QueryClient/tRPC + ActivePetProvider + auth redirect
|
|
(auth)/
|
|
sign-in.tsx # email/password + Clerk Device Trust email-code step
|
|
(app)/
|
|
_layout.tsx # Stack hosting the (tabs) group + the messages/search/story/comment screens
|
|
(tabs)/
|
|
_layout.tsx # bottom Tabs: Feed/Explore/Notifications/Pets + header Search/Messages buttons
|
|
index.tsx # Feed (StoryTray header + infinite post list)
|
|
explore.tsx # pet discovery grid
|
|
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/
|
|
api/trpc.ts # tRPC client, AppRouter type import, query client factory
|
|
api/types.ts # inferRouterOutputs<AppRouter> 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
|
|
lib/media-url.ts # Supabase public storage URL builder
|
|
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
|
|
|
|
Both platforms build from this one Expo codebase (`expo run:android` /
|
|
`expo run:ios`) — there's no separate iOS source tree to scaffold
|
|
separately. "Starting with Android" means:
|
|
|
|
- `app.json`'s `android.package` (`org.pawfeed.app`) is set now;
|
|
`ios.bundleIdentifier` is set to the same reverse-DNS id but untested.
|
|
- Android needs a Google Play Console account (~25 € one-time, already
|
|
budgeted in `../mobil.md`) before an internal test track can be created.
|
|
- iOS needs a Mac (for `expo run:ios` / EAS builds with credentials) and an
|
|
Apple Developer account (99 €/year) — deferred until Android is validated.
|
|
- EAS Build (`eas.json`, not yet added) is the recommended path for
|
|
producing signed Android/iOS binaries without a local Android
|
|
Studio/Xcode toolchain; add it when a first real build is needed.
|
|
|
|
## What's not built yet
|
|
|
|
Phase 1 (Feed/Explore/Notifications/Pets tabs, reactions, active-pet
|
|
switching, persisted cache for fast reopen) plus a full pet profile
|
|
(view/follow/edit/followers-following), post creation/upload, comments,
|
|
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`),
|
|
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:
|
|
|
|
- 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.
|