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
This is a Kotlin Multiplatform project targeting Android, iOS, Desktop (JVM).
-
/composeApp is for code that will be shared across your Compose Multiplatform applications. It contains several subfolders:
- commonMain is for code that’s common for all targets.
- Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, the iosMain folder would be the right place for such calls. Similarly, if you want to edit the Desktop (JVM) specific part, the jvmMain folder is the appropriate location.
-
/iosApp contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.
Build and Run Android Application
To build and run the development version of the Android app, use the run configuration from the run widget in your IDE’s toolbar or build it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:assembleDebug - on Windows
.\gradlew.bat :composeApp:assembleDebug
Build and Run Desktop (JVM) Application
To build and run the development version of the desktop app, use the run configuration from the run widget in your IDE’s toolbar or run it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:run - on Windows
.\gradlew.bat :composeApp:run
Build and Run iOS Application
To build and run the development version of the iOS app, use the run configuration from the run widget in your IDE’s toolbar or open the /iosApp directory in Xcode and run it from there.
Learn more about Kotlin Multiplatform…