mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Only offer to verify if a cross-signed device is available and improve the UX whilst waiting. (#4710)
* Only offer to verify if a cross-signed device is available * Wait until we know which verification options are available before showing them. --------- Co-authored-by: Hubert Chathi <hubertc@matrix.org>
This commit is contained in:
@@ -67,6 +67,7 @@ extension ClientProxyMock {
|
|||||||
notificationSettings = configuration.notificationSettings
|
notificationSettings = configuration.notificationSettings
|
||||||
|
|
||||||
isOnlyDeviceLeftReturnValue = .success(false)
|
isOnlyDeviceLeftReturnValue = .success(false)
|
||||||
|
hasDevicesToVerifyAgainstReturnValue = .success(true)
|
||||||
accountURLActionReturnValue = "https://matrix.org/account"
|
accountURLActionReturnValue = "https://matrix.org/account"
|
||||||
canDeactivateAccount = false
|
canDeactivateAccount = false
|
||||||
directRoomForUserIDReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
directRoomForUserIDReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||||
|
|||||||
@@ -2287,6 +2287,70 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
|
|||||||
return isOnlyDeviceLeftReturnValue
|
return isOnlyDeviceLeftReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//MARK: - hasDevicesToVerifyAgainst
|
||||||
|
|
||||||
|
var hasDevicesToVerifyAgainstUnderlyingCallsCount = 0
|
||||||
|
var hasDevicesToVerifyAgainstCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return hasDevicesToVerifyAgainstUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = hasDevicesToVerifyAgainstUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
hasDevicesToVerifyAgainstUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
hasDevicesToVerifyAgainstUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var hasDevicesToVerifyAgainstCalled: Bool {
|
||||||
|
return hasDevicesToVerifyAgainstCallsCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasDevicesToVerifyAgainstUnderlyingReturnValue: Result<Bool, ClientProxyError>!
|
||||||
|
var hasDevicesToVerifyAgainstReturnValue: Result<Bool, ClientProxyError>! {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return hasDevicesToVerifyAgainstUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: Result<Bool, ClientProxyError>? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = hasDevicesToVerifyAgainstUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
hasDevicesToVerifyAgainstUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
hasDevicesToVerifyAgainstUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var hasDevicesToVerifyAgainstClosure: (() async -> Result<Bool, ClientProxyError>)?
|
||||||
|
|
||||||
|
func hasDevicesToVerifyAgainst() async -> Result<Bool, ClientProxyError> {
|
||||||
|
hasDevicesToVerifyAgainstCallsCount += 1
|
||||||
|
if let hasDevicesToVerifyAgainstClosure = hasDevicesToVerifyAgainstClosure {
|
||||||
|
return await hasDevicesToVerifyAgainstClosure()
|
||||||
|
} else {
|
||||||
|
return hasDevicesToVerifyAgainstReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
//MARK: - startSync
|
//MARK: - startSync
|
||||||
|
|
||||||
var startSyncUnderlyingCallsCount = 0
|
var startSyncUnderlyingCallsCount = 0
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ struct IdentityConfirmationScreenViewState: BindableState {
|
|||||||
case interactiveVerification
|
case interactiveVerification
|
||||||
}
|
}
|
||||||
|
|
||||||
var availableActions: [AvailableActions] = []
|
var availableActions: [AvailableActions]?
|
||||||
let learnMoreURL: URL
|
let learnMoreURL: URL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -9,7 +9,7 @@
|
|||||||
import Combine
|
import Combine
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
typealias IdentityConfirmationScreenViewModelType = StateStoreViewModel<IdentityConfirmationScreenViewState, IdentityConfirmationScreenViewAction>
|
typealias IdentityConfirmationScreenViewModelType = StateStoreViewModelV2<IdentityConfirmationScreenViewState, IdentityConfirmationScreenViewAction>
|
||||||
|
|
||||||
class IdentityConfirmationScreenViewModel: IdentityConfirmationScreenViewModelType, IdentityConfirmationScreenViewModelProtocol {
|
class IdentityConfirmationScreenViewModel: IdentityConfirmationScreenViewModelType, IdentityConfirmationScreenViewModelProtocol {
|
||||||
private let userSession: UserSessionProtocol
|
private let userSession: UserSessionProtocol
|
||||||
@@ -62,14 +62,22 @@ class IdentityConfirmationScreenViewModel: IdentityConfirmationScreenViewModelTy
|
|||||||
hideLoadingIndicator()
|
hideLoadingIndicator()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: Until the actions are unset, there's a disabled action button with a loading spinner.
|
||||||
|
|
||||||
guard sessionSecurityState.verificationState == .unverified else {
|
guard sessionSecurityState.verificationState == .unverified else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Continue to show the loading action button until we know that there's a recovery set up.
|
||||||
|
// https://github.com/element-hq/element-x-ios/issues/4699
|
||||||
|
guard sessionSecurityState.recoveryState != .unknown else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var availableActions: [IdentityConfirmationScreenViewState.AvailableActions] = []
|
var availableActions: [IdentityConfirmationScreenViewState.AvailableActions] = []
|
||||||
|
|
||||||
if case let .success(isOnlyDeviceLeft) = await userSession.clientProxy.isOnlyDeviceLeft(),
|
if case let .success(hasDevicesToVerifyAgainst) = await userSession.clientProxy.hasDevicesToVerifyAgainst(),
|
||||||
!isOnlyDeviceLeft {
|
hasDevicesToVerifyAgainst {
|
||||||
availableActions.append(.interactiveVerification)
|
availableActions.append(.interactiveVerification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+41
-21
@@ -10,7 +10,7 @@ import Compound
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct IdentityConfirmationScreen: View {
|
struct IdentityConfirmationScreen: View {
|
||||||
@ObservedObject var context: IdentityConfirmationScreenViewModel.Context
|
let context: IdentityConfirmationScreenViewModel.Context
|
||||||
|
|
||||||
var shouldShowSkipButton: Bool {
|
var shouldShowSkipButton: Bool {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@@ -63,25 +63,38 @@ struct IdentityConfirmationScreen: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var actionButtons: some View {
|
private var actionButtons: some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
if context.viewState.availableActions.contains(.interactiveVerification) {
|
if let availableActions = context.viewState.availableActions {
|
||||||
Button(L10n.screenIdentityConfirmationUseAnotherDevice) {
|
if availableActions.contains(.interactiveVerification) {
|
||||||
context.send(viewAction: .otherDevice)
|
Button(L10n.screenIdentityConfirmationUseAnotherDevice) {
|
||||||
|
context.send(viewAction: .otherDevice)
|
||||||
|
}
|
||||||
|
.buttonStyle(.compound(.primary))
|
||||||
|
}
|
||||||
|
|
||||||
|
if availableActions.contains(.recovery) {
|
||||||
|
Button(L10n.screenIdentityConfirmationUseRecoveryKey) {
|
||||||
|
context.send(viewAction: .recoveryKey)
|
||||||
|
}
|
||||||
|
.buttonStyle(.compound(.primary))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(L10n.screenIdentityConfirmationCannotConfirm) {
|
||||||
|
context.send(viewAction: .reset)
|
||||||
|
}
|
||||||
|
.buttonStyle(.compound(.secondary))
|
||||||
|
} else {
|
||||||
|
Button { /* Placeholder button, there is no action */ } label: {
|
||||||
|
Label {
|
||||||
|
Text(L10n.commonLoading)
|
||||||
|
} icon: {
|
||||||
|
ProgressView()
|
||||||
|
.tint(.compound.iconOnSolidPrimary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.compound(.primary))
|
.buttonStyle(.compound(.primary))
|
||||||
|
.disabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if context.viewState.availableActions.contains(.recovery) {
|
|
||||||
Button(L10n.screenIdentityConfirmationUseRecoveryKey) {
|
|
||||||
context.send(viewAction: .recoveryKey)
|
|
||||||
}
|
|
||||||
.buttonStyle(.compound(.primary))
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(L10n.screenIdentityConfirmationCannotConfirm) {
|
|
||||||
context.send(viewAction: .reset)
|
|
||||||
}
|
|
||||||
.buttonStyle(.compound(.secondary))
|
|
||||||
|
|
||||||
if shouldShowSkipButton {
|
if shouldShowSkipButton {
|
||||||
Button("\(L10n.actionSkip) 🙉") {
|
Button("\(L10n.actionSkip) 🙉") {
|
||||||
context.send(viewAction: .skip)
|
context.send(viewAction: .skip)
|
||||||
@@ -105,20 +118,27 @@ struct IdentityConfirmationScreen: View {
|
|||||||
|
|
||||||
struct IdentityConfirmationScreen_Previews: PreviewProvider, TestablePreview {
|
struct IdentityConfirmationScreen_Previews: PreviewProvider, TestablePreview {
|
||||||
static var viewModel = makeViewModel()
|
static var viewModel = makeViewModel()
|
||||||
|
static var loadingViewModel = makeViewModel(recoveryState: .unknown)
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
IdentityConfirmationScreen(context: viewModel.context)
|
IdentityConfirmationScreen(context: viewModel.context)
|
||||||
}
|
}
|
||||||
.snapshotPreferences(expect: viewModel.context.$viewState.map { state in
|
.previewDisplayName("Actions")
|
||||||
state.availableActions.contains([.interactiveVerification, .recovery])
|
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.availableActions).map { actions in
|
||||||
})
|
actions?.contains([.interactiveVerification, .recovery]) == true
|
||||||
|
}.eraseToStream())
|
||||||
|
|
||||||
|
NavigationStack {
|
||||||
|
IdentityConfirmationScreen(context: loadingViewModel.context)
|
||||||
|
}
|
||||||
|
.previewDisplayName("Loading")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func makeViewModel() -> IdentityConfirmationScreenViewModel {
|
static func makeViewModel(recoveryState: SecureBackupRecoveryState = .enabled) -> IdentityConfirmationScreenViewModel {
|
||||||
let clientProxy = ClientProxyMock(.init())
|
let clientProxy = ClientProxyMock(.init())
|
||||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||||
userSession.sessionSecurityStatePublisher = CurrentValuePublisher<SessionSecurityState, Never>(.init(verificationState: .unverified, recoveryState: .enabled))
|
userSession.sessionSecurityStatePublisher = CurrentValuePublisher<SessionSecurityState, Never>(.init(verificationState: .unverified, recoveryState: recoveryState))
|
||||||
|
|
||||||
return IdentityConfirmationScreenViewModel(userSession: userSession,
|
return IdentityConfirmationScreenViewModel(userSession: userSession,
|
||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
|
|||||||
@@ -342,6 +342,16 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasDevicesToVerifyAgainst() async -> Result<Bool, ClientProxyError> {
|
||||||
|
do {
|
||||||
|
let result = try await client.encryption().hasDevicesToVerifyAgainst()
|
||||||
|
return .success(result)
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed checking hasDevicesToVerifyAgainst with error: \(error)")
|
||||||
|
return .failure(.sdkError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func startSync() {
|
func startSync() {
|
||||||
guard !hasEncounteredAuthError else {
|
guard !hasEncounteredAuthError else {
|
||||||
MXLog.warning("Ignoring request, this client has an unknown token.")
|
MXLog.warning("Ignoring request, this client has an unknown token.")
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ protocol ClientProxyProtocol: AnyObject {
|
|||||||
|
|
||||||
func isOnlyDeviceLeft() async -> Result<Bool, ClientProxyError>
|
func isOnlyDeviceLeft() async -> Result<Bool, ClientProxyError>
|
||||||
|
|
||||||
|
func hasDevicesToVerifyAgainst() async -> Result<Bool, ClientProxyError>
|
||||||
|
|
||||||
func startSync()
|
func startSync()
|
||||||
|
|
||||||
func stopSync()
|
func stopSync()
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:069031b694fefcff95b0946e838a55bb202f9a2f1736db103df5048b30f9b7da
|
||||||
|
size 95823
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b2f43a07b61fde95bd1702d6a8508b4e61aa551eab94acfaf4c3e3321b74ff82
|
||||||
|
size 106363
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:06a79601a0bf98a6bcc52dd36e06494f5b6dc28a41c47de09602b9374db1a74f
|
||||||
|
size 49850
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:89650ec13fbbf63c4299d9faf8f8201e59d2b885359151fd490d8b013d2a1461
|
||||||
|
size 64111
|
||||||
Reference in New Issue
Block a user