diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj
index 0e97c11ec..cf4cfcecd 100644
--- a/ElementX.xcodeproj/project.pbxproj
+++ b/ElementX.xcodeproj/project.pbxproj
@@ -10303,9 +10303,9 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CLASSIC_APP_DEEP_LINK_URL = "element://open";
- CLASSIC_APP_GROUP_IDENTIFIER = group.im.vector;
- CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).im.vector.app.keychain.shared";
- CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "im.vector.app.encryption-manager-service";
+ CLASSIC_APP_GROUP_IDENTIFIER = group.ch.healthchat;
+ CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).ch.healthchat.element.keychain.shared";
+ CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "ch.healthchat.element.encryption-manager-service";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
@@ -10378,9 +10378,9 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CLASSIC_APP_DEEP_LINK_URL = "element://open";
- CLASSIC_APP_GROUP_IDENTIFIER = group.im.vector;
- CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).im.vector.app.keychain.shared";
- CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "im.vector.app.encryption-manager-service";
+ CLASSIC_APP_GROUP_IDENTIFIER = group.ch.healthchat;
+ CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).ch.healthchat.element.keychain.shared";
+ CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "ch.healthchat.element.encryption-manager-service";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
diff --git a/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenViewModel.swift
index 2483296b3..c366b94bc 100644
--- a/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenViewModel.swift
+++ b/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenViewModel.swift
@@ -111,13 +111,14 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
private func login(classicAppAccount: ClassicAppAccount? = nil) async {
if let classicAppAccount {
- if classicAppAccount.state.availableSecrets == .requiresBackup {
- state.bindings.showClassicAppBackupInstructions = true
- } else {
- await configureAccountProvider(classicAppAccount.serverName,
- loginHint: "mxid:\(classicAppAccount.userID)",
- fallbackHomeserverURL: classicAppAccount.homeserverURL)
- }
+ // 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 {
diff --git a/ElementX/SupportingFiles/ElementX.entitlements b/ElementX/SupportingFiles/ElementX.entitlements
index 6bb0cab4f..8758015f2 100644
--- a/ElementX/SupportingFiles/ElementX.entitlements
+++ b/ElementX/SupportingFiles/ElementX.entitlements
@@ -23,6 +23,7 @@
keychain-access-groups
$(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
+ $(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
diff --git a/ElementX/SupportingFiles/target.yml b/ElementX/SupportingFiles/target.yml
index aa9fd2f0e..743c845f3 100644
--- a/ElementX/SupportingFiles/target.yml
+++ b/ElementX/SupportingFiles/target.yml
@@ -128,12 +128,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:
diff --git a/UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift b/UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift
index c19270261..9c1551177 100644
--- a/UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift
+++ b/UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift
@@ -238,8 +238,8 @@ final class AuthenticationStartScreenViewModelTests {
}
@Test
- func classicAppAccountRequiresBackup() async throws {
- // Given a view model with a Classic app account that requires backup before signing in.
+ func classicAppAccountRequiresBackupFallsThroughToLogin() async throws {
+ // Given a view model with a Classic app account that has no usable key backup.
let classicAppAccount = makeClassicAppAccount()
await setupViewModel(classicAppAccount: classicAppAccount, availableSecrets: .requiresBackup)
guard case .welcomeBack(let account) = context.viewState.classicAppMode else {
@@ -248,27 +248,15 @@ final class AuthenticationStartScreenViewModelTests {
}
#expect(account.state.availableSecrets == .requiresBackup)
- // When continuing with the Classic account while backup is required.
- var deferred = deferFulfillment(context.observe(\.viewState.bindings.showClassicAppBackupInstructions)) { $0 }
+ // When continuing with the Classic account.
+ let deferred = deferFulfillment(viewModel.actions) { $0.isLoginDirectlyWithOAuth }
context.send(viewAction: .continueWithClassic(classicAppAccount))
- // Then the backup instructions should be shown.
+ // Then the flow continues to a normal sign-in rather than the backup instructions sheet.
+ // HealthChat replaces the Classic app in place, so those instructions could never be followed.
try await deferred.fulfill()
-
- // When the user completes the backup in the Classic app and the app returns to the foreground.
- classicAppManager?.availableSecretsForReturnValue = .complete
- deferred = deferFulfillment(context.observe(\.viewState.bindings.showClassicAppBackupInstructions)) { !$0 }
- notificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil)
-
- // Then the backup instructions sheet should be dismissed.
- try await deferred.fulfill()
-
- // When the user continues with the Classic account again.
- let deferredAction = deferFulfillment(viewModel.actions) { $0.isLoginDirectlyWithOAuth }
- context.send(viewAction: .continueWithClassic(classicAppAccount))
-
- // Then the flow should continue the login process.
- try await deferredAction.fulfill()
+ #expect(context.viewState.bindings.showClassicAppBackupInstructions == false)
+ #expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint == "mxid:\(classicAppAccount.userID)")
}
// MARK: - Helpers
diff --git a/Variants/HealthChat/healthchat.yml b/Variants/HealthChat/healthchat.yml
index c03356763..aabf7145d 100644
--- a/Variants/HealthChat/healthchat.yml
+++ b/Variants/HealthChat/healthchat.yml
@@ -11,3 +11,11 @@ settings:
APP_GROUP_IDENTIFIER: group.ch.healthchat
BASE_BUNDLE_IDENTIFIER: ch.healthchat.element
DEVELOPMENT_TEAM: 6G37K72W82
+
+ # The legacy element-ios HealthChat's identifiers. ClassicAppManager reads the account and
+ # crypto-store keys from these to migrate existing users on an in-place update. Upstream's
+ # defaults in app.yml point at Element's own (group.im.vector / im.vector.app.*), which this
+ # team cannot provision — repoint, never remove. Failure here is silent.
+ CLASSIC_APP_GROUP_IDENTIFIER: group.ch.healthchat
+ CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER: ch.healthchat.element.encryption-manager-service
+ CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER: "$(DEVELOPMENT_TEAM).ch.healthchat.element.keychain.shared"