Ensure that the current user is the device owner before linking a new device. (#5773)

This commit is contained in:
Doug
2026-06-26 16:24:18 +01:00
committed by GitHub
parent 3f3aa79a48
commit f3da459d16
23 changed files with 558 additions and 18 deletions
@@ -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)