Files
letro-ios/ElementX/Sources/Services/Authentication/LinkNewDeviceService.swift
Mauro 9442b6981f Rename Sign Out to Remove Device + Update SDK 26.03.18 (#5280)
* update the sdk

* updated preview tests

# Conflicts:
#	ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift
#	ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LiveLocationRoomTimelineView.swift

* update sdk to 26.03.18 + regenerated preview tests after sign out copy change

* updated UI tests

* pr suggestions
2026-03-18 17:34:45 +01:00

252 lines
9.5 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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 CoreImage.CIFilterBuiltins
import MatrixRustSDK
import SwiftUI
// sourcery: AutoMockable
protocol LinkNewDeviceServiceProtocol {
/// Links a new device by showing it a QR code.
func linkMobileDevice() -> LinkNewDeviceService.LinkMobileProgressPublisher
/// Links a new device using a QR code generated by said device.
func linkDesktopDevice(with scannedQRData: Data) -> LinkNewDeviceService.LinkDesktopProgressPublisher
}
class LinkNewDeviceService: LinkNewDeviceServiceProtocol {
/// 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>
/// The progress of linking a new device by showing it a QR code.
enum LinkMobileProgress: Equatable {
case starting
case qrReady(UIImage)
case qrScanned(CheckCodeSenderProxy)
case waitingForAuthorisation(verificationURL: URL)
case syncingSecrets
case done
}
/// The progress of linking a new device by scanning a QR code generated by said device.
enum LinkDesktopProgress: Equatable {
case starting
case establishingSecureChannel(checkCodeString: String)
case waitingForAuthorisation(verificationURL: URL)
case syncingSecrets
case done
}
private let grantLoginHandler: GrantLoginWithQrCodeHandlerProtocol
init(handler: GrantLoginWithQrCodeHandlerProtocol) {
grantLoginHandler = handler
}
/// 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))
} catch {
MXLog.error("Invalid GenerateProgress")
progressSubject.send(completion: .failure(.unknown))
}
}
Task {
do {
// Note: The SDK doesn't provide us with a way to cancel the 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()
}
/// 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))
} catch {
MXLog.error("Invalid ScanProgress")
progressSubject.send(completion: .failure(.unknown))
}
}
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()
}
// At some stage the SDK will have a `qrCodeData.intent` which we should check before continuing here.
// Note the equivalent check will also happen for sign in with QR code in the AuthenticationService.
Task {
do {
// Note: The SDK doesn't provide us with a way to cancel the grant if the user hit 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.LinkMobileProgress: CustomStringConvertible {
enum Error: Swift.Error {
case invalidQRCodeData
case invalidVerificationURI(String)
}
init(rustProgress: GrantGeneratedQrLoginProgress) throws {
self = switch rustProgress {
case .starting: .starting
case .qrReady(let qrCode):
if let image = UIImage(qrCodeData: qrCode.toBytes()) {
.qrReady(image)
} else {
throw Error.invalidQRCodeData
}
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) {
.waitingForAuthorisation(verificationURL: url)
} else {
throw Error.invalidVerificationURI(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.LinkDesktopProgress: CustomStringConvertible {
enum Error: Swift.Error { case invalidVerificationURI(String) }
init(rustProgress: GrantQrLoginProgress) throws {
self = switch rustProgress {
case .starting: .starting
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) {
.waitingForAuthorisation(verificationURL: url)
} else {
throw Error.invalidVerificationURI(verificationURI)
}
case .syncingSecrets: .syncingSecrets
case .done: .done
}
}
var description: String {
switch self {
case .starting: "starting"
case .establishingSecureChannel: "establishingSecureChannel"
case .waitingForAuthorisation: "waitingForAuthorisation"
case .syncingSecrets: "syncingSecrets"
case .done: "done"
}
}
}
private extension QRCodeLoginError {
init(rustError: HumanQrGrantLoginError) {
self = switch rustError {
case .InvalidCheckCode, .ConnectionInsecure:
.connectionInsecure
case .UnsupportedProtocol:
.linkingNotSupported
case .Expired, .NotFound, .DeviceNotFound:
.expired
case .Cancelled:
.cancelled
case .OtherDeviceAlreadySignedIn:
.deviceAlreadySignedIn
case .UnsupportedQrCodeType:
.invalidQRCode
case .Unknown, .MissingSecretsBackup, .DeviceIdAlreadyInUse:
.unknown
}
}
}
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)
}
/// Bypassed for now whilst we wait for an SDK update (however its worth noting that
/// things should still fail if the wrong code is provided, just not necessarily with
/// the right error being shown). https://github.com/matrix-org/matrix-rust-sdk/pull/5957
func validate(checkCode: UInt8) -> Bool {
true
}
}
extension UIImage {
convenience init?(qrCodeData: Data) {
let qrContext = CIContext()
let qrFilter = CIFilter.qrCodeGenerator()
qrFilter.message = qrCodeData
qrFilter.correctionLevel = "Q"
guard let outputImage = qrFilter.outputImage,
let cgImage = qrContext.createCGImage(outputImage, from: outputImage.extent) else {
MXLog.error("Failed to generate an image from the supplied QR code data.")
return nil
}
self.init(cgImage: cgImage)
}
}