found.md (iOS-Feedback): Push kam an, aber die Sprechblase zeigte nichts.
MessagesHeaderButton zeigt jetzt conversations.unreadCount im selben Badge
wie die Glocke (gemeinsame UnreadBadge-Komponente), aktualisiert alle 30 s;
Lesen eines Chats invalidiert den Zaehler bereits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Profil bearbeiten (Geburtstag/Adoptionstag), Gesundheit, neuer Beitrag,
neuer Meilenstein, Tier anlegen/Onboarding und Einladen hatten keinen
Tastatur-Ausgleich: das fokussierte Feld lag unter der Tastatur.
automaticallyAdjustKeyboardInsets setzt den unteren Inset und scrollt
das aktive Feld in den sichtbaren Bereich (RN Fabric ScrollView),
keyboardShouldPersistTaps="handled" laesst Feldwechsel ohne Zuklappen zu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Datum (found.md, iOS-Feedback): neue DateField-Komponente mit Ziffern-
tastatur und automatisch eingefügten Punkten (TT.MM.JJJJ) für Geburtstag,
Adoptionstag, Gewicht, Tierarzt und Impfungen. Parser akzeptiert weiter
"-" und "/" sowie einstellige Tage/Monate. Wheel-Picker bleibt in
PENDING-NATIVE-BUILD.md (natives Modul).
Impf-Erinnerung (found.md, iOS-Feedback): täglicher Cron
/api/cron/vaccine-reminders erinnert einmalig 30 Tage vor
Vaccine.nextDueDate per Mitteilung (VACCINE_DUE) + Push mit Impfname und
Datum. Schema: NotificationType.VACCINE_DUE, Notification.vaccineId
(Cascade), Vaccine.reminderSentAt (Claim gegen Doppelversand, wird bei
Fehler wieder freigegeben). Web + App zeigen die Mitteilung an, Tippen
öffnet die Gesundheitsseite. App: Schnellauswahl +1/+3 Jahre für den
Folgetermin plus Hinweis auf die Erinnerung.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Statt nur Spinner: „Upload wird vorbereitet …“, dann Balken + Prozent aus
expo-file-system File.upload onProgress (nativ seit 57.0.7 im 1.0.6-Build,
daher OTA). Vorschau ist während des Uploads nicht mehr antippbar.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged feature/mobile-video-stories (Gitea #37) auf den seit 02.10.
umgebauten Story-Viewer: Galerie-Picker nimmt Videos bis 30 s, Upload
direkt zu Mux, Viewer spielt READY-Videos ab (Fortschritt aus der echten
Wiedergabe, Weiter bei playToEnd) und zeigt PROCESSING/ERROR. Offene
Kommentar-/Zuschauer-Sheets pausieren das Video wie den Foto-Timer.
Nur JS — expo-video ist seit dem 1.0.6-Build nativ enthalten, also OTA.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Pet.isFoundingMember: in pets.create per bewachter SQL-Anweisung für die
ersten 250 Tiere gesetzt, nie entzogen; Abzeichen (Spross) in Feed und
Profil, Web + App, unabhängig vom Empfehlungsprogramm
- Kurator "PawFeed Team": folgt jedem neuen Tier und schickt eine
Begrüßungs-Direktnachricht (DE/EN nach Sprache beim Anlegen), im
Hintergrund — blockiert das Onboarding nicht
- CuratorSettings + Admin-Seite /p/[secret]/curator: Schalter, Texte mit
Pflicht-Platzhalter {name} und Vorschau, Zähler der Gründungsmitglieder
- prisma/backfill_founding_members.sql, prisma/enable_rls_curator.sql
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- stories.listActive: der Owner bekommt seine Storys der letzten 7 Tage mit
isExpired, alle anderen weiterhin nur die aktiven 24 Stunden
- stories.hasActiveStory: optional includeOwnRecent → grauer Ring
(expiredOnly) nur in der Story-Leiste, nicht an Profilbildern im Feed
- Viewer: Hinweis "Abgelaufen – nur für dich sichtbar", Kommentare bei
abgelaufenen Storys nur lesbar, Pfote nur als Zähler; Löschen und
Zuschauerliste bleiben
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- stories.delete: nur Owner; Aufrufe, Story (Pfoten/Kommentare per Cascade),
danach Bild in Supabase Storage bzw. Mux-Asset best-effort
- Web: Papierkorb neben der Zuschauerzahl + Rückfrage, Timer pausiert,
gelöscht wird die beim Antippen gemerkte Story
- App: gleicher Papierkorb mit nativer Rückfrage, Timer pausiert
- found.md: Nutzer-Feedback "Story löschbar" erledigt, 7-Tage-Wunsch mit
Befund als offener Punkt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- App-Viewer: Pfote, Kommentar-Sheet (schreiben, eigene löschen) und für die
eigene Story die Zuschauerliste; Timer pausiert, solange ein Sheet offen ist
- stories.listActive: optionales viewerPetId → reactedByViewer, damit die
Pfote gefüllt startet (abwärtskompatibel für alte App-Builds)
- Web-StoryPawButton nutzt reactedByViewer ebenfalls und wird pro Story
neu initialisiert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- App: post-new.tsx sortierte HEIC/HEIF still aus, obwohl resizeForUpload()
ohnehin in JPEG umwandelt — jetzt erlaubt, plus Hinweis bei übersprungenen
Fotos
- Web: neues src/lib/heic-to-jpeg.ts wandelt HEIC vor dem Upload im Browser
in JPEG um (Safari kann HEIC nativ dekodieren); eingebaut in Beiträge,
Storys, Meilensteine und Tierbild, accept um HEIC ergänzt. Browser ohne
HEIC-Dekodierung bekommen eine klare Meldung statt "falsches Format".
- Server-Whitelist (JPEG/PNG/WebP) bleibt unverändert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Proxy-Sperre für /sign-up, INVITE_REQUIRED, set-invite/consume-invite,
invites-Router, Admin-Invite-Seite und -Prozeduren, Startcodes in
pets.create entfernt — Registrierung ist offen
- /join?code=… leitet auf /r/<code> um, /invite auf /einladen; alte geteilte
Links laufen nicht ins Leere
- InviteCode-Tabelle bleibt nur als Historie (Kontolöschung räumt sie weiter auf)
- Datenschutzerklärung 4b (Empfehlungsprogramm, Herkunftsangabe, pf_ref-Cookie)
und Nutzungsbedingungen 3b (Programmbedingungen, Bestandsschutz, Missbrauch)
- CLAUDE.md: Abschnitt "Referral program" statt "Invite system"
Build, Typecheck und alle Tests grün.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Bildschirm "Freunde einladen" (/invite, aus den Einstellungen erreichbar):
eigener Code mit nativem Teilen-Dialog, Rangleiter mit Fortschritt,
eingeladene Tiere, nachträgliche Code-Eingabe im Einlösefenster
- Onboarding-Schritt "Wie hast du von PawFeed erfahren?" mit Code-Eingabe,
einmal pro Owner (referrals.onboardingState)
- Rang-Abzeichen in PetIdentityBadges (Feed und Profil), Profilring in
Rangfarbe, "Eingeladen von …" im Tierprofil
- Benachrichtigungen REFERRAL_JOINED / REFERRAL_RANK_UP
- Keine neuen nativen Module (React Native Share) — per OTA auslieferbar
Typecheck grün; nicht auf einem Gerät getestet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Android is edge-to-edge, and stack screens (no tab bar below) had no
bottom inset on their scroll containers — the first post on a pet profile
sat in the gesture area and couldn't be tapped (found.md). New
useBottomSafePadding hook adds insets.bottom to the contentContainerStyle
of pet profile, followers, following, health, milestones, edit pet,
settings, search, messages list and delete account.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
iOS labels the back button with the previous screen's title, which was
the untitled (tabs) route group. Use headerBackButtonDisplayMode
"minimal" (chevron only) for all stack screens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- eas.json: new `preview-simulator` profile (extends preview, ios.simulator)
for EAS cloud simulator builds without an Apple Developer account
- app.json: iOS deployment target 17.0 via expo-build-properties
(@clerk/expo's ClerkKit SPM package requires iOS 17 — pod install failed
on the default 16.4), iPhone-only (supportsTablet: false),
usesAppleSignIn, ITSAppUsesNonExemptEncryption: false
- AppleSignInButton: native Apple sheet via Clerk's useSignInWithApple
(App Store Guideline 4.8, since Google login is offered); same
legal-consent handling as GoogleSignInButton; iOS-only
- Not yet testable end-to-end: needs Apple Developer account, the
Sign in with Apple capability on the App ID and Apple enabled in Clerk
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Multiple users reported "Unmatched Route pawfeed://sso-callback" after
Google sign-in. Root cause: Android's custom tab delivers the OAuth
redirect intent to the app twice — once caught correctly by
expo-web-browser's openAuthSessionAsync() session, but also as a plain
deep link seen by expo-router's own Linking listener. With no matching
route, expo-router briefly showed its built-in Unmatched Route error
screen instead of letting the real sign-in flow (GoogleSignInButton ->
setActive -> router.replace) take over.
Adds app/sso-callback.tsx as a themed loading spinner so expo-router has
a valid screen for that window; no auth logic needed there since
useProtectedRoute() in app/_layout.tsx already redirects based on
Clerk's session state. JS-only change, OTA-deployable, no native rebuild
needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends the gallery picker to accept videos, uploads them via Mux direct
upload (same flow web already uses), and adds PROCESSING/READY/ERROR
handling to the story viewer with real playback-driven progress instead
of a fixed timer.
Fixes a video-viewer bug found during device testing: play() was called
before the player reached readyToPlay, leaving duration at 0 and firing
playToEnd immediately — the viewer closed within ~1s instead of playing.
Now waits for statusChange to readyToPlay before calling play().
Also fixes the story-creation "Story teilen" button sitting flush against
gesture navigation on devices without a bottom safe-area inset applied.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes Gitea #36. Plan: ~/.claude/plans/mobile-settings-tab-extraction.md.
Extracts the 5 footer sections pets.tsx had grown (Darstellung, Sprache,
Tier-Vorschlaege opt-out, Rechtliches+Version/OTA, Abmelden) into their
own files under src/components/settings/, and a new app/(app)/settings.tsx
screen reached via a gear icon in every tab's header, next to Messages -
not a 5th bottom tab. Chosen over a tab (see the two mockups in the plan
folder) because Settings is a low-frequency destination for a typical
user; the header icon still reaches it in one tap from anywhere without
spending permanent bottom-nav space on it.
settings.tsx lives outside (tabs)/, registered as a plain Stack.Screen
(app/(app)/_layout.tsx) matching search.tsx's pattern - normal back-arrow
push, no presentation:modal, no tab bar visible while it's open.
SettingsHeaderButton is wired into (tabs)/_layout.tsx's global headerRight
default plus the 3 tabs (index/explore/pets) that override it.
pets.tsx is pet-list-only again. FooterErrorBoundary now wraps the whole
Settings screen instead of only the logout button, closing a gap a prior
code review found (AppearanceSection/LegalSection were rendering
unprotected).
Code-reviewed before commit (WORKFLOW.md step 6) - fixed two of three
findings: extracted the section/sectionTitle style duplicated across
AppearanceSection/LanguageSection/PetSuggestionsSection into a shared
sectionTitleStyle.ts, and reduced every header icon's hitSlop 12->8 (rows
can now hold 3 icons with gap:16; 12+12=24 overlapped the gap, 8+8=16
exactly meets it - see the comment on headerButtons in (tabs)/_layout.tsx).
Left the pets.* i18n namespace as-is on the moved strings (documented in
settings.tsx's doc comment) - pets.language* is shared with the pre-login
LanguageSwitcher, so a partial rename would misdescribe those call sites.
Typecheck + lint clean across all touched/new files, i18n key parity
verified (de/en).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Meine Tiere tab footer now shows 'Version X.X.X - OTA <short-id>' (or
'Build (kein OTA)' when running the embedded/non-OTA bundle) via
expo-updates' Updates.isEmbeddedLaunch/updateId. Directly answers the
support question 'did the update actually arrive on this device?' by
comparing the shown short id against the 'Android update ID' an
'eas update' publish prints - the exact ambiguity that cost a multi-day
debugging session earlier (see .claude/handoffs/2026-09-12-ota-update-
mystery-expo-dev-client-root-cause.md).
Code-reviewed before commit (per WORKFLOW.md step 6): fixed two of four
findings - wrapped the expo-updates reads in a try/catch (LegalSection
sits outside FooterErrorBoundary, so an unguarded throw here would have
taken down the whole footer) and moved the hardcoded 'OTA ' prefix into
i18n (pets.otaId). The web-shim-always-unknown finding is a documented
dead branch (this app ships no web build - eas update only ever runs
--platform android). No test coverage added - no RN test infrastructure
exists in mobile/ yet (pre-existing gap, tracked in WORKFLOW.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same root cause as post-new.tsx/story-new.tsx: search.tsx imported the
static light-only colors/typography instead of useTheme(). Bigger
refactor than the other two since TabButton/PetRow/Loading/EmptyState
are separate top-level components, not closures inside the screen -
they now receive styles/colors as props from the theme-aware
makeStyles(colors) built in SearchScreen, rather than importing the
static tokens themselves.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
post-new.tsx (and story-new.tsx, same latent bug) still imported the
static, light-only colors/typography from theme/tokens.ts instead of
useTheme() - container background stayed permanently light while the
shared TextField correctly pulled dark-mode text color from the theme,
producing white text on a white background in the caption input.
Migrated both to the useTheme() + makeStyles(colors) pattern already
used by milestone-new.tsx. search.tsx has the same underlying issue but
needs a larger refactor (several sub-components reference the static
tokens directly) - left for a separate pass, noted in found.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Catches up several native builds' worth of work that was already tested
live and shipped to the Play Store (per mobile/found.md TODO Neu4-Neu6)
but never made it into a git commit:
- Google SSO via expo-auth-session/expo-web-browser (GoogleSignInButton,
useWarmUpBrowser) on sign-in/sign-up.
- Upload-time image downscaling (resize-image.ts) to fix slow first
content loads — capped at 1600px, re-encoded JPEG @0.8.
- @sentry/react-native (GlitchTip-compatible) crash reporting, plus a
10s auth-loading timeout screen.
- Dark-mode splash screen background, breed/birthday editing in
PetCreateForm, and the OptionPicker Modal rewrite that fixed the
Android nested-scroll dropdown bug across Explore/onboarding/edit.
- expo-dev-client removed from the shared plugin list (was silently
blocking OTA updates in production), app.json bumped to 1.0.6/
versionCode 7, several expo-* packages patched via `expo install --fix`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three pieces of work from this session:
- Pet-suggestion notifications: a weekly cron (species/breed affinity
heuristic) suggests pets to follow, with an owner opt-out and web/mobile
notification UI wiring.
- Story music overlay: admin-curated royalty-free track catalog
(/p/[secret]/music), attachable to stories on web (StoryForm/StoryViewer),
with mute-original support for video stories. Mobile UI is deferred to
the next native build (needs an audio-playback module) — tracked in
mobile/PENDING-NATIVE-BUILD.md.
- Auth UX fixes: sign-in/sign-up now use KeyboardAvoidingView + ScrollView
and a shared PasswordField with a show/hide toggle (mobile). Web's
ActivePetContext gained clearGactivePetId(), wired to sign-out via a new
SignOutCleanup component, fixing a false "you do not have permission to
access this pet" error after switching accounts on the same browser.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Sign-up's Terms/Privacy links overflowed the screen width — they
reused onboarding's long descriptive sentences ("Alle Details in
unseren Nutzungsbedingungen") in a compact side-by-side row with no
wrap. Switched to the existing short pets.legalTerms/legalPrivacy
labels and added flexWrap as a safety net.
- Neither sign-in nor sign-up let you pick a language before logging
in — extracted pets.tsx's language toggle into a shared
LanguageSwitcher component and added it to both auth screens.
- Set up expo-updates (Gitea #35) via `eas update:configure`:
runtimeVersion policy "appVersion", per-profile update channels in
eas.json (development/preview/production). Future JS-only fixes can
now ship via `eas update` without a new Play Store review — this
build itself still needs a full native build since expo-updates is
a native module. Manually removed a duplicate RECORD_AUDIO
permission entry that update:configure's app.json rewrite introduced.
- Bumped to 1.0.2 (versionCode 3).
- Videos kept looping forever after scrolling off-screen (expo-video has
no visibility awareness). New useVisiblePostIds hook tracks FlatList
viewability and pauses VideoPlayer once a post scrolls away, wired
into both the feed and the pet-profile post list.
- Pet edit screen couldn't change breed or set birthday/adoption date,
even though the backend (pets.update) already supported all three —
web already used them. Extracted BreedPicker out of PetCreateForm
into a shared component, added it plus TT-MM-JJJJ date fields (same
validation pattern as the health forms) to edit.tsx. Its BreedPicker
nests a ScrollView, so edit.tsx's outer ScrollView was swapped for a
FlatList to avoid the same nested-scroll gesture conflict fixed
earlier for onboarding/pet.tsx.
- Pet profile's own avatar was a plain, non-tappable Image. Tapping it
now opens the existing /image-viewer (same pinch-to-zoom viewer post
images already use) when the pet has an avatar set.
- milestones.tsx and milestone-new.tsx still imported the static
light-only colors/typography tokens instead of useTheme() — switched
to the established makeStyles(colors) pattern.
- PetCreateForm.tsx and onboarding/pet.tsx used the static light-only
colors/typography tokens instead of useTheme() — pet creation stayed
light even in dark mode.
- onboarding/pet.tsx wrapped its steps in a plain ScrollView; the breed
OptionPicker nests its own vertical ScrollView, and two same-axis
ScrollViews swallow the inner one's scroll gesture on Android even
with nestedScrollEnabled. Switched the outer container to a FlatList
(empty data + ListHeaderComponent), matching explore.tsx's already
working fix for the same component.
- pets.create's mutation never invalidated trpc.pets.list, so
ActivePetContext kept seeing zero pets after the first pet was
created — OnboardingRedirect then bounced the user straight back to
/onboarding/pet after the avatar step, with only an app restart
breaking the loop. Added the missing invalidateQueries call.
New sign-ups got stuck after entering the email verification code:
Clerk's status stayed on "missing_requirements" (surfaced via the
generic auth.signUpIncompleteError as "Sign-up incomplete") instead of
"complete", with no way to proceed. Checked this Clerk instance's
public /v1/environment config directly: first_name and last_name are
both required, and sign_up.legal_consent_enabled is true — web's
prebuilt <SignUp> component discovers and renders these automatically,
but this hand-built native form only ever collected email + password.
Added first/last name fields and a legal-consent checkbox (linking to
the actual Terms/Privacy pages), passed as firstName/lastName/
legalAccepted in the same signUp.password() call (Clerk's Futures API
accepts all three there — no separate .update() step needed). Submit
is disabled until all fields are filled and consent is checked.
This blocked 100% of new mobile sign-ups until fixed.
New public page at /kinderschutz — required by Google Play's Child
Safety Standards policy for apps with user-generated content/social
features (https://support.google.com/googleplay/android-developer/answer/14747720).
Registered as public + maintenance-exempt in proxy.ts, matching the
existing /impressum, /datenschutz, /nutzungsbedingungen pattern.
Content covers only mechanisms that actually exist in this codebase:
in-app reporting (ReportSheet + admin moderation review), a named
responsible contact (same identity as /impressum), and the German/EU
legal framework (StGB §184b/c, DSA, JuSchG) for law-enforcement
cooperation on confirmed CSAE content — no invented automated-
detection claims.
Cross-linked from the other legal pages' footers, the web /pets
footer, and the mobile app's My Pets legal-links section (new
pets.legalChildSafety i18n key, DE/EN).
Messages (conversation list + chat detail) used the static (light-only)
color tokens instead of useTheme() — same bug class as the earlier
auth-screen/comments/RepostCard fixes this session.
Also fixed: the comment and chat input rows sat flush against the
bottom edge with a flat 12px padding, no bottom safe-area inset. On
devices with Android's gesture nav bar this put the send button right
where the system back/home gesture also listens, making it error-prone
to tap ("in einem Schutzbereich sehr schwer anklickbar"). Both input
rows now pad by max(12, insets.bottom) instead of a flat 12px.
Build #4 failed at the expo doctor pre-build check EAS runs, with two
real issues:
- app.json's newArchEnabled key is no longer a valid Expo config field
in SDK 57 (New Architecture is the only architecture now, so the
opt-in toggle was removed) — the schema validator rejected it as an
unknown property.
- expo-router was pinned to 5.1.11, an old-style version number
predating Expo's SDK-aligned versioning — way behind the ~57.0.19
expected for this SDK. expo-linking/expo-secure-store were a patch
version behind too. Fixed via `npx expo install --fix` rather than
guessing version numbers.
The expo-router bump changed tabBarIcon's expected color prop type
from `string` to RN's `ColorValue`; updated NotificationsTabIcon's
signature to match.
`npx expo-doctor` now reports 18/18 checks passed.
FeedScreen's feedItems useMemo (interleaveTrending) sat after the
loading/error early returns instead of before them, violating the
Rules of Hooks: the loading-state render calls fewer hooks than the
loaded-state render, so React throws "Rendered fewer hooks than
expected" the moment the feed finishes loading — crashing the app on
every real device right after login. Moved the useMemo (and the plain
posts/trendingPosts derivations it depends on) above both early
returns so every render calls the same hooks in the same order.
Also add graphify-out to the root .easignore.
Sign-in/sign-up and RepostCard used the static (light-only) color
tokens instead of useTheme() — in dark mode the login screen kept a
light background with light-theme-colored text baked in, and
RepostCard's text color was hardcoded independent of its (already
theme-aware) surrounding background, both landing on illegible
low-contrast text. Same bug class as earlier dark-mode fixes this
session, converted the same way (useTheme() + makeStyles(colors)).
Also add a root-level .easignore (EAS Build's archive root may resolve
above mobile/ for monorepo purposes) excluding backend/tooling
directories the mobile app never needs at build or runtime (.claude,
.impeccable, .planning, docker, prisma — the last is only a type-only
dependency via mobile's tRPC router import, erased before Metro
bundles anything).
Notification taps for post-related types (REACTION/COMMENT/MENTION/
PAW_BACK) landed on a bare "no comments yet" screen with no sign of the
actual post. The comments screen now fetches the post via the existing
posts.getById and shows it (image/caption/header) via the shared
PostCard above the comment list.
The actor's name in each notification row is now its own tappable text
run that links to their profile, separate from tapping the rest of the
row (which still opens the notification's target) — ported from web's
notifications page, which does the same split between a Link and plain
text.
Also fixed: the comments screen used the static (light-only) color
tokens throughout, so embedding the theme-aware PostCard produced
near-invisible light text on its own light background in dark mode.
Converted the screen to useTheme() like the rest of the dark-mode-
converted screens.
Notifications: add a visible hairline separator between rows and make
each row tappable — it marks read and navigates to the relevant post
(comments screen) or pet profile, porting web's getHref()/getPostId()
routing logic.
Feed: the "..." menu on a post now opens a delete confirmation instead
of the report sheet when the post belongs to the currently active pet;
report sheet stays unchanged for posts from other pets. Reuses the
pre-existing posts.delete mutation, no backend changes needed.
- explore.getTrending posts are now interleaved into the chronological
Feed at semi-random intervals (src/lib/interleave-trending.ts),
marked with a small "Trending" badge on PostCard — a lightweight
stand-in for web's separate Explore "Trending" tab, which mobile
wasn't going to get as its own screen. No backend change: reuses the
existing explore.getTrending procedure as-is.
- update app icon / splash / favicon / Android adaptive-icon assets
(user-provided)
- README: correct the now-stale Dark Mode "not yet converted" list
(Explore/Notifications/pet profile/health were converted in the
found.md batch) and document the report/date-format/pinch-zoom/
legal-links fixes
- new report feature: a "..." button on every post opens a reason-picker
sheet (reports.create), matching web's ReportSheet reasons/copy
- pet profile, edit, followers/following, and the whole health screen
(incl. consent gate) converted to the makeStyles(colors) dark-mode
pattern
- fixed the invisible back button on every pushed (app)/_layout.tsx
screen: headerTintColor was never set, so the native-stack default
back chevron kept its platform tint instead of the theme color —
black-on-black in dark mode looked like a missing button entirely
- health forms (weight/vet visit/vaccine) now take dates as DD-MM-YYYY
instead of the backend's raw YYYY-MM-DD, converting internally
(src/lib/date-format.ts); invalid dates now show an error instead of
the mutation silently failing with no onError handler at all — this,
not persistence, was the actual "Gesundheitsdaten werden nicht
gespeichert" bug
- pinch-to-zoom on the full-screen image viewer via a plain
PanResponder (no new native dependency)
- Pets tab: language switcher now uses flag emoji (🇩🇪/🇬🇧/🌐), and a
new footer section links Impressum/Datenschutz/Nutzungsbedingungen
plus the installed app version
All live-verified against the deployed backend except pinch-zoom
(two-finger gestures aren't simulatable over adb).
- i18next + react-i18next + expo-localization: device-locale detection,
an MMKV-persisted DE/EN/Automatisch override (mirrors web's cookie-only
locale, no backend field needed), and a language switcher on the Pets
tab next to the theme toggle
- every screen and shared component converted from hardcoded German
strings to translation keys; de.json/en.json kept in exact key-parity
(218 keys each, verified programmatically)
- backend: src/trpc/init.ts now accepts an x-locale header as a fallback
locale source (cookie still wins, web unaffected) since mobile has no
cookie jar to set NEXT_LOCALE in; mobile's tRPC client sends the
current language on every request
- fixes a latent bug: species/breed pickers were hardcoded to locale
"de" while every other pet query resolved names via ctx.locale's
default (English, since mobile never sent a locale cookie) — both now
use the same current-language value
- expo-localization is a new native module (config plugin) and needs a
native rebuild to actually link; deviceLocale() falls back to English
instead of crashing when it isn't linked yet, so the app stays usable
before that rebuild happens
Needs a NAS deploy (src/trpc/init.ts is shared backend code) before the
x-locale fallback and the species/breed locale fix take effect for mobile.
- Explore and Notifications tabs now read colors from ThemeContext
instead of the static light-only tokens
- OptionPicker's option list is position:absolute so it overlays
following content instead of pushing it down, with zIndex on the
component root (and FlatList's ListHeaderComponentStyle on Explore)
so it actually paints above sibling content instead of underneath it
- check off found.md's resolved items, record the three newly reported ones
- 5-step onboarding (guidelines, upload consent, spider opt-in, pet
creation, avatar) that gates first launch until a pet exists
- Add/delete pet flows on the Pets tab, with a shared OptionPicker
(ScrollView-based, not FlatList, to avoid nested-VirtualizedList
warnings inside onboarding's outer ScrollView)
- App-wide light/dark/auto theme via ThemeContext, synced with the
existing owner.getThemePreference/setThemePreference backend
procedures; core navigation chrome, Feed, Pets tab, and shared
primitives (Button, TextField, Card, PetAvatar, PostCard, StoryTray)
converted to the makeStyles(colors) pattern
- found.md fixes: safe-area insets on image/story viewers so controls
clear the status bar, auto-mark-all-read on the notifications tab,
and an Explore species/breed filter
sign-in.tsx only ever covered login (per its own comment "sign-up screens
are follow-up work"). Adds the other half: email/password sign-up using
Clerk's Futures API (signUp.password() -> verifications.sendEmailCode()/
verifyEmailCode() -> finalize()), mirroring sign-in.tsx's two-step
credentials/verify shape. No invite-code step, since INVITE_REQUIRED is
globally off on the backend since launch.
Verified live on the Android emulator: real Clerk validation surfaces
correctly (breached-password rejection), and a valid submission correctly
sends a verification email and transitions to the code-entry step.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Story tray/creation/viewer with PawRing seen/unseen indicator, expo-video
playback for VIDEO posts (replacing the static placeholder), and a
pets/people/hashtags search screen reachable from the Explore tab header.
Extracts FollowButton from the pet-profile screen since search results
need the same follow/unfollow toggle.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Post creation: gallery/camera picker (expo-image-picker) uploads each
image straight to Supabase Storage via a presigned PUT
(posts.getPresignedUrl + expo-file-system), then posts.create with the
resulting storage keys — reachable via a new "+" header button on the
Feed tab. No client-side blurhash (browser Canvas API, no RN
equivalent) or AI-disclosure/mention support yet, matching the rest of
mobile's plain-text captions.
Comments: a dedicated screen (posts/[postId]/comments) for reading,
posting, and deleting one's own comments, reachable by tapping the
comment icon on a PostCard. Ported from web's PostDetailDialog comment
panel, scoped down to just that panel since mobile's image full-view
already has its own route.
Header now shows the PawFeed paw+text logo on the Feed tab (matching
web's Sidebar wordmark). Tapping a post image opens a full-screen
viewer as a route-based fullScreenModal, with a blurred edge-to-edge
copy of the same photo behind the sharp foreground image instead of a
flat black backdrop, for a more dynamic Photos-app-style lightbox.
Fixes from user testing on a real device:
- Feed: replaced an invisible spacing-only separator with an actual
hairline divider between posts.
- Android launcher icon showed Expo's default placeholder (a blue arrow)
because the app icon and the Android adaptive-icon layers
(foreground/background/monochrome) are two separate asset sets — only
icon.png/favicon.png had been replaced. Added scripts/derive-adaptive-
icon.js to crop the paw glyph out of icon.png (detected by its marigold
color) and generate all three adaptive-icon layers plus the splash icon;
this is an explicit placeholder per user's own call, not final art.
New: full pet profile (app/(app)/pets/[petId]/), matching web's
src/app/(app)/pets/[petId]/page.tsx:
- View: identity, species, bio/adoption story, follower/following counts
(tappable → list screens), the pet's own posts below.
- Follow/unfollow with real initial state (follows.isFollowing) and
optimistic toggle — unlike feed reactions, this endpoint actually
exists, so no need to fake an initial state.
- Edit screen (name/nickname/bio/adoption story via pets.update) for the
owner's own pets.
- Tapping a pet (feed header, Explore card, Pets-tab row) now opens this
profile instead of nowhere; the Pets tab's active-pet switch is now a
separate checkmark control instead of overloading the whole row tap.
PostCard's post-shape prop is now structural rather than tied to
feed.getFeed's output type, since posts.byPetId selects `pet` via the
narrower petIdentitySelect (no isVerified/owner) — both need to satisfy
the same component.
Verified end-to-end on-device: feed separator visible, own-pet profile
shows a working Bearbeiten button whose form is correctly prefilled (a
stray edit was made to Bianca's nickname field by mistake during testing
and was discarded via back-navigation without saving — never sent to the
server), someone else's pet profile shows a working Follow/Unfollow
toggle, followers list navigates correctly, and the home-screen launcher
icon now shows the paw instead of the default Expo icon.
Moves the mobile app from a connectivity scaffold to a real Phase 1 app:
- Migrate off Expo Go onto a local custom dev client (expo-dev-client,
built via expo prebuild + expo run:android against the local Android
SDK — no EAS account needed). Needed because react-native-mmkv requires
native code Expo Go doesn't ship.
- Persisted, MMKV-backed TanStack Query cache (PersistQueryClientProvider)
plus expo-image, so a relaunch renders the last-known feed/pets/
notifications instantly instead of a blank screen while refetching.
- ActivePetContext: every pet-scoped procedure requires an explicit petId
server-side (no server-side "current pet"), ported from web's
ActivePetContext, MMKV-backed instead of localStorage.
- Bottom Tabs (Feed/Explore/Notifications/Pets) mirroring the web app's
actual mobile nav, with Messages as a header button rather than a tab
(matches web). Feed, PostCard, PawButton reactions, pet discovery grid,
notifications list, and a conversations list are all wired to the real
backend and verified against the live database.
- Design tokens/components extended with PostCard/PawButton/PetAvatar/
PetIdentityBadges, all styled from the existing src/theme/tokens.ts.
- Fixed a react/react-dom version mismatch surfaced along the way, and
handle Clerk's needs_client_trust (Device Trust) sign-in status with an
email-code step.
Verified end-to-end in the Android emulator against pawfeed.org: feed
scrolls and paginates with real posts/images, paw reactions toggle
optimistically and persist, pet switching updates every screen, and
notifications/explore show live data.
- Add design tokens (colors, radii, typography) mirroring DESIGN.md's
"Spotlight Porch" system, plus Button/TextField/Card components built
on them. Restyle the sign-in and placeholder home screens to match.
- Load Inter via @expo-google-fonts/inter, gate rendering on fonts +
Clerk being ready instead of a blank screen.
- Pin react-dom to match react's version — Metro/Hermes hard-require
exact equality, and react-dom wasn't pinned anywhere before.
- Handle Clerk's `needs_client_trust` status (Device Trust, tied to the
dashboard's Native Applications API): send/verify an email code
before finalizing sign-in.
Verified end-to-end in the Android emulator against the live backend:
sign-in, device-trust code verification, and the home screen showing
real stats.getPublicStats data.
Connectivity + auth scaffold only: Expo Router, Clerk session auth via
expo-secure-store, and a type-only tRPC AppRouter import from the web
app's router — no backend changes needed. See mobile/README.md.