Files
yattee/Yattee/Core/AccountRemoval.swift
Arkadiusz Fal 69179c9fe9 Give every login a screen, including the ones with nowhere to go
An account whose source is gone was listed nowhere: `orphanedAccounts` had
no readers, and the credential behind it could not be reached at all —
`AppEnvironment.accountCredentialsManager(for:)` resolves the store from
the account's instance, and an orphan has none. The new accounts screen
lists them and removes them by account ID, asking every account-keyed
store because the backend is no longer knowable. The pre-revamp
instance-keyed item is deliberately left behind: identifying it needs the
instance too.

Deleting a source has always signed out and removed the accounts on it.
Both confirmations now say so, and name them.

The sign-in that files a credential against an account is now one
implementation instead of a private method on the source editor, so the
accounts screen and the new optional sign-in step after adding a source
attach a login exactly as the editor does. Extracted verbatim — the
identity-scope guard, the account re-resolve, the write-then-read-back
and the rollback are what keep a half-written login from looking real.

Signing out from the per-instance section now asks first, and both
sign-out paths run one cascade rather than two copies of it.
2026-09-04 23:35:51 +02:00

43 lines
1.5 KiB
Swift

//
// AccountRemoval.swift
// Yattee
//
// Taking an account off this device by hand.
//
import Foundation
/// The removal cascade for an account whose instance is gone.
///
/// Kept out of `AppEnvironment` so the ordering can be exercised directly: an
/// orphan is exactly the case where the usual lookups return nothing, and a
/// cascade that quietly skips a step leaves a secret behind with no screen left
/// to reach it from.
@MainActor
enum AccountRemoval {
/// Removes an orphaned account and the credential filed under its ID.
///
/// `AppEnvironment.accountCredentialsManager(for:)` resolves the store from
/// the account's instance, and an orphan has none — going through it would
/// return `nil` and delete nothing. Every account-keyed store is asked to
/// drop the ID instead; at most one of them holds anything, and the scoped
/// key needs only the ID. The unscoped legacy item is deliberately left
/// alone: identifying it needs the instance, which is precisely what is
/// missing.
static func removeOrphan(
_ accountID: UUID,
stores: [any AccountCredentialsManager],
accountsManager: AccountsManager,
discardData: (UUID) -> Void
) {
for store in stores {
store.deleteCredential(forAccountID: accountID)
}
accountsManager.removeAccount(accountID)
// Whatever the account had in memory goes with it, or its subscriptions
// and playlists would keep answering for an account nobody has.
discardData(accountID)
}
}