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:
Doug
2026-01-07 12:18:39 +00:00
committed by GitHub
parent 9269f3057b
commit c1bb838517
62 changed files with 959 additions and 388 deletions
@@ -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()