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
Copy link
Copy Markdown
Owner

@DocNR DocNR commented May 7, 2026

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

File Before After Δ
Clave/AppState.swift 1,934 LOC 1,770 LOC −164 (−8.5%)
Clave/AppState+NostrConnect.swift 180 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)

Decision Choice
Type shape extension AppState (matches Stage 2 LegacyMigrations pattern, before deletion)
File path Clave/AppState+NostrConnect.swift (matches SharedKeychain+Enumeration.swift precedent)
Target Clave only (NSE never traverses the handshake path)
Access modifiers internal (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 main May 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 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.

1 participant