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"