Skip to content

refactor: extract NostrConnect handlers into AppState extension (Stage 3a) - #34

Merged
DocNR merged 1 commit into
mainfrom
refactor/nostr-connect-extension
May 7, 2026
Merged

refactor: extract NostrConnect handlers into AppState extension (Stage 3a)#34
DocNR merged 1 commit into
mainfrom
refactor/nostr-connect-extension

Conversation

@DocNR

@DocNRDocNR commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

Stage 3a of the AppState god-object split. Smallest of the three Stage 3 sub-extractions (NostrConnect, ProfileFetcher, ProxyClient) — establishes the extension-with-mutating-state pattern that Stages 3b and 3c will reuse.

Numbers

FileBeforeAfterΔ
Clave/AppState.swift1,934 LOC1,770 LOC−164 (−8.5%)
Clave/AppState+NostrConnect.swift180 LOC+180 (new)

Combined Stages 1+2+3a: AppState.swift down from 2,338 → 1,770 LOC (−24.3%).

What moved

Into Clave/AppState+NostrConnect.swift:

  • handleDeeplink(url:)DeeplinkRouter wiring + dispatch
  • handleNostrConnect(parsedURI:permissions:boundAccountPubkey:) — the full multi-relay handshake (~143 LOC)
  • The // MARK: - Deeplink handling (Task 3.3) section header (now redundant in AppState.swift)

What stays in AppState.swift:

  • The 3 @Observable stored properties (pendingNostrconnectURI, pendingDeeplinkAccountChoice, deeplinkBoundAccount). Swift forbids stored properties in extensions, so they remain on the class. Pointer comment added so readers can find the handlers.
  • The NotificationCenter observer in AppState.init that calls self.handleDeeplink(url:) — works fine as an extension method call.

Design decisions (carried over from Stage 2)

DecisionChoice
Type shapeextension AppState (matches Stage 2 LegacyMigrations pattern, before deletion)
File pathClave/AppState+NostrConnect.swift (matches SharedKeychain+Enumeration.swift precedent)
TargetClave only (NSE never traverses the handshake path)
Access modifiersinternal (no change)

Why no pbxproj edits

Clave/ is a PBXFileSystemSynchronizedRootGroup — the new file is picked up automatically by the Clave target. (Compare to Stage 1 where Shared/ is a regular PBXGroup and required 4 explicit pbxproj entries for RelayUtils.swift.)

Zero behavior change

  • Function bodies preserved byte-for-byte
  • @MainActor on handleDeeplink preserved
  • All Shared/ dependencies (RelayUtils from Stage 1, LightSigner, LightCrypto, LightEvent, SharedKeychain, SharedStorage, DeeplinkRouter, NostrConnectParser, Bech32) work unchanged from the extension
  • pairClientWithProxy(...) call inside handleNostrConnect resolves to self (still on AppState until Stage 3c moves it; the cross-extension call will keep working since both will be extension AppState)

External callers (ClaveApp.swift, HomeView.swift, ConnectSheet.swift) unchanged — they call appState.handleNostrConnect(...) which resolves to the extension method.

Test plan

  • xcodebuild test -skip-testing:ClaveUITests on iPhone 17 / iOS 26.4 against main (pre-baseline): 232 passed / 0 failed
  • Same command on this branch: 232 passed / 0 failed
  • ** TEST SUCCEEDED ** in both runs (no test count change — no tests directly exercise these methods; integration is verified via TF)
  • After merge: separate chore/pbxproj-build-66 PR; archive build 66 + on-device verify nostrconnect pair flow with control client (e.g. Jumble / noStrudel)

Stages 3b + 3c

ProfileFetcher (~300 LOC) and ProxyClient (~400 LOC) follow as separate PRs after this verifies clean.

🤖 Generated with Claude Code

…e 3a)
Stage 3a of the AppState god-object split — the smallest, lowest-risk
of the three Stage 3 sub-extractions (NostrConnect, ProfileFetcher,
ProxyClient). Establishes the extension-with-mutating-state pattern
for the remaining Stage 3 work.
Moves out of Clave/AppState.swift:
- handleDeeplink(url:) — DeeplinkRouter wiring + dispatch to
pendingNostrconnectURI vs pendingDeeplinkAccountChoice based on
account count
- handleNostrConnect(parsedURI:permissions:boundAccountPubkey:) — the
full multi-relay nostrconnect handshake (~143 LOC): parallel connect,
publish-with-retry, listen, dispatch RPCs to LightSigner, fire
pairClientWithProxy on first success
- "// MARK: - Deeplink handling (Task 3.3)" header
Into the new file:
- Clave/AppState+NostrConnect.swift (180 LOC) — extension AppState
What stays in AppState.swift:
- The 3 @observable stored properties (pendingNostrconnectURI,
pendingDeeplinkAccountChoice, deeplinkBoundAccount). Swift forbids
stored properties in extensions. Pointer comment added so readers
can find the handlers.
- The NotificationCenter observer in AppState.init that calls
self.handleDeeplink(url:) — works fine as an extension method call.
Zero behavior change:
- Function bodies preserved byte-for-byte (no logic edits, only file
relocation).
- @mainactor on handleDeeplink preserved.
- All Shared/ dependencies (RelayUtils from Stage 1, LightSigner,
LightCrypto, LightEvent, SharedKeychain, SharedStorage, etc.) work
unchanged from the extension.
- pairClientWithProxy(...) call inside handleNostrConnect resolves to
the extension's `self` (still on AppState until Stage 3c).
External callers unchanged:
- ClaveApp.swift posts .deeplinkReceived to NotificationCenter; the
observer in AppState.init dispatches to handleDeeplink.
- HomeView.swift and ConnectSheet.swift call appState.handleNostrConnect
directly — works as an extension method on AppState.
No pbxproj edits — Clave/ directory is a PBXFileSystemSynchronizedRootGroup,
so the new file is picked up automatically by the Clave target.
Verification:
- xcodebuild test -skip-testing:ClaveUITests on iPhone 17 / iOS 26.4:
- Pre-baseline (main @ 81576e7): 232 passed / 0 failed
- Post-extraction (this branch): 232 passed / 0 failed
- ** TEST SUCCEEDED ** in both runs
AppState.swift: 1,934 -> 1,770 LOC (-164, -8.5%).
Combined Stages 1+2+3a: 2,338 -> 1,770 LOC (-24.3% from original).
Stage 3b (ProfileFetcher) and 3c (ProxyClient) follow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@DocNR
DocNR merged commit 4c507b5 into mainMay 7, 2026
@DocNR
DocNR deleted the refactor/nostr-connect-extension branch May 7, 2026 21:35
DocNR added a commit that referenced this pull request May 7, 2026
…ght (#35)
Internal-only build for on-device verification of the AppState
NostrConnect handler extraction (#34). Bumps CURRENT_PROJECT_VERSION
65 -> 66 across all 4 targets (Clave, ClaveTests, ClaveUITests,
ClaveNSE) in both Debug and Release configs.
Stage 3a verification gates:
- xcodebuild test passes pre/post (232/232; no test count change) ✓
- Build 66 archives + uploads to TestFlight (this PR enables)
- nostrconnect pair + sign on device exercises the moved handlers:
- handleDeeplink (Universal Link / URL scheme entry)
- handleNostrConnect (multi-relay handshake)
Stages 3b (ProfileFetcher) and 3c (ProxyClient) follow.
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