refactor: extract PendingApprovalCoordinator (Stage 4b — final stage)#45
Merged
Merged
Conversation
…(Stage 4b) Final stage of the AppState god-object split. Extracts the approve/deny lifecycle, the alert-chain state machine (build 55-60 polish sprint), and the lock-screen action static handler. After this lands, AppState.swift hits the BACKLOG-sketched slim target. Moves out of Clave/AppState.swift (12 methods + nested enum): - dismissActiveAlert, dismissAllActiveAlerts - refreshPendingRequests, purgeStalePendingRequests - setKindOverride - approvePendingRequest, denyPendingRequest, advanceChainPosition (private) - static handlePendingApprovalAction (lock-screen action handler) - static performApprove, performDeny, performRemovePending (private) - enum ApproveOutcome (nested type) Into the new file: - Clave/AppState+PendingApprovalCoordinator.swift (276 LOC) — extension AppState What stays in AppState.swift: - Stored properties: pendingRequests, dismissedAlertRequestIds, processedInChain (Swift constraint) - Computed properties: freshPendingRequests, activeApprovalRequest, pendingApprovalQueueDepth, chainPosition, chainTotal — kept on the class defensively (Stage 3b precedent for @Observable-bound computeds that drive UI binding) - static let pendingRequestTTLSeconds — read by both class-side computed freshPendingRequests and extension-side purgeStalePendingRequests; static members visible across class+extension boundary so location is just about adjacency to consumers Two AppState.swift access widenings (same pattern as bunkerSecretsTick in Stage 4a): - private var dismissedAlertRequestIds -> var (extension dismissActive* methods need to mutate cross-file) - private(set) var processedInChain -> var (extension dismissAll*, refreshPendingRequests, advanceChainPosition need to mutate) Zero behavior change: - Function bodies preserved byte-for-byte - private modifiers preserved on advanceChainPosition, performRemovePending (only called within extension file) - Static methods relocate; AppState.handlePendingApprovalAction call sites in AppDelegate / NSE-side handler resolve identically since extension static methods are accessible via Type.method() - Cross-extension calls (approvePendingRequest -> Self.performApprove, denyPendingRequest -> Self.performDeny) all stay in same extension file Test impact: zero changes. - AppStatePendingApprovalTests calls appState.<method> via extension dispatch + AppState.<staticMethod> via type dispatch — works unchanged - LockScreenActionRoutingTests calls AppState.handlePendingApprovalAction via static dispatch — works unchanged External callers unchanged: - MainTabView (refresh / purge on scenePhase) - InboxView (approve / deny / dismiss) - PendingRequestDetailView (approve / deny / setKindOverride) - HomeView (dismiss handlers, root alert binding) - AppDelegate (lock-screen action handler -> static handlePendingApprovalAction) No pbxproj edits — Clave/ directory is auto-synced. Verification: - xcodebuild test -skip-testing:ClaveUITests on iPhone 17 / iOS 26.4: - Pre-baseline (main @ 3b35719): 231 passed / 0 failed - Post-extraction (this branch): 231 passed / 0 failed - ** TEST SUCCEEDED ** in both runs AppState.swift: 611 -> 348 LOC (-263, -43.0%). SPRINT TOTAL: 2,338 -> 348 LOC (-85.1% from sprint start). This completes the AppState god-object refactor sprint: - Stage 1: RelayUtils.swift (78 LOC pure utilities) - Stage 2: legacy migration deletion (-524 LOC dead code) - Stage 3a: AppState+NostrConnect.swift (NIP-46 handshake) - Stage 3b: AppState+ProfileFetcher.swift (kind:0 fetch) - Stage 3c: AppState+ProxyClient.swift (NIP-98 + retry queue) - Petname removal (-108 LOC dead code) - Stage 4a: AppState+AccountManager.swift (account lifecycle) - Stage 4b: AppState+PendingApprovalCoordinator.swift (alert chain) - this PR AppState.swift is now a slim @observable container with init, loadState coordinator, and the @observable storage + computed property surface. 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>
4 tasks
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
🏁 Final stage of the AppState god-object refactor sprint. Extracts the approve/deny lifecycle, the alert-chain state machine (build 55–60 polish sprint), and the lock-screen action static handler. After this lands, AppState.swift hits the BACKLOG-sketched slim target.
Numbers
Clave/AppState.swiftClave/AppState+PendingApprovalCoordinator.swift🎯 Sprint total
AppState.swift: 2,338 → 348 LOC (−85.1%)
AppState.swift is now a slim
@Observablecontainer with init,loadStatecoordinator, and the@Observablestorage + computed property surface.What moved (12 methods + nested enum)
dismissActiveAlert,dismissAllActiveAlertsrefreshPendingRequests,purgeStalePendingRequestssetKindOverrideapprovePendingRequest,denyPendingRequest,advanceChainPosition(private)static handlePendingApprovalAction(lock-screen action handler)static performApprove,performDeny,performRemovePending(private)enum ApproveOutcome(nested type)What stays in AppState.swift
pendingRequests,dismissedAlertRequestIds,processedInChain@Observable-bound UI-driving computeds):freshPendingRequests,activeApprovalRequest,pendingApprovalQueueDepth,chainPosition,chainTotalstatic let pendingRequestTTLSeconds— read by both class-sidefreshPendingRequestsand extension-sidepurgeStalePendingRequests. Static members are visible across class+extension boundary so location is just adjacency to consumers.Two access widenings (same pattern as bunkerSecretsTick in Stage 4a)
private var dismissedAlertRequestIds→var(extension methods need cross-file mutation)private(set) var processedInChain→var(extensionadvanceChainPosition,dismissAllActiveAlerts,refreshPendingRequestsneed cross-file mutation)Zero behavior change
privatepreserved onadvanceChainPosition,performRemovePending(only called within extension file)AppState.handlePendingApprovalActionresolves identically via type dispatch fromAppDelegate/ NSEapprovePendingRequest→Self.performApprove, etc.) all stay within the new extension fileTest impact: zero
AppStatePendingApprovalTestscallsappState.<method>via extension dispatch +AppState.<staticMethod>via type dispatch — works unchangedLockScreenActionRoutingTestscallsAppState.handlePendingApprovalActionvia static dispatch — works unchangedTest plan
3b35719): 231 passed / 0 failed ✅** TEST SUCCEEDED **in both runs (no test count change)"expired"entrySprint complete
This PR completes the AppState god-object refactor. AppState.swift is now a slim coordination layer; all functional concerns live in dedicated extension files:
Shared/RelayUtils.swift(Stage 1)Clave/AppState+NostrConnect.swift(Stage 3a)Clave/AppState+ProfileFetcher.swift(Stage 3b)Clave/AppState+ProxyClient.swift(Stage 3c)Clave/AppState+AccountManager.swift(Stage 4a)Clave/AppState+PendingApprovalCoordinator.swift(Stage 4b — this PR)🤖 Generated with Claude Code