mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Ensure that the current user is the device owner before linking a new device. (#5773)
This commit is contained in:
@@ -16,6 +16,7 @@ enum LinkNewDeviceScreenCoordinatorAction {
|
||||
|
||||
struct LinkNewDeviceScreenCoordinatorParameters {
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let appLockService: AppLockServiceProtocol
|
||||
let orientationManager: OrientationManagerProtocol
|
||||
}
|
||||
|
||||
@@ -31,7 +32,8 @@ final class LinkNewDeviceScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
|
||||
init(parameters: LinkNewDeviceScreenCoordinatorParameters) {
|
||||
viewModel = LinkNewDeviceScreenViewModel(clientProxy: parameters.clientProxy)
|
||||
viewModel = LinkNewDeviceScreenViewModel(clientProxy: parameters.clientProxy,
|
||||
appLockService: parameters.appLockService)
|
||||
orientationManager = parameters.orientationManager
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,14 @@ enum LinkNewDeviceScreenViewModelAction {
|
||||
|
||||
struct LinkNewDeviceScreenViewState: BindableState {
|
||||
nonisolated enum Mode: Equatable {
|
||||
enum ReadyState: Equatable {
|
||||
case idle
|
||||
case verifyingDeviceOwner
|
||||
case generatingCode
|
||||
}
|
||||
|
||||
case loading
|
||||
case readyToLink(isGeneratingCode: Bool)
|
||||
case readyToLink(ReadyState)
|
||||
case error(QRCodeLoginState.ErrorState)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,15 +12,19 @@ import SwiftUI
|
||||
typealias LinkNewDeviceScreenViewModelType = StateStoreViewModelV2<LinkNewDeviceScreenViewState, LinkNewDeviceScreenViewAction>
|
||||
|
||||
class LinkNewDeviceScreenViewModel: LinkNewDeviceScreenViewModelType, LinkNewDeviceScreenViewModelProtocol {
|
||||
private enum Device { case mobileDevice, desktopComputer }
|
||||
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
private let appLockService: AppLockServiceProtocol
|
||||
|
||||
private let actionsSubject: PassthroughSubject<LinkNewDeviceScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<LinkNewDeviceScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(clientProxy: ClientProxyProtocol, initialState: LinkNewDeviceScreenViewState? = nil) {
|
||||
init(clientProxy: ClientProxyProtocol, appLockService: AppLockServiceProtocol, initialState: LinkNewDeviceScreenViewState? = nil) {
|
||||
self.clientProxy = clientProxy
|
||||
self.appLockService = appLockService
|
||||
|
||||
if let initialState {
|
||||
super.init(initialViewState: initialState)
|
||||
@@ -41,9 +45,9 @@ class LinkNewDeviceScreenViewModel: LinkNewDeviceScreenViewModelType, LinkNewDev
|
||||
|
||||
switch viewAction {
|
||||
case .linkMobileDevice:
|
||||
Task { await linkMobileDevice() }
|
||||
Task { await verifyAndLink(.mobileDevice) }
|
||||
case .linkDesktopComputer:
|
||||
actionsSubject.send(.linkDesktopComputer)
|
||||
Task { await verifyAndLink(.desktopComputer) }
|
||||
case .errorAction(let action):
|
||||
handleErrorAction(action)
|
||||
case .dismiss:
|
||||
@@ -55,14 +59,41 @@ class LinkNewDeviceScreenViewModel: LinkNewDeviceScreenViewModelType, LinkNewDev
|
||||
|
||||
private func checkQRCodeLoginSupport() async {
|
||||
if await clientProxy.isLoginWithQRCodeSupported {
|
||||
state.mode = .readyToLink(isGeneratingCode: false)
|
||||
state.mode = .readyToLink(.idle)
|
||||
} else {
|
||||
state.mode = .error(.notSupported)
|
||||
}
|
||||
}
|
||||
|
||||
private func verifyAndLink(_ device: Device) async {
|
||||
state.mode = .readyToLink(.verifyingDeviceOwner)
|
||||
|
||||
switch await appLockService.verifyDeviceOwner(reason: L10n.screenLinkNewDeviceAuthenticationReasonIos) {
|
||||
case .verified, .unavailable:
|
||||
break
|
||||
case .appLockPINRequired:
|
||||
// Follow-up PR: present the App Lock PIN screen to verify the device owner.
|
||||
return
|
||||
case .cancelled, .unverified:
|
||||
state.mode = .readyToLink(.idle)
|
||||
return
|
||||
case .error:
|
||||
// TODO: Failure UX is pending product confirmation; using the generic error state for now.
|
||||
state.mode = .error(.unknown)
|
||||
return
|
||||
}
|
||||
|
||||
switch device {
|
||||
case .mobileDevice:
|
||||
await linkMobileDevice() // Automatically sets the state.
|
||||
case .desktopComputer:
|
||||
actionsSubject.send(.linkDesktopComputer)
|
||||
state.mode = .readyToLink(.idle)
|
||||
}
|
||||
}
|
||||
|
||||
private func linkMobileDevice() async {
|
||||
state.mode = .readyToLink(isGeneratingCode: true)
|
||||
state.mode = .readyToLink(.generatingCode)
|
||||
|
||||
let linkNewDeviceService = clientProxy.linkNewDeviceService()
|
||||
|
||||
@@ -77,7 +108,7 @@ class LinkNewDeviceScreenViewModel: LinkNewDeviceScreenViewModelType, LinkNewDev
|
||||
}
|
||||
|
||||
actionsSubject.send(.linkMobileDevice(progressPublisher))
|
||||
state.mode = .readyToLink(isGeneratingCode: false)
|
||||
state.mode = .readyToLink(.idle)
|
||||
} catch {
|
||||
// This is hard to share a mapping from the QRCodeLoginError with the
|
||||
// QRCodeLoginScreen given that some of those are scan errors…
|
||||
@@ -89,7 +120,7 @@ class LinkNewDeviceScreenViewModel: LinkNewDeviceScreenViewModelType, LinkNewDev
|
||||
switch action {
|
||||
case .startOver:
|
||||
// Reset to ready state to allow trying again.
|
||||
state.mode = .readyToLink(isGeneratingCode: false)
|
||||
state.mode = .readyToLink(.idle)
|
||||
case .openSettings, .signInManually:
|
||||
MXLog.error("Unexpected error action: \(action)")
|
||||
actionsSubject.send(.dismiss)
|
||||
|
||||
@@ -59,7 +59,8 @@ struct LinkNewDeviceScreen: View {
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
.disabled(true)
|
||||
case .readyToLink(let isGeneratingCode):
|
||||
case .readyToLink(let linkState):
|
||||
let isGeneratingCode = linkState == .generatingCode
|
||||
VStack(spacing: 16) {
|
||||
Button { context.send(viewAction: .linkMobileDevice) } label: {
|
||||
Label {
|
||||
@@ -84,7 +85,7 @@ struct LinkNewDeviceScreen: View {
|
||||
.accessibilityIdentifier(A11yIdentifiers.linkNewDeviceScreen.desktopComputer)
|
||||
}
|
||||
}
|
||||
.disabled(isGeneratingCode)
|
||||
.disabled(linkState != .idle)
|
||||
case .error:
|
||||
EmptyView() // Not reachable.
|
||||
}
|
||||
@@ -103,8 +104,9 @@ struct LinkNewDeviceScreen: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct LinkNewDeviceScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = makeViewModel(mode: .readyToLink(isGeneratingCode: false))
|
||||
static let generatingViewModel = makeViewModel(mode: .readyToLink(isGeneratingCode: true))
|
||||
static let viewModel = makeViewModel(mode: .readyToLink(.idle))
|
||||
static let verifyingViewModel = makeViewModel(mode: .readyToLink(.verifyingDeviceOwner))
|
||||
static let generatingViewModel = makeViewModel(mode: .readyToLink(.generatingCode))
|
||||
static let loadingViewModel = makeViewModel(mode: .loading)
|
||||
static let unsupportedViewModel = makeViewModel(mode: .error(.notSupported))
|
||||
static let unknownErrorViewModel = makeViewModel(mode: .error(.unknown))
|
||||
@@ -114,13 +116,19 @@ struct LinkNewDeviceScreen_Previews: PreviewProvider, TestablePreview {
|
||||
LinkNewDeviceScreen(context: viewModel.context)
|
||||
}
|
||||
.previewDisplayName("Ready")
|
||||
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.mode).map { $0 == .readyToLink(isGeneratingCode: false) })
|
||||
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.mode).map { $0 == .readyToLink(.idle) })
|
||||
|
||||
ElementNavigationStack {
|
||||
LinkNewDeviceScreen(context: verifyingViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Verifying")
|
||||
.snapshotPreferences(expect: verifyingViewModel.context.observe(\.viewState.mode).map { $0 == .readyToLink(.verifyingDeviceOwner) })
|
||||
|
||||
ElementNavigationStack {
|
||||
LinkNewDeviceScreen(context: generatingViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Generating")
|
||||
.snapshotPreferences(expect: generatingViewModel.context.observe(\.viewState.mode).map { $0 == .readyToLink(isGeneratingCode: true) })
|
||||
.snapshotPreferences(expect: generatingViewModel.context.observe(\.viewState.mode).map { $0 == .readyToLink(.generatingCode) })
|
||||
|
||||
ElementNavigationStack {
|
||||
LinkNewDeviceScreen(context: loadingViewModel.context)
|
||||
@@ -142,6 +150,7 @@ struct LinkNewDeviceScreen_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
static func makeViewModel(mode: LinkNewDeviceScreenViewState.Mode) -> LinkNewDeviceScreenViewModel {
|
||||
LinkNewDeviceScreenViewModel(clientProxy: ClientProxyMock(.init()),
|
||||
appLockService: AppLockServiceMock.mock(),
|
||||
initialState: .init(mode: mode,
|
||||
showLinkDesktopComputerButton: true))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user