Skip to content

fix: nostrconnect connect-flow polish (same-device overlay copy + QR rescan dedup) - #36

Merged
DocNR merged 3 commits into
mainfrom
same-device-overlay-copy
May 8, 2026
Merged

fix: nostrconnect connect-flow polish (same-device overlay copy + QR rescan dedup)#36
DocNR merged 3 commits into
mainfrom
same-device-overlay-copy

Conversation

@DocNR

@DocNRDocNR commented May 8, 2026

Copy link
Copy Markdown
Owner

Two related connect-flow polish bugs surfaced during build 66 verification, both contained to ConnectNostrconnectTabView.swift (+ overlay copy in ConnectSheet.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:

  • paste/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 "Stay in Clave for a few seconds".

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 parsedURI 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 — within ~1s the AVFoundation pipeline detects it again, looping the user straight back into ApprovalSheet they just dismissed.

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. State clears when this view re-mounts (i.e. ConnectSheet is reopened).

Why provenance is plumbed at the parse callback, not in AppState

The "Connecting…" overlay is only rendered in ConnectSheet.connectingOverlay — i.e. the in-app paste/QR-scan flow. The deeplink path in HomeView (pendingNostrconnectURIdeeplinkApprovalURI) runs appState.handleNostrConnect in a fire-and-forget Task with no progress UI, so it doesn't need a same-device flag. The provenance distinction is added as a top-level NostrConnectURISource enum in ConnectNostrconnectTabView.swift and threaded through the onParsed callback into ConnectSheet's lastParsedSource state — no AppState changes, no protocol changes.

Bundles docs/nip46-compatibility.md updates

The 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.

  • Added "iOS same-device pairing" section recommending bunker:// for same-device iOS pairing; explains why nostrconnect:// is brittle there with nostr-toolslimit: 0 + frozen-WebSocket details; lists mitigations for clients that want to support it.
  • Added "Improving post-pair delivery reliability for Clave-paired clients" note recommending 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

  • Auto-dismissing the overlay early when the handshake reports success — would require surfacing a "first response received" callback from handleNostrConnect, and the overlay is rarely seen past ~3s anyway because the user has already switched to the client app. Skip.
  • Adding a progress overlay to the deeplink path in HomeView. Different code path, separate UX call. Skip.
  • Reframing the Connect tab UX to steer same-device users away from nostrconnect entirely (e.g. "Recommended for same-device" hint on Bunker, or a topology-first picker). Worth doing — separate sprint, will brainstorm next.
  • Stage 3b/3c god-object refactor — explicitly decoupled.

Test plan

Same-device overlay copy

  • Install build with this change on iPhone.
  • Open noStrudel (same-device, iOS Safari/standalone), generate a nostrconnect:// URI, copy it.
  • Open Clave → Connect a Client → Nostrconnect tab → tap "Paste Nostrconnect URI" → approve.
  • Confirm overlay subtitle reads "Switch back to your client app to finish connecting. Clave keeps running in the background."
  • Quick-switch to noStrudel, confirm pairing completes.
  • Repeat with Jumble.
  • (Optional) Repeat with QR-scan path against a cross-device client (e.g. desktop nostrudel showing a QR), confirm overlay still says "Stay in Clave for a few seconds".

QR rescan dedup

  • On a different device, generate a nostrconnect QR (any web client). Point Clave's nostrconnect tab camera at it.
  • Confirm ApprovalSheet appears.
  • Tap Cancel on ApprovalSheet — keep the QR in frame.
  • Confirm camera viewfinder is live (not frozen) but ApprovalSheet does NOT re-present for the same QR.
  • Generate a new QR (refresh client) — confirm new QR scans normally.

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

DocNRand others added 2 commits May 7, 2026 22:22
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>
@DocNRDocNR changed the title fix: switch-back overlay copy for same-device nostrconnect pairsfix: nostrconnect connect-flow polish (same-device overlay copy + QR rescan dedup)May 8, 2026
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>
@DocNR
DocNR merged commit a61520e into mainMay 8, 2026
@DocNR
DocNR deleted the same-device-overlay-copy branch May 8, 2026 02:49
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>
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>
Sign up for freeto 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.

1 participant

@DocNR