Skip to content

refactor: extract AccountManager into AppState extension (Stage 4a) - #43

Merged
DocNR merged 1 commit into
mainfrom
refactor/account-manager-extension
May 9, 2026
Merged

refactor: extract AccountManager into AppState extension (Stage 4a)#43
DocNR merged 1 commit into
mainfrom
refactor/account-manager-extension

Conversation

@DocNR

@DocNRDocNR commented May 9, 2026

Copy link
Copy Markdown
Owner

Summary

Stage 4a — penultimate sub-extraction in the AppState god-object split. Moves all account lifecycle + bunker-URI + persistence logic out of AppState.swift into a dedicated extension file. Cleanup follow-up to the petname removal PR (#42) that simplified the surface first.

Numbers

FileBeforeAfterΔ
Clave/AppState.swift893 LOC611 LOC−282 (−31.6%)
Clave/AppState+AccountManager.swift312 LOC+312 (new)

Combined Stages 1+2+3+petname+4a: AppState.swift down from 2,338 → 611 LOC (−73.9%).

What moved (16 methods + 1 MARK)

  • var bunkerURI (computed) + bunkerURI(for:) (per-account)
  • loadAccounts — was private, now internal (called by loadState cross-file)
  • recoverAccountsFromKeychainIfNeeded — Stage 2 deferred decision; moves now
  • cleanupOrphanLegacyKeychainEntry — Stage 2 deferred; moves now
  • persistAccountsListprivate, only inter-extension calls
  • persistAccounts
  • persistCurrentAccountPubkeyprivate, only inter-extension calls
  • switchToAccount(pubkey:)
  • addAccount(nsec:)
  • generateAccount()
  • deleteAccount(pubkey:)
  • rotateBunkerSecret()
  • importKey / generateKey / deleteKey legacy wrappers
  • refreshBunkerSecret() (no-op kept for source-compat)
  • // MARK: - Multi-account methods (Task 5) header

What stays in AppState.swift

  • @Observable stored properties (accounts, currentAccount, bunkerSecretsTick, etc.) — Swift forbids stored properties in extensions. Pointer comment added.
  • init() with NotificationCenter observers
  • loadState() coordinator — calls into the extension via cross-file extension dispatch

One AppState.swift access change

bunkerSecretsTick was private(set) var (file-scope setter). rotateBunkerSecret in the extension needs to mutate it cross-file, so dropped private(set) to internal-set. Same constraint pattern as Stage 3b's persistAccounts widen.

Cross-extension calls all resolve correctly

  • addAccountregisterWithProxy (Stage 3c) + fetchProfileIfNeeded (Stage 3b) ✓
  • deleteAccountunregisterWithProxy + unpairClientWithProxy (Stage 3c) + cachedImageURL (Stage 3b) ✓
  • switchToAccountloadCachedProfileImage + fetchProfileIfNeeded (Stage 3b) ✓

Zero behavior change

  • Function bodies preserved byte-for-byte
  • private preserved on persistAccountsList + persistCurrentAccountPubkey (same-file inter-extension calls only)
  • Modifiers loosened only where required by cross-file calls (loadAccounts, recoverAccountsFromKeychainIfNeeded, cleanupOrphanLegacyKeychainEntry: privateinternal)

Test impact: zero

  • AppStateMultiAccountTests calls appState.<method> via extension dispatch — works unchanged
  • AccountModelTests unaffected (Account struct unchanged from petname PR baseline)

Test plan

  • Pre-baseline on main (f51bc18): 231 passed / 0 failed
  • Post-extraction on branch: 231 passed / 0 failed
  • ** TEST SUCCEEDED ** in both runs (no test count change)
  • After merge: bump pbxproj 69 → 70 (covers sprint 5a + petname removal + this Stage 4a all bundled). Archive build 70 + on-device verify of account flows:
    • Cold launch → loadState exercises moved methods (loadAccounts, recoverAccountsFromKeychainIfNeeded, cleanupOrphanLegacyKeychainEntry)
    • Add a new account → addAccount + cross-extension calls fire
    • Switch accounts → switchToAccount + profile reload
    • Delete an account → ordered cleanup (Bug D fix preserved)
    • Bunker URI display in ConnectBunkerTabView reads appState.bunkerURI cross-file

Stage 4b

PendingApprovalCoordinator (~290 LOC) is the final extraction. After it lands, AppState should reach the BACKLOG-sketched ~300 LOC slim target — ~−87% from the original 2,338.

🤖 Generated with Claude Code

Stage 4a — penultimate sub-extraction in the AppState god-object split.
Moves all account lifecycle + bunker-URI + persistence logic out of
AppState.swift into a dedicated extension file. Cleanup follow-up to
the petname-removal PR (#42) that simplified the surface first.
Moves out of Clave/AppState.swift (16 methods + 1 MARK header):
- var bunkerURI (computed) + bunkerURI(for:) (per-account)
- loadAccounts (was private, now internal — called by loadState)
- recoverAccountsFromKeychainIfNeeded (Stage 2 deferred — moves now)
- cleanupOrphanLegacyKeychainEntry (Stage 2 deferred — moves now)
- persistAccountsList (private, stays private — only inter-extension)
- persistAccounts
- persistCurrentAccountPubkey (private, stays private)
- switchToAccount(pubkey:)
- addAccount(nsec:)
- generateAccount()
- deleteAccount(pubkey:)
- rotateBunkerSecret()
- importKey / generateKey / deleteKey (legacy wrappers preserved)
- refreshBunkerSecret() (no-op kept for source-compat)
- "// MARK: - Multi-account methods (Task 5)" header
Into the new file:
- Clave/AppState+AccountManager.swift (312 LOC) — extension AppState
What stays in AppState.swift:
- @observable stored properties (accounts, currentAccount,
bunkerSecretsTick, etc.) — Swift forbids stored properties in
extensions. Pointer comment added at the multi-account state block.
- init() with NotificationCenter observers
- loadState() coordinator (calls into the extension via cross-file
extension dispatch — works because all are extension AppState on
the same final class in the same module)
One AppState.swift access change required:
- bunkerSecretsTick was `private(set) var` (file-scope setter).
rotateBunkerSecret in the extension needs to mutate it across
files, so dropped `private(set)` to internal-set. Same constraint
pattern as Stage 3b's persistAccounts widen.
Cross-extension calls all resolve correctly:
- addAccount calls registerWithProxy (Stage 3c) + fetchProfileIfNeeded
(Stage 3b) — works
- deleteAccount calls unregisterWithProxy + unpairClientWithProxy
(Stage 3c) + cachedImageURL (Stage 3b) — works
- switchToAccount calls loadCachedProfileImage + fetchProfileIfNeeded
(Stage 3b) — works
Zero behavior change:
- Function bodies preserved byte-for-byte
- All `private` modifiers preserved on persistAccountsList +
persistCurrentAccountPubkey (only called within extension file —
same-file private access)
- Modifiers loosened only where strictly required by cross-file
calls (loadAccounts, recoverAccountsFromKeychainIfNeeded,
cleanupOrphanLegacyKeychainEntry: private -> internal because
loadState in AppState.swift calls them)
Test impact: zero changes.
- AppStateMultiAccountTests calls appState.<method> via extension
dispatch — works unchanged.
External callers unchanged:
- ClaveApp + 7 view files call appState.<method> — extension dispatch.
No pbxproj edits — Clave/ directory is auto-synced.
Verification:
- xcodebuild test -skip-testing:ClaveUITests on iPhone 17 / iOS 26.4:
- Pre-baseline (main @ f51bc18): 231 passed / 0 failed
- Post-extraction (this branch): 231 passed / 0 failed
- ** TEST SUCCEEDED ** in both runs
AppState.swift: 893 -> 611 LOC (-282, -31.6%).
Combined Stages 1+2+3+petname+4a: 2,338 -> 611 LOC (-73.9% from sprint
start).
Stage 4b (PendingApprovalCoordinator, ~290 LOC) is the final
extraction. After it lands, AppState should reach the BACKLOG
sketched ~300 LOC slim target.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@DocNR
DocNR merged commit cdb72f7 into mainMay 9, 2026
@DocNR
DocNR deleted the refactor/account-manager-extension branch May 9, 2026 03:04
DocNR added a commit that referenced this pull request May 9, 2026
Internal-only build bundling three merges since build 69 was tagged:
- Sprint 5a (#41): bunker pair-cap bypass fix
- Petname removal (#42): -108 LOC dead code
- Stage 4a (#43): AccountManager extension, -282 AppState LOC
Bumps CURRENT_PROJECT_VERSION 69 -> 70 across 4 targets x 2 configs.
Cumulative AppState refactor reduction: 2,338 -> 611 LOC (-73.9%).
Stage 4b (PendingApprovalCoordinator) is the final refactor PR.
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