Recovering on a new phone was walked end to end (a fixture was wiped, then
restored through the app's own flow). The Recovery screen itself is good — the
app detects the missing key and routes there by itself, and the copy is honest.
Everything around it had two holes, one of them destructive.
1. Sign-in could send a returning user into new-user profile setup (P2, data
loss). OnboardingViewModel treated "couldn't read the profile" and "has no
profile" as the same thing, so a slow read right after sign-in — a real race,
hit live — routed them to CREATE_PROFILE, which asks "What should your
partner call you?" over a `🔒 Couldn't unlock on this device` value. Anyone
tapping through it re-encrypts and overwrites the name we had just failed to
read, and never reaches Recovery. The repository already distinguishes the
cases (missing doc = success(null), failed read throws), so only a successful
read may now route to profile setup; a read that never succeeds retries and
then goes Home, which is non-destructive and routes to Recovery on its own.
2. A correct phrase was reported as wrong. It is Argon2id key material, so it is
byte-exact, and only .trim() was applied — while the field allowed the
keyboard to capitalise the first word and autocorrect the wordlist. Every
generated phrase is lowercase a-z single-spaced (all 248 WORDLIST entries
checked), so folding typed input to that canonical form is lossless and
cannot weaken the KDF: it only removes failures that were never about the
phrase being wrong. "That phrase doesn't match" now means it actually
doesn't. Also sets KeyboardCapitalization.None + autoCorrectEnabled = false.
3. The escape hatch was styled as a footnote. Most people arriving here are on a
new phone and never saved the phrase, so "ask my partner" — not the field —
is their real way through, yet it was a bare TextButton under the one control
they can't use. It is now a full-width OutlinedButton with plainer copy
("I don't have the phrase — ask my partner"). It stays below the field rather
than replacing it: someone who does have the phrase pasted from a message is
unlocked instantly and offline, while this path waits on the partner.
Adds RecoveryPhraseNormalizationTest (7 cases): auto-capitalisation, shouting,
double/tab/newline spacing and chat-pasted text all fold to the canonical phrase,
while a genuinely wrong phrase still differs. Unit suite green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>