mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
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:
@@ -244,9 +244,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
switch action {
|
||||
case .otherDevice:
|
||||
Task {
|
||||
await self.presentSessionVerificationScreen()
|
||||
}
|
||||
presentSessionVerificationScreen()
|
||||
case .recoveryKey:
|
||||
presentRecoveryKeyScreen()
|
||||
case .skip:
|
||||
@@ -263,12 +261,13 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
|
||||
presentCoordinator(coordinator)
|
||||
}
|
||||
|
||||
private func presentSessionVerificationScreen() async {
|
||||
guard case let .success(sessionVerificationController) = await userSession.clientProxy.sessionVerificationControllerProxy() else {
|
||||
private func presentSessionVerificationScreen() {
|
||||
guard let sessionVerificationController = userSession.clientProxy.sessionVerificationController else {
|
||||
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)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import AVKit
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
enum UserSessionFlowCoordinatorAction {
|
||||
@@ -59,7 +60,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
/// For testing purposes.
|
||||
var statePublisher: AnyPublisher<UserSessionFlowCoordinatorStateMachine.State, Never> { stateMachine.statePublisher }
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
init(userSession: UserSessionProtocol,
|
||||
navigationRootCoordinator: NavigationRootCoordinator,
|
||||
appLockService: AppLockServiceProtocol,
|
||||
@@ -113,87 +113,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
setupStateMachine()
|
||||
|
||||
userSession.sessionSecurityStatePublisher
|
||||
.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)
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
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() {
|
||||
let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
|
||||
bugReportService: bugReportService,
|
||||
@@ -569,7 +612,9 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
self?.stateMachine.processEvent(.dismissedStartChatScreen)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Session Verification
|
||||
|
||||
// MARK: Calls
|
||||
|
||||
private func presentCallScreen(genericCallLink url: URL) {
|
||||
|
||||
@@ -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") }
|
||||
/// Upgrade available
|
||||
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
|
||||
internal static var commonAbout: String { return L10n.tr("Localizable", "common_about") }
|
||||
/// Acceptable use policy
|
||||
@@ -1009,7 +1011,7 @@ internal enum L10n {
|
||||
internal static var screenChangeServerSubtitle: String { return L10n.tr("Localizable", "screen_change_server_subtitle") }
|
||||
/// Select your server
|
||||
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") }
|
||||
/// Turn on backup
|
||||
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") }
|
||||
/// Are you sure you want to turn off backup?
|
||||
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") }
|
||||
/// 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") }
|
||||
/// 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 {
|
||||
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") }
|
||||
/// This account has been deactivated.
|
||||
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") }
|
||||
/// Have you saved your recovery key?
|
||||
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") }
|
||||
/// Generate your recovery 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 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") }
|
||||
/// Set up recovery
|
||||
/// Set up recovery to protect your account
|
||||
internal static var title: String { return L10n.tr("Localizable", "banner.set_up_recovery.title") }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@ extension ClientProxyMock {
|
||||
logoutReturnValue = nil
|
||||
searchUsersSearchTermLimitReturnValue = .success(.init(results: [], limited: false))
|
||||
profileForReturnValue = .success(.init(userID: "@a:b.com", displayName: "Some user"))
|
||||
sessionVerificationControllerProxyReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
ignoreUserReturnValue = .success(())
|
||||
unignoreUserReturnValue = .success(())
|
||||
|
||||
|
||||
@@ -2210,6 +2210,7 @@ class ClientProxyMock: ClientProxyProtocol {
|
||||
set(value) { underlyingSecureBackupController = value }
|
||||
}
|
||||
var underlyingSecureBackupController: SecureBackupControllerProtocol!
|
||||
var sessionVerificationController: SessionVerificationControllerProxyProtocol?
|
||||
|
||||
//MARK: - isOnlyDeviceLeft
|
||||
|
||||
@@ -3519,70 +3520,6 @@ class ClientProxyMock: ClientProxyProtocol {
|
||||
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
|
||||
|
||||
var deactivateAccountPasswordEraseDataUnderlyingCallsCount = 0
|
||||
@@ -13435,12 +13372,146 @@ class SecureBackupControllerMock: SecureBackupControllerProtocol {
|
||||
}
|
||||
}
|
||||
class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol {
|
||||
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> {
|
||||
get { return underlyingCallbacks }
|
||||
set(value) { underlyingCallbacks = value }
|
||||
var actions: PassthroughSubject<SessionVerificationControllerProxyAction, Never> {
|
||||
get { return underlyingActions }
|
||||
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
|
||||
|
||||
var requestVerificationUnderlyingCallsCount = 0
|
||||
|
||||
@@ -16529,6 +16529,92 @@ open class SessionVerificationControllerSDKMock: MatrixRustSDK.SessionVerificati
|
||||
|
||||
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
|
||||
|
||||
open var approveVerificationThrowableError: Error?
|
||||
@@ -16649,75 +16735,6 @@ open class SessionVerificationControllerSDKMock: MatrixRustSDK.SessionVerificati
|
||||
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
|
||||
|
||||
open var requestVerificationThrowableError: Error?
|
||||
|
||||
@@ -16,16 +16,18 @@ extension SessionVerificationControllerProxyMock {
|
||||
SessionVerificationEmoji(symbol: "🏁", description: "Flag"),
|
||||
SessionVerificationEmoji(symbol: "🌏", description: "Globe")]
|
||||
|
||||
static func configureMock(callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> = .init(),
|
||||
static func configureMock(actions: PassthroughSubject<SessionVerificationControllerProxyAction, Never> = .init(),
|
||||
isVerified: Bool = false,
|
||||
requestDelay: Duration = .seconds(1)) -> SessionVerificationControllerProxyMock {
|
||||
let mock = SessionVerificationControllerProxyMock()
|
||||
mock.underlyingCallbacks = callbacks
|
||||
mock.underlyingActions = actions
|
||||
|
||||
mock.acknowledgeVerificationRequestDetailsReturnValue = .success(())
|
||||
|
||||
mock.requestVerificationClosure = { [unowned mock] in
|
||||
Task.detached {
|
||||
try await Task.sleep(for: requestDelay)
|
||||
mock.callbacks.send(.acceptedVerificationRequest)
|
||||
mock.actions.send(.acceptedVerificationRequest)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
@@ -34,11 +36,11 @@ extension SessionVerificationControllerProxyMock {
|
||||
mock.startSasVerificationClosure = { [unowned mock] in
|
||||
Task.detached {
|
||||
try await Task.sleep(for: requestDelay)
|
||||
mock.callbacks.send(.startedSasVerification)
|
||||
mock.actions.send(.startedSasVerification)
|
||||
|
||||
Task.detached {
|
||||
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
|
||||
Task.detached {
|
||||
try await Task.sleep(for: requestDelay)
|
||||
mock.callbacks.send(.finished)
|
||||
mock.actions.send(.finished)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
@@ -57,7 +59,7 @@ extension SessionVerificationControllerProxyMock {
|
||||
mock.declineVerificationClosure = { [unowned mock] in
|
||||
Task.detached {
|
||||
try await Task.sleep(for: requestDelay)
|
||||
mock.callbacks.send(.cancelled)
|
||||
mock.actions.send(.cancelled)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
@@ -66,7 +68,7 @@ extension SessionVerificationControllerProxyMock {
|
||||
mock.cancelVerificationClosure = { [unowned mock] in
|
||||
Task.detached {
|
||||
try await Task.sleep(for: requestDelay)
|
||||
mock.callbacks.send(.cancelled)
|
||||
mock.actions.send(.cancelled)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
|
||||
@@ -184,6 +184,8 @@ enum A11yIdentifiers {
|
||||
}
|
||||
|
||||
struct SessionVerificationScreen {
|
||||
let acceptVerificationRequest = "session_verification-accept_verification_request"
|
||||
let ignoreVerificationRequest = "session_verification-ignore_verification_request"
|
||||
let requestVerification = "session_verification-request_verification"
|
||||
let startSasVerification = "session_verification-start_sas_verification"
|
||||
let acceptChallenge = "session_verification-accept_challenge"
|
||||
|
||||
+9
-1
@@ -6,14 +6,21 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
enum SessionVerificationScreenCoordinatorAction {
|
||||
case done
|
||||
}
|
||||
|
||||
enum SessionVerificationScreenFlow {
|
||||
case initiator
|
||||
case responder(details: SessionVerificationRequestDetails)
|
||||
}
|
||||
|
||||
struct SessionVerificationScreenCoordinatorParameters {
|
||||
let sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol
|
||||
let flow: SessionVerificationScreenFlow
|
||||
}
|
||||
|
||||
final class SessionVerificationScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -27,7 +34,8 @@ final class SessionVerificationScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
|
||||
init(parameters: SessionVerificationScreenCoordinatorParameters) {
|
||||
viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: parameters.sessionVerificationControllerProxy)
|
||||
viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: parameters.sessionVerificationControllerProxy,
|
||||
flow: parameters.flow)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
+62
-15
@@ -11,13 +11,61 @@ enum SessionVerificationScreenViewModelAction {
|
||||
case finished
|
||||
}
|
||||
|
||||
enum SessionVerificationScreenViewAction {
|
||||
case acceptVerificationRequest
|
||||
case ignoreVerificationRequest
|
||||
case requestVerification
|
||||
case startSasVerification
|
||||
case restart
|
||||
case accept
|
||||
case decline
|
||||
case done
|
||||
}
|
||||
|
||||
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? {
|
||||
switch verificationState {
|
||||
case .initial:
|
||||
return L10n.screenSessionVerificationOpenExistingSessionTitle
|
||||
switch flow {
|
||||
case .initiator:
|
||||
return L10n.screenSessionVerificationOpenExistingSessionTitle
|
||||
case .responder:
|
||||
return L10n.screenSessionVerificationRequestTitle
|
||||
}
|
||||
case .acceptingVerificationRequest:
|
||||
return L10n.screenSessionVerificationRequestTitle
|
||||
case .requestingVerification:
|
||||
return L10n.screenSessionVerificationWaitingToAcceptTitle
|
||||
case .verificationRequestAccepted:
|
||||
@@ -31,13 +79,13 @@ struct SessionVerificationScreenViewState: BindableState {
|
||||
case .acceptingChallenge:
|
||||
return L10n.screenSessionVerificationCompareEmojisTitle
|
||||
case .decliningChallenge:
|
||||
return nil
|
||||
return L10n.screenSessionVerificationCompareEmojisTitle
|
||||
case .verified:
|
||||
return L10n.commonVerificationComplete
|
||||
case .cancelling:
|
||||
return nil
|
||||
case .cancelled:
|
||||
return L10n.commonVerificationCancelled
|
||||
return L10n.commonVerificationFailed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +96,14 @@ struct SessionVerificationScreenViewState: BindableState {
|
||||
var message: String {
|
||||
switch verificationState {
|
||||
case .initial:
|
||||
return L10n.screenSessionVerificationOpenExistingSessionSubtitle
|
||||
switch flow {
|
||||
case .initiator:
|
||||
return L10n.screenSessionVerificationOpenExistingSessionSubtitle
|
||||
case .responder:
|
||||
return L10n.screenSessionVerificationRequestSubtitle
|
||||
}
|
||||
case .acceptingVerificationRequest:
|
||||
return L10n.screenSessionVerificationRequestSubtitle
|
||||
case .requestingVerification:
|
||||
return L10n.screenSessionVerificationWaitingToAcceptSubtitle
|
||||
case .verificationRequestAccepted:
|
||||
@@ -60,7 +115,7 @@ struct SessionVerificationScreenViewState: BindableState {
|
||||
case .acceptingChallenge:
|
||||
return L10n.screenSessionVerificationCompareEmojisSubtitle
|
||||
case .decliningChallenge:
|
||||
return L10n.commonWaiting
|
||||
return L10n.screenSessionVerificationCompareEmojisSubtitle
|
||||
case .cancelling:
|
||||
return L10n.commonWaiting
|
||||
case .showingChallenge:
|
||||
@@ -68,15 +123,7 @@ struct SessionVerificationScreenViewState: BindableState {
|
||||
case .verified:
|
||||
return L10n.screenSessionVerificationCompleteSubtitle
|
||||
case .cancelled:
|
||||
return L10n.screenSessionVerificationCancelledSubtitle
|
||||
return L10n.screenSessionVerificationFailedSubtitle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SessionVerificationScreenViewAction {
|
||||
case requestVerification
|
||||
case startSasVerification
|
||||
case restart
|
||||
case accept
|
||||
case decline
|
||||
}
|
||||
|
||||
+15
-4
@@ -13,6 +13,8 @@ class SessionVerificationScreenStateMachine {
|
||||
enum State: StateType {
|
||||
/// The initial state, before verification started
|
||||
case initial
|
||||
/// Accepting the remote verification request
|
||||
case acceptingVerificationRequest
|
||||
/// Waiting for verification acceptance
|
||||
case requestingVerification
|
||||
/// Verification request accepted. Waiting for start
|
||||
@@ -37,6 +39,8 @@ class SessionVerificationScreenStateMachine {
|
||||
|
||||
/// Events that can be triggered on the SessionVerification state machine
|
||||
enum Event: EventType {
|
||||
/// Accept the remote verification request
|
||||
case acceptVerificationRequest
|
||||
/// Request verification
|
||||
case requestVerification
|
||||
/// The current verification request has been accepted
|
||||
@@ -69,16 +73,23 @@ class SessionVerificationScreenStateMachine {
|
||||
stateMachine.state
|
||||
}
|
||||
|
||||
init() {
|
||||
stateMachine = StateMachine(state: .initial)
|
||||
init(state: State) {
|
||||
stateMachine = StateMachine(state: state)
|
||||
configure()
|
||||
}
|
||||
|
||||
private func configure() {
|
||||
stateMachine.addRoutes(event: .acceptVerificationRequest, transitions: [.initial => .acceptingVerificationRequest])
|
||||
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: .didFail, transitions: [.requestingVerification => .initial])
|
||||
|
||||
stateMachine.addRoutes(event: .didFail, transitions: [.requestingVerification => .initial,
|
||||
.acceptingVerificationRequest => .initial])
|
||||
|
||||
stateMachine.addRoutes(event: .restart, transitions: [.cancelled => .initial])
|
||||
|
||||
// Transitions with associated values need to be handled through `addRouteMapping`
|
||||
|
||||
+45
-6
@@ -12,6 +12,7 @@ typealias SessionVerificationViewModelType = StateStoreViewModel<SessionVerifica
|
||||
|
||||
class SessionVerificationScreenViewModel: SessionVerificationViewModelType, SessionVerificationScreenViewModelProtocol {
|
||||
private let sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol
|
||||
private let flow: SessionVerificationScreenFlow
|
||||
|
||||
private var stateMachine: SessionVerificationScreenStateMachine
|
||||
|
||||
@@ -22,21 +23,25 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess
|
||||
}
|
||||
|
||||
init(sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol,
|
||||
flow: SessionVerificationScreenFlow,
|
||||
verificationState: SessionVerificationScreenStateMachine.State = .initial) {
|
||||
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()
|
||||
|
||||
sessionVerificationControllerProxy.callbacks
|
||||
sessionVerificationControllerProxy.actions
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
guard let self else { return }
|
||||
|
||||
switch callback {
|
||||
case .receivedVerificationRequest:
|
||||
break // Incoming verification requests are handled on the higher levels
|
||||
case .acceptedVerificationRequest:
|
||||
self.stateMachine.processEvent(.didAcceptVerificationRequest)
|
||||
case .startedSasVerification:
|
||||
@@ -57,10 +62,20 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
if case .responder(let details) = flow {
|
||||
Task {
|
||||
await self.sessionVerificationControllerProxy.acknowledgeVerificationRequest(details: details)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func process(viewAction: SessionVerificationScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .acceptVerificationRequest:
|
||||
stateMachine.processEvent(.acceptVerificationRequest)
|
||||
case .ignoreVerificationRequest:
|
||||
actionsSubject.send(.finished)
|
||||
case .requestVerification:
|
||||
stateMachine.processEvent(.requestVerification)
|
||||
case .startSasVerification:
|
||||
@@ -71,13 +86,16 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess
|
||||
stateMachine.processEvent(.acceptChallenge)
|
||||
case .decline:
|
||||
stateMachine.processEvent(.declineChallenge)
|
||||
case .done:
|
||||
actionsSubject.send(.finished)
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
let uncancellableStates: [SessionVerificationScreenStateMachine.State] = [.initial, .verified, .cancelled]
|
||||
|
||||
if !uncancellableStates.contains(stateMachine.state) {
|
||||
switch stateMachine.state {
|
||||
case .initial, .verified, .cancelled: // non-cancellable states
|
||||
return
|
||||
default:
|
||||
stateMachine.processEvent(.cancel)
|
||||
}
|
||||
}
|
||||
@@ -91,6 +109,8 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess
|
||||
state.verificationState = context.toState
|
||||
|
||||
switch (context.fromState, context.event, context.toState) {
|
||||
case (.initial, .acceptVerificationRequest, .acceptingVerificationRequest):
|
||||
acceptVerificationRequest()
|
||||
case (.initial, .requestVerification, .requestingVerification):
|
||||
requestVerification()
|
||||
case (.verificationRequestAccepted, .startSasVerification, .startingSasVerification):
|
||||
@@ -103,6 +123,10 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess
|
||||
cancelVerification()
|
||||
case (_, _, .verified):
|
||||
actionsSubject.send(.finished)
|
||||
case (.initial, _, .cancelled):
|
||||
if case .responder = flow {
|
||||
actionsSubject.send(.finished)
|
||||
}
|
||||
default:
|
||||
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() {
|
||||
Task {
|
||||
switch await sessionVerificationControllerProxy.requestVerification() {
|
||||
|
||||
+80
@@ -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)
|
||||
}
|
||||
}
|
||||
+71
-68
@@ -5,6 +5,8 @@
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
struct SessionVerificationScreen: View {
|
||||
@@ -14,7 +16,6 @@ struct SessionVerificationScreen: View {
|
||||
FullscreenDialog {
|
||||
VStack(spacing: 32) {
|
||||
screenHeader
|
||||
Spacer()
|
||||
mainContent
|
||||
}
|
||||
} bottomContent: {
|
||||
@@ -27,33 +28,6 @@ struct SessionVerificationScreen: View {
|
||||
|
||||
// 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
|
||||
private var screenHeader: some View {
|
||||
VStack(spacing: 0) {
|
||||
@@ -61,11 +35,11 @@ struct SessionVerificationScreen: View {
|
||||
BigIcon(icon: \.lockSolid)
|
||||
.padding(.bottom, 16)
|
||||
} else {
|
||||
Image(systemName: headerImageName)
|
||||
Image(systemName: context.viewState.headerImageName)
|
||||
.bigIcon()
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
|
||||
|
||||
Text(context.viewState.title ?? "")
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
@@ -83,18 +57,17 @@ struct SessionVerificationScreen: View {
|
||||
@ViewBuilder
|
||||
private var mainContent: some View {
|
||||
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)
|
||||
.accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.emojiWrapper)
|
||||
case .acceptingChallenge(let emojis):
|
||||
emojisPanel(with: emojis)
|
||||
.accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.emojiWrapper)
|
||||
case .requestingVerification:
|
||||
ProgressView()
|
||||
.tint(.compound.textSecondary)
|
||||
.scaleEffect(2)
|
||||
default:
|
||||
// In All other cases, we just want an empty view
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
@@ -119,18 +92,41 @@ struct SessionVerificationScreen: View {
|
||||
private var actionButtons: some View {
|
||||
switch context.viewState.verificationState {
|
||||
case .initial:
|
||||
VStack(spacing: 32) {
|
||||
switch context.viewState.flow {
|
||||
case .initiator:
|
||||
Button(L10n.actionStartVerification) {
|
||||
context.send(viewAction: .requestVerification)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
.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:
|
||||
Button(L10n.actionRetry) {
|
||||
context.send(viewAction: .restart)
|
||||
switch context.viewState.flow {
|
||||
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:
|
||||
Button(L10n.actionStart) {
|
||||
@@ -150,31 +146,15 @@ struct SessionVerificationScreen: View {
|
||||
Button(L10n.screenSessionVerificationTheyDontMatch) {
|
||||
context.send(viewAction: .decline)
|
||||
}
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.buttonStyle(.compound(.plain))
|
||||
.accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.declineChallenge)
|
||||
}
|
||||
|
||||
case .acceptingChallenge:
|
||||
VStack(spacing: 32) {
|
||||
Button { context.send(viewAction: .accept) } label: {
|
||||
HStack(spacing: 16) {
|
||||
ProgressView()
|
||||
.tint(.compound.textOnSolidPrimary)
|
||||
Text(L10n.screenSessionVerificationTheyMatch)
|
||||
}
|
||||
}
|
||||
case .acceptingVerificationRequest, .acceptingChallenge, .decliningChallenge, .requestingVerification:
|
||||
Button(L10n.screenIdentityWaitingOnOtherDevice) { }
|
||||
.buttonStyle(.compound(.primary))
|
||||
.accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.acceptChallenge)
|
||||
.disabled(true)
|
||||
|
||||
Button(L10n.screenSessionVerificationTheyDontMatch) {
|
||||
context.send(viewAction: .decline)
|
||||
}
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.declineChallenge)
|
||||
.disabled(true)
|
||||
}
|
||||
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
@@ -196,27 +176,50 @@ struct SessionVerificationScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct SessionVerification_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
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)
|
||||
.previewDisplayName("Requesting Verification")
|
||||
sessionVerificationScreen(state: .verificationRequestAccepted)
|
||||
.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))
|
||||
.previewDisplayName("Showing Challenge")
|
||||
sessionVerificationScreen(state: .acceptingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
.previewDisplayName("Accepting Challenge")
|
||||
sessionVerificationScreen(state: .decliningChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
.previewDisplayName("Declining Challenge")
|
||||
|
||||
sessionVerificationScreen(state: .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(),
|
||||
flow: flow,
|
||||
verificationState: state)
|
||||
|
||||
return SessionVerificationScreen(context: viewModel.context)
|
||||
|
||||
@@ -91,6 +91,7 @@ class TimelineInteractionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
func handleTimelineItemMenuAction(_ action: TimelineItemMenuAction, itemID: TimelineItemIdentifier) {
|
||||
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID),
|
||||
let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else {
|
||||
|
||||
@@ -50,6 +50,8 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
let secureBackupController: SecureBackupControllerProtocol
|
||||
|
||||
private(set) var sessionVerificationController: SessionVerificationControllerProxyProtocol?
|
||||
|
||||
private static var roomCreationPowerLevelOverrides: PowerLevels {
|
||||
.init(usersDefault: nil,
|
||||
eventsDefault: nil,
|
||||
@@ -157,7 +159,16 @@ class ClientProxy: ClientProxyProtocol {
|
||||
updateVerificationState(client.encryption().verificationState())
|
||||
|
||||
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
|
||||
@@ -484,7 +495,8 @@ class ClientProxy: ClientProxyProtocol {
|
||||
func roomPreviewForIdentifier(_ identifier: String, via: [String]) async -> Result<RoomPreviewDetails, ClientProxyError> {
|
||||
do {
|
||||
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 {
|
||||
return .failure(.roomPreviewIsPrivate)
|
||||
} 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? {
|
||||
do {
|
||||
return try await client.logout().flatMap(URL.init(string:))
|
||||
@@ -728,6 +730,19 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
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() {
|
||||
loadCachedAvatarURLTask = Task {
|
||||
@@ -1079,17 +1094,17 @@ private class SendQueueRoomErrorListenerProxy: SendQueueRoomErrorListener {
|
||||
}
|
||||
|
||||
private extension RoomPreviewDetails {
|
||||
init(_ roomPreview: RoomPreview) {
|
||||
self = RoomPreviewDetails(roomID: roomPreview.roomId,
|
||||
name: roomPreview.name,
|
||||
canonicalAlias: roomPreview.canonicalAlias,
|
||||
topic: roomPreview.topic,
|
||||
avatarURL: roomPreview.avatarUrl.flatMap(URL.init(string:)),
|
||||
memberCount: UInt(roomPreview.numJoinedMembers),
|
||||
isHistoryWorldReadable: roomPreview.isHistoryWorldReadable,
|
||||
isJoined: roomPreview.isJoined,
|
||||
isInvited: roomPreview.isInvited,
|
||||
isPublic: roomPreview.isPublic,
|
||||
canKnock: roomPreview.canKnock)
|
||||
init(_ roomPreviewInfo: RoomPreviewInfo) {
|
||||
self = RoomPreviewDetails(roomID: roomPreviewInfo.roomId,
|
||||
name: roomPreviewInfo.name,
|
||||
canonicalAlias: roomPreviewInfo.canonicalAlias,
|
||||
topic: roomPreviewInfo.topic,
|
||||
avatarURL: roomPreviewInfo.avatarUrl.flatMap(URL.init(string:)),
|
||||
memberCount: UInt(roomPreviewInfo.numJoinedMembers),
|
||||
isHistoryWorldReadable: roomPreviewInfo.isHistoryWorldReadable,
|
||||
isJoined: roomPreviewInfo.membership == .joined,
|
||||
isInvited: roomPreviewInfo.membership == .invited,
|
||||
isPublic: roomPreviewInfo.joinRule == .public,
|
||||
canKnock: roomPreviewInfo.joinRule == .knock)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
|
||||
var secureBackupController: SecureBackupControllerProtocol { get }
|
||||
|
||||
var sessionVerificationController: SessionVerificationControllerProxyProtocol? { get }
|
||||
|
||||
func isOnlyDeviceLeft() async -> Result<Bool, ClientProxyError>
|
||||
|
||||
func startSync()
|
||||
@@ -155,8 +157,6 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
func setUserAvatar(media: MediaInfo) 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>
|
||||
|
||||
|
||||
@@ -246,10 +246,8 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
||||
#if targetEnvironment(simulator)
|
||||
// This gets called for no reason on simulators, where CallKit
|
||||
// isn't even supported. Ignore
|
||||
return
|
||||
#endif
|
||||
|
||||
// isn't even supported, ignore it.
|
||||
#else
|
||||
if let ongoingCallID {
|
||||
actionsSubject.send(.endCall(roomID: ongoingCallID.roomID))
|
||||
}
|
||||
@@ -257,6 +255,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
tearDownCallSession(sendEndCallAction: false)
|
||||
|
||||
action.fulfill()
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
+68
-8
@@ -18,6 +18,10 @@ private class WeakSessionVerificationControllerProxy: SessionVerificationControl
|
||||
|
||||
// MARK: - SessionVerificationControllerDelegate
|
||||
|
||||
func didReceiveVerificationRequest(details: MatrixRustSDK.SessionVerificationRequestDetails) {
|
||||
proxy?.didReceiveVerificationRequest(details: details)
|
||||
}
|
||||
|
||||
func didReceiveVerificationData(data: MatrixRustSDK.SessionVerificationData) {
|
||||
switch data {
|
||||
// We can handle only emojis for now
|
||||
@@ -54,86 +58,142 @@ class SessionVerificationControllerProxy: SessionVerificationControllerProxyProt
|
||||
|
||||
init(sessionVerificationController: SessionVerificationController) {
|
||||
self.sessionVerificationController = sessionVerificationController
|
||||
sessionVerificationController.setDelegate(delegate: WeakSessionVerificationControllerProxy(proxy: self))
|
||||
}
|
||||
|
||||
deinit {
|
||||
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> {
|
||||
sessionVerificationController.setDelegate(delegate: WeakSessionVerificationControllerProxy(proxy: self))
|
||||
MXLog.info("Requesting session verification")
|
||||
|
||||
do {
|
||||
try await sessionVerificationController.requestVerification()
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed requesting session verification with error: \(error)")
|
||||
return .failure(.failedRequestingVerification)
|
||||
}
|
||||
}
|
||||
|
||||
func startSasVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
|
||||
MXLog.info("Starting SAS verification")
|
||||
|
||||
do {
|
||||
try await sessionVerificationController.startSasVerification()
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed starting SAS verification with error: \(error)")
|
||||
return .failure(.failedStartingSasVerification)
|
||||
}
|
||||
}
|
||||
|
||||
func approveVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
|
||||
MXLog.info("Approving verification")
|
||||
|
||||
do {
|
||||
try await sessionVerificationController.approveVerification()
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed approving verification with error: \(error)")
|
||||
return .failure(.failedApprovingVerification)
|
||||
}
|
||||
}
|
||||
|
||||
func declineVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
|
||||
MXLog.info("Declining verification")
|
||||
|
||||
do {
|
||||
try await sessionVerificationController.declineVerification()
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed declining verification with error: \(error)")
|
||||
return .failure(.failedDecliningVerification)
|
||||
}
|
||||
}
|
||||
|
||||
func cancelVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
|
||||
MXLog.info("Cancelling verification")
|
||||
|
||||
do {
|
||||
try await sessionVerificationController.cancelVerification()
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed cancelling verification with error: \(error)")
|
||||
return .failure(.failedCancellingVerification)
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
callbacks.send(.acceptedVerificationRequest)
|
||||
MXLog.info("Accepted verification request")
|
||||
|
||||
actions.send(.acceptedVerificationRequest)
|
||||
}
|
||||
|
||||
fileprivate func didStartSasVerification() {
|
||||
callbacks.send(.startedSasVerification)
|
||||
MXLog.info("Started SAS verification")
|
||||
|
||||
actions.send(.startedSasVerification)
|
||||
}
|
||||
|
||||
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())
|
||||
}))
|
||||
}
|
||||
|
||||
fileprivate func didFail() {
|
||||
callbacks.send(.failed)
|
||||
actions.send(.failed)
|
||||
}
|
||||
|
||||
fileprivate func didFinish() {
|
||||
callbacks.send(.finished)
|
||||
actions.send(.finished)
|
||||
}
|
||||
|
||||
fileprivate func didCancel() {
|
||||
callbacks.send(.cancelled)
|
||||
actions.send(.cancelled)
|
||||
}
|
||||
}
|
||||
|
||||
+18
-2
@@ -7,8 +7,11 @@
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
enum SessionVerificationControllerProxyError: Error {
|
||||
case failedAcknowledgingVerificationRequest
|
||||
case failedAcceptingVerificationRequest
|
||||
case failedRequestingVerification
|
||||
case failedStartingSasVerification
|
||||
case failedApprovingVerification
|
||||
@@ -16,7 +19,8 @@ enum SessionVerificationControllerProxyError: Error {
|
||||
case failedCancellingVerification
|
||||
}
|
||||
|
||||
enum SessionVerificationControllerProxyCallback {
|
||||
enum SessionVerificationControllerProxyAction {
|
||||
case receivedVerificationRequest(details: SessionVerificationRequestDetails)
|
||||
case acceptedVerificationRequest
|
||||
case startedSasVerification
|
||||
case receivedVerificationData([SessionVerificationEmoji])
|
||||
@@ -25,6 +29,14 @@ enum SessionVerificationControllerProxyCallback {
|
||||
case failed
|
||||
}
|
||||
|
||||
struct SessionVerificationRequestDetails {
|
||||
let senderID: String
|
||||
let flowID: String
|
||||
let deviceID: String
|
||||
let displayName: String?
|
||||
let firstSeenDate: Date
|
||||
}
|
||||
|
||||
struct SessionVerificationEmoji: Hashable {
|
||||
let symbol: String
|
||||
let description: String
|
||||
@@ -36,7 +48,11 @@ struct SessionVerificationEmoji: Hashable {
|
||||
|
||||
// sourcery: AutoMockable
|
||||
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>
|
||||
|
||||
|
||||
@@ -519,7 +519,8 @@ class MockScreen: Identifiable {
|
||||
return navigationStackCoordinator
|
||||
case .sessionVerification:
|
||||
var sessionVerificationControllerProxy = SessionVerificationControllerProxyMock.configureMock(requestDelay: .seconds(5))
|
||||
let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationControllerProxy)
|
||||
let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationControllerProxy,
|
||||
flow: .initiator)
|
||||
return SessionVerificationScreenCoordinator(parameters: parameters)
|
||||
case .userSessionScreen, .userSessionScreenReply:
|
||||
let appSettings: AppSettings = ServiceLocator.shared.settings
|
||||
|
||||
Reference in New Issue
Block a user