Add a service and flow coordinator for the LinkNewDevice feature. (#4859)

* Add a LinkNewDeviceService that exposes the SDK's grant QR code login methods.

* Add a flow coordinator for linking a new device.

Changes the presentation too.
This commit is contained in:
Doug
2025-12-15 14:44:26 +00:00
committed by GitHub
parent 27ca265991
commit 93dfc84a57
38 changed files with 536 additions and 73 deletions
@@ -9,7 +9,7 @@ import Combine
import SwiftUI
enum LinkNewDeviceScreenCoordinatorAction {
case linkMobileDevice
case linkMobileDevice(LinkNewDeviceService.GenerateProgressPublisher)
case linkDesktopComputer
case dismiss
}
@@ -38,8 +38,8 @@ final class LinkNewDeviceScreenCoordinator: CoordinatorProtocol {
guard let self else { return }
switch action {
case .linkMobileDevice:
actionsSubject.send(.linkMobileDevice)
case .linkMobileDevice(let progressPublisher):
actionsSubject.send(.linkMobileDevice(progressPublisher))
case .linkDesktopComputer:
actionsSubject.send(.linkDesktopComputer)
case .dismiss:
@@ -8,7 +8,7 @@
import Foundation
enum LinkNewDeviceScreenViewModelAction {
case linkMobileDevice
case linkMobileDevice(LinkNewDeviceService.GenerateProgressPublisher)
case linkDesktopComputer
case dismiss
}
@@ -6,6 +6,7 @@
//
import Combine
import MatrixRustSDK
import SwiftUI
typealias LinkNewDeviceScreenViewModelType = StateStoreViewModelV2<LinkNewDeviceScreenViewState, LinkNewDeviceScreenViewAction>
@@ -33,7 +34,7 @@ class LinkNewDeviceScreenViewModel: LinkNewDeviceScreenViewModelType, LinkNewDev
switch viewAction {
case .linkMobileDevice:
linkMobileDevice()
Task { await linkMobileDevice() }
case .linkDesktopComputer:
actionsSubject.send(.linkDesktopComputer)
case .dismiss:
@@ -51,11 +52,27 @@ class LinkNewDeviceScreenViewModel: LinkNewDeviceScreenViewModelType, LinkNewDev
}
}
private func linkMobileDevice() {
private func linkMobileDevice() async {
state.mode = .readyToLink(isGeneratingCode: true)
// TODO: Generate a QR code.
let linkNewDeviceService = clientProxy.linkNewDeviceService()
actionsSubject.send(.linkMobileDevice)
let progressPublisher = linkNewDeviceService.generateQRCode()
do {
_ = try await progressPublisher.values
.first { progress in
switch progress {
case .qrReady: true
default: false
}
}
actionsSubject.send(.linkMobileDevice(progressPublisher))
state.mode = .readyToLink(isGeneratingCode: false)
} catch {
#warning("Needs some form of re-usable error handling, will handle with the next screen.")
state.mode = .notSupported
}
}
}
@@ -21,6 +21,7 @@ struct LinkNewDeviceScreen: View {
.backgroundStyle(.compound.bgSubtleSecondary)
.navigationTitle(L10n.commonLinkNewDevice)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
}
@ViewBuilder
@@ -86,11 +87,13 @@ struct LinkNewDeviceScreen: View {
}
}
.buttonStyle(.compound(.primary))
.accessibilityIdentifier(A11yIdentifiers.linkNewDeviceScreen.mobileDevice)
Button { context.send(viewAction: .linkDesktopComputer) } label: {
Label(L10n.screenLinkNewDeviceRootDesktopComputer, icon: \.computer)
}
.buttonStyle(.compound(.primary))
.accessibilityIdentifier(A11yIdentifiers.linkNewDeviceScreen.desktopComputer)
}
.disabled(isGeneratingCode)
case .notSupported:
@@ -100,6 +103,15 @@ struct LinkNewDeviceScreen: View {
.buttonStyle(.compound(.primary))
}
}
var toolbar: some ToolbarContent {
ToolbarItem(placement: .cancellationAction) {
Button(L10n.actionCancel) {
context.send(viewAction: .dismiss)
}
.accessibilityIdentifier(A11yIdentifiers.linkNewDeviceScreen.cancel)
}
}
}
// MARK: - Previews
@@ -148,6 +160,7 @@ struct LinkNewDeviceScreen_Previews: PreviewProvider, TestablePreview {
return false
}
}
clientProxy.linkNewDeviceServiceReturnValue = .init(handler: GrantLoginWithQrCodeHandlerSDKMock(.init(generateDelay: .seconds(20))))
let viewModel = LinkNewDeviceScreenViewModel(clientProxy: clientProxy)