Point the classic app identifiers at HealthChat rather than Element (#1)

ClassicAppManager migrates an existing user's session and encryption keys by
reading the legacy app's app-group container and keychain. Those identifiers
defaulted to Element's own, which this team cannot provision, so loadAccounts()
threw invalidAppGroupIdentifier — caught and logged, never surfaced. Since both
apps share a bundle id, the result would be every existing user silently logged
out and losing local history when they update.

The legacy identifiers belong to HealthChat and are provisionable under the same
team, so repoint them rather than dropping them.

Also stop sending accounts without key backup to a dead end. Upstream shows those
users a sheet asking them to open Element Classic and set up backup first, but this
update replaces that app and nothing registers its URL scheme, so the sheet's only
button does nothing. Sign in directly instead: the user re-verifies the device and
loses only history that by definition has no backup.
This commit is contained in:
2026-07-17 17:11:51 +02:00
committed by Sedat Qaja
parent 9969af6984
commit 1da4c0302e
6 changed files with 47 additions and 47 deletions
@@ -40,30 +40,27 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
canReportProblem = isBugReportServiceEnabled
let isQRCodeScanningSupported = !ProcessInfo.processInfo.isiOSAppOnMac
let isClassicAppAccountAllowed = if let classicAppAccount = authenticationService.classicAppAccount {
appSettings.accountProviders.contains(serverName: classicAppAccount.serverName, orBaseURL: classicAppAccount.homeserverURL)
} else {
false
}
let classicAppAccountProvider = authenticationService.classicAppAccount?.serverName
let isClassicAppAccountAllowed = classicAppAccountProvider.map { appSettings.accountProviders.contains($0) } ?? false
let initialViewState = if !appSettings.allowOtherAccountProviders {
// We don't show the create account button when custom providers are disallowed.
// The assumption here being that if you're running a custom app, your users will already be created.
AuthenticationStartScreenViewState(serverNameOrBaseURL: appSettings.accountProviders.count == 1 ? appSettings.accountProviders[0].serverNameOrBaseURL : nil,
AuthenticationStartScreenViewState(serverName: appSettings.accountProviders.count == 1 ? appSettings.accountProviders[0] : nil,
showCreateAccountButton: false,
showQRCodeLoginButton: isQRCodeScanningSupported,
classicAppMode: isClassicAppAccountAllowed ? authenticationService.classicAppAccount.map { .welcomeBack($0) } : nil,
hideBrandChrome: appSettings.hideBrandChrome)
} else if let provisioningParameters {
// We only show the "Sign in to …" button when using a provisioning link.
AuthenticationStartScreenViewState(serverNameOrBaseURL: provisioningParameters.accountProvider,
AuthenticationStartScreenViewState(serverName: provisioningParameters.accountProvider,
showCreateAccountButton: false,
showQRCodeLoginButton: false,
classicAppMode: nil,
hideBrandChrome: appSettings.hideBrandChrome)
} else {
// The default configuration.
AuthenticationStartScreenViewState(serverNameOrBaseURL: nil,
AuthenticationStartScreenViewState(serverName: nil,
showCreateAccountButton: appSettings.showCreateAccountButton,
showQRCodeLoginButton: isQRCodeScanningSupported,
classicAppMode: authenticationService.classicAppAccount.map { .welcomeBack($0) },
@@ -114,25 +111,26 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
private func login(classicAppAccount: ClassicAppAccount? = nil) async {
if let classicAppAccount {
if classicAppAccount.state.availableSecrets == .requiresBackup {
state.bindings.showClassicAppBackupInstructions = true
} else {
await loginDirectly(using: classicAppAccount.serverName,
loginHint: "mxid:\(classicAppAccount.userID)",
fallbackHomeserverURL: classicAppAccount.homeserverURL)
}
} else if let serverNameOrBaseURL = state.serverNameOrBaseURL {
await loginDirectly(using: serverNameOrBaseURL, loginHint: provisioningParameters?.loginHint)
// HealthChat diverges from upstream here. Upstream sends `.requiresBackup` accounts to a sheet
// asking the user to set up key backup in Element Classic first. HealthChat ships with the same
// bundle id as the legacy app, so this update *replaces* it — there is no Classic app left to
// open, and the sheet's only button resolves to nothing. Fall through to a normal sign-in
// instead: the user re-verifies and loses only history that by definition has no backup.
await configureAccountProvider(classicAppAccount.serverName,
loginHint: "mxid:\(classicAppAccount.userID)",
fallbackHomeserverURL: classicAppAccount.homeserverURL)
} else if let serverName = state.serverName {
await configureAccountProvider(serverName, loginHint: provisioningParameters?.loginHint)
} else {
actionsSubject.send(.login) // No need to configure anything here, continue the flow.
}
}
private func loginDirectly(using serverNameOrBaseURL: String, loginHint: String? = nil, fallbackHomeserverURL: URL? = nil) async {
private func configureAccountProvider(_ accountProvider: String, loginHint: String? = nil, fallbackHomeserverURL: URL? = nil) async {
startLoading()
defer { stopLoading() }
if case .failure = await authenticationService.configure(for: serverNameOrBaseURL, flow: .login) {
if case .failure = await authenticationService.configure(for: accountProvider, flow: .login) {
// Try the fallback URL before showing an error.
if let fallbackHomeserverURL,
case .success = await authenticationService.configure(for: fallbackHomeserverURL.absoluteString, flow: .login) {
@@ -23,6 +23,7 @@
<key>keychain-access-groups</key>
<array>
<string>$(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
<string>$(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
</array>
</dict>
</plist>
+7 -2
View File
@@ -129,12 +129,17 @@ targets:
com.apple.developer.usernotifications.communication: true
com.apple.security.app-sandbox: true
com.apple.security.application-groups:
# HealthChat: dropped $(CLASSIC_APP_GROUP_IDENTIFIER) (group.im.vector) — owned by Element's team, not provisionable here.
# HealthChat: dropped $(CLASSIC_APP_GROUP_IDENTIFIER) — not because it is un-provisionable,
# but because it resolves to group.ch.healthchat, the same value as $(APP_GROUP_IDENTIFIER)
# below. The legacy container is already entitled; listing it twice would duplicate the entry.
- $(APP_GROUP_IDENTIFIER)
com.apple.security.network.client: true
keychain-access-groups:
# HealthChat: dropped $(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER) (im.vector.app.*) — not provisionable here.
- $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
# HealthChat: the legacy app's keychain group. ClassicAppManager reads the account and
# crypto-store keys from here to migrate existing users. Same team prefix, so it is
# provisionable — unlike Element's im.vector.app.* default in app.yml.
- $(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
settings:
base: