Add support for linking new devices in the QRCodeLoginScreen. (#4891)
* Adds the remaining parts for showing/scanning a QR code to link a new device. * Refactor the QRCodeLoginService to work the same way as the LinkNewDeviceService.
This commit is contained in:
@@ -24,11 +24,6 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { homeserverSubject.asCurrentValuePublisher() }
|
||||
private(set) var flow: AuthenticationFlow
|
||||
|
||||
private let qrLoginProgressSubject = PassthroughSubject<QrLoginProgress, Never>()
|
||||
var qrLoginProgressPublisher: AnyPublisher<QrLoginProgress, Never> {
|
||||
qrLoginProgressSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(userSessionStore: UserSessionStoreProtocol,
|
||||
encryptionKeyProvider: EncryptionKeyProviderProtocol,
|
||||
clientFactory: AuthenticationClientFactoryProtocol = AuthenticationClientFactory(),
|
||||
@@ -153,43 +148,59 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func loginWithQRCode(data: Data) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
func loginWithQRCode(data: Data) -> QRLoginProgressPublisher {
|
||||
let progressSubject = CurrentValueSubject<QRLoginProgress, AuthenticationServiceError>(.starting)
|
||||
|
||||
let qrData: QrCodeData
|
||||
do {
|
||||
qrData = try QrCodeData.fromBytes(bytes: data)
|
||||
} catch {
|
||||
MXLog.error("QRCode decode error: \(error)")
|
||||
return .failure(.qrCodeError(.invalidQRCode))
|
||||
progressSubject.send(completion: .failure(.qrCodeError(.invalidQRCode)))
|
||||
return progressSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
guard let scannedServerName = qrData.serverName() else {
|
||||
MXLog.error("The QR code is from a device that is not yet signed in.")
|
||||
return .failure(.qrCodeError(.deviceNotSignedIn))
|
||||
progressSubject.send(completion: .failure(.qrCodeError(.deviceNotSignedIn)))
|
||||
return progressSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
if !appSettings.allowOtherAccountProviders, !appSettings.accountProviders.contains(scannedServerName) {
|
||||
MXLog.error("The scanned device's server is not allowed: \(scannedServerName)")
|
||||
return .failure(.qrCodeError(.providerNotAllowed(scannedProvider: scannedServerName, allowedProviders: appSettings.accountProviders)))
|
||||
progressSubject.send(completion: .failure(.qrCodeError(.providerNotAllowed(scannedProvider: scannedServerName, allowedProviders: appSettings.accountProviders))))
|
||||
return progressSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
let listener = SDKListener { [weak self] progress in
|
||||
self?.qrLoginProgressSubject.send(progress)
|
||||
let listener = SDKListener { progress in
|
||||
guard let progress = QRLoginProgress(rustProgress: progress) else { return }
|
||||
progressSubject.send(progress)
|
||||
}
|
||||
|
||||
do {
|
||||
let client = try await makeClient(homeserverAddress: scannedServerName)
|
||||
let qrCodeHandler = client.newLoginWithQrCodeHandler(oidcConfiguration: appSettings.oidcConfiguration.rustValue)
|
||||
try await qrCodeHandler.scan(qrCodeData: qrData, progressListener: listener)
|
||||
return await userSession(for: client)
|
||||
} catch let error as HumanQrLoginError {
|
||||
MXLog.error("QRCode login error: \(error)")
|
||||
return .failure(error.serviceError)
|
||||
} catch RemoteSettingsError.elementProRequired(let serverName) {
|
||||
return .failure(.elementProRequired(serverName: serverName))
|
||||
} catch {
|
||||
MXLog.error("QRCode login unknown error: \(error)")
|
||||
return .failure(.qrCodeError(.unknown))
|
||||
Task {
|
||||
do {
|
||||
let client = try await makeClient(homeserverAddress: scannedServerName)
|
||||
let qrCodeHandler = client.newLoginWithQrCodeHandler(oidcConfiguration: appSettings.oidcConfiguration.rustValue)
|
||||
try await qrCodeHandler.scan(qrCodeData: qrData, progressListener: listener)
|
||||
|
||||
switch await userSession(for: client) {
|
||||
case .success(let userSession):
|
||||
progressSubject.send(.signedIn(userSession))
|
||||
case .failure(let error):
|
||||
progressSubject.send(completion: .failure(error))
|
||||
}
|
||||
} catch let error as HumanQrLoginError {
|
||||
MXLog.error("QRCode login error: \(error)")
|
||||
progressSubject.send(completion: .failure(error.serviceError))
|
||||
} catch RemoteSettingsError.elementProRequired(let serverName) {
|
||||
progressSubject.send(completion: .failure(.elementProRequired(serverName: serverName)))
|
||||
} catch {
|
||||
MXLog.error("QRCode login unknown error: \(error)")
|
||||
progressSubject.send(completion: .failure(.qrCodeError(.unknown)))
|
||||
}
|
||||
}
|
||||
|
||||
return progressSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
func reset() {
|
||||
|
||||
@@ -111,7 +111,58 @@ enum QRCodeLoginError: Error, Equatable {
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol QRCodeLoginServiceProtocol {
|
||||
var qrLoginProgressPublisher: AnyPublisher<QrLoginProgress, Never> { get }
|
||||
|
||||
func loginWithQRCode(data: Data) async -> Result<UserSessionProtocol, AuthenticationServiceError>
|
||||
typealias QRLoginProgressPublisher = CurrentValuePublisher<QRLoginProgress, AuthenticationServiceError>
|
||||
func loginWithQRCode(data: Data) -> QRLoginProgressPublisher
|
||||
}
|
||||
|
||||
enum QRLoginProgress {
|
||||
case starting
|
||||
case establishingSecureChannel(checkCode: UInt8, checkCodeString: String)
|
||||
case waitingForToken(userCode: String)
|
||||
case syncingSecrets
|
||||
case signedIn(UserSessionProtocol)
|
||||
|
||||
init?(rustProgress: QrLoginProgress) {
|
||||
switch rustProgress {
|
||||
case .starting:
|
||||
self = .starting
|
||||
case .establishingSecureChannel(let checkCode, let checkCodeString):
|
||||
self = .establishingSecureChannel(checkCode: checkCode, checkCodeString: checkCodeString)
|
||||
case .waitingForToken(let userCode):
|
||||
self = .waitingForToken(userCode: userCode)
|
||||
case .syncingSecrets:
|
||||
self = .syncingSecrets
|
||||
case .done:
|
||||
return nil // The SDK is done, but the app still needs to set up the UserSession.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension QRLoginProgress: Equatable, CustomStringConvertible {
|
||||
static func == (lhs: QRLoginProgress, rhs: QRLoginProgress) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.starting, .starting):
|
||||
true
|
||||
case let (.establishingSecureChannel(lhsCheckCode, lhsCheckCodeString), .establishingSecureChannel(rhsCheckCode, rhsCheckCodeString)):
|
||||
lhsCheckCode == rhsCheckCode && lhsCheckCodeString == rhsCheckCodeString
|
||||
case let (.waitingForToken(lhsUserCode), .waitingForToken(rhsUserCode)):
|
||||
lhsUserCode == rhsUserCode
|
||||
case (.syncingSecrets, .syncingSecrets):
|
||||
true
|
||||
case (.signedIn, .signedIn):
|
||||
true
|
||||
default:
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .starting: "starting"
|
||||
case .establishingSecureChannel: "establishingSecureChannel"
|
||||
case .waitingForToken: "waitingForToken"
|
||||
case .syncingSecrets: "syncingSecrets"
|
||||
case .signedIn: "signedIn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,21 +11,25 @@ import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
class LinkNewDeviceService {
|
||||
typealias GenerateProgressPublisher = CurrentValuePublisher<GenerateProgress, QRCodeLoginError>
|
||||
typealias ScanProgressPublisher = CurrentValuePublisher<ScanProgress, QRCodeLoginError>
|
||||
/// Publishes the progress of linking a new device by showing it a QR code.
|
||||
typealias LinkMobileProgressPublisher = CurrentValuePublisher<LinkMobileProgress, QRCodeLoginError>
|
||||
/// Publishes the progress of linking a new device by scanning a QR code generated by said device.
|
||||
typealias LinkDesktopProgressPublisher = CurrentValuePublisher<LinkDesktopProgress, QRCodeLoginError>
|
||||
|
||||
enum GenerateProgress {
|
||||
/// The progress of linking a new device by showing it a QR code.
|
||||
enum LinkMobileProgress: Equatable {
|
||||
case starting
|
||||
case qrReady(UIImage)
|
||||
case qrScanned(CheckCodeSenderProtocol)
|
||||
case qrScanned(CheckCodeSenderProxy)
|
||||
case waitingForAuthorisation(verificationURL: URL)
|
||||
case syncingSecrets
|
||||
case done
|
||||
}
|
||||
|
||||
enum ScanProgress {
|
||||
|
||||
/// The progress of linking a new device by scanning a QR code generated by said device.
|
||||
enum LinkDesktopProgress: Equatable {
|
||||
case starting
|
||||
case establishingSecureChannel(checkCode: UInt8, checkCodeString: String)
|
||||
case establishingSecureChannel(checkCodeString: String)
|
||||
case waitingForAuthorisation(verificationURL: URL)
|
||||
case syncingSecrets
|
||||
case done
|
||||
@@ -37,8 +41,9 @@ class LinkNewDeviceService {
|
||||
grantLoginHandler = handler
|
||||
}
|
||||
|
||||
func generateQRCode() -> GenerateProgressPublisher {
|
||||
let progressSubject = CurrentValueSubject<GenerateProgress, QRCodeLoginError>(.starting)
|
||||
/// Links a new device by showing it a QR code.
|
||||
func linkMobileDevice() -> LinkMobileProgressPublisher {
|
||||
let progressSubject = CurrentValueSubject<LinkMobileProgress, QRCodeLoginError>(.starting)
|
||||
let listener = SDKListener {
|
||||
do {
|
||||
try progressSubject.send(.init(rustProgress: $0))
|
||||
@@ -64,8 +69,9 @@ class LinkNewDeviceService {
|
||||
return progressSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
func scanQRCode(_ scannedQRData: Data) -> ScanProgressPublisher {
|
||||
let progressSubject = CurrentValueSubject<ScanProgress, QRCodeLoginError>(.starting)
|
||||
/// Links a new device using a QR code generated by said device.
|
||||
func linkDesktopDevice(with scannedQRData: Data) -> LinkDesktopProgressPublisher {
|
||||
let progressSubject = CurrentValueSubject<LinkDesktopProgress, QRCodeLoginError>(.starting)
|
||||
let listener = SDKListener {
|
||||
do {
|
||||
try progressSubject.send(.init(rustProgress: $0))
|
||||
@@ -84,7 +90,8 @@ class LinkNewDeviceService {
|
||||
return progressSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
#warning("Check intent/server name here…")
|
||||
#warning("Check intent/server name??")
|
||||
#warning("Check Element Pro here??")
|
||||
|
||||
Task {
|
||||
do {
|
||||
@@ -103,7 +110,7 @@ class LinkNewDeviceService {
|
||||
}
|
||||
}
|
||||
|
||||
extension LinkNewDeviceService.GenerateProgress: CustomStringConvertible {
|
||||
extension LinkNewDeviceService.LinkMobileProgress: CustomStringConvertible {
|
||||
enum Error: Swift.Error {
|
||||
case invalidQRCodeData
|
||||
case invalidVerificationURI(String)
|
||||
@@ -118,7 +125,7 @@ extension LinkNewDeviceService.GenerateProgress: CustomStringConvertible {
|
||||
} else {
|
||||
throw Error.invalidQRCodeData
|
||||
}
|
||||
case .qrScanned(let checkCodeSender): .qrScanned(checkCodeSender)
|
||||
case .qrScanned(let checkCodeSender): .qrScanned(.init(underlyingSender: checkCodeSender))
|
||||
case .waitingForAuth(let verificationURI):
|
||||
// verificationURI is a String; ASWebAuthenticationSession requires a URL.
|
||||
if let url = URL(string: verificationURI) {
|
||||
@@ -143,13 +150,13 @@ extension LinkNewDeviceService.GenerateProgress: CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
extension LinkNewDeviceService.ScanProgress: CustomStringConvertible {
|
||||
extension LinkNewDeviceService.LinkDesktopProgress: CustomStringConvertible {
|
||||
enum Error: Swift.Error { case invalidVerificationURI(String) }
|
||||
|
||||
init(rustProgress: GrantQrLoginProgress) throws {
|
||||
self = switch rustProgress {
|
||||
case .starting: .starting
|
||||
case .establishingSecureChannel(let checkCode, let checkCodeString): .establishingSecureChannel(checkCode: checkCode, checkCodeString: checkCodeString)
|
||||
case .establishingSecureChannel(_, let checkCodeString): .establishingSecureChannel(checkCodeString: checkCodeString)
|
||||
case .waitingForAuth(let verificationURI):
|
||||
// verificationURI is a String; ASWebAuthenticationSession requires a URL.
|
||||
if let url = URL(string: verificationURI) {
|
||||
@@ -186,7 +193,26 @@ private extension QRCodeLoginError {
|
||||
}
|
||||
}
|
||||
|
||||
private extension UIImage {
|
||||
class CheckCodeSenderProxy: Equatable {
|
||||
static func == (lhs: CheckCodeSenderProxy, rhs: CheckCodeSenderProxy) -> Bool {
|
||||
lhs.underlyingSender === rhs.underlyingSender
|
||||
}
|
||||
|
||||
let underlyingSender: CheckCodeSenderProtocol
|
||||
|
||||
init(underlyingSender: CheckCodeSenderProtocol) {
|
||||
self.underlyingSender = underlyingSender
|
||||
}
|
||||
|
||||
func send(code: UInt8) async throws {
|
||||
try await underlyingSender.send(code: code)
|
||||
}
|
||||
|
||||
#warning("Waiting for an SDK update to use the underlying sender.")
|
||||
func validate(checkCode: UInt8) -> Bool { true }
|
||||
}
|
||||
|
||||
extension UIImage {
|
||||
convenience init?(qrCodeData: Data) {
|
||||
let qrContext = CIContext()
|
||||
let qrFilter = CIFilter.qrCodeGenerator()
|
||||
|
||||
Reference in New Issue
Block a user