Skip to content

refactor: extract PendingApprovalCoordinator (Stage 4b — final stage)#45

Merged
DocNR merged 1 commit into
mainfrom
refactor/pending-approval-coordinator-extension
May 9, 2026
Merged

refactor: extract PendingApprovalCoordinator (Stage 4b — final stage)#45
DocNR merged 1 commit into
mainfrom
refactor/pending-approval-coordinator-extension

Conversation

@DocNR
Copy link
Copy Markdown
Owner

@DocNR DocNR commented May 9, 2026

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

File Before After Δ
Clave/AppState.swift 611 LOC 348 LOC −263 (−43.0%)
Clave/AppState+PendingApprovalCoordinator.swift 276 LOC +276 (new)

🎯 Sprint total

AppState.swift: 2,338 → 348 LOC (−85.1%)

Stage LOC removed from AppState
Stage 1 (RelayUtils.swift) −78
Stage 2 (legacy migration deletion) −524
Stage 3a (AppState+NostrConnect) −164
Stage 3b (AppState+ProfileFetcher) −287
Stage 3c (AppState+ProxyClient) −561
Petname removal −108
Stage 4a (AppState+AccountManager) −282
Stage 4b (AppState+PendingApprovalCoordinator) −263
Total −1,990 LOC

AppState.swift is now a slim @Observable container with init, loadState coordinator, and the @Observable storage + computed property surface.

What moved (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)

What stays in AppState.swift

  • Stored properties (Swift constraint): pendingRequests, dismissedAlertRequestIds, processedInChain
  • Computed properties (defensive — Stage 3b precedent for @Observable-bound UI-driving computeds): freshPendingRequests, activeApprovalRequest, pendingApprovalQueueDepth, chainPosition, chainTotal
  • static let pendingRequestTTLSeconds — read by both class-side freshPendingRequests and extension-side purgeStalePendingRequests. 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 dismissedAlertRequestIdsvar (extension methods need cross-file mutation)
  • private(set) var processedInChainvar (extension advanceChainPosition, dismissAllActiveAlerts, refreshPendingRequests need cross-file mutation)

Zero behavior change

  • Function bodies preserved byte-for-byte
  • private preserved on advanceChainPosition, performRemovePending (only called within extension file)
  • Static methods relocate; AppState.handlePendingApprovalAction resolves identically via type dispatch from AppDelegate / NSE
  • Cross-extension calls (approvePendingRequestSelf.performApprove, etc.) all stay within the new extension file

Test impact: zero

  • AppStatePendingApprovalTests calls appState.<method> via extension dispatch + AppState.<staticMethod> via type dispatch — works unchanged
  • LockScreenActionRoutingTests calls AppState.handlePendingApprovalAction via static dispatch — works unchanged

Test plan

  • Pre-baseline (main @ 3b35719): 231 passed / 0 failed
  • Post-extraction on this branch: 231 passed / 0 failed
  • ** TEST SUCCEEDED ** in both runs (no test count change)
  • After merge: bump pbxproj 70 → 71. Archive build 71 + on-device verify of the alert-chain UI binding (highest behavior-relevance):
    • Pair a client, sign multiple events back-to-back so a queue forms — verify chain advances "1 of N → 2 of N → 3 of N"
    • Lock-screen approve from notification — verify static handler still works
    • Lock-screen deny (same)
    • Background → foreground → check pending bell badge updates correctly
    • "Not now" dismiss-all — alert chain closes, bell badge stays accurate
    • Wait 5+ minutes with a pending request → TTL purge fires + activity log gets "expired" entry

Sprint 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

…(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>
@DocNR DocNR merged commit 933aeee into main May 9, 2026
@DocNR DocNR deleted the refactor/pending-approval-coordinator-extension branch May 9, 2026 04:05
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 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