fix: nostrconnect connect-flow polish (same-device overlay copy + QR rescan dedup) - #36
Merged
Conversation
The "Stay in Clave for a few seconds" overlay shipped in build 62 alongside the UIBackgroundTask handshake protection is actively misleading for same-device nostrconnect pairs: iOS suspends the client app's WebSocket subscription as soon as it loses foreground, so a user who follows that advice never receives the connect-response Clave publishes. Quick-switching back to the client app is what actually lets the handshake complete (verified on build 66 against noStrudel and Jumble). Track the URI source (paste vs QR scan) at the parse callback in ConnectNostrconnectTabView and store it on ConnectSheet so the "Connecting…" overlay can switch its subtitle accordingly: - paste (clipboard / text field) → same device → "Switch back to your client app to finish connecting. Clave keeps running in the background." - QR scan → cross-device (camera scanned another screen) → keep the original "Stay in Clave for a few seconds" copy. The deeplink path through HomeView's pendingNostrconnectURI sheet chain doesn't render this overlay at all, so no plumbing through AppState is needed — the change is contained to ConnectSheet and ConnectNostrconnectTabView. Also documents the same-device iOS WebSocket suspension constraint in docs/nip46-compatibility.md (recommends bunker:// for same-device iOS pairing; explains why nostrconnect:// is brittle there; lists mitigations for clients that want to support it). Adds a separate note recommending wss://relay.powr.build/ in nostrconnect URI relay sets for client→Clave delivery reliability via the proxy's primary subscription. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the user scans a nostrconnect QR, ApprovalSheet presents over ConnectSheet and the scanner stops. If the user then cancels ApprovalSheet, parsedURI flips back to nil and the existing onChange handler re-arms the scanner so a different QR can be scanned without closing+reopening the sheet. But the QR the user just scanned is almost always still in frame (same physical position, same screen on the other device), so within ~1s the AVFoundation pipeline detects it again — looping the user straight back into ApprovalSheet they just cancelled. Dedup at handleScannedCode against a per-mount lastAcceptedScanCode. Each nostrconnect URI carries a fresh secret, so different sources produce different codes and legitimate retries pass through. Cleared when this view re-mounts (i.e. ConnectSheet is reopened). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Universal Link routing (clave.casa/connect/?uri=...) avoids nostrconnect:// scheme-squatting and improves UX but doesn't extend the listener's WebSocket lifetime — iOS backgrounds the source app the same way as a manual app-switch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
DocNR added a commit
that referenced
this pull request
May 8, 2026
…ht (#38) Internal-only build for on-device verification of two merges: - Stage 3b ProfileFetcher extraction (#37): 287 LOC moved from AppState.swift into Clave/AppState+ProfileFetcher.swift; -36.6% AppState size since the start of the refactor sprint. - Nostrconnect connect-flow polish (#36): switch-back overlay copy for same-device pairs (paste path), QR rescan dedup, and Universal-Link clarification in docs/nip46-compatibility.md. Bumps CURRENT_PROJECT_VERSION 66 -> 67 across all 4 targets in both Debug and Release configs. On-device verification gates (all in one TF cycle): - Profile flows: account add/switch/delete, PFP correctness across switches, AccountDetailView pull-to-refresh, multi-account profile fan-out fetch. - Same-device nostrconnect via paste -> new "Switch back to your client app" subtitle should render. - Cross-device nostrconnect via QR scan -> original "Stay in Clave" subtitle preserved. - QR cancel-and-rescan loop should now break (dedup against lastAcceptedScanCode). Stage 3c (ProxyClient, ~400 LOC) is the next refactor PR. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
DocNR added a commit
that referenced
this pull request
May 9, 2026
…tFlight (#46) Internal-only build for verification of Stage 4b (#45) — the final extraction in the AppState god-object refactor sprint. Bumps CURRENT_PROJECT_VERSION 70 -> 71 across all 4 targets in both Debug and Release configs. Build 71 cumulative content (since last external build 62): - Sprint 5a: bunker pair-cap bypass fix + in-sheet cap gate (#41) - AppState refactor sprint, Stages 1+2+3a+3b+3c+petname+4a+4b (8 PRs total, AppState.swift 2,338 -> 348 LOC, -85.1%) - Same-device nostrconnect overlay copy + QR rescan dedup (#36) Verification gates for build 71 (final TF cycle of the sprint): - xcodebuild test passes (231/231; identical pre/post Stage 4b) ✓ - On-device exercise of alert-chain UI binding (most behavior-relevant Stage 4 verification): - Pair client + sign multiple events back-to-back -> "X of N" advances - Lock-screen approve/deny from notification (static handler check) - "Not now" dismiss-all -> chain closes, badge stays accurate - 5+ min wait with pending request -> TTL purge fires + "expired" activity entry - Background -> foreground -> bell badge updates correctly After this verifies clean, the AppState refactor sprint is officially complete. AppState.swift is a slim @observable container delegating all functional concerns to 6 extension files (5 in Clave/, 1 in Shared/). 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 freeto 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.
Two related connect-flow polish bugs surfaced during build 66 verification, both contained to
ConnectNostrconnectTabView.swift(+ overlay copy inConnectSheet.swift).1. Misleading "Stay in Clave" overlay for same-device nostrconnect
The overlay shipped in build 62 (PR #26) is actively misleading for same-device nostrconnect pairs — iOS suspends the client app's WebSocket subscription as soon as it loses foreground, so users who follow that advice never receive the connect-response Clave publishes. Quick-switching back to the client app is what actually completes the handshake. Verified on build 66 against noStrudel and Jumble.
Switch the overlay subtitle based on URI provenance:
2. QR scanner re-fires the same code after ApprovalSheet cancel
When the user scans a nostrconnect QR, ApprovalSheet presents and the scanner stops. Cancelling ApprovalSheet flips
parsedURIback to nil and the existingonChangehandler re-arms the scanner so a different QR can be scanned without closing+reopening the sheet. But the QR the user just scanned is almost always still in frame — within ~1s the AVFoundation pipeline detects it again, looping the user straight back into ApprovalSheet they just dismissed.Dedup at
handleScannedCodeagainst a per-mountlastAcceptedScanCode. Each nostrconnect URI carries a fresh secret, so different sources produce different codes and legitimate retries pass through. State clears when this view re-mounts (i.e. ConnectSheet is reopened).Why provenance is plumbed at the parse callback, not in
AppStateThe "Connecting…" overlay is only rendered in
ConnectSheet.connectingOverlay— i.e. the in-app paste/QR-scan flow. The deeplink path inHomeView(pendingNostrconnectURI→deeplinkApprovalURI) runsappState.handleNostrConnectin a fire-and-forgetTaskwith no progress UI, so it doesn't need a same-device flag. The provenance distinction is added as a top-levelNostrConnectURISourceenum inConnectNostrconnectTabView.swiftand threaded through theonParsedcallback into ConnectSheet'slastParsedSourcestate — noAppStatechanges, no protocol changes.Bundles
docs/nip46-compatibility.mdupdatesThe doc edit was already in the working tree from earlier today's verification work and explains the exact iOS WebSocket suspension constraint this code change addresses (same 2026-05-07 date stamp, same build-66 verification). Shipping them together keeps the public-facing explanation and the in-app UX consistent in one atomic change.
bunker://for same-device iOS pairing; explains whynostrconnect://is brittle there withnostr-toolslimit: 0+ frozen-WebSocket details; lists mitigations for clients that want to support it.wss://relay.powr.build/in nostrconnect URI relay sets so client→Clave RPCs catch on the proxy's primary subscription regardless of foreground state.Out of scope
handleNostrConnect, and the overlay is rarely seen past ~3s anyway because the user has already switched to the client app. Skip.HomeView. Different code path, separate UX call. Skip.Test plan
Same-device overlay copy
nostrconnect://URI, copy it.QR rescan dedup
Build / unit tests
xcodebuild test -scheme Clave— all suites pass on iPhone 17 sim (run on first commit; QR fix is a strict superset and has no test impact).🤖 Generated with Claude Code