Phase 4 of docs/nsec-sign-in.md. SignInToProfileScreen stops being a sentence
about alpha testing and becomes the screen: one field, two things it accepts,
and the npub it signs as shown before anything is written.
One field, because a user does not choose an input type -- they paste what they
have -- and the two shapes are unambiguous: words have spaces, keys do not.
CredentialParser is that decision as a pure function. Words go through
MnemonicCode.validate and derive their NIP-06 key; an nsec, a nostr:-prefixed
nsec or sixty-four hex characters decode to a PrivateKey and must pass
isValid(). The two shapes a reasonable person might paste and that cannot
work are refused by name rather than lumped in with garbage: an npub is
PublicKeyOnly (nothing here can sign with it), an ncryptsec is EncryptedKey
(NIP-49; a follow-on). Words are not lower-cased -- the wordlist already is,
and a capital is a paste artefact worth showing rather than silently fixing.
Problems with the input stay on the field, as supporting text, while the
prompt is still showing; problems after confirming -- the key was already
here, the write failed -- are a state of the screen, through ErrorState with
a retry back to the field. Four states, wrapped in ScreenStateTransition.
Confirm shows the npub in full, in monospace, and which kind was recognised,
with the one consequence that differs: a recovery phrase also restores the
wallet, a bare key comes with none. The secret itself is never echoed.
SignInToProfileViewModel.commit is the effect, and its order is the point.
The secret is written to its store first -- IdentityWriter.writeNostrKey for
an nsec, SovereignWalletViewModel.writeSeed (isRestoringWallet = true) for a
phrase, both injected as functions so the commit can be driven without a key
store -- and only on success is the placeholder account planted with
nostrRepository.signInToProfile. The placeholder before the write would send
the user to fetch a profile they can never sign for; the write before the
placeholder is what the nav host then does. Both writers already refuse a
duplicate by nostr public key, so a wallet's own nostr secret pasted as an
nsec is AlreadyOnThisDevice, and so is a seed whose key is already here bare.
The nav host wires the route with the same tail the create flow uses after
writeSeed: re-list identities, select the new one, go to startup with the form
popped, so back does not return to a field holding a secret. Startup finds
the identity, activates it, and NavigationViewModel takes it from the
placeholder to UnqueuedProfileSynchronization -- which Phase 3 made safe from
both entrances.
Strings: nineteen new, sentence case, including the refusals as sentences
that say what to do instead. The six Torch-era sign-in strings nothing
referenced any more are gone, and the landing caption no longer promises a
remote signer this does not deliver.
Tests. CredentialParserJvmTest is every row of the table in and out, all
three spellings of NIP-06's vector -- words, nsec, hex, with and without the
nostr: prefix, in either case -- asserted to land on the same public key,
which is the equality the duplicate check rests on; and each refusal by name.
SignInToProfileViewModelJvmTest records the effects of a commit and asserts
their order and count: write then plant for both kinds, and no planting after
a refusal or a throw.
Verified with :composeApp:compileDebugKotlinAndroid, :composeApp:jvmTest (885
tests) and :composeApp:m3Audit, which found no spacing literal or title-case
string in the new screen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pulled-From: curated/curated@220616019e