- 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>
Persönliche, unbegrenzt nutzbare Empfehlungscodes (z. B. BUBI-7K3M), die erst
nach der Registrierung im Onboarding eingelöst werden — keine Kopplung an den
Clerk-Sign-up-Ablauf mehr. Registrierung ohne Code bleibt möglich.
- Schema (rein additiv): Owner-Felder für Code, Einladungsstatus, Zähler und
Rang-Cache, ReferralRank / ReferralRankAchievement / ReferralSettings,
AcquisitionSource, NotificationType REFERRAL_JOINED / REFERRAL_RANK_UP
- Kernlogik src/lib/referrals.ts: Einlösen (7-Tage-Fenster, kein eigener Code,
keine Kreis-Einladungen, gesperrte Einladende ungültig), gegenseitiges
Folgen, Qualifizierung erst nach Tier + erstem Beitrag (Foto oder fertiges
Video), Tageslimit mit Prüf-Markierung, Ränge mit Bestandsschutz
- Web: /einladen, öffentliche Seite /r/[code], Onboarding-Schritt
"Wie hast du von PawFeed erfahren?", Rang-Abzeichen, Profilring,
"Eingeladen von …", neue Benachrichtigungen
- Admin /p/[secret]/referrals: Ränge anlegen/bearbeiten/deaktivieren mit
Live-Vorschau, Einstellungen, Übersicht mit Prüfung und Aberkennung;
bevorzugte Verifizierung als Vorteil
- prisma/scripts/referral-setup.ts: Start-Ränge, Settings, Code-Backfill,
optional --credit-existing für Alt-Einladungen
Noch offen: Mobile-App, Abbau des alten InviteCode-Ablaufs, Rechtstexte,
db push + Deploy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build 2 was rejected in TestFlight beta review (missing demo credentials)
and cannot be resubmitted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also includes the user's own pending found.md edits (new entries, iOS
section header, trailing-whitespace cleanup).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The middle toe beans reach y = -9 in the 0–100 viewBox. Web draws the SVG
with overflow: visible; react-native-svg clips at the viewBox, cutting the
paw off. Extend the viewBox upward and grow the box to match; the "add
story" tile uses the same box so it lines up with the rings.
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>
ArachnophobiaOptIn, UploadConsent, OnboardingGuidelines and AvatarStep
still used the static light-palette tokens in module-level
StyleSheet.create, so titles and checkbox labels rendered dark-on-dark.
Switched them to useTheme() + makeStyles(colors) like the other screens.
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>
- Replace next/font/google with next/font/local + vendored Inter variable
woff2 (latin, @fontsource-variable/inter@5.3.0), so neither visitors nor
the Docker build contact Google's servers.
- Add PlayStoreBadge (official Google Play artwork, DE/EN by locale, served
locally from /public/badges) to the hero, a new "PawFeed in your pocket"
section, and the shared landing footer (also covers /ueber-uns).
- Centralize the Play Store URL in src/lib/app-links.ts (derived from the
android.package id org.pawfeed.app).
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>
The story upload input's accept attribute listed explicit image+video
MIME subtypes (image/jpeg,...,video/mp4,...). Several mobile
browsers'/OS's native 'Photos & Videos' pickers only reliably offer
both media kinds when given the broad image/*,video/* wildcard
categories - the explicit mixed list was silently narrowing the picker
to photos only, matching the user's report ('nur Zugriff auf Fotos,
Videos werden gar nicht angezeigt').
The actual accept/reject decision is unchanged (still enforced in
handleFile() against the specific ALLOWED_IMAGE_TYPES/
ALLOWED_VIDEO_TYPES lists) - this only relaxes what the OS offers in
the picker dialog.
Code-reviewed before commit (WORKFLOW.md step 6) - fixed the one
finding that became newly reachable by this fix: videos were
previously never selectable at all, so a parameterized/codec-suffixed
MIME string (e.g. from some Android camera-roll
pickers) could never surface as a rejection; now strips codec
parameters before the ALLOWED_VIDEO_TYPES comparison. Documented two
other findings as accepted, not-fixed-here trade-offs in code comments
(iOS HEIC auto-transcode interaction, broader picker surfacing formats
that still get rejected post-selection) rather than silently ignoring
them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two independent root causes behind 'admin music upload fails':
1. Supabase bucket name mismatch — code expects 'music-tracks' (plural,
MUSIC_STORAGE_BUCKET), but the bucket was created as 'music-track'
(singular). Every upload failed with a bucket-not-found error from
Supabase Storage, surfaced only as a generic toast. Fixed by creating
the correctly-named bucket (public, 5MB limit, audio/mpeg|mp4|aac
allowed_mime_types matching the app's own constants) via the Supabase
Storage API; verified end-to-end with a signed-upload-url + PUT probe.
The old empty 'music-track' bucket is unused but still exists -
deleting it needs the user's own action (blocked here by the
auto-mode classifier's cloud-storage-delete guard).
2. Client-side validation trusted the browser-reported File.type, which
is sniffed from the OS's MIME registry and is inconsistent for
.m4a/.aac (e.g. Windows Chrome reports .m4a as audio/x-m4a, not
audio/mp4) - a perfectly valid file could get rejected as 'wrong
type'. Now derives the content-type from the file extension instead.
Code-reviewed before commit (WORKFLOW.md step 6) - fixed two of four
findings: the extension lookup now uses a Map instead of a plain object
(prototype-shadowing via a file named e.g. 'x.constructor' previously
bypassed the rejection), and requires an actual '.' before the
extension (a bare filename like 'mp3' no longer matches). Extracted the
allowed-types/size constants into a new client-safe src/lib/music-
content-types.ts (supabase-storage.ts re-exports them) so the client
form and server-side Zod schema share one source of truth instead of
two independently maintained copies. Left the trust-the-extension
trade-off as-is (documented in code): this panel sits behind
ADMIN_SECRET + a role check, not the public upload surface, and the
old File.type check was never real content-based validation either.
Full test suite green (one unrelated legal.test.ts timeout in the
full-suite run was confirmed flaky - passes cleanly in isolation).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Google SSO broke live with 'redirect url ... does not match an
authorized redirect URI ... pawfeed://sso-callback', despite the SSO
code (99575dd) being unchanged. Root cause was outside the repo
entirely: Clerk's per-instance authorized-redirect-URL allowlist is
server-side config (CLERK_SECRET_KEY-authenticated Backend API), not
tracked in git, and only had a stale org.pawfeed.app2 entry - the
current pawfeed:// scheme's callback was never registered.
Fixed live via POST https://api.clerk.com/v1/redirect_urls (no app
deploy needed), verified through the Google consent screen on an
emulator and end-to-end by the user on a real device.
Documented in both mobile/found.md (full incident) and README.md's
Known Issues table (the reusable gotcha: check Clerk's redirect-URL
allowlist before debugging app code when SSO breaks unexpectedly).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the merge commit (30c57b6), emulator end-to-end verification, and
OTA update group reference to the Settings-header-icon found.md entry.
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>
Adds WORKFLOW.md (the living, discussion-oriented version) and a
contributor-facing summary in README.md, reflecting four automation
decisions made 2026-09-19:
- OTA publish and web rolling-deploy run automatically once
typecheck+lint+code-review are clean, no per-change confirmation
- a code-review pass (code-review skill - the code-reviewer agent named
in .claude/rules/ecc isn't installed in this environment) runs on
every diff before commit
- twin instances of a found bug pattern get fixed in the same pass,
not just reported
- large mobile features go through a planning step (Plan agent /
EnterPlanMode - the installed equivalent of the referenced planner
agent) and their own feature branch, merging to main (and only then
triggering the automatic deploy) once complete
Also adds a Mermaid system architecture diagram to README.md covering
both clients (web + mobile) against the shared Next.js/tRPC backend,
data/media services, and the two deploy paths (NAS rolling-deploy vs.
EAS Build/Update) - validated by rendering it through mermaid-cli.
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>
Two Known-Issues entries from tonight's OTA-publish session:
- eas update needs --platform android explicitly (no web target/
react-native-web in this project, --platform all fails bundling)
- mobile/node_modules can end up with silently missing nested files after
a bad install, invisible to npm install itself (seen across @expo/cli,
@sentry/browser, @sentry-internal/browser-utils in the same session) -
full rm -rf node_modules && npm install is the fix, not a targeted one
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docker/restore-db.sh - emergency-only, not wired into any crontab. Lists
available backups when run without args; restores via pg_restore
(--clean --if-exists --no-owner --no-privileges -j 4) against DIRECT_URL,
same throwaway postgres:17-alpine container pattern as backup-db.sh.
Requires typing RESTORE to confirm (FORCE=1 skips it for scripted use).
README.md gets a new 'Database backups & disaster recovery' section
(setup step 9) documenting all three scripts, the DIRECT_URL vs
DATABASE_URL reasoning, and the storage-metadata restore caveat. No
admin-panel UI for this by design - restore is SSH-only, deliberately
higher-friction than a browser button.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same gap as run-cron.sh: the crontab's 03:00 backup-db.sh entry pointed
at a script that never actually existed, so there have been no automated
DB backups since that entry was added.
pg_dump via a throwaway postgres:17-alpine container (matches Supabase's
server version; no postgresql-client on the NAS host), against DIRECT_URL
(non-pooled) rather than the pgbouncer-fronted DATABASE_URL, custom
format (-Fc). Writes to /srv/dev-disk-by-uuid-ad295d7f-a870-4f70-9de2-dfded1fabf7f/BACKUPALL/pawfeed/
with 30-day retention, logs to docker/backup.log for visibility (no local
mail delivery configured on this NAS).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both existing crontab entries (trim-feeds, anniversaries) and backup-db.sh
referenced a run-cron.sh that was never actually created/committed - cron
has been executing it daily (04:00/08:00) with exit 127 since at least
today, silently, because no local mail delivery is configured for the
cron user's failure notifications.
Recreates it per README's documented curl pattern (bearer token via
CRON_SECRET, --resolve for the origin TLS cert quirk) and adds logging
to docker/cron.log so future failures are visible without relying on
cron's mail notification.
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>
Schema groundwork for the Story music overlay: an admin-curated
MusicTrack catalog and three new optional Story fields (musicTrackId,
musicOffsetSecs, musicMuteOriginal). No user-upload path — licensing
liability stays with the admin who uploads a track.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backend groundwork for a new pet-suggestion notification: a
PET_SUGGESTION NotificationType value and an Owner.petSuggestionsEnabled
opt-out (defaults to true), mirroring the existing hideSpiders pattern.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
README still said "no EAS Build has been run yet" — the app has since
gone through several production builds and is now live on Google Play
(closed testing). Also documents the new eas update / EAS Update
workflow for shipping JS-only fixes without a new store review,
closing out Gitea #35's last remaining task item.
- 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).
Play Console requires a strictly increasing versionCode per upload —
the approved submission used versionCode 1. This release bundles the
sign-up fix, dark mode fixes, video-loop fix, and the pet edit/avatar
improvements from the last two commits.
- 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.
Google rejected the first submission ("Violation of Play Console
Requirements" — apps in the "Health apps" category require an
organization account). The likely cause: this draft recommended
declaring pet weight/vet-visit/vaccination data under Data Safety's
"Health and fitness" category "as a precaution." That category is for
data about the person using the app, not their pet, and apparently
triggered Google's automated Health-app classification.
Reversed the recommendation (don't declare it there) and added a
section on checking the Play Store app category is "Social", not a
health/medical category — the rejection email names both as possible
causes.
Confirmed 2026-09-07 while deploying the Child Safety Standards page:
/Dockers/PawFeed on the NAS is its own git checkout tracking the
self-hosted Gitea remote, kept up to date via git pull rather than the
manual FTP upload this file previously described. Documents the actual
deploy steps (ssh daniel@192.168.1.222 on the standard port, git pull,
then rolling-deploy.sh) so this doesn't need rediscovering next time.
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).
feature-graphic-1024x500.png — exact 1024x500, 24-bit PNG (no alpha,
per Play Console's spec), brand-matching gradient with the paw glyph
and wordmark.
beschreibung.md — short + full store listing description drafts based
on actual app features (posts, stories, milestones, health tracking
with consent, messaging, species/breed discovery, spider-hiding
preference), ready to paste into Play Console.
store-assets/play-store-icon-512.png — dedicated 512x512 RGBA copy of
the (now-fixed) app icon for the Play Console listing upload.
store-assets/play-console-data-safety.md — Data Safety form draft
based on an actual read of mobile/package.json's dependencies and the
relevant hooks/routers (push token flow, health-consent module), not
guessed: no payment/location/analytics SDK present in the mobile app,
account deletion exists but only via the web app (documented with the
URL Play Console needs).
Icon fix: android-icon-foreground/monochrome.png (and icon.png/
favicon.png/splash-icon.png) were the wide website wordmark logo
(paw + "PawFeed" text), with the actual paw glyph confined to a tiny
78x79px region on the far left of a 512x512 canvas. Android's adaptive
icon system only renders the center ~66% safe zone, so almost none of
the design was ever visible — explaining the "nearly all black" home
screen icon. Isolated the paw glyph via its orange pixel bounding box
and re-centered it properly on each target canvas (transparent for
foreground/monochrome/splash, solid background for icon.png/favicon).
monochrome.png is now a true white-alpha silhouette (Android's spec)
instead of sharp's tint() output, which produced gray instead of white.
Also removed android-icon-background.png — it was the same oversized
wordmark logo, redundant with (and overriding) the adaptiveIcon
backgroundColor that was already set to the same color.
Splash screen: expo-splash-screen was never installed despite
splash-icon.png existing in assets/ — app had no splash config at all,
which explains the light-gray placeholder/broken-image glyph seen on
launch (a generic fallback, not this app's own asset). Installed and
configured with the corrected splash-icon.png.
Play Store prep: added android.versionCode (required once
appVersionSource is "local" and unset until now) and made the
production build profile explicit about android.buildType: app-bundle
(required for Play Store; "preview" stays APK for direct install).
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.