Caught on-device before it shipped, by doing the thing I'd been putting off:
creating a real account and signing in again. My own isSignInStub — added two
commits ago to fix C-AUTH-001 — sent every brand-new email/password user straight
back into profile setup on their next sign-in, forever. Name filled in, profile
finished, still asked "What should your partner call you?" every launch.
It tested email.isBlank() && createdAt == 0L && coupleId == null, on the reasoning
that every path creating a real user writes email and createdAt first, so their
absence must mean nothing is written. Both halves are false:
- an email/password signup never writes `email` at all — CreateProfileViewModel
builds its User without one;
- it only writes `createdAt` through createUser, which it calls only when it
reads no document — a race it normally LOSES to FCM registration, so it takes
the targeted-update branch (updateDisplayName/updateSex) and writes neither.
So a finished profile — displayName and sex set, nothing else — read as a stub
forever, and the retry loop I added then dutifully classified it as a new user.
The predicate now asks "is anything set at all?", which is what "nothing has been
written here yet" actually means. Any one field is enough to trust the document.
Two things I got wrong and am correcting rather than leaving:
- the previous commit claimed isSignInStub was "now shared rather than private to
OnboardingViewModel". It wasn't. The private copy stayed and shadowed the new
shared one, so the domain-model version was dead code and the narrow local one
was what actually ran — which is why the first attempt at this fix changed
nothing on device. The private copy is gone; there is one definition now. The
duplicated-logic trap I'd just written a commit message about, walked into.
- the manual's C-AUTH-001 entry advised requiring email/createdAt before believing
a read. That advice would reproduce this exact bug, so it now says the opposite,
with the reason.
Verified live on the throwaway, both directions, because they fail in opposite ways
and fixing one blindly breaks the other: a real signup (account created, profile
completed, re-signed-in) now lands on Home; the paired fixture still lands on
Recovery. The disposable account was deleted through the app's own flow afterwards.
Tests: aFinishedProfileWithNoEmailOrCreatedAtIsNotAStub pins the regression;
anyOneIdentifyingFieldIsEnoughToTrustTheDocument pins the rule. Suite green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>