Migrate a saved login without asking about it

The pre-revamp release reads its unscoped Keychain item with
kSecAttrSynchronizableAny and no identity in the key, so a credential
retained across an Apple Account change is already served to whoever
holds the device. Asking before reusing it prevents nothing that is not
already happening, and it cost every upgrading user a trip into the
instance editor to find a button they had no reason to look for.

What silent adoption could genuinely add is re-filing that secret as
synchronizable under the identity signed in now, which uploads a login
of unproven provenance into that person's iCloud Keychain and spreads it
to their devices. So the copy is written device-local instead, and the
migration runs where the status is computed. The real owner loses
nothing: the item it was copied from syncs, so each of their devices
finds it and adopts its own copy, and adoption still never deletes it.

Two things keep that closed. Turning iCloud sync on rewrites stored
credentials to follow the setting, which would undo the whole point, so
adopted ones are left out of it. And only the account the instance's
legacy login became may inherit the secret — the old item is keyed by
instance, so a second login the user added themselves would otherwise
match it.
This commit is contained in:
Arkadiusz Fal
2026-08-31 23:59:36 +02:00
parent 35b7d313c9
commit 53adfd2d66
7 changed files with 184 additions and 93 deletions

View File

@@ -54,8 +54,6 @@ struct InstanceAccountsSection: View {
} footer: {
Text(String(localized: accounts.isEmpty ? "login.footer.loginBenefits" : "login.footer.loggedIn"))
}
adoptionSection
}
// MARK: - Rows
@@ -114,38 +112,6 @@ struct InstanceAccountsSection: View {
#endif
}
/// The one-tap adoption of a login saved by an earlier version.
///
/// Shown only when such an item exists and has not been adopted. It is a
/// deliberate action because a retained Keychain item carries no proof of
/// which Apple Account it belonged to — the app cannot tell an inherited
/// credential from the user's own.
@ViewBuilder
private var adoptionSection: some View {
let adoptable = accounts.filter { account in
appEnvironment?
.accountCredentialsManager(for: account)?
.hasAdoptableLegacyCredential(for: account) == true
}
if !adoptable.isEmpty {
Section {
ForEach(adoptable) { account in
Button {
adoptLegacyCredential(for: account)
} label: {
Label(String(localized: "accounts.useSavedLogin"), systemImage: "key.horizontal")
}
#if os(tvOS)
.buttonStyle(TVSettingsButtonStyle())
#endif
}
} footer: {
Text(String(localized: "accounts.useSavedLogin.footer"))
}
}
}
// MARK: - Presentation
private func displayName(for account: Account) -> String {
@@ -198,18 +164,4 @@ struct InstanceAccountsSection: View {
appEnvironment.instancesManager.setUsesAccountLogin(false, for: instance)
}
}
private func adoptLegacyCredential(for account: Account) {
guard let appEnvironment,
let credentials = appEnvironment.accountCredentialsManager(for: account)
else { return }
if credentials.adoptLegacyCredential(for: account) {
appEnvironment.accountsManager.setStatus(.signedIn, for: account.id)
} else {
appEnvironment.toastManager.showError(
String(localized: "accounts.useSavedLogin.failed")
)
}
}
}