Skip to content

refactor(login): one entry point for first run and add-account - #449

Merged
barrydeen merged 2 commits into
mainfrom
refactor/one-login-entry
Sep 16, 2026
Merged

barrydeen merged 2 commits into
mainfrom
refactor/one-login-entry

Conversation

@dmnyc

@dmnyc dmnyc commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Adding a second account presented LoginView — its own screen — while a cold launch presented SplashView with a bottom sheet. Two hand-written copies of "choose an account", and they had drifted apart in three ways that cost real functionality.

Supersedes #446. That PR's commit is included here rather than rebased away, so the Cancel fix survives LoginView being deleted instead of going with it. #446 can be closed unmerged.

The three gaps in add-account

No watch-only. NostrLoginSheet.login() accepts an npub / nprofile via Nip19.decodeNostrUri and calls NostrKey.saveWatchOnly. LoginView.login() only ran parseNsec — its placeholder read nsec1... and its error didn't mention npub — so a watch-only second account couldn't be added at all. Multi-account supports it fine: saveWatchOnlysaveaddToAccountList.

No Apple or Google. Add-account offered nsec only, in the same build that shows both buttons at first launch (gated on AppleAuthConfig.isConfigured / GoogleAuthConfig.isConfigured). A second account whose key lives in iCloud Keychain or Google had no way in.

No trimming. The sheet trims pasted input; LoginView passed the raw string to parseNsec. An nsec with a trailing newline — i.e. from any password manager — failed on one screen and worked on the other.

Plus the cosmetic divergence you'd expect: different logo, title, button shape, and error copy.

The change

SplashView was already callback-shaped and holds no first-run-only state, so it takes a Mode instead:

  • .firstRun — unchanged behavior, including the 1.8s curtain.
  • .addAccount — shows Cancel, skips the curtain.

The curtain hides the launch animation's safe-area settling. Presented over a running app the safe area is already stable, so keeping it would mean 1.8s of blank screen after tapping Add account.

ContentView grows one loginEntry(mode:) carrying the Nostr sheet and the Google / Apple covers, plus a shared finishLogin. What actually differs between the two entry points turns out to be two lines — dismissing the cover, and the shorter loading delay an account switch gets.

Net −288 / +122, and the three gaps close for free: add-account is now literally the same code path as first launch.

Decisions worth a look

Apple / Google are offered in add-account. They're how a cloud-backed key gets in, so excluding them would leave the gap half-closed. It does mean someone can add an account that's already signed in elsewhere. Easy to gate to .firstRun if you'd rather.

The avatar collage stays in both modes. It costs a profile fetch and a metrics socket per open, but it is the screen's design — without it, add-account is a bare screen with three buttons, which isn't "one view" in any useful sense. viewModel.cancel() on disappear already tears both sockets down.

LoginView.swift is deleted along with its four project.pbxproj references — it lived at the repo root, which is individually listed rather than a fileSystemSynchronizedGroup. The build passing is the check that the right four lines went.

Testing

Built and installed on the simulator. Two things worth a reviewer's attention, since they're what a refactor like this would break: that first launch still settles behind the curtain, and that Apple's brand-new-account path still routes into the sign-up wizard rather than the loading screen — that branch in finishLogin is the only routing I restructured rather than moved.

The Cancel button sat in a bottom overlay with a fixed 120pt inset,
which put it on top of the Log In button, and the view already sets
ignoresSafeArea(.keyboard, edges: .bottom) so a bottom anchor had no
relationship to the keyboard at all. It moves to the top-leading corner.

This is the only way out of add-account: the cover is presented with
interactiveDismissDisabled(), so swipe-to-dismiss is off.

White text on 15%-white also read as scenery rather than a button. The
background goes to 92% white with a black label.
Adding a second account presented `LoginView`, its own screen, while a
cold launch presented `SplashView` with a bottom sheet. Two hand-written
copies of "choose an account", which had drifted apart in three ways
that cost real functionality:

  - No watch-only. The splash sheet accepts an npub / nprofile and calls
    `NostrKey.saveWatchOnly`; LoginView only ran `parseNsec`, so a
    watch-only second account could not be added at all. Multi-account
    supports it fine — `saveWatchOnly` → `save` → `addToAccountList`.
  - No Apple or Google. Add-account offered nsec only, in the same build
    that shows both buttons at first launch.
  - No trimming. The sheet trims pasted input; LoginView passed the raw
    string to `parseNsec`, so an nsec with a trailing newline (any
    password manager) failed on one screen and worked on the other.

`SplashView` was already callback-shaped and holds no first-run-only
state, so it takes a `Mode` instead: `.addAccount` shows Cancel and
`.firstRun` keeps the launch behavior. ContentView grows one
`loginEntry(mode:)` carrying the Nostr sheet and the Google / Apple
covers, plus a shared `finishLogin`. What actually differs between the
two entry points turns out to be two lines — dismissing the cover, and
the shorter loading delay an account *switch* gets.

The 1.8s curtain that hides the launch animation's safe-area settling is
skipped in `.addAccount`: presented over a running app the safe area is
already stable, so it would only be 1.8s of blank screen after tapping
Add account.

The avatar collage stays in both modes. It costs a profile fetch and a
metrics socket per open, but it is the screen's design — without it
add-account would be a bare screen with three buttons, which is not
"one view" in any useful sense. `viewModel.cancel()` on disappear
already tears both sockets down.

Deletes LoginView.swift and its four project.pbxproj references — it
lived at the repo root, which is individually listed rather than a
fileSystemSynchronizedGroup.

Includes the Cancel-position commit from #446, which this supersedes:
that button now lives on the shared view, so the fix survives LoginView
being deleted instead of going with it.

@barrydeen barrydeen left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed; the 'two paths differ by two lines' claim checks out line by line.

  • First-run: every old splash branch (Nostr onLogin, Google onDone, Apple new/restored) maps 1:1 onto loginEntry(mode: .firstRun) + finishLogin, including the only restructured branch — Apple brand-new still sets keypair/signUpExistingKeypair and routes to the sign-up wizard. accountSwitchInProgress remains add-account-only; onboarding-first ordering preserved both directions.
  • Add-account: old LoginView callback semantics reproduced exactly (dismiss cover, switch-delay flag only when already onboarded).
  • LoginView.swift and all four pbxproj refs gone with zero surviving code references; the sole other SplashView() caller is the #Preview, compiling via the defaulted mode.
  • The three gaps (watch-only npub, Apple/Google, trim) close for free since NostrLoginSheet becomes the single nostr path; #446's Cancel fix survives.
  • Mode-gated curtain skip is right — the 1.8s hold exists only for launch-animation safe-area settling.

Non-blocking nits:

  1. Cancel hardcodes .foregroundStyle(.black) on the white capsule — fine on the splash design, but one of the few hardcoded colors here.
  2. mode: Mode = .firstRun defaults silently to the uncancellable launch variant for any future call site; a required parameter is the safer contract.
  3. Apple/Google in add-account (allows adding an account signed in elsewhere) is the product call the PR flags — fine to keep.

Note: no CI on the branch and no iOS toolchain at the reviewer's machine; build correctness relies on the author's simulator verification (and the pbxproj deletion check is 'build passes', which I couldn't run).

@barrydeen
barrydeen merged commit 7a061f2 into main Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants