Incoming session verification support (#3428)

* Fixes #1227 - Add support for receiving and interacting with incoming session verification requests.
* Fix a couple of random small warnings
* Move static view config to the view state
* Update snapshots
This commit is contained in:
Stefan Ceriu 2024-10-29 15:21:17 +02:00 committed by GitHub
parent cf5e9fb313
commit d77bb935b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
104 changed files with 995 additions and 483 deletions

View File

@ -666,6 +666,7 @@
915B4CDAF220D9AEB4047D45 /* PollInteractionHandlerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 259E5B05BDE6E20C26CF11B4 /* PollInteractionHandlerProtocol.swift */; }; 915B4CDAF220D9AEB4047D45 /* PollInteractionHandlerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 259E5B05BDE6E20C26CF11B4 /* PollInteractionHandlerProtocol.swift */; };
91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */; }; 91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */; };
91C6AC0E9D2B9C0C76CC6AD4 /* RoomDirectorySearchScreenScreenModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3984C93B8E9B10C92DADF9EE /* RoomDirectorySearchScreenScreenModelProtocol.swift */; }; 91C6AC0E9D2B9C0C76CC6AD4 /* RoomDirectorySearchScreenScreenModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3984C93B8E9B10C92DADF9EE /* RoomDirectorySearchScreenScreenModelProtocol.swift */; };
91D1A46A733EC24C081DD353 /* SessionVerificationRequestDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A1265FAF2C0AF1C30605BE7 /* SessionVerificationRequestDetailsView.swift */; };
92012C96039BC8C2CAEBA9E2 /* AuthenticationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671C338B7259DC5774816885 /* AuthenticationServiceTests.swift */; }; 92012C96039BC8C2CAEBA9E2 /* AuthenticationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671C338B7259DC5774816885 /* AuthenticationServiceTests.swift */; };
9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; }; 9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; };
92720AB0DA9AB5EEF1DAF56B /* SecureBackupLogoutConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DC017C3CB6B0F7C63F460F2 /* SecureBackupLogoutConfirmationScreenViewModel.swift */; }; 92720AB0DA9AB5EEF1DAF56B /* SecureBackupLogoutConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DC017C3CB6B0F7C63F460F2 /* SecureBackupLogoutConfirmationScreenViewModel.swift */; };
@ -1314,6 +1315,7 @@
190EC7285D3CFEF0D3011BCF /* GeoURI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURI.swift; sourceTree = "<group>"; }; 190EC7285D3CFEF0D3011BCF /* GeoURI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURI.swift; sourceTree = "<group>"; };
196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTrackerViewModifier.swift; sourceTree = "<group>"; }; 196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTrackerViewModifier.swift; sourceTree = "<group>"; };
1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderProtocol.swift; sourceTree = "<group>"; }; 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderProtocol.swift; sourceTree = "<group>"; };
1A1265FAF2C0AF1C30605BE7 /* SessionVerificationRequestDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationRequestDetailsView.swift; sourceTree = "<group>"; };
1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = "<group>"; }; 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = "<group>"; };
1A4D29F2683F5772AC72406F /* MapTilerStaticMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStaticMap.swift; sourceTree = "<group>"; }; 1A4D29F2683F5772AC72406F /* MapTilerStaticMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStaticMap.swift; sourceTree = "<group>"; };
1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURITests.swift; sourceTree = "<group>"; }; 1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURITests.swift; sourceTree = "<group>"; };
@ -4650,6 +4652,7 @@
A722D372674EE5687E1A67E4 /* View */ = { A722D372674EE5687E1A67E4 /* View */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
1A1265FAF2C0AF1C30605BE7 /* SessionVerificationRequestDetailsView.swift */,
5FACD034DB52525A3CEF2BDF /* SessionVerificationScreen.swift */, 5FACD034DB52525A3CEF2BDF /* SessionVerificationScreen.swift */,
); );
path = View; path = View;
@ -6904,6 +6907,7 @@
237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */, 237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */,
AE1A73B24D63DA3D63DC4EE3 /* SessionVerificationControllerProxyMock.swift in Sources */, AE1A73B24D63DA3D63DC4EE3 /* SessionVerificationControllerProxyMock.swift in Sources */,
94A65DD8A353DF112EBEF67A /* SessionVerificationControllerProxyProtocol.swift in Sources */, 94A65DD8A353DF112EBEF67A /* SessionVerificationControllerProxyProtocol.swift in Sources */,
91D1A46A733EC24C081DD353 /* SessionVerificationRequestDetailsView.swift in Sources */,
707E49BE07E8EB8A13C0EB1E /* SessionVerificationScreen.swift in Sources */, 707E49BE07E8EB8A13C0EB1E /* SessionVerificationScreen.swift in Sources */,
D02DEB36D32A72A1B365E452 /* SessionVerificationScreenCoordinator.swift in Sources */, D02DEB36D32A72A1B365E452 /* SessionVerificationScreenCoordinator.swift in Sources */,
5710AAB27D5D866292C1FE06 /* SessionVerificationScreenModels.swift in Sources */, 5710AAB27D5D866292C1FE06 /* SessionVerificationScreenModels.swift in Sources */,
@ -7828,7 +7832,7 @@
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift"; repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
requirement = { requirement = {
kind = exactVersion; kind = exactVersion;
version = 1.0.61; version = 1.0.62;
}; };
}; };
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {

View File

@ -149,8 +149,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/matrix-rust-components-swift", "location" : "https://github.com/element-hq/matrix-rust-components-swift",
"state" : { "state" : {
"revision" : "2e6378514e79a648d436e8faeb8cd8106910cf0b", "revision" : "9b26e40ae6c27c56e233577c863569ff074f84fd",
"version" : "1.0.61" "version" : "1.0.62"
} }
}, },
{ {

View File

@ -115,8 +115,9 @@
"banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; "banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later.";
"banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app.";
"banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; "banner_migrate_to_native_sliding_sync_title" = "Upgrade available";
"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; "banner_set_up_recovery_submit" = "Set up recovery";
"banner.set_up_recovery.title" = "Set up recovery"; "banner.set_up_recovery.content" = "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices.";
"banner.set_up_recovery.title" = "Set up recovery to protect your account";
"common_about" = "About"; "common_about" = "About";
"common_acceptable_use_policy" = "Acceptable use policy"; "common_acceptable_use_policy" = "Acceptable use policy";
"common_advanced_settings" = "Advanced settings"; "common_advanced_settings" = "Advanced settings";
@ -454,7 +455,7 @@
"screen_change_server_form_notice" = "You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$@"; "screen_change_server_form_notice" = "You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$@";
"screen_change_server_subtitle" = "What is the address of your server?"; "screen_change_server_subtitle" = "What is the address of your server?";
"screen_change_server_title" = "Select your server"; "screen_change_server_title" = "Select your server";
"screen_chat_backup_key_backup_action_disable" = "Turn off backup"; "screen_chat_backup_key_backup_action_disable" = "Delete key storage";
"screen_chat_backup_key_backup_action_enable" = "Turn on backup"; "screen_chat_backup_key_backup_action_enable" = "Turn on backup";
"screen_chat_backup_key_backup_description" = "Store your cryptographic identity and message keys securely on the server. This will allow you to view your message history on any new devices. %1$@."; "screen_chat_backup_key_backup_description" = "Store your cryptographic identity and message keys securely on the server. This will allow you to view your message history on any new devices. %1$@.";
"screen_chat_backup_key_backup_title" = "Key storage"; "screen_chat_backup_key_backup_title" = "Key storage";
@ -538,10 +539,10 @@
"screen_key_backup_disable_confirmation_action_turn_off" = "Turn off"; "screen_key_backup_disable_confirmation_action_turn_off" = "Turn off";
"screen_key_backup_disable_confirmation_description" = "You will lose your encrypted messages if you are signed out of all devices."; "screen_key_backup_disable_confirmation_description" = "You will lose your encrypted messages if you are signed out of all devices.";
"screen_key_backup_disable_confirmation_title" = "Are you sure you want to turn off backup?"; "screen_key_backup_disable_confirmation_title" = "Are you sure you want to turn off backup?";
"screen_key_backup_disable_description" = "Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:"; "screen_key_backup_disable_description" = "Deleting key storage will remove your cryptographic identity and message keys from the server and turn off the following security features:";
"screen_key_backup_disable_description_point_1" = "Not have encrypted message history on new devices"; "screen_key_backup_disable_description_point_1" = "You will not have encrypted message history on new devices";
"screen_key_backup_disable_description_point_2" = "Lose access to your encrypted messages if you are signed out of %1$@ everywhere"; "screen_key_backup_disable_description_point_2" = "You will lose access to your encrypted messages if you are signed out of %1$@ everywhere";
"screen_key_backup_disable_title" = "Are you sure you want to turn off backup?"; "screen_key_backup_disable_title" = "Are you sure you want to turn off key storage and delete it?";
"screen_login_error_deactivated_account" = "This account has been deactivated."; "screen_login_error_deactivated_account" = "This account has been deactivated.";
"screen_login_error_invalid_credentials" = "Incorrect username and/or password"; "screen_login_error_invalid_credentials" = "Incorrect username and/or password";
"screen_login_error_invalid_user_id" = "This is not a valid user identifier. Expected format: @user:homeserver.org"; "screen_login_error_invalid_user_id" = "This is not a valid user identifier. Expected format: @user:homeserver.org";
@ -652,7 +653,7 @@
"screen_recovery_key_save_title" = "Save your recovery key somewhere safe"; "screen_recovery_key_save_title" = "Save your recovery key somewhere safe";
"screen_recovery_key_setup_confirmation_description" = "You will not be able to access your new recovery key after this step."; "screen_recovery_key_setup_confirmation_description" = "You will not be able to access your new recovery key after this step.";
"screen_recovery_key_setup_confirmation_title" = "Have you saved your recovery key?"; "screen_recovery_key_setup_confirmation_title" = "Have you saved your recovery key?";
"screen_recovery_key_setup_description" = "Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting Change recovery key."; "screen_recovery_key_setup_description" = "Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting Change recovery key.";
"screen_recovery_key_setup_generate_key" = "Generate your recovery key"; "screen_recovery_key_setup_generate_key" = "Generate your recovery key";
"screen_recovery_key_setup_generate_key_description" = "Do not share this with anyone!"; "screen_recovery_key_setup_generate_key_description" = "Do not share this with anyone!";
"screen_recovery_key_setup_success" = "Recovery setup successful"; "screen_recovery_key_setup_success" = "Recovery setup successful";

View File

@ -244,9 +244,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
switch action { switch action {
case .otherDevice: case .otherDevice:
Task { presentSessionVerificationScreen()
await self.presentSessionVerificationScreen()
}
case .recoveryKey: case .recoveryKey:
presentRecoveryKeyScreen() presentRecoveryKeyScreen()
case .skip: case .skip:
@ -263,12 +261,13 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
presentCoordinator(coordinator) presentCoordinator(coordinator)
} }
private func presentSessionVerificationScreen() async { private func presentSessionVerificationScreen() {
guard case let .success(sessionVerificationController) = await userSession.clientProxy.sessionVerificationControllerProxy() else { guard let sessionVerificationController = userSession.clientProxy.sessionVerificationController else {
fatalError("The sessionVerificationController should aways be valid at this point") fatalError("The sessionVerificationController should aways be valid at this point")
} }
let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController) let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController,
flow: .initiator)
let coordinator = SessionVerificationScreenCoordinator(parameters: parameters) let coordinator = SessionVerificationScreenCoordinator(parameters: parameters)

View File

@ -7,6 +7,7 @@
import AVKit import AVKit
import Combine import Combine
import MatrixRustSDK
import SwiftUI import SwiftUI
enum UserSessionFlowCoordinatorAction { enum UserSessionFlowCoordinatorAction {
@ -59,7 +60,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
/// For testing purposes. /// For testing purposes.
var statePublisher: AnyPublisher<UserSessionFlowCoordinatorStateMachine.State, Never> { stateMachine.statePublisher } var statePublisher: AnyPublisher<UserSessionFlowCoordinatorStateMachine.State, Never> { stateMachine.statePublisher }
// swiftlint:disable:next function_body_length
init(userSession: UserSessionProtocol, init(userSession: UserSessionProtocol,
navigationRootCoordinator: NavigationRootCoordinator, navigationRootCoordinator: NavigationRootCoordinator,
appLockService: AppLockServiceProtocol, appLockService: AppLockServiceProtocol,
@ -113,87 +113,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
setupStateMachine() setupStateMachine()
userSession.sessionSecurityStatePublisher setupObservers()
.map(\.verificationState)
.filter { $0 != .unknown }
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self else { return }
attemptStartingOnboarding()
}
.store(in: &cancellables)
settingsFlowCoordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .presentedSettings:
stateMachine.processEvent(.showSettingsScreen)
case .dismissedSettings:
stateMachine.processEvent(.dismissedSettingsScreen)
case .runLogoutFlow:
Task { await self.runLogoutFlow() }
case .clearCache:
actionsSubject.send(.clearCache)
case .forceLogout:
actionsSubject.send(.forceLogout)
}
}
.store(in: &cancellables)
userSession.clientProxy.actionsPublisher
.receive(on: DispatchQueue.main)
.sink { action in
guard case let .receivedDecryptionError(info) = action else {
return
}
let timeToDecryptMs: Int
if let unsignedTimeToDecryptMs = info.timeToDecryptMs {
timeToDecryptMs = Int(unsignedTimeToDecryptMs)
} else {
timeToDecryptMs = -1
}
switch info.cause {
case .unknown:
analytics.trackError(context: nil, domain: .E2EE, name: .UnknownError, timeToDecryptMillis: timeToDecryptMs)
case .unknownDevice:
analytics.trackError(context: nil, domain: .E2EE, name: .ExpectedSentByInsecureDevice, timeToDecryptMillis: timeToDecryptMs)
case .unsignedDevice:
analytics.trackError(context: nil, domain: .E2EE, name: .ExpectedSentByInsecureDevice, timeToDecryptMillis: timeToDecryptMs)
case .verificationViolation:
analytics.trackError(context: nil, domain: .E2EE, name: .ExpectedVerificationViolation, timeToDecryptMillis: timeToDecryptMs)
case .sentBeforeWeJoined:
analytics.trackError(context: nil, domain: .E2EE, name: .ExpectedDueToMembership, timeToDecryptMillis: timeToDecryptMs)
}
}
.store(in: &cancellables)
elementCallService.actions
.receive(on: DispatchQueue.main)
.sink { [weak self] action in
switch action {
case .endCall:
self?.dismissCallScreenIfNeeded()
default:
break
}
}
.store(in: &cancellables)
onboardingFlowCoordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .logout:
logout()
}
}
.store(in: &cancellables)
} }
func start() { func start() {
@ -385,6 +305,129 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
} }
} }
private func setupObservers() {
userSession.sessionSecurityStatePublisher
.map(\.verificationState)
.filter { $0 != .unknown }
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self else { return }
attemptStartingOnboarding()
setupSessionVerificationRequestsObserver()
}
.store(in: &cancellables)
settingsFlowCoordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .presentedSettings:
stateMachine.processEvent(.showSettingsScreen)
case .dismissedSettings:
stateMachine.processEvent(.dismissedSettingsScreen)
case .runLogoutFlow:
Task { await self.runLogoutFlow() }
case .clearCache:
actionsSubject.send(.clearCache)
case .forceLogout:
actionsSubject.send(.forceLogout)
}
}
.store(in: &cancellables)
userSession.clientProxy.actionsPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] action in
guard let self, case let .receivedDecryptionError(info) = action else {
return
}
let timeToDecryptMs: Int
if let unsignedTimeToDecryptMs = info.timeToDecryptMs {
timeToDecryptMs = Int(unsignedTimeToDecryptMs)
} else {
timeToDecryptMs = -1
}
switch info.cause {
case .unknown:
analytics.trackError(context: nil, domain: .E2EE, name: .UnknownError, timeToDecryptMillis: timeToDecryptMs)
case .unknownDevice:
analytics.trackError(context: nil, domain: .E2EE, name: .ExpectedSentByInsecureDevice, timeToDecryptMillis: timeToDecryptMs)
case .unsignedDevice:
analytics.trackError(context: nil, domain: .E2EE, name: .ExpectedSentByInsecureDevice, timeToDecryptMillis: timeToDecryptMs)
case .verificationViolation:
analytics.trackError(context: nil, domain: .E2EE, name: .ExpectedVerificationViolation, timeToDecryptMillis: timeToDecryptMs)
case .sentBeforeWeJoined:
analytics.trackError(context: nil, domain: .E2EE, name: .ExpectedDueToMembership, timeToDecryptMillis: timeToDecryptMs)
}
}
.store(in: &cancellables)
elementCallService.actions
.receive(on: DispatchQueue.main)
.sink { [weak self] action in
switch action {
case .endCall:
self?.dismissCallScreenIfNeeded()
default:
break
}
}
.store(in: &cancellables)
onboardingFlowCoordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .logout:
logout()
}
}
.store(in: &cancellables)
}
private func setupSessionVerificationRequestsObserver() {
userSession.clientProxy.sessionVerificationController?.actions
.receive(on: DispatchQueue.main)
.sink { [weak self] action in
guard let self, case .receivedVerificationRequest(let details) = action else {
return
}
MXLog.info("Received session verification request")
presentSessionVerificationScreen(details: details)
}
.store(in: &cancellables)
}
private func presentSessionVerificationScreen(details: SessionVerificationRequestDetails) {
guard let sessionVerificationController = userSession.clientProxy.sessionVerificationController else {
fatalError("The sessionVerificationController should aways be valid at this point")
}
let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController,
flow: .responder(details: details))
let coordinator = SessionVerificationScreenCoordinator(parameters: parameters)
coordinator.actions
.sink { [weak self] action in
switch action {
case .done:
self?.navigationSplitCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
navigationSplitCoordinator.setSheetCoordinator(coordinator)
}
private func presentHomeScreen() { private func presentHomeScreen() {
let parameters = HomeScreenCoordinatorParameters(userSession: userSession, let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
bugReportService: bugReportService, bugReportService: bugReportService,
@ -569,7 +612,9 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
self?.stateMachine.processEvent(.dismissedStartChatScreen) self?.stateMachine.processEvent(.dismissedStartChatScreen)
} }
} }
// MARK: Session Verification
// MARK: Calls // MARK: Calls
private func presentCallScreen(genericCallLink url: URL) { private func presentCallScreen(genericCallLink url: URL) {

View File

@ -266,6 +266,8 @@ internal enum L10n {
internal static var bannerMigrateToNativeSlidingSyncForceLogoutTitle: String { return L10n.tr("Localizable", "banner_migrate_to_native_sliding_sync_force_logout_title") } internal static var bannerMigrateToNativeSlidingSyncForceLogoutTitle: String { return L10n.tr("Localizable", "banner_migrate_to_native_sliding_sync_force_logout_title") }
/// Upgrade available /// Upgrade available
internal static var bannerMigrateToNativeSlidingSyncTitle: String { return L10n.tr("Localizable", "banner_migrate_to_native_sliding_sync_title") } internal static var bannerMigrateToNativeSlidingSyncTitle: String { return L10n.tr("Localizable", "banner_migrate_to_native_sliding_sync_title") }
/// Set up recovery
internal static var bannerSetUpRecoverySubmit: String { return L10n.tr("Localizable", "banner_set_up_recovery_submit") }
/// About /// About
internal static var commonAbout: String { return L10n.tr("Localizable", "common_about") } internal static var commonAbout: String { return L10n.tr("Localizable", "common_about") }
/// Acceptable use policy /// Acceptable use policy
@ -1009,7 +1011,7 @@ internal enum L10n {
internal static var screenChangeServerSubtitle: String { return L10n.tr("Localizable", "screen_change_server_subtitle") } internal static var screenChangeServerSubtitle: String { return L10n.tr("Localizable", "screen_change_server_subtitle") }
/// Select your server /// Select your server
internal static var screenChangeServerTitle: String { return L10n.tr("Localizable", "screen_change_server_title") } internal static var screenChangeServerTitle: String { return L10n.tr("Localizable", "screen_change_server_title") }
/// Turn off backup /// Delete key storage
internal static var screenChatBackupKeyBackupActionDisable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_disable") } internal static var screenChatBackupKeyBackupActionDisable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_disable") }
/// Turn on backup /// Turn on backup
internal static var screenChatBackupKeyBackupActionEnable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_enable") } internal static var screenChatBackupKeyBackupActionEnable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_enable") }
@ -1258,15 +1260,15 @@ internal enum L10n {
internal static var screenKeyBackupDisableConfirmationDescription: String { return L10n.tr("Localizable", "screen_key_backup_disable_confirmation_description") } internal static var screenKeyBackupDisableConfirmationDescription: String { return L10n.tr("Localizable", "screen_key_backup_disable_confirmation_description") }
/// Are you sure you want to turn off backup? /// Are you sure you want to turn off backup?
internal static var screenKeyBackupDisableConfirmationTitle: String { return L10n.tr("Localizable", "screen_key_backup_disable_confirmation_title") } internal static var screenKeyBackupDisableConfirmationTitle: String { return L10n.tr("Localizable", "screen_key_backup_disable_confirmation_title") }
/// Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will: /// Deleting key storage will remove your cryptographic identity and message keys from the server and turn off the following security features:
internal static var screenKeyBackupDisableDescription: String { return L10n.tr("Localizable", "screen_key_backup_disable_description") } internal static var screenKeyBackupDisableDescription: String { return L10n.tr("Localizable", "screen_key_backup_disable_description") }
/// Not have encrypted message history on new devices /// You will not have encrypted message history on new devices
internal static var screenKeyBackupDisableDescriptionPoint1: String { return L10n.tr("Localizable", "screen_key_backup_disable_description_point_1") } internal static var screenKeyBackupDisableDescriptionPoint1: String { return L10n.tr("Localizable", "screen_key_backup_disable_description_point_1") }
/// Lose access to your encrypted messages if you are signed out of %1$@ everywhere /// You will lose access to your encrypted messages if you are signed out of %1$@ everywhere
internal static func screenKeyBackupDisableDescriptionPoint2(_ p1: Any) -> String { internal static func screenKeyBackupDisableDescriptionPoint2(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_key_backup_disable_description_point_2", String(describing: p1)) return L10n.tr("Localizable", "screen_key_backup_disable_description_point_2", String(describing: p1))
} }
/// Are you sure you want to turn off backup? /// Are you sure you want to turn off key storage and delete it?
internal static var screenKeyBackupDisableTitle: String { return L10n.tr("Localizable", "screen_key_backup_disable_title") } internal static var screenKeyBackupDisableTitle: String { return L10n.tr("Localizable", "screen_key_backup_disable_title") }
/// This account has been deactivated. /// This account has been deactivated.
internal static var screenLoginErrorDeactivatedAccount: String { return L10n.tr("Localizable", "screen_login_error_deactivated_account") } internal static var screenLoginErrorDeactivatedAccount: String { return L10n.tr("Localizable", "screen_login_error_deactivated_account") }
@ -1536,7 +1538,7 @@ internal enum L10n {
internal static var screenRecoveryKeySetupConfirmationDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_confirmation_description") } internal static var screenRecoveryKeySetupConfirmationDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_confirmation_description") }
/// Have you saved your recovery key? /// Have you saved your recovery key?
internal static var screenRecoveryKeySetupConfirmationTitle: String { return L10n.tr("Localizable", "screen_recovery_key_setup_confirmation_title") } internal static var screenRecoveryKeySetupConfirmationTitle: String { return L10n.tr("Localizable", "screen_recovery_key_setup_confirmation_title") }
/// Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting Change recovery key. /// Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting Change recovery key.
internal static var screenRecoveryKeySetupDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_description") } internal static var screenRecoveryKeySetupDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_description") }
/// Generate your recovery key /// Generate your recovery key
internal static var screenRecoveryKeySetupGenerateKey: String { return L10n.tr("Localizable", "screen_recovery_key_setup_generate_key") } internal static var screenRecoveryKeySetupGenerateKey: String { return L10n.tr("Localizable", "screen_recovery_key_setup_generate_key") }
@ -2499,9 +2501,9 @@ internal enum L10n {
internal enum Banner { internal enum Banner {
internal enum SetUpRecovery { internal enum SetUpRecovery {
/// Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices. /// Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices.
internal static var content: String { return L10n.tr("Localizable", "banner.set_up_recovery.content") } internal static var content: String { return L10n.tr("Localizable", "banner.set_up_recovery.content") }
/// Set up recovery /// Set up recovery to protect your account
internal static var title: String { return L10n.tr("Localizable", "banner.set_up_recovery.title") } internal static var title: String { return L10n.tr("Localizable", "banner.set_up_recovery.title") }
} }
} }

View File

@ -60,7 +60,6 @@ extension ClientProxyMock {
logoutReturnValue = nil logoutReturnValue = nil
searchUsersSearchTermLimitReturnValue = .success(.init(results: [], limited: false)) searchUsersSearchTermLimitReturnValue = .success(.init(results: [], limited: false))
profileForReturnValue = .success(.init(userID: "@a:b.com", displayName: "Some user")) profileForReturnValue = .success(.init(userID: "@a:b.com", displayName: "Some user"))
sessionVerificationControllerProxyReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
ignoreUserReturnValue = .success(()) ignoreUserReturnValue = .success(())
unignoreUserReturnValue = .success(()) unignoreUserReturnValue = .success(())

View File

@ -2210,6 +2210,7 @@ class ClientProxyMock: ClientProxyProtocol {
set(value) { underlyingSecureBackupController = value } set(value) { underlyingSecureBackupController = value }
} }
var underlyingSecureBackupController: SecureBackupControllerProtocol! var underlyingSecureBackupController: SecureBackupControllerProtocol!
var sessionVerificationController: SessionVerificationControllerProxyProtocol?
//MARK: - isOnlyDeviceLeft //MARK: - isOnlyDeviceLeft
@ -3519,70 +3520,6 @@ class ClientProxyMock: ClientProxyProtocol {
return removeUserAvatarReturnValue return removeUserAvatarReturnValue
} }
} }
//MARK: - sessionVerificationControllerProxy
var sessionVerificationControllerProxyUnderlyingCallsCount = 0
var sessionVerificationControllerProxyCallsCount: Int {
get {
if Thread.isMainThread {
return sessionVerificationControllerProxyUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = sessionVerificationControllerProxyUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
sessionVerificationControllerProxyUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
sessionVerificationControllerProxyUnderlyingCallsCount = newValue
}
}
}
}
var sessionVerificationControllerProxyCalled: Bool {
return sessionVerificationControllerProxyCallsCount > 0
}
var sessionVerificationControllerProxyUnderlyingReturnValue: Result<SessionVerificationControllerProxyProtocol, ClientProxyError>!
var sessionVerificationControllerProxyReturnValue: Result<SessionVerificationControllerProxyProtocol, ClientProxyError>! {
get {
if Thread.isMainThread {
return sessionVerificationControllerProxyUnderlyingReturnValue
} else {
var returnValue: Result<SessionVerificationControllerProxyProtocol, ClientProxyError>? = nil
DispatchQueue.main.sync {
returnValue = sessionVerificationControllerProxyUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
sessionVerificationControllerProxyUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
sessionVerificationControllerProxyUnderlyingReturnValue = newValue
}
}
}
}
var sessionVerificationControllerProxyClosure: (() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError>)?
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
sessionVerificationControllerProxyCallsCount += 1
if let sessionVerificationControllerProxyClosure = sessionVerificationControllerProxyClosure {
return await sessionVerificationControllerProxyClosure()
} else {
return sessionVerificationControllerProxyReturnValue
}
}
//MARK: - deactivateAccount //MARK: - deactivateAccount
var deactivateAccountPasswordEraseDataUnderlyingCallsCount = 0 var deactivateAccountPasswordEraseDataUnderlyingCallsCount = 0
@ -13435,12 +13372,146 @@ class SecureBackupControllerMock: SecureBackupControllerProtocol {
} }
} }
class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol { class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol {
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> { var actions: PassthroughSubject<SessionVerificationControllerProxyAction, Never> {
get { return underlyingCallbacks } get { return underlyingActions }
set(value) { underlyingCallbacks = value } set(value) { underlyingActions = value }
} }
var underlyingCallbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never>! var underlyingActions: PassthroughSubject<SessionVerificationControllerProxyAction, Never>!
//MARK: - acknowledgeVerificationRequest
var acknowledgeVerificationRequestDetailsUnderlyingCallsCount = 0
var acknowledgeVerificationRequestDetailsCallsCount: Int {
get {
if Thread.isMainThread {
return acknowledgeVerificationRequestDetailsUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = acknowledgeVerificationRequestDetailsUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
acknowledgeVerificationRequestDetailsUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
acknowledgeVerificationRequestDetailsUnderlyingCallsCount = newValue
}
}
}
}
var acknowledgeVerificationRequestDetailsCalled: Bool {
return acknowledgeVerificationRequestDetailsCallsCount > 0
}
var acknowledgeVerificationRequestDetailsReceivedDetails: SessionVerificationRequestDetails?
var acknowledgeVerificationRequestDetailsReceivedInvocations: [SessionVerificationRequestDetails] = []
var acknowledgeVerificationRequestDetailsUnderlyingReturnValue: Result<Void, SessionVerificationControllerProxyError>!
var acknowledgeVerificationRequestDetailsReturnValue: Result<Void, SessionVerificationControllerProxyError>! {
get {
if Thread.isMainThread {
return acknowledgeVerificationRequestDetailsUnderlyingReturnValue
} else {
var returnValue: Result<Void, SessionVerificationControllerProxyError>? = nil
DispatchQueue.main.sync {
returnValue = acknowledgeVerificationRequestDetailsUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
acknowledgeVerificationRequestDetailsUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
acknowledgeVerificationRequestDetailsUnderlyingReturnValue = newValue
}
}
}
}
var acknowledgeVerificationRequestDetailsClosure: ((SessionVerificationRequestDetails) async -> Result<Void, SessionVerificationControllerProxyError>)?
func acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) async -> Result<Void, SessionVerificationControllerProxyError> {
acknowledgeVerificationRequestDetailsCallsCount += 1
acknowledgeVerificationRequestDetailsReceivedDetails = details
DispatchQueue.main.async {
self.acknowledgeVerificationRequestDetailsReceivedInvocations.append(details)
}
if let acknowledgeVerificationRequestDetailsClosure = acknowledgeVerificationRequestDetailsClosure {
return await acknowledgeVerificationRequestDetailsClosure(details)
} else {
return acknowledgeVerificationRequestDetailsReturnValue
}
}
//MARK: - acceptVerificationRequest
var acceptVerificationRequestUnderlyingCallsCount = 0
var acceptVerificationRequestCallsCount: Int {
get {
if Thread.isMainThread {
return acceptVerificationRequestUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = acceptVerificationRequestUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
acceptVerificationRequestUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
acceptVerificationRequestUnderlyingCallsCount = newValue
}
}
}
}
var acceptVerificationRequestCalled: Bool {
return acceptVerificationRequestCallsCount > 0
}
var acceptVerificationRequestUnderlyingReturnValue: Result<Void, SessionVerificationControllerProxyError>!
var acceptVerificationRequestReturnValue: Result<Void, SessionVerificationControllerProxyError>! {
get {
if Thread.isMainThread {
return acceptVerificationRequestUnderlyingReturnValue
} else {
var returnValue: Result<Void, SessionVerificationControllerProxyError>? = nil
DispatchQueue.main.sync {
returnValue = acceptVerificationRequestUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
acceptVerificationRequestUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
acceptVerificationRequestUnderlyingReturnValue = newValue
}
}
}
}
var acceptVerificationRequestClosure: (() async -> Result<Void, SessionVerificationControllerProxyError>)?
func acceptVerificationRequest() async -> Result<Void, SessionVerificationControllerProxyError> {
acceptVerificationRequestCallsCount += 1
if let acceptVerificationRequestClosure = acceptVerificationRequestClosure {
return await acceptVerificationRequestClosure()
} else {
return acceptVerificationRequestReturnValue
}
}
//MARK: - requestVerification //MARK: - requestVerification
var requestVerificationUnderlyingCallsCount = 0 var requestVerificationUnderlyingCallsCount = 0

View File

@ -16529,6 +16529,92 @@ open class SessionVerificationControllerSDKMock: MatrixRustSDK.SessionVerificati
fileprivate var pointer: UnsafeMutableRawPointer! fileprivate var pointer: UnsafeMutableRawPointer!
//MARK: - acceptVerificationRequest
open var acceptVerificationRequestThrowableError: Error?
var acceptVerificationRequestUnderlyingCallsCount = 0
open var acceptVerificationRequestCallsCount: Int {
get {
if Thread.isMainThread {
return acceptVerificationRequestUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = acceptVerificationRequestUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
acceptVerificationRequestUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
acceptVerificationRequestUnderlyingCallsCount = newValue
}
}
}
}
open var acceptVerificationRequestCalled: Bool {
return acceptVerificationRequestCallsCount > 0
}
open var acceptVerificationRequestClosure: (() async throws -> Void)?
open override func acceptVerificationRequest() async throws {
if let error = acceptVerificationRequestThrowableError {
throw error
}
acceptVerificationRequestCallsCount += 1
try await acceptVerificationRequestClosure?()
}
//MARK: - acknowledgeVerificationRequest
open var acknowledgeVerificationRequestSenderIdFlowIdThrowableError: Error?
var acknowledgeVerificationRequestSenderIdFlowIdUnderlyingCallsCount = 0
open var acknowledgeVerificationRequestSenderIdFlowIdCallsCount: Int {
get {
if Thread.isMainThread {
return acknowledgeVerificationRequestSenderIdFlowIdUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = acknowledgeVerificationRequestSenderIdFlowIdUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
acknowledgeVerificationRequestSenderIdFlowIdUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
acknowledgeVerificationRequestSenderIdFlowIdUnderlyingCallsCount = newValue
}
}
}
}
open var acknowledgeVerificationRequestSenderIdFlowIdCalled: Bool {
return acknowledgeVerificationRequestSenderIdFlowIdCallsCount > 0
}
open var acknowledgeVerificationRequestSenderIdFlowIdReceivedArguments: (senderId: String, flowId: String)?
open var acknowledgeVerificationRequestSenderIdFlowIdReceivedInvocations: [(senderId: String, flowId: String)] = []
open var acknowledgeVerificationRequestSenderIdFlowIdClosure: ((String, String) async throws -> Void)?
open override func acknowledgeVerificationRequest(senderId: String, flowId: String) async throws {
if let error = acknowledgeVerificationRequestSenderIdFlowIdThrowableError {
throw error
}
acknowledgeVerificationRequestSenderIdFlowIdCallsCount += 1
acknowledgeVerificationRequestSenderIdFlowIdReceivedArguments = (senderId: senderId, flowId: flowId)
DispatchQueue.main.async {
self.acknowledgeVerificationRequestSenderIdFlowIdReceivedInvocations.append((senderId: senderId, flowId: flowId))
}
try await acknowledgeVerificationRequestSenderIdFlowIdClosure?(senderId, flowId)
}
//MARK: - approveVerification //MARK: - approveVerification
open var approveVerificationThrowableError: Error? open var approveVerificationThrowableError: Error?
@ -16649,75 +16735,6 @@ open class SessionVerificationControllerSDKMock: MatrixRustSDK.SessionVerificati
try await declineVerificationClosure?() try await declineVerificationClosure?()
} }
//MARK: - isVerified
open var isVerifiedThrowableError: Error?
var isVerifiedUnderlyingCallsCount = 0
open var isVerifiedCallsCount: Int {
get {
if Thread.isMainThread {
return isVerifiedUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = isVerifiedUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
isVerifiedUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
isVerifiedUnderlyingCallsCount = newValue
}
}
}
}
open var isVerifiedCalled: Bool {
return isVerifiedCallsCount > 0
}
var isVerifiedUnderlyingReturnValue: Bool!
open var isVerifiedReturnValue: Bool! {
get {
if Thread.isMainThread {
return isVerifiedUnderlyingReturnValue
} else {
var returnValue: Bool? = nil
DispatchQueue.main.sync {
returnValue = isVerifiedUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
isVerifiedUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
isVerifiedUnderlyingReturnValue = newValue
}
}
}
}
open var isVerifiedClosure: (() async throws -> Bool)?
open override func isVerified() async throws -> Bool {
if let error = isVerifiedThrowableError {
throw error
}
isVerifiedCallsCount += 1
if let isVerifiedClosure = isVerifiedClosure {
return try await isVerifiedClosure()
} else {
return isVerifiedReturnValue
}
}
//MARK: - requestVerification //MARK: - requestVerification
open var requestVerificationThrowableError: Error? open var requestVerificationThrowableError: Error?

View File

@ -16,16 +16,18 @@ extension SessionVerificationControllerProxyMock {
SessionVerificationEmoji(symbol: "🏁", description: "Flag"), SessionVerificationEmoji(symbol: "🏁", description: "Flag"),
SessionVerificationEmoji(symbol: "🌏", description: "Globe")] SessionVerificationEmoji(symbol: "🌏", description: "Globe")]
static func configureMock(callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> = .init(), static func configureMock(actions: PassthroughSubject<SessionVerificationControllerProxyAction, Never> = .init(),
isVerified: Bool = false, isVerified: Bool = false,
requestDelay: Duration = .seconds(1)) -> SessionVerificationControllerProxyMock { requestDelay: Duration = .seconds(1)) -> SessionVerificationControllerProxyMock {
let mock = SessionVerificationControllerProxyMock() let mock = SessionVerificationControllerProxyMock()
mock.underlyingCallbacks = callbacks mock.underlyingActions = actions
mock.acknowledgeVerificationRequestDetailsReturnValue = .success(())
mock.requestVerificationClosure = { [unowned mock] in mock.requestVerificationClosure = { [unowned mock] in
Task.detached { Task.detached {
try await Task.sleep(for: requestDelay) try await Task.sleep(for: requestDelay)
mock.callbacks.send(.acceptedVerificationRequest) mock.actions.send(.acceptedVerificationRequest)
} }
return .success(()) return .success(())
@ -34,11 +36,11 @@ extension SessionVerificationControllerProxyMock {
mock.startSasVerificationClosure = { [unowned mock] in mock.startSasVerificationClosure = { [unowned mock] in
Task.detached { Task.detached {
try await Task.sleep(for: requestDelay) try await Task.sleep(for: requestDelay)
mock.callbacks.send(.startedSasVerification) mock.actions.send(.startedSasVerification)
Task.detached { Task.detached {
try await Task.sleep(for: requestDelay) try await Task.sleep(for: requestDelay)
mock.callbacks.send(.receivedVerificationData(emojis)) mock.actions.send(.receivedVerificationData(emojis))
} }
} }
@ -48,7 +50,7 @@ extension SessionVerificationControllerProxyMock {
mock.approveVerificationClosure = { [unowned mock] in mock.approveVerificationClosure = { [unowned mock] in
Task.detached { Task.detached {
try await Task.sleep(for: requestDelay) try await Task.sleep(for: requestDelay)
mock.callbacks.send(.finished) mock.actions.send(.finished)
} }
return .success(()) return .success(())
@ -57,7 +59,7 @@ extension SessionVerificationControllerProxyMock {
mock.declineVerificationClosure = { [unowned mock] in mock.declineVerificationClosure = { [unowned mock] in
Task.detached { Task.detached {
try await Task.sleep(for: requestDelay) try await Task.sleep(for: requestDelay)
mock.callbacks.send(.cancelled) mock.actions.send(.cancelled)
} }
return .success(()) return .success(())
@ -66,7 +68,7 @@ extension SessionVerificationControllerProxyMock {
mock.cancelVerificationClosure = { [unowned mock] in mock.cancelVerificationClosure = { [unowned mock] in
Task.detached { Task.detached {
try await Task.sleep(for: requestDelay) try await Task.sleep(for: requestDelay)
mock.callbacks.send(.cancelled) mock.actions.send(.cancelled)
} }
return .success(()) return .success(())

View File

@ -184,6 +184,8 @@ enum A11yIdentifiers {
} }
struct SessionVerificationScreen { struct SessionVerificationScreen {
let acceptVerificationRequest = "session_verification-accept_verification_request"
let ignoreVerificationRequest = "session_verification-ignore_verification_request"
let requestVerification = "session_verification-request_verification" let requestVerification = "session_verification-request_verification"
let startSasVerification = "session_verification-start_sas_verification" let startSasVerification = "session_verification-start_sas_verification"
let acceptChallenge = "session_verification-accept_challenge" let acceptChallenge = "session_verification-accept_challenge"

View File

@ -6,14 +6,21 @@
// //
import Combine import Combine
import MatrixRustSDK
import SwiftUI import SwiftUI
enum SessionVerificationScreenCoordinatorAction { enum SessionVerificationScreenCoordinatorAction {
case done case done
} }
enum SessionVerificationScreenFlow {
case initiator
case responder(details: SessionVerificationRequestDetails)
}
struct SessionVerificationScreenCoordinatorParameters { struct SessionVerificationScreenCoordinatorParameters {
let sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol let sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol
let flow: SessionVerificationScreenFlow
} }
final class SessionVerificationScreenCoordinator: CoordinatorProtocol { final class SessionVerificationScreenCoordinator: CoordinatorProtocol {
@ -27,7 +34,8 @@ final class SessionVerificationScreenCoordinator: CoordinatorProtocol {
} }
init(parameters: SessionVerificationScreenCoordinatorParameters) { init(parameters: SessionVerificationScreenCoordinatorParameters) {
viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: parameters.sessionVerificationControllerProxy) viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: parameters.sessionVerificationControllerProxy,
flow: parameters.flow)
} }
// MARK: - Public // MARK: - Public

View File

@ -11,13 +11,61 @@ enum SessionVerificationScreenViewModelAction {
case finished case finished
} }
enum SessionVerificationScreenViewAction {
case acceptVerificationRequest
case ignoreVerificationRequest
case requestVerification
case startSasVerification
case restart
case accept
case decline
case done
}
struct SessionVerificationScreenViewState: BindableState { struct SessionVerificationScreenViewState: BindableState {
var verificationState: SessionVerificationScreenStateMachine.State = .initial let flow: SessionVerificationScreenFlow
var verificationState: SessionVerificationScreenStateMachine.State
var headerImageName: String {
switch verificationState {
case .initial:
return "lock"
case .acceptingVerificationRequest:
return "hourglass"
case .requestingVerification:
return "hourglass"
case .verificationRequestAccepted:
return "face.smiling"
case .startingSasVerification:
return "hourglass"
case .sasVerificationStarted:
return "hourglass"
case .cancelling:
return "hourglass"
case .acceptingChallenge:
return "hourglass"
case .decliningChallenge:
return "hourglass"
case .showingChallenge:
return "face.smiling"
case .verified:
return "checkmark.shield"
case .cancelled:
return "exclamationmark.shield"
}
}
var title: String? { var title: String? {
switch verificationState { switch verificationState {
case .initial: case .initial:
return L10n.screenSessionVerificationOpenExistingSessionTitle switch flow {
case .initiator:
return L10n.screenSessionVerificationOpenExistingSessionTitle
case .responder:
return L10n.screenSessionVerificationRequestTitle
}
case .acceptingVerificationRequest:
return L10n.screenSessionVerificationRequestTitle
case .requestingVerification: case .requestingVerification:
return L10n.screenSessionVerificationWaitingToAcceptTitle return L10n.screenSessionVerificationWaitingToAcceptTitle
case .verificationRequestAccepted: case .verificationRequestAccepted:
@ -31,13 +79,13 @@ struct SessionVerificationScreenViewState: BindableState {
case .acceptingChallenge: case .acceptingChallenge:
return L10n.screenSessionVerificationCompareEmojisTitle return L10n.screenSessionVerificationCompareEmojisTitle
case .decliningChallenge: case .decliningChallenge:
return nil return L10n.screenSessionVerificationCompareEmojisTitle
case .verified: case .verified:
return L10n.commonVerificationComplete return L10n.commonVerificationComplete
case .cancelling: case .cancelling:
return nil return nil
case .cancelled: case .cancelled:
return L10n.commonVerificationCancelled return L10n.commonVerificationFailed
} }
} }
@ -48,7 +96,14 @@ struct SessionVerificationScreenViewState: BindableState {
var message: String { var message: String {
switch verificationState { switch verificationState {
case .initial: case .initial:
return L10n.screenSessionVerificationOpenExistingSessionSubtitle switch flow {
case .initiator:
return L10n.screenSessionVerificationOpenExistingSessionSubtitle
case .responder:
return L10n.screenSessionVerificationRequestSubtitle
}
case .acceptingVerificationRequest:
return L10n.screenSessionVerificationRequestSubtitle
case .requestingVerification: case .requestingVerification:
return L10n.screenSessionVerificationWaitingToAcceptSubtitle return L10n.screenSessionVerificationWaitingToAcceptSubtitle
case .verificationRequestAccepted: case .verificationRequestAccepted:
@ -60,7 +115,7 @@ struct SessionVerificationScreenViewState: BindableState {
case .acceptingChallenge: case .acceptingChallenge:
return L10n.screenSessionVerificationCompareEmojisSubtitle return L10n.screenSessionVerificationCompareEmojisSubtitle
case .decliningChallenge: case .decliningChallenge:
return L10n.commonWaiting return L10n.screenSessionVerificationCompareEmojisSubtitle
case .cancelling: case .cancelling:
return L10n.commonWaiting return L10n.commonWaiting
case .showingChallenge: case .showingChallenge:
@ -68,15 +123,7 @@ struct SessionVerificationScreenViewState: BindableState {
case .verified: case .verified:
return L10n.screenSessionVerificationCompleteSubtitle return L10n.screenSessionVerificationCompleteSubtitle
case .cancelled: case .cancelled:
return L10n.screenSessionVerificationCancelledSubtitle return L10n.screenSessionVerificationFailedSubtitle
} }
} }
} }
enum SessionVerificationScreenViewAction {
case requestVerification
case startSasVerification
case restart
case accept
case decline
}

View File

@ -13,6 +13,8 @@ class SessionVerificationScreenStateMachine {
enum State: StateType { enum State: StateType {
/// The initial state, before verification started /// The initial state, before verification started
case initial case initial
/// Accepting the remote verification request
case acceptingVerificationRequest
/// Waiting for verification acceptance /// Waiting for verification acceptance
case requestingVerification case requestingVerification
/// Verification request accepted. Waiting for start /// Verification request accepted. Waiting for start
@ -37,6 +39,8 @@ class SessionVerificationScreenStateMachine {
/// Events that can be triggered on the SessionVerification state machine /// Events that can be triggered on the SessionVerification state machine
enum Event: EventType { enum Event: EventType {
/// Accept the remote verification request
case acceptVerificationRequest
/// Request verification /// Request verification
case requestVerification case requestVerification
/// The current verification request has been accepted /// The current verification request has been accepted
@ -69,16 +73,23 @@ class SessionVerificationScreenStateMachine {
stateMachine.state stateMachine.state
} }
init() { init(state: State) {
stateMachine = StateMachine(state: .initial) stateMachine = StateMachine(state: state)
configure() configure()
} }
private func configure() { private func configure() {
stateMachine.addRoutes(event: .acceptVerificationRequest, transitions: [.initial => .acceptingVerificationRequest])
stateMachine.addRoutes(event: .requestVerification, transitions: [.initial => .requestingVerification]) stateMachine.addRoutes(event: .requestVerification, transitions: [.initial => .requestingVerification])
stateMachine.addRoutes(event: .didAcceptVerificationRequest, transitions: [.requestingVerification => .verificationRequestAccepted])
stateMachine.addRoutes(event: .didAcceptVerificationRequest, transitions: [.acceptingVerificationRequest => .verificationRequestAccepted,
.requestingVerification => .verificationRequestAccepted])
stateMachine.addRoutes(event: .startSasVerification, transitions: [.verificationRequestAccepted => .startingSasVerification]) stateMachine.addRoutes(event: .startSasVerification, transitions: [.verificationRequestAccepted => .startingSasVerification])
stateMachine.addRoutes(event: .didFail, transitions: [.requestingVerification => .initial])
stateMachine.addRoutes(event: .didFail, transitions: [.requestingVerification => .initial,
.acceptingVerificationRequest => .initial])
stateMachine.addRoutes(event: .restart, transitions: [.cancelled => .initial]) stateMachine.addRoutes(event: .restart, transitions: [.cancelled => .initial])
// Transitions with associated values need to be handled through `addRouteMapping` // Transitions with associated values need to be handled through `addRouteMapping`

View File

@ -12,6 +12,7 @@ typealias SessionVerificationViewModelType = StateStoreViewModel<SessionVerifica
class SessionVerificationScreenViewModel: SessionVerificationViewModelType, SessionVerificationScreenViewModelProtocol { class SessionVerificationScreenViewModel: SessionVerificationViewModelType, SessionVerificationScreenViewModelProtocol {
private let sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol private let sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol
private let flow: SessionVerificationScreenFlow
private var stateMachine: SessionVerificationScreenStateMachine private var stateMachine: SessionVerificationScreenStateMachine
@ -22,21 +23,25 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess
} }
init(sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol, init(sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol,
flow: SessionVerificationScreenFlow,
verificationState: SessionVerificationScreenStateMachine.State = .initial) { verificationState: SessionVerificationScreenStateMachine.State = .initial) {
self.sessionVerificationControllerProxy = sessionVerificationControllerProxy self.sessionVerificationControllerProxy = sessionVerificationControllerProxy
self.flow = flow
stateMachine = SessionVerificationScreenStateMachine() stateMachine = SessionVerificationScreenStateMachine(state: verificationState)
super.init(initialViewState: .init(verificationState: verificationState)) super.init(initialViewState: .init(flow: flow, verificationState: verificationState))
setupStateMachine() setupStateMachine()
sessionVerificationControllerProxy.callbacks sessionVerificationControllerProxy.actions
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] callback in .sink { [weak self] callback in
guard let self else { return } guard let self else { return }
switch callback { switch callback {
case .receivedVerificationRequest:
break // Incoming verification requests are handled on the higher levels
case .acceptedVerificationRequest: case .acceptedVerificationRequest:
self.stateMachine.processEvent(.didAcceptVerificationRequest) self.stateMachine.processEvent(.didAcceptVerificationRequest)
case .startedSasVerification: case .startedSasVerification:
@ -57,10 +62,20 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
if case .responder(let details) = flow {
Task {
await self.sessionVerificationControllerProxy.acknowledgeVerificationRequest(details: details)
}
}
} }
override func process(viewAction: SessionVerificationScreenViewAction) { override func process(viewAction: SessionVerificationScreenViewAction) {
switch viewAction { switch viewAction {
case .acceptVerificationRequest:
stateMachine.processEvent(.acceptVerificationRequest)
case .ignoreVerificationRequest:
actionsSubject.send(.finished)
case .requestVerification: case .requestVerification:
stateMachine.processEvent(.requestVerification) stateMachine.processEvent(.requestVerification)
case .startSasVerification: case .startSasVerification:
@ -71,13 +86,16 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess
stateMachine.processEvent(.acceptChallenge) stateMachine.processEvent(.acceptChallenge)
case .decline: case .decline:
stateMachine.processEvent(.declineChallenge) stateMachine.processEvent(.declineChallenge)
case .done:
actionsSubject.send(.finished)
} }
} }
func stop() { func stop() {
let uncancellableStates: [SessionVerificationScreenStateMachine.State] = [.initial, .verified, .cancelled] switch stateMachine.state {
case .initial, .verified, .cancelled: // non-cancellable states
if !uncancellableStates.contains(stateMachine.state) { return
default:
stateMachine.processEvent(.cancel) stateMachine.processEvent(.cancel)
} }
} }
@ -91,6 +109,8 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess
state.verificationState = context.toState state.verificationState = context.toState
switch (context.fromState, context.event, context.toState) { switch (context.fromState, context.event, context.toState) {
case (.initial, .acceptVerificationRequest, .acceptingVerificationRequest):
acceptVerificationRequest()
case (.initial, .requestVerification, .requestingVerification): case (.initial, .requestVerification, .requestingVerification):
requestVerification() requestVerification()
case (.verificationRequestAccepted, .startSasVerification, .startingSasVerification): case (.verificationRequestAccepted, .startSasVerification, .startingSasVerification):
@ -103,6 +123,10 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess
cancelVerification() cancelVerification()
case (_, _, .verified): case (_, _, .verified):
actionsSubject.send(.finished) actionsSubject.send(.finished)
case (.initial, _, .cancelled):
if case .responder = flow {
actionsSubject.send(.finished)
}
default: default:
break break
} }
@ -113,6 +137,21 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess
} }
} }
private func acceptVerificationRequest() {
Task {
guard case .responder = flow else {
fatalError("Incorrect API usage.")
}
switch await sessionVerificationControllerProxy.acceptVerificationRequest() {
case .success:
stateMachine.processEvent(.didAcceptVerificationRequest)
case .failure:
stateMachine.processEvent(.didFail)
}
}
}
private func requestVerification() { private func requestVerification() {
Task { Task {
switch await sessionVerificationControllerProxy.requestVerification() { switch await sessionVerificationControllerProxy.requestVerification() {

View File

@ -0,0 +1,80 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Compound
import MatrixRustSDK
import SwiftUI
struct SessionVerificationRequestDetailsView: View {
@ScaledMetric private var iconSize = 30.0
private let outerShape = RoundedRectangle(cornerRadius: 8)
let details: SessionVerificationRequestDetails
var body: some View {
VStack(spacing: 24) {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 16) {
CompoundIcon(\.devices)
.frame(width: iconSize, height: iconSize)
.foregroundColor(.compound.iconSecondary)
.padding(6)
.background(.compound.bgSubtleSecondary)
.clipShape(RoundedRectangle(cornerRadius: 8))
Text(details.displayName ?? details.senderID)
.font(.compound.bodyMDSemibold)
.foregroundColor(.compound.textPrimary)
.frame(maxWidth: .infinity, alignment: .leading)
}
HStack(spacing: 40) {
VStack(alignment: .leading, spacing: 0) {
Text(L10n.screenSessionVerificationRequestDetailsTimestamp)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
Text(details.firstSeenDate.formattedMinimal())
.font(.compound.bodyMD)
.foregroundColor(.compound.textPrimary)
}
VStack(alignment: .leading, spacing: 0) {
Text(L10n.commonDeviceId)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
Text(details.deviceID)
.font(.compound.bodyMD)
.foregroundColor(.compound.textPrimary)
}
}
}
.padding(24)
.clipShape(outerShape)
.overlay {
outerShape
.inset(by: 0.25)
.stroke(.compound.borderDisabled)
}
Text(L10n.screenSessionVerificationRequestFooter)
.font(.compound.bodyMDSemibold)
.foregroundColor(.compound.textPrimary)
}
}
}
struct SessionVerificationRequestDetailsView_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
let details = SessionVerificationRequestDetails(senderID: "@bob:matrix.org",
flowID: "123",
deviceID: "CODEMISTAKE",
displayName: "Bob's Element X iOS",
firstSeenDate: .init(timeIntervalSince1970: 0))
SessionVerificationRequestDetailsView(details: details)
}
}

View File

@ -5,6 +5,8 @@
// Please see LICENSE in the repository root for full details. // Please see LICENSE in the repository root for full details.
// //
import Compound
import MatrixRustSDK
import SwiftUI import SwiftUI
struct SessionVerificationScreen: View { struct SessionVerificationScreen: View {
@ -14,7 +16,6 @@ struct SessionVerificationScreen: View {
FullscreenDialog { FullscreenDialog {
VStack(spacing: 32) { VStack(spacing: 32) {
screenHeader screenHeader
Spacer()
mainContent mainContent
} }
} bottomContent: { } bottomContent: {
@ -27,33 +28,6 @@ struct SessionVerificationScreen: View {
// MARK: - Private // MARK: - Private
private var headerImageName: String {
switch context.viewState.verificationState {
case .initial:
return "lock"
case .cancelled:
return "exclamationmark.shield"
case .requestingVerification:
return "hourglass"
case .verificationRequestAccepted:
return "face.smiling"
case .startingSasVerification:
return "hourglass"
case .sasVerificationStarted:
return "hourglass"
case .cancelling:
return "hourglass"
case .acceptingChallenge:
return "hourglass"
case .decliningChallenge:
return "hourglass"
case .showingChallenge:
return "face.smiling"
case .verified:
return "checkmark.shield"
}
}
@ViewBuilder @ViewBuilder
private var screenHeader: some View { private var screenHeader: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@ -61,11 +35,11 @@ struct SessionVerificationScreen: View {
BigIcon(icon: \.lockSolid) BigIcon(icon: \.lockSolid)
.padding(.bottom, 16) .padding(.bottom, 16)
} else { } else {
Image(systemName: headerImageName) Image(systemName: context.viewState.headerImageName)
.bigIcon() .bigIcon()
.padding(.bottom, 16) .padding(.bottom, 16)
} }
Text(context.viewState.title ?? "") Text(context.viewState.title ?? "")
.font(.compound.headingMDBold) .font(.compound.headingMDBold)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@ -83,18 +57,17 @@ struct SessionVerificationScreen: View {
@ViewBuilder @ViewBuilder
private var mainContent: some View { private var mainContent: some View {
switch context.viewState.verificationState { switch context.viewState.verificationState {
case .showingChallenge(let emojis): case .initial:
switch context.viewState.flow {
case .responder(let details):
SessionVerificationRequestDetailsView(details: details)
default:
EmptyView()
}
case .showingChallenge(let emojis), .acceptingChallenge(let emojis), .decliningChallenge(let emojis):
emojisPanel(with: emojis) emojisPanel(with: emojis)
.accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.emojiWrapper) .accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.emojiWrapper)
case .acceptingChallenge(let emojis):
emojisPanel(with: emojis)
.accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.emojiWrapper)
case .requestingVerification:
ProgressView()
.tint(.compound.textSecondary)
.scaleEffect(2)
default: default:
// In All other cases, we just want an empty view
EmptyView() EmptyView()
} }
} }
@ -119,18 +92,41 @@ struct SessionVerificationScreen: View {
private var actionButtons: some View { private var actionButtons: some View {
switch context.viewState.verificationState { switch context.viewState.verificationState {
case .initial: case .initial:
VStack(spacing: 32) { switch context.viewState.flow {
case .initiator:
Button(L10n.actionStartVerification) { Button(L10n.actionStartVerification) {
context.send(viewAction: .requestVerification) context.send(viewAction: .requestVerification)
} }
.buttonStyle(.compound(.primary)) .buttonStyle(.compound(.primary))
.accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.requestVerification) .accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.requestVerification)
case .responder:
VStack(spacing: 16) {
Button(L10n.actionStart) {
context.send(viewAction: .acceptVerificationRequest)
}
.buttonStyle(.compound(.primary))
.accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.acceptVerificationRequest)
Button(L10n.actionIgnore) {
context.send(viewAction: .ignoreVerificationRequest)
}
.buttonStyle(.compound(.plain))
.accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.ignoreVerificationRequest)
}
} }
case .cancelled: case .cancelled:
Button(L10n.actionRetry) { switch context.viewState.flow {
context.send(viewAction: .restart) case .initiator:
Button(L10n.actionRetry) {
context.send(viewAction: .restart)
}
.buttonStyle(.compound(.primary))
case .responder:
Button(L10n.actionDone) {
context.send(viewAction: .done)
}
.buttonStyle(.compound(.primary))
} }
.buttonStyle(.compound(.primary))
case .verificationRequestAccepted: case .verificationRequestAccepted:
Button(L10n.actionStart) { Button(L10n.actionStart) {
@ -150,31 +146,15 @@ struct SessionVerificationScreen: View {
Button(L10n.screenSessionVerificationTheyDontMatch) { Button(L10n.screenSessionVerificationTheyDontMatch) {
context.send(viewAction: .decline) context.send(viewAction: .decline)
} }
.font(.compound.bodyLGSemibold) .buttonStyle(.compound(.plain))
.accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.declineChallenge) .accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.declineChallenge)
} }
case .acceptingChallenge: case .acceptingVerificationRequest, .acceptingChallenge, .decliningChallenge, .requestingVerification:
VStack(spacing: 32) { Button(L10n.screenIdentityWaitingOnOtherDevice) { }
Button { context.send(viewAction: .accept) } label: {
HStack(spacing: 16) {
ProgressView()
.tint(.compound.textOnSolidPrimary)
Text(L10n.screenSessionVerificationTheyMatch)
}
}
.buttonStyle(.compound(.primary)) .buttonStyle(.compound(.primary))
.accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.acceptChallenge)
.disabled(true) .disabled(true)
Button(L10n.screenSessionVerificationTheyDontMatch) {
context.send(viewAction: .decline)
}
.font(.compound.bodyLGSemibold)
.accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.declineChallenge)
.disabled(true)
}
default: default:
EmptyView() EmptyView()
} }
@ -196,27 +176,50 @@ struct SessionVerificationScreen: View {
} }
} }
// MARK: - Previews
struct SessionVerification_Previews: PreviewProvider, TestablePreview { struct SessionVerification_Previews: PreviewProvider, TestablePreview {
static var previews: some View { static var previews: some View {
sessionVerificationScreen(state: .initial) sessionVerificationScreen(state: .initial)
.previewDisplayName("Initial") .previewDisplayName("Initial - Initiator")
let details = SessionVerificationRequestDetails(senderID: "@bob:matrix.org",
flowID: "123",
deviceID: "CODEMISTAKE",
displayName: "Bob's Element X iOS",
firstSeenDate: .init(timeIntervalSince1970: 0))
sessionVerificationScreen(state: .initial, flow: .responder(details: details))
.previewDisplayName("Initial - Responder")
sessionVerificationScreen(state: .acceptingVerificationRequest)
.previewDisplayName("Accepting Verification Request")
sessionVerificationScreen(state: .requestingVerification) sessionVerificationScreen(state: .requestingVerification)
.previewDisplayName("Requesting Verification") .previewDisplayName("Requesting Verification")
sessionVerificationScreen(state: .verificationRequestAccepted) sessionVerificationScreen(state: .verificationRequestAccepted)
.previewDisplayName("Request Accepted") .previewDisplayName("Request Accepted")
sessionVerificationScreen(state: .cancelled)
.previewDisplayName("Cancelled") sessionVerificationScreen(state: .startingSasVerification)
.previewDisplayName("Starting SAS Verification")
sessionVerificationScreen(state: .sasVerificationStarted)
.previewDisplayName("SAS Verification started")
sessionVerificationScreen(state: .showingChallenge(emojis: SessionVerificationControllerProxyMock.emojis)) sessionVerificationScreen(state: .showingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
.previewDisplayName("Showing Challenge") .previewDisplayName("Showing Challenge")
sessionVerificationScreen(state: .acceptingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
.previewDisplayName("Accepting Challenge")
sessionVerificationScreen(state: .decliningChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
.previewDisplayName("Declining Challenge")
sessionVerificationScreen(state: .verified) sessionVerificationScreen(state: .verified)
.previewDisplayName("Verified") .previewDisplayName("Verified")
sessionVerificationScreen(state: .cancelled)
.previewDisplayName("Cancelled")
} }
static func sessionVerificationScreen(state: SessionVerificationScreenStateMachine.State) -> some View { static func sessionVerificationScreen(state: SessionVerificationScreenStateMachine.State,
flow: SessionVerificationScreenFlow = .initiator) -> some View {
let viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: SessionVerificationControllerProxyMock.configureMock(), let viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: SessionVerificationControllerProxyMock.configureMock(),
flow: flow,
verificationState: state) verificationState: state)
return SessionVerificationScreen(context: viewModel.context) return SessionVerificationScreen(context: viewModel.context)

View File

@ -91,6 +91,7 @@ class TimelineInteractionHandler {
} }
} }
// swiftlint:disable:next cyclomatic_complexity
func handleTimelineItemMenuAction(_ action: TimelineItemMenuAction, itemID: TimelineItemIdentifier) { func handleTimelineItemMenuAction(_ action: TimelineItemMenuAction, itemID: TimelineItemIdentifier) {
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID), guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID),
let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else {

View File

@ -50,6 +50,8 @@ class ClientProxy: ClientProxyProtocol {
let secureBackupController: SecureBackupControllerProtocol let secureBackupController: SecureBackupControllerProtocol
private(set) var sessionVerificationController: SessionVerificationControllerProxyProtocol?
private static var roomCreationPowerLevelOverrides: PowerLevels { private static var roomCreationPowerLevelOverrides: PowerLevels {
.init(usersDefault: nil, .init(usersDefault: nil,
eventsDefault: nil, eventsDefault: nil,
@ -157,7 +159,16 @@ class ClientProxy: ClientProxyProtocol {
updateVerificationState(client.encryption().verificationState()) updateVerificationState(client.encryption().verificationState())
verificationStateListenerTaskHandle = client.encryption().verificationStateListener(listener: VerificationStateListenerProxy { [weak self] verificationState in verificationStateListenerTaskHandle = client.encryption().verificationStateListener(listener: VerificationStateListenerProxy { [weak self] verificationState in
self?.updateVerificationState(verificationState) guard let self else { return }
updateVerificationState(verificationState)
// The session verification controller requires the user's identity which
// isn't available before a keys query response. Use the verification
// state updates as an aproximation for when that happens.
Task {
await self.buildSessionVerificationControllerProxyIfPossible(verificationState: verificationState)
}
}) })
sendQueueListenerTaskHandle = client.subscribeToSendQueueStatus(listener: SendQueueRoomErrorListenerProxy { [weak self] roomID, error in sendQueueListenerTaskHandle = client.subscribeToSendQueueStatus(listener: SendQueueRoomErrorListenerProxy { [weak self] roomID, error in
@ -484,7 +495,8 @@ class ClientProxy: ClientProxyProtocol {
func roomPreviewForIdentifier(_ identifier: String, via: [String]) async -> Result<RoomPreviewDetails, ClientProxyError> { func roomPreviewForIdentifier(_ identifier: String, via: [String]) async -> Result<RoomPreviewDetails, ClientProxyError> {
do { do {
let roomPreview = try await client.getRoomPreviewFromRoomId(roomId: identifier, viaServers: via) let roomPreview = try await client.getRoomPreviewFromRoomId(roomId: identifier, viaServers: via)
return .success(.init(roomPreview)) let roomPreviewInfo = try roomPreview.info()
return .success(.init(roomPreviewInfo))
} catch let error as ClientError where error.code == .forbidden { } catch let error as ClientError where error.code == .forbidden {
return .failure(.roomPreviewIsPrivate) return .failure(.roomPreviewIsPrivate)
} catch { } catch {
@ -561,16 +573,6 @@ class ClientProxy: ClientProxyProtocol {
} }
} }
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
do {
let sessionVerificationController = try await client.getSessionVerificationController()
return .success(SessionVerificationControllerProxy(sessionVerificationController: sessionVerificationController))
} catch {
MXLog.error("Failed retrieving session verification controller proxy with error: \(error)")
return .failure(.sdkError(error))
}
}
func logout() async -> URL? { func logout() async -> URL? {
do { do {
return try await client.logout().flatMap(URL.init(string:)) return try await client.logout().flatMap(URL.init(string:))
@ -728,6 +730,19 @@ class ClientProxy: ClientProxyProtocol {
verificationStateSubject.send(verificationState) verificationStateSubject.send(verificationState)
} }
private func buildSessionVerificationControllerProxyIfPossible(verificationState: VerificationState) async {
guard sessionVerificationController == nil, verificationState != .unknown else {
return
}
do {
let sessionVerificationController = try await client.getSessionVerificationController()
self.sessionVerificationController = SessionVerificationControllerProxy(sessionVerificationController: sessionVerificationController)
} catch {
MXLog.error("Failed retrieving session verification controller proxy with error: \(error)")
}
}
private func loadUserAvatarURLFromCache() { private func loadUserAvatarURLFromCache() {
loadCachedAvatarURLTask = Task { loadCachedAvatarURLTask = Task {
@ -1079,17 +1094,17 @@ private class SendQueueRoomErrorListenerProxy: SendQueueRoomErrorListener {
} }
private extension RoomPreviewDetails { private extension RoomPreviewDetails {
init(_ roomPreview: RoomPreview) { init(_ roomPreviewInfo: RoomPreviewInfo) {
self = RoomPreviewDetails(roomID: roomPreview.roomId, self = RoomPreviewDetails(roomID: roomPreviewInfo.roomId,
name: roomPreview.name, name: roomPreviewInfo.name,
canonicalAlias: roomPreview.canonicalAlias, canonicalAlias: roomPreviewInfo.canonicalAlias,
topic: roomPreview.topic, topic: roomPreviewInfo.topic,
avatarURL: roomPreview.avatarUrl.flatMap(URL.init(string:)), avatarURL: roomPreviewInfo.avatarUrl.flatMap(URL.init(string:)),
memberCount: UInt(roomPreview.numJoinedMembers), memberCount: UInt(roomPreviewInfo.numJoinedMembers),
isHistoryWorldReadable: roomPreview.isHistoryWorldReadable, isHistoryWorldReadable: roomPreviewInfo.isHistoryWorldReadable,
isJoined: roomPreview.isJoined, isJoined: roomPreviewInfo.membership == .joined,
isInvited: roomPreview.isInvited, isInvited: roomPreviewInfo.membership == .invited,
isPublic: roomPreview.isPublic, isPublic: roomPreviewInfo.joinRule == .public,
canKnock: roomPreview.canKnock) canKnock: roomPreviewInfo.joinRule == .knock)
} }
} }

View File

@ -115,6 +115,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
var secureBackupController: SecureBackupControllerProtocol { get } var secureBackupController: SecureBackupControllerProtocol { get }
var sessionVerificationController: SessionVerificationControllerProxyProtocol? { get }
func isOnlyDeviceLeft() async -> Result<Bool, ClientProxyError> func isOnlyDeviceLeft() async -> Result<Bool, ClientProxyError>
func startSync() func startSync()
@ -155,8 +157,6 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
func setUserAvatar(media: MediaInfo) async -> Result<Void, ClientProxyError> func setUserAvatar(media: MediaInfo) async -> Result<Void, ClientProxyError>
func removeUserAvatar() async -> Result<Void, ClientProxyError> func removeUserAvatar() async -> Result<Void, ClientProxyError>
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError>
func deactivateAccount(password: String?, eraseData: Bool) async -> Result<Void, ClientProxyError> func deactivateAccount(password: String?, eraseData: Bool) async -> Result<Void, ClientProxyError>

View File

@ -246,10 +246,8 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
func provider(_ provider: CXProvider, perform action: CXEndCallAction) { func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
// This gets called for no reason on simulators, where CallKit // This gets called for no reason on simulators, where CallKit
// isn't even supported. Ignore // isn't even supported, ignore it.
return #else
#endif
if let ongoingCallID { if let ongoingCallID {
actionsSubject.send(.endCall(roomID: ongoingCallID.roomID)) actionsSubject.send(.endCall(roomID: ongoingCallID.roomID))
} }
@ -257,6 +255,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
tearDownCallSession(sendEndCallAction: false) tearDownCallSession(sendEndCallAction: false)
action.fulfill() action.fulfill()
#endif
} }
// MARK: - Private // MARK: - Private

View File

@ -18,6 +18,10 @@ private class WeakSessionVerificationControllerProxy: SessionVerificationControl
// MARK: - SessionVerificationControllerDelegate // MARK: - SessionVerificationControllerDelegate
func didReceiveVerificationRequest(details: MatrixRustSDK.SessionVerificationRequestDetails) {
proxy?.didReceiveVerificationRequest(details: details)
}
func didReceiveVerificationData(data: MatrixRustSDK.SessionVerificationData) { func didReceiveVerificationData(data: MatrixRustSDK.SessionVerificationData) {
switch data { switch data {
// We can handle only emojis for now // We can handle only emojis for now
@ -54,86 +58,142 @@ class SessionVerificationControllerProxy: SessionVerificationControllerProxyProt
init(sessionVerificationController: SessionVerificationController) { init(sessionVerificationController: SessionVerificationController) {
self.sessionVerificationController = sessionVerificationController self.sessionVerificationController = sessionVerificationController
sessionVerificationController.setDelegate(delegate: WeakSessionVerificationControllerProxy(proxy: self))
} }
deinit { deinit {
sessionVerificationController.setDelegate(delegate: nil) sessionVerificationController.setDelegate(delegate: nil)
} }
let callbacks = PassthroughSubject<SessionVerificationControllerProxyCallback, Never>() let actions = PassthroughSubject<SessionVerificationControllerProxyAction, Never>()
func acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) async -> Result<Void, SessionVerificationControllerProxyError> {
MXLog.info("Acknowledging verification request")
do {
try await sessionVerificationController.acknowledgeVerificationRequest(senderId: details.senderID, flowId: details.flowID)
return .success(())
} catch {
MXLog.error("Failed requesting session verification with error: \(error)")
return .failure(.failedAcknowledgingVerificationRequest)
}
}
func acceptVerificationRequest() async -> Result<Void, SessionVerificationControllerProxyError> {
MXLog.info("Accepting verification request")
do {
try await sessionVerificationController.acceptVerificationRequest()
return .success(())
} catch {
MXLog.error("Failed requesting session verification with error: \(error)")
return .failure(.failedAcceptingVerificationRequest)
}
}
func requestVerification() async -> Result<Void, SessionVerificationControllerProxyError> { func requestVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
sessionVerificationController.setDelegate(delegate: WeakSessionVerificationControllerProxy(proxy: self)) MXLog.info("Requesting session verification")
do { do {
try await sessionVerificationController.requestVerification() try await sessionVerificationController.requestVerification()
return .success(()) return .success(())
} catch { } catch {
MXLog.error("Failed requesting session verification with error: \(error)")
return .failure(.failedRequestingVerification) return .failure(.failedRequestingVerification)
} }
} }
func startSasVerification() async -> Result<Void, SessionVerificationControllerProxyError> { func startSasVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
MXLog.info("Starting SAS verification")
do { do {
try await sessionVerificationController.startSasVerification() try await sessionVerificationController.startSasVerification()
return .success(()) return .success(())
} catch { } catch {
MXLog.error("Failed starting SAS verification with error: \(error)")
return .failure(.failedStartingSasVerification) return .failure(.failedStartingSasVerification)
} }
} }
func approveVerification() async -> Result<Void, SessionVerificationControllerProxyError> { func approveVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
MXLog.info("Approving verification")
do { do {
try await sessionVerificationController.approveVerification() try await sessionVerificationController.approveVerification()
return .success(()) return .success(())
} catch { } catch {
MXLog.error("Failed approving verification with error: \(error)")
return .failure(.failedApprovingVerification) return .failure(.failedApprovingVerification)
} }
} }
func declineVerification() async -> Result<Void, SessionVerificationControllerProxyError> { func declineVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
MXLog.info("Declining verification")
do { do {
try await sessionVerificationController.declineVerification() try await sessionVerificationController.declineVerification()
return .success(()) return .success(())
} catch { } catch {
MXLog.error("Failed declining verification with error: \(error)")
return .failure(.failedDecliningVerification) return .failure(.failedDecliningVerification)
} }
} }
func cancelVerification() async -> Result<Void, SessionVerificationControllerProxyError> { func cancelVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
MXLog.info("Cancelling verification")
do { do {
try await sessionVerificationController.cancelVerification() try await sessionVerificationController.cancelVerification()
return .success(()) return .success(())
} catch { } catch {
MXLog.error("Failed cancelling verification with error: \(error)")
return .failure(.failedCancellingVerification) return .failure(.failedCancellingVerification)
} }
} }
// MARK: - Private // MARK: - Private
fileprivate func didReceiveVerificationRequest(details: MatrixRustSDK.SessionVerificationRequestDetails) {
MXLog.info("Received verification request \(details)")
let details = SessionVerificationRequestDetails(senderID: details.senderId,
flowID: details.flowId,
deviceID: details.deviceId,
displayName: details.displayName,
firstSeenDate: Date(timeIntervalSince1970: TimeInterval(details.firstSeenTimestamp / 1000)))
actions.send(.receivedVerificationRequest(details: details))
}
fileprivate func didAcceptVerificationRequest() { fileprivate func didAcceptVerificationRequest() {
callbacks.send(.acceptedVerificationRequest) MXLog.info("Accepted verification request")
actions.send(.acceptedVerificationRequest)
} }
fileprivate func didStartSasVerification() { fileprivate func didStartSasVerification() {
callbacks.send(.startedSasVerification) MXLog.info("Started SAS verification")
actions.send(.startedSasVerification)
} }
fileprivate func didReceiveData(_ data: [MatrixRustSDK.SessionVerificationEmoji]) { fileprivate func didReceiveData(_ data: [MatrixRustSDK.SessionVerificationEmoji]) {
callbacks.send(.receivedVerificationData(data.map { emoji in MXLog.info("Received verification data")
actions.send(.receivedVerificationData(data.map { emoji in
SessionVerificationEmoji(symbol: emoji.symbol(), description: emoji.description()) SessionVerificationEmoji(symbol: emoji.symbol(), description: emoji.description())
})) }))
} }
fileprivate func didFail() { fileprivate func didFail() {
callbacks.send(.failed) actions.send(.failed)
} }
fileprivate func didFinish() { fileprivate func didFinish() {
callbacks.send(.finished) actions.send(.finished)
} }
fileprivate func didCancel() { fileprivate func didCancel() {
callbacks.send(.cancelled) actions.send(.cancelled)
} }
} }

View File

@ -7,8 +7,11 @@
import Combine import Combine
import Foundation import Foundation
import MatrixRustSDK
enum SessionVerificationControllerProxyError: Error { enum SessionVerificationControllerProxyError: Error {
case failedAcknowledgingVerificationRequest
case failedAcceptingVerificationRequest
case failedRequestingVerification case failedRequestingVerification
case failedStartingSasVerification case failedStartingSasVerification
case failedApprovingVerification case failedApprovingVerification
@ -16,7 +19,8 @@ enum SessionVerificationControllerProxyError: Error {
case failedCancellingVerification case failedCancellingVerification
} }
enum SessionVerificationControllerProxyCallback { enum SessionVerificationControllerProxyAction {
case receivedVerificationRequest(details: SessionVerificationRequestDetails)
case acceptedVerificationRequest case acceptedVerificationRequest
case startedSasVerification case startedSasVerification
case receivedVerificationData([SessionVerificationEmoji]) case receivedVerificationData([SessionVerificationEmoji])
@ -25,6 +29,14 @@ enum SessionVerificationControllerProxyCallback {
case failed case failed
} }
struct SessionVerificationRequestDetails {
let senderID: String
let flowID: String
let deviceID: String
let displayName: String?
let firstSeenDate: Date
}
struct SessionVerificationEmoji: Hashable { struct SessionVerificationEmoji: Hashable {
let symbol: String let symbol: String
let description: String let description: String
@ -36,7 +48,11 @@ struct SessionVerificationEmoji: Hashable {
// sourcery: AutoMockable // sourcery: AutoMockable
protocol SessionVerificationControllerProxyProtocol { protocol SessionVerificationControllerProxyProtocol {
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> { get } var actions: PassthroughSubject<SessionVerificationControllerProxyAction, Never> { get }
func acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) async -> Result<Void, SessionVerificationControllerProxyError>
func acceptVerificationRequest() async -> Result<Void, SessionVerificationControllerProxyError>
func requestVerification() async -> Result<Void, SessionVerificationControllerProxyError> func requestVerification() async -> Result<Void, SessionVerificationControllerProxyError>

View File

@ -519,7 +519,8 @@ class MockScreen: Identifiable {
return navigationStackCoordinator return navigationStackCoordinator
case .sessionVerification: case .sessionVerification:
var sessionVerificationControllerProxy = SessionVerificationControllerProxyMock.configureMock(requestDelay: .seconds(5)) var sessionVerificationControllerProxy = SessionVerificationControllerProxyMock.configureMock(requestDelay: .seconds(5))
let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationControllerProxy) let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationControllerProxy,
flow: .initiator)
return SessionVerificationScreenCoordinator(parameters: parameters) return SessionVerificationScreenCoordinator(parameters: parameters)
case .userSessionScreen, .userSessionScreenReply: case .userSessionScreen, .userSessionScreenReply:
let appSettings: AppSettings = ServiceLocator.shared.settings let appSettings: AppSettings = ServiceLocator.shared.settings

View File

@ -755,6 +755,12 @@ extension PreviewTests {
} }
} }
func test_sessionVerificationRequestDetailsView() {
for preview in SessionVerificationRequestDetailsView_Previews._allPreviews {
assertSnapshots(matching: preview)
}
}
func test_sessionVerification() { func test_sessionVerification() {
for preview in SessionVerification_Previews._allPreviews { for preview in SessionVerification_Previews._allPreviews {
assertSnapshots(matching: preview) assertSnapshots(matching: preview)

Some files were not shown because too many files have changed in this diff Show More