Round three of review.
The subscription picker is fed by SubscriptionAccountValidator, which still
decided availability from the instance-keyed credential managers. A login
created after the revamp writes only an account-scoped key, so it never
appeared in the picker at all - which meant the account resolver added last
round could not even be reached. Availability now comes from AccountsManager
and account status.
SubscriptionFeedCache dispatched on the legacy setting and picked its
instance from it, so an account waiting on its credential produced
subscriptions from local data while the feed reported notAuthenticated, and
deleting the selected account could leave the route pointing at its server.
Both the route and the credential now come from the effective context, with
the legacy lookup kept only as a fallback for state that predates accounts.
Foreground activation re-resolved the identity scope but only recomputed
statuses when the scope actually moved. An iCloud Keychain credential
arriving after launch does not move it, so the account stayed
credentialMissing and reads stayed on the local fallback until relaunch.
Statuses are now recomputed on every activation in both scene-phase
branches.
Providers are bound to an account but never told it anything: a revoked SID
left the account signedIn, so the validator kept treating it as valid and
neither the banner nor the read fallback engaged. Server calls now record
their outcome. Conservatively - a raw 401 marks the credential rejected only
when no reverse-proxy basic auth is configured, since otherwise the 401 may
have come from the proxy. The two-stage probe that disambiguates the rest is
Phase 2; until then an ambiguous failure leaves the status alone rather than
wrongly telling the user to sign in again.
SubscriptionService held one provider per backend and each resolved its
instance from settingsManager.subscriptionAccount, falling back to "the
first enabled Invidious instance" when that was unset. With several logins
possible on one instance that model breaks twice: two accounts would share
a single in-memory subscription cache, and the fallback could send an
account's request to a server it holds no credential for.
Providers are now built per account and cached by account ID, and each
resolves its instance and credential from the account it was built for -
no fallback. The protocol's accountType becomes a DataContext, which is
what actually identifies a provider now.
Views that branched on "is the subscription account local" read the context
instead, and the settings picker writes both stores. Keeping the legacy
setting in step is deliberate for now: several views still read it, and
letting the two diverge would show one account in the UI while serving
another's data. The Phase 3 switcher removes the last readers.
Signing out discards the account's provider so its cached subscriptions
cannot be served after the account is gone.
Yattee 2 could hold at most one login per instance: a Keychain item keyed by
the instance UUID and a Bool on the instance. This adds the account layer
the rest of the revamp builds on.
DataContext (.local or .account) replaces SubscriptionAccount as the thing
that selects a data set. The legacy value maps onto it as an identity
mapping, because migrated accounts deliberately reuse their instance UUID.
Contexts carry an app-managed changedAt, and a migrated one is stamped at
the epoch so any explicit choice on any device outranks it whatever order
they arrive in.
AccountCapabilities makes what a backend supports a single switch in
InstanceType.accountCapabilities, verified against each API surface -
Invidious has history and playlist privacy, Piped has neither. Call sites
ask for capabilities instead of switching on instance type, which is the
seam the planned per-user Yattee Server accounts land on.
AccountsManager owns accounts and the selected context, stored per identity
scope so a switch of Apple Account cannot show or upload the previous one's
list. Legacy single-logins become accounts through a repeatable
reconciliation rather than a one-shot at launch, because instances arrive
over iCloud whenever they arrive; a permanent marker set is what makes
re-running safe, since usesAccountLogin stays true on the instance forever
and would otherwise recreate a deliberately deleted account. A dirty-flag
outbox records what still needs uploading so an offline change survives a
crash. The legacy subscriptionAccount selection is migrated one way,
reading local defaults first and then the raw KVS value - the previous
release published that key to iCloud but never read it back, so on a fresh
install it exists only there.
DataContextValidator replaces SubscriptionAccountValidator, which corrected
any selection it could not immediately verify. That was safe when a context
was just "an instance I am logged into", but with credentials arriving
asynchronously it would discard the user's choice during a slow Keychain
sync. Validity is now tri-state: anything that might still resolve keeps
the selection and falls back to local for reads only, with server mutations
disabled rather than silently retargeted at local data; only an actual
deletion corrects the stored context.
EditSourceView's single log in/out pair becomes a list of accounts with
per-row status, sign-in-again and sign-out-and-remove, plus the explicit
"use saved login from this device" action - the one-tap adoption that
replaced automatic adoption after the Keychain provenance measurement. On
tvOS a credential-less account offers a local sign-in instead, since Apple
never syncs app Keychain items there and the state will not resolve on its
own.
InstanceLoginView now returns the identifier alongside the credential so
the caller can file it in the Keychain beside the secret, keeping login
identifiers out of synced metadata.
When an Invidious/Piped account is active, several views read the local
SwiftData subscription store instead of the account, so counts
contradicted each other (Home tile said 10 while the Channels list
showed the server's 2):
- Home Channels tile now uses the provider's count via new
SubscriptionService.cachedSubscriptionCount (fetched once in the
background when the server cache isn't populated yet)
- Export footer and export content use the active account's list;
export runs async with a spinner and surfaces fetch errors as a toast
- CSV/OPML import routes through SubscriptionService.importSubscriptions,
subscribing on the server for server accounts instead of silently
writing to the invisible local store
- ChannelView subscribe-state is corrected via the provider after the
optimistic local-store read; unused *Sync write helpers removed
- Server-account subscribe/unsubscribe/import now post
subscriptionsDidChange so other views refresh
- New "Delete Local Subscription Data" section in Subscriptions
settings (visible with a server account) clears the local store and
queues CloudKit deletions so iCloud doesn't restore it