mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:02:10 +00:00
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:
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
enum LinkNewDeviceFlowCoordinatorAction {
|
||||
case requestOIDCAuthorisation(URL)
|
||||
case dismiss
|
||||
}
|
||||
|
||||
class LinkNewDeviceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let navigationStackCoordinator: NavigationStackCoordinator
|
||||
private let flowParameters: CommonFlowParameters
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<LinkNewDeviceFlowCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<LinkNewDeviceFlowCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(navigationStackCoordinator: NavigationStackCoordinator,
|
||||
flowParameters: CommonFlowParameters) {
|
||||
self.navigationStackCoordinator = navigationStackCoordinator
|
||||
self.flowParameters = flowParameters
|
||||
}
|
||||
|
||||
func start(animated: Bool) {
|
||||
presentLinkNewDeviceScreen()
|
||||
}
|
||||
|
||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func clearRoute(animated: Bool) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
private func presentLinkNewDeviceScreen() {
|
||||
let coordinator = LinkNewDeviceScreenCoordinator(parameters: .init(clientProxy: flowParameters.userSession.clientProxy))
|
||||
coordinator.actionsPublisher
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .linkMobileDevice(let progressPublisher):
|
||||
break
|
||||
case .linkDesktopComputer:
|
||||
break
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private var bugReportFlowCoordinator: BugReportFlowCoordinator?
|
||||
// periphery:ignore - retaining purpose
|
||||
private var encryptionSettingsFlowCoordinator: EncryptionSettingsFlowCoordinator?
|
||||
// periphery:ignore - retaining purpose
|
||||
private var linkNewDeviceFlowCoordinator: LinkNewDeviceFlowCoordinator?
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
@@ -84,7 +86,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case .userDetails:
|
||||
presentUserDetailsEditScreen()
|
||||
case .linkNewDevice:
|
||||
presentLinkNewDeviceScreen()
|
||||
startLinkNewDeviceFlow()
|
||||
case let .manageAccount(url):
|
||||
presentAccountManagementURL(url)
|
||||
case .analytics:
|
||||
@@ -169,6 +171,31 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private func startLinkNewDeviceFlow() {
|
||||
let stackCoordinator = NavigationStackCoordinator()
|
||||
let flowCoordinator = LinkNewDeviceFlowCoordinator(navigationStackCoordinator: stackCoordinator,
|
||||
flowParameters: flowParameters)
|
||||
flowCoordinator.actionsPublisher
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .dismiss:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
case .requestOIDCAuthorisation(let url):
|
||||
presentAccountManagementURL(url)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
linkNewDeviceFlowCoordinator = flowCoordinator
|
||||
flowCoordinator.start()
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in
|
||||
self?.linkNewDeviceFlowCoordinator = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func presentAnalyticsScreen() {
|
||||
let coordinator = AnalyticsSettingsScreenCoordinator(parameters: .init(appSettings: flowParameters.appSettings,
|
||||
analytics: flowParameters.analytics))
|
||||
@@ -259,9 +286,9 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
|
||||
// MARK: OIDC Account Management
|
||||
|
||||
|
||||
private var accountSettingsPresenter: OIDCAccountSettingsPresenter?
|
||||
private func presentAccountManagementURL(_ url: URL) {
|
||||
// Note to anyone in the future if you come back here to make this open in Safari instead of a WAS.
|
||||
@@ -271,27 +298,4 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
appSettings: flowParameters.appSettings)
|
||||
accountSettingsPresenter?.start()
|
||||
}
|
||||
|
||||
// MARK: - Link New Device
|
||||
|
||||
private func presentLinkNewDeviceScreen() {
|
||||
let parameters = LinkNewDeviceScreenCoordinatorParameters(clientProxy: flowParameters.userSession.clientProxy)
|
||||
let coordinator = LinkNewDeviceScreenCoordinator(parameters: parameters)
|
||||
coordinator.actionsPublisher
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .linkMobileDevice:
|
||||
break
|
||||
case .linkDesktopComputer:
|
||||
break
|
||||
case .dismiss:
|
||||
navigationStackCoordinator.pop()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4019,6 +4019,70 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
|
||||
return removeUserAvatarReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - linkNewDeviceService
|
||||
|
||||
var linkNewDeviceServiceUnderlyingCallsCount = 0
|
||||
var linkNewDeviceServiceCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return linkNewDeviceServiceUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = linkNewDeviceServiceUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
linkNewDeviceServiceUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
linkNewDeviceServiceUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var linkNewDeviceServiceCalled: Bool {
|
||||
return linkNewDeviceServiceCallsCount > 0
|
||||
}
|
||||
|
||||
var linkNewDeviceServiceUnderlyingReturnValue: LinkNewDeviceService!
|
||||
var linkNewDeviceServiceReturnValue: LinkNewDeviceService! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return linkNewDeviceServiceUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: LinkNewDeviceService? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = linkNewDeviceServiceUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
linkNewDeviceServiceUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
linkNewDeviceServiceUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var linkNewDeviceServiceClosure: (() -> LinkNewDeviceService)?
|
||||
|
||||
func linkNewDeviceService() -> LinkNewDeviceService {
|
||||
linkNewDeviceServiceCallsCount += 1
|
||||
if let linkNewDeviceServiceClosure = linkNewDeviceServiceClosure {
|
||||
return linkNewDeviceServiceClosure()
|
||||
} else {
|
||||
return linkNewDeviceServiceReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - deactivateAccount
|
||||
|
||||
var deactivateAccountPasswordEraseDataUnderlyingCallsCount = 0
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
extension GrantLoginWithQrCodeHandlerSDKMock {
|
||||
struct Configuration {
|
||||
var generateDelay: Duration = .seconds(0)
|
||||
var generatedBase64QRCode = """
|
||||
TUFUUklYAgS0yzZ1QVpQ1jlnoxWX3d5jrWRFfELxjS2gN7pz9y+3PABaaHR0
|
||||
cHM6Ly9zeW5hcHNlLW9pZGMubGFiLmVsZW1lbnQuZGV2L19zeW5hcHNlL2Ns
|
||||
aWVudC9yZW5kZXp2b3VzLzAxSFg5SzAwUTFINktQRDQ3RUc0RzFUM1hHACVo
|
||||
dHRwczovL3N5bmFwc2Utb2lkYy5sYWIuZWxlbWVudC5kZXYv
|
||||
"""
|
||||
}
|
||||
|
||||
convenience init(_ configuration: Configuration) {
|
||||
self.init()
|
||||
|
||||
generateProgressListenerClosure = { listener in
|
||||
Task {
|
||||
try await Task.sleep(for: configuration.generateDelay)
|
||||
let bytes = Data(base64Encoded: configuration.generatedBase64QRCode) ?? Data()
|
||||
try listener.onUpdate(state: .qrReady(qrCode: .fromBytes(bytes: bytes)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ enum A11yIdentifiers {
|
||||
static let roomPollsHistoryScreen = RoomPollsHistoryScreen()
|
||||
static let manageRoomMemberSheet = ManageRoomMemberSheet()
|
||||
static let spaceListScreen = SpaceListScreen()
|
||||
static let linkNewDeviceScreen = LinkNewDeviceScreen()
|
||||
|
||||
struct AlertInfo {
|
||||
let primaryButton = "alert_info-primary_button"
|
||||
@@ -314,4 +315,10 @@ enum A11yIdentifiers {
|
||||
"\(roomNamePrefix):\(name)"
|
||||
}
|
||||
}
|
||||
|
||||
struct LinkNewDeviceScreen {
|
||||
let cancel = "link_new_device_screen-cancel"
|
||||
let mobileDevice = "link_new_device_screen-mobile_device"
|
||||
let desktopComputer = "link_new_device_screen-desktop_computer"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,14 @@ extension SDKListener: QrLoginProgressListener where T == QrLoginProgress {
|
||||
func onUpdate(state: QrLoginProgress) { onUpdateClosure(state) }
|
||||
}
|
||||
|
||||
extension SDKListener: GrantQrLoginProgressListener where T == GrantQrLoginProgress {
|
||||
func onUpdate(state: GrantQrLoginProgress) { onUpdateClosure(state) }
|
||||
}
|
||||
|
||||
extension SDKListener: GrantGeneratedQrLoginProgressListener where T == GrantGeneratedQrLoginProgress {
|
||||
func onUpdate(state: GrantGeneratedQrLoginProgress) { onUpdateClosure(state) }
|
||||
}
|
||||
|
||||
// MARK: ClientProxy
|
||||
|
||||
extension SDKListener: MediaPreviewConfigListener where T == MediaPreviewConfig? {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
|
||||
state.state = .error(.expired)
|
||||
case .deviceNotSupported:
|
||||
state.state = .error(.deviceNotSupported)
|
||||
case .unknown:
|
||||
case .deviceAlreadySignedIn, .unknown:
|
||||
state.state = .error(.unknown)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ enum QRCodeLoginError: Error, Equatable {
|
||||
case expired
|
||||
case deviceNotSupported
|
||||
case deviceNotSignedIn
|
||||
case deviceAlreadySignedIn
|
||||
case unknown
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
class LinkNewDeviceService {
|
||||
typealias GenerateProgressPublisher = CurrentValuePublisher<GenerateProgress, QRCodeLoginError>
|
||||
typealias ScanProgressPublisher = CurrentValuePublisher<ScanProgress, QRCodeLoginError>
|
||||
|
||||
enum GenerateProgress {
|
||||
case starting
|
||||
case qrReady(QrCodeData)
|
||||
case qrScanned(CheckCodeSenderProtocol)
|
||||
case waitingForAuthorisation(verificationURI: String)
|
||||
case syncingSecrets
|
||||
case done
|
||||
}
|
||||
|
||||
enum ScanProgress {
|
||||
case starting
|
||||
case establishingSecureChannel(checkCode: UInt8, checkCodeString: String)
|
||||
case waitingForAuth(verificationURI: String)
|
||||
case syncingSecrets
|
||||
case done
|
||||
}
|
||||
|
||||
private let grantLoginHandler: GrantLoginWithQrCodeHandlerProtocol
|
||||
|
||||
init(handler: GrantLoginWithQrCodeHandlerProtocol) {
|
||||
grantLoginHandler = handler
|
||||
}
|
||||
|
||||
func generateQRCode() -> GenerateProgressPublisher {
|
||||
let progressSubject = CurrentValueSubject<GenerateProgress, QRCodeLoginError>(.starting)
|
||||
let listener = SDKListener { progressSubject.send(.init(rustProgress: $0)) }
|
||||
|
||||
Task {
|
||||
do {
|
||||
// TODO: we need a way to cancel the in progress grant if the user hit the cancel button
|
||||
try await grantLoginHandler.generate(progressListener: listener) // The success state is handled by the listener.
|
||||
} catch let error as HumanQrGrantLoginError {
|
||||
MXLog.error("QR code reciprocate error: \(error)")
|
||||
progressSubject.send(completion: .failure(.init(rustError: error)))
|
||||
} catch {
|
||||
MXLog.error("QR code reciprocate unknown error: \(error)")
|
||||
progressSubject.send(completion: .failure(.unknown))
|
||||
}
|
||||
}
|
||||
|
||||
return progressSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
func scanQRCode(_ scannedQRData: Data) -> ScanProgressPublisher {
|
||||
let progressSubject = CurrentValueSubject<ScanProgress, QRCodeLoginError>(.starting)
|
||||
let listener = SDKListener { progressSubject.send(.init(rustProgress: $0)) }
|
||||
|
||||
let qrCodeData: QrCodeData
|
||||
do {
|
||||
qrCodeData = try QrCodeData.fromBytes(bytes: scannedQRData)
|
||||
} catch {
|
||||
MXLog.error("QR code decode error: \(error)")
|
||||
progressSubject.send(completion: .failure(.invalidQRCode))
|
||||
return progressSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
#warning("Check intent/server name here…")
|
||||
|
||||
Task {
|
||||
do {
|
||||
// TODO: it would be nice to be able to cancel the grant at the SDK level if the user hits the cancel button
|
||||
try await grantLoginHandler.scan(qrCodeData: qrCodeData, progressListener: listener) // The success state is handled by the listener.
|
||||
} catch let error as HumanQrGrantLoginError {
|
||||
MXLog.error("QR code reciprocate error: \(error)")
|
||||
progressSubject.send(completion: .failure(.init(rustError: error)))
|
||||
} catch {
|
||||
MXLog.error("QR code reciprocate unknown error: \(error)")
|
||||
progressSubject.send(completion: .failure(.unknown))
|
||||
}
|
||||
}
|
||||
|
||||
return progressSubject.asCurrentValuePublisher()
|
||||
}
|
||||
}
|
||||
|
||||
extension LinkNewDeviceService.GenerateProgress: CustomStringConvertible {
|
||||
init(rustProgress: GrantGeneratedQrLoginProgress) {
|
||||
self = switch rustProgress {
|
||||
case .starting: .starting
|
||||
case .qrReady(let qrCode): .qrReady(qrCode)
|
||||
case .qrScanned(let checkCodeSender): .qrScanned(checkCodeSender)
|
||||
case .waitingForAuth(let verificationUri): .waitingForAuthorisation(verificationURI: verificationUri)
|
||||
case .syncingSecrets: .syncingSecrets
|
||||
case .done: .done
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .starting: "starting"
|
||||
case .qrReady: "qrReady"
|
||||
case .qrScanned: "qrScanned"
|
||||
case .waitingForAuthorisation: "waitingForAuthorisation"
|
||||
case .syncingSecrets: "syncingSecrets"
|
||||
case .done: "done"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LinkNewDeviceService.ScanProgress: CustomStringConvertible {
|
||||
init(rustProgress: GrantQrLoginProgress) {
|
||||
self = switch rustProgress {
|
||||
case .starting: .starting
|
||||
case .establishingSecureChannel(let checkCode, let checkCodeString): .establishingSecureChannel(checkCode: checkCode, checkCodeString: checkCodeString)
|
||||
case .waitingForAuth(let verificationUri): .waitingForAuth(verificationURI: verificationUri)
|
||||
case .syncingSecrets: .syncingSecrets
|
||||
case .done: .done
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .starting: "starting"
|
||||
case .establishingSecureChannel: "establishingSecureChannel"
|
||||
case .waitingForAuth: "waitingForAuth"
|
||||
case .syncingSecrets: "syncingSecrets"
|
||||
case .done: "done"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension QRCodeLoginError {
|
||||
init(rustError: HumanQrGrantLoginError) {
|
||||
self = switch rustError {
|
||||
case .InvalidCheckCode:
|
||||
.connectionInsecure
|
||||
case .UnsupportedProtocol:
|
||||
.linkingNotSupported
|
||||
case .Unknown, .NotFound, .MissingSecretsBackup, .DeviceIdAlreadyInUse, .UnableToCreateDevice:
|
||||
.unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -715,13 +715,9 @@ class ClientProxy: ClientProxyProtocol {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
func logout() async {
|
||||
do {
|
||||
try await client.logout()
|
||||
} catch {
|
||||
MXLog.error("Failed logging out with error: \(error)")
|
||||
}
|
||||
|
||||
func linkNewDeviceService() -> LinkNewDeviceService {
|
||||
LinkNewDeviceService(handler: client.newGrantLoginWithQrCodeHandler())
|
||||
}
|
||||
|
||||
func deactivateAccount(password: String?, eraseData: Bool) async -> Result<Void, ClientProxyError> {
|
||||
@@ -734,6 +730,14 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func logout() async {
|
||||
do {
|
||||
try await client.logout()
|
||||
} catch {
|
||||
MXLog.error("Failed logging out with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func setPusher(with configuration: PusherConfiguration) async throws {
|
||||
try await client.setPusher(identifiers: configuration.identifiers,
|
||||
kind: configuration.kind,
|
||||
|
||||
@@ -194,7 +194,9 @@ protocol ClientProxyProtocol: AnyObject {
|
||||
func setUserAvatar(media: MediaInfo) async -> Result<Void, ClientProxyError>
|
||||
|
||||
func removeUserAvatar() async -> Result<Void, ClientProxyError>
|
||||
|
||||
|
||||
func linkNewDeviceService() -> LinkNewDeviceService
|
||||
|
||||
func deactivateAccount(password: String?, eraseData: Bool) async -> Result<Void, ClientProxyError>
|
||||
|
||||
func logout() async
|
||||
|
||||
@@ -734,6 +734,40 @@ class MockScreen: Identifiable {
|
||||
coordinator.start()
|
||||
|
||||
return navigationStackCoordinator
|
||||
case .linkNewDevice:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let flowCoordinator = LinkNewDeviceFlowCoordinator(navigationStackCoordinator: navigationStackCoordinator,
|
||||
flowParameters: CommonFlowParameters(userSession: UserSessionMock(.init()),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
elementCallService: ElementCallServiceMock(.init()),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
linkMetadataProvider: LinkMetadataProvider(),
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
appHooks: AppHooks(),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
notificationManager: NotificationManagerMock(),
|
||||
stateMachineFactory: StateMachineFactory()))
|
||||
flowCoordinator.actionsPublisher
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .dismiss:
|
||||
navigationRootCoordinator.setSheetCoordinator(nil)
|
||||
case .requestOIDCAuthorisation:
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
retainedState.append(flowCoordinator)
|
||||
flowCoordinator.start()
|
||||
|
||||
// Use a sheet on top the the placeholder so we can test the dismissal.
|
||||
navigationRootCoordinator.setSheetCoordinator(navigationStackCoordinator)
|
||||
return PlaceholderScreenCoordinator(hideBrandChrome: false)
|
||||
case .autoUpdatingTimeline:
|
||||
let appSettings: AppSettings = ServiceLocator.shared.settings
|
||||
appSettings.hasRunIdentityConfirmationOnboarding = true
|
||||
|
||||
@@ -25,6 +25,7 @@ enum UITestsScreenIdentifier: String {
|
||||
case encryptionSettings
|
||||
case encryptionSettingsOutOfSync
|
||||
case encryptionReset
|
||||
case linkNewDevice
|
||||
case roomLayoutBottom
|
||||
case roomLayoutMiddle
|
||||
case roomLayoutTop
|
||||
|
||||
Reference in New Issue
Block a user