feat(connect): Phase 1 — Connect tab + picker unification#52
Merged
Conversation
Phase 1 of multi-account NostrConnect. New return type for the refactored handleNostrConnect signature: tracks succeeded + failed signer pubkeys with derived isAllSuccess / isAllFailure / isPartialFailure conveniences. FailedSigner stores error as String for Equatable conformance (tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sult Phase 1 of multi-account NostrConnect. Refactors handleNostrConnect from `(parsedURI:, permissions:, boundAccountPubkey:)` to `(parsedURI:, signerPubkeys:, permissions:) → HandshakeResult`. The implementation iterates over the array; Phase 1 always passes exactly 1 element so behavior is identical to today. Body of the existing function moves into a private runSingleConnect helper that takes the single signer pubkey explicitly. The public function is the iteration wrapper that collects per-signer success / failure into HandshakeResult. All known call sites updated to pass a 1-element array. Empty signerPubkeys throws ClaveError.noSignerKey at the boundary. No protocol change; pure refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Task 1 implementation made isAllSuccess vacuously true on empty results (failed.isEmpty alone) to satisfy the plan's contradictory test assertion. Task 2's code review flagged this as a Phase 2 hazard — partial-failure UX in ApprovalSheet keys on isAllSuccess, and an empty result auto-dismissing as "signed in for nobody" would be a broken UI state. Tightens to !succeeded.isEmpty && failed.isEmpty: empty result is explicitly NOT a success. Empty input remains unreachable in production (handleNostrConnect guards against empty signerPubkeys), so this is a type-contract clarification, not a behavioral change. testEmptyResult assertion updated to match: empty → all three booleans are false (no state — neither success nor failure). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of multi-account NostrConnect. The deeplink picker becomes the unified account-binding component used by all connect entry paths: in-app NostrConnect (future task), in-app bunker (future task), and external deeplinks (this rename's primary call site). Picker gains a Mode enum — only .single is implemented in Phase 1; .multi lands in Phase 2. onPick changes from `(String) -> Void` to `([String]) -> Void` to match handleNostrConnect's array signature. Single mode always passes a 1-element array. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two findings from code quality review: 1. Shared/DeeplinkRouter.swift:11 still referenced the old DeeplinkAccountPicker name in a doc comment — missed by the rename sweep in 5f502f8. 2. Clave/Views/Home/HomeView.swift used `pubkeys[0]` to extract the single-mode element. The picker .single mode always sends a 1-element array today, but the subscript is a latent crash surface if a future caller ever sends an empty array. Replaced with `guard let pubkey = pubkeys.first else { return }`. Both are mechanical cleanups; no behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of multi-account NostrConnect. Static predicate the caller checks BEFORE presenting the picker — when true, the caller calls onPick directly with the single account's pubkey rather than rendering a degenerate one-row sheet. Skip rule: accountCount == 1. Zero accounts is NOT skipped (caller routes to onboarding); 2+ accounts presents normally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of multi-account NostrConnect. The primary surface inside the new Connect tab — camera viewfinder + paste field + help link, plus a new "Or share a code from Clave →" secondary affordance at the bottom that pushes into the bunker view (Task 6). Camera, paste, scan dedup, and validation logic are direct copies of the to-be-deleted ConnectNostrconnectTabView (Task 11). The bunker affordance is new — implements UX shape γ per the spec. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of multi-account NostrConnect. Bunker child route under the new Connect tab. Picker fires first (when N >= 2) so the user picks the account whose bunker URI to share; otherwise auto-skips and renders directly. BunkerURIRender takes signerPubkey as an explicit parameter — fixes the latent footgun where today's bunker tab shows currentAccount's URI even when the user might intend a different account. AppState.rotateBunkerSecret(for:) added as the per-signer variant of rotateBunkerSecret() — delegates to SharedStorage.rotateBunkerSecret(for:) and bumps bunkerSecretsTick so views re-evaluate. bunkerURI(for:) was already per-signer (no change needed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tate Two findings from code quality review: 1. ConnectBunkerView's .onAppear could fire more than once after a sheet dismissal, re-presenting the picker the user just cancelled (trapping multi-account users who tap Cancel). Added didInvokePickerOrAutoSkip one-shot guard so presentOrAutoSkip runs at most once per view lifetime. 2. rotateBunkerSecret() (current-account variant) now delegates to rotateBunkerSecret(for: signerPubkeyHex) so it inherits the new accounts.contains(...) membership guard for free. Eliminates a theoretical gap where a stale currentAccount reference could rotate secrets for a no-longer-known account. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of multi-account NostrConnect. Top-level view for the new Connect tab. Hosts ConnectNostrConnectSurface as primary content, navigates to ConnectBunkerView via the "Or share a code from Clave" secondary affordance. State machine for NostrConnect flow: paste/scan → parse → picker (auto-skipped if N=1) → ApprovalSheet → handshake under UIBackgroundTask (build-62 pattern preserved). Single-mode only; multi-mode wiring lands in Phase 2. Not yet wired into MainTabView — that happens in Task 8. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of multi-account NostrConnect. Promotes Connect from a HomeView-presented sheet to a top-level cross-account tab. Position: between Home and Activity. Rationale per spec — Home is per-account dashboard (primary), Connect is action (second), Activity is review (third), Settings last. The old Connect sheet trigger on HomeView is removed in Task 9. The old ConnectSheet/Connect/ files are deleted in Task 11. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of multi-account NostrConnect. HomeView no longer presents ConnectSheet — Connect is now its own top-level tab (Task 8). The sheet trigger button is removed; the empty-state CTA for users with zero paired clients is replaced with a Text label pointing at the Connect tab. This makes Connect a cross-account action surface (matches Activity and Settings, which were already cross-account). Per-account binding for connect-time actions now happens explicitly via ConnectAccountPicker at the moment of pairing, not implicitly via the identity-bar selection. ConnectSheet.swift itself is deleted in Task 11. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…emoval Task 9 removed handlePairNewConnectionTap (the only setter of showConnectionCapAlert in HomeView), leaving the @State declaration and the .alert modifier as dead code — the alert could never fire. Removing both. Note: ApprovalSheet has its own independent showConnectionCapAlert state which is unrelated and unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of multi-account NostrConnect. Removes files in Clave/Views/Home/Connect/ that have been superseded: - ConnectSheet.swift (replaced by ConnectTabView, Task 7) - ConnectNostrconnectTabView.swift (replaced by ConnectNostrConnectSurface, Task 5) - ConnectBunkerTabView.swift (replaced by ConnectBunkerView + BunkerURIRender, Task 6) Moves Clave/Views/Home/Connect/ConnectHelpSheet.swift → Clave/Views/Connect/ConnectHelpSheet.swift (still in use by ConnectNostrConnectSurface). DeeplinkAccountPicker.swift was already removed in Task 3. ConnectAccountContextBar.swift: deleted (only used by removed ConnectSheet). NostrConnectURISource enum was defined inside ConnectNostrconnectTabView; rescued into ConnectNostrConnectSurface.swift (which owns the onParsed callback that uses the type) so ConnectTabView.swift compiles cleanly. The Clave/Views/Home/Connect/ directory is now empty and removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of multi-account NostrConnect (feat/connect-tab-restructure). 14-task implementation complete; this build is for internal TestFlight smoke testing before PR/merge to main. Build 72 skipped (was burned in a prior archive that didn't ship). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Build 73 was uploaded from feat/ios-entitlement-service by mistake (wrong worktree). That upload contained the entitlement work, not Phase 1's Connect tab. Bumping to 74 for the correct Phase 1 TestFlight archive. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-reported smoke-test bug: tapping "Or share a code from Clave"
on the Connect tab pushed an empty ConnectBunkerView that immediately
presented the picker as a sheet from .onAppear. If the user cancelled
the picker, the blank pushed view remained in the nav stack — confusing
"blank sheet behind the half sheet" UX.
Fix: move picker presentation up to ConnectTabView's tap handler for
the bunker affordance.
- N=1: navigate directly to BunkerURIRender (no picker)
- N>=2: present picker as a sheet from ConnectTabView; on pick,
navigate to BunkerURIRender; on cancel, just dismiss the sheet
(no orphan push)
ConnectBunkerView is deleted — no longer needed; nav destination
renders BunkerURIRender directly with the title applied inline.
Also widen ConnectAccountPicker detents from [.medium] to
[.medium, .large] — medium got cut off on multi-account devices
with 4+ accounts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of multi-account NostrConnect, smoke-test follow-up. New tab order: Home / Activity / Connect / Discover / Settings. DiscoverView is a stub — will eventually surface NIP-46-compatible Nostr clients (Damus, Yakihonne, Stacker.news, nostrudel, Tableau, etc.) with one-tap pairing flows. For now, "Coming soon" placeholder. Tab order rationale: Home is the per-account dashboard; Activity is the cross-account log (review-oriented); Connect is the action surface (sits in the natural middle position); Discover is the new ecosystem surface; Settings stays last. A center-prominent visual treatment for the Connect tab was discussed but deferred to a follow-up PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…75 → 76 User feedback during smoke test: the link icon felt wrong for the Connect tab. Switching to bolt.fill (matches the mockup in the tab-redesign discussion) — feels more action-oriented for what is fundamentally a "do a thing" surface. Build bump folded in to avoid a separate commit; 75 was bumped but not archived. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three smoke-test fixes to ConnectAccountPicker:
1. Removed .presentationDetents([.medium, .large]) so the sheet
defaults to full height. Half-sheet was getting cut off on
multi-account devices with 4+ accounts.
2. accountAvatar(for:) helper wraps AvatarView in AsyncImage when
account.profile?.pictureURL is available — matches the pattern
in HomeView.clientRow and ApprovalSheet. AvatarView (initials +
pubkey-derived gradient) stays as the loading/failure placeholder.
3. Subtitle now shows bech32-encoded npub ("npub1abc234…wxyz")
instead of raw hex. Added Nip19.encodeNpub helper alongside the
existing encodeNote / encodeNevent.
Bumps pbxproj 76 → 77 in the same commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visual reminder of which account's bunker URI you're sharing — a
labelled header card at the top of the bunker render screen
("Sharing as @alice" + truncated npub + avatar). Helps when you
navigate here from the picker since the URI itself is opaque hex.
Reuses the accountAvatar pattern from ConnectAccountPicker
(AsyncImage with AvatarView fallback) and Nip19.encodeNpub for the
truncated npub display.
Bumps pbxproj 77 → 78 in the same commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ods guide
Promote bunker affordance from a buried "Or share a code from Clave →"
at the bottom of the NostrConnect surface to a prominent action card
positioned right below the paste field — qrcode icon, accent border,
larger title weight, dropped the "Or" prefix.
Repurpose the help link as an educational entry point: was "What's a
nostrconnect URI?" → now "What's the difference between Nostrconnect
and Bunker?" Smaller (footnote) and positioned below the bunker card.
Rewrite ConnectHelpSheet to be a methods comparison:
- Card per method (Nostrconnect, Bunker) with URI scheme chip and
plain-language description
- Same-device callout (orange tint) explaining the iOS-suspends-
WebSocket gotcha that makes Bunker preferred for on-device pairing,
citing the failure mode (handshake fails silently) without going
into protocol jargon
Bumps pbxproj 78 → 79.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 1 of multi-account NostrConnect. Connect graduates from a HomeView-presented sheet to a top-level cross-account tab; all account-binding consent for pairing flows through a single unified
ConnectAccountPicker. No protocol changes; Phase 2 (accounts=multiopt-in) follows separately.What ships
bolt.fill) + new Discover stub tab (5th)ConnectAccountPicker(renamed fromDeeplinkAccountPicker) used by all 3 entry paths (in-app NostrConnect, in-app bunker, external deeplink) — auto-skips at N=1, full-sheet with profile pictures + truncated npubs when N≥2handleNostrConnectsignature refactored to(parsedURI:, signerPubkeys: [String], permissions:) → HandshakeResult. Phase 1 always passes a 1-element array; Phase 2 enables N > 1rotateBunkerSecret(for:)per-signer (fixes a latent footgun where the bunker tab always showed currentAccount's URI)ConnectHelpSheet: "Nostrconnect vs Bunker" comparison including a callout about the same-device pairing reliability gotcha (iOS suspending WebSockets on the other app during nostrconnect handshakes)Clave/Views/Home/Connect/directory deleted (ConnectSheet,ConnectNostrconnectTabView,ConnectBunkerTabView,ConnectAccountContextBar,DeeplinkAccountPicker— all superseded)ConnectSheetBuild state
HandshakeResultTests,AppStateHandshakeSignatureTests,ConnectAccountPickerAutoSkipTestsOut of scope
accounts=multiURI flag, multi-select picker, N-up handshake loop) — separate PR gated on Tableau-side implementation/Users/danielwyler/tableau/docs/superpowers/plans/— separate brainstorm cycle when Phase 2 is near mergeSpec + plan
docs/superpowers/specs/2026-05-10-multi-account-nostrconnect-design.md(on local branchspec/multi-account-nostrconnect— can push separately if useful for review reference)docs/superpowers/plans/2026-05-10-multi-account-nostrconnect-plan.md(same branch)Test plan
xcodebuild test)v0.2.0-build79, GitHub release marked Pre-release perv0.2.0-build60/62convention23 commits on the branch; recommend squash-merge per project convention (#45, #46 were squashed).
🤖 Generated with Claude Code