General tidy-up related to QR codes. (#4865)

* Tidy-up Rust to Swift mapping.

* Refactor out a dedicated QRCodeErrorView.

* Use the new QRCodeErrorView for most error state snapshots.

* Simplify QRCodeErrorView structure.

Also updates the background to match the designs.

* Fix a small compile error in the unit tests.
This commit is contained in:
Doug
2025-12-16 15:59:59 +00:00
committed by GitHub
parent 189da20419
commit 841cdc291a
86 changed files with 466 additions and 334 deletions
@@ -112,6 +112,7 @@ enum TestablePreviewsDictionary {
"PollOptionView_Previews" : PollOptionView_Previews.self,
"PollRoomTimelineView_Previews" : PollRoomTimelineView_Previews.self,
"PollView_Previews" : PollView_Previews.self,
"QRCodeErrorView_Previews" : QRCodeErrorView_Previews.self,
"QRCodeLoginScreen_Previews" : QRCodeLoginScreen_Previews.self,
"ReactionsSummaryView_Previews" : ReactionsSummaryView_Previews.self,
"ReadMarkerRoomTimelineView_Previews" : ReadMarkerRoomTimelineView_Previews.self,
@@ -52,21 +52,20 @@ struct QRCodeLoginScreenViewStateBindings {
enum QRCodeLoginScreenViewAction {
case cancel
case startScan
case signInManually
case openSettings
case errorAction(QRCodeErrorView.Action)
}
enum QRCodeLoginState: Equatable {
/// Initial state where the user is informed how to perform the scan
case initial
/// The camera is scanning
case scan(QRCodeLoginScanningState)
case scan(ScanningState)
/// Codes are being shown
case displayCode(QRCodeLoginDisplayCodeState)
/// Any full screen error state
case error(QRCodeLoginErrorState)
case error(ErrorState)
enum QRCodeLoginErrorState: Equatable {
enum ErrorState: Equatable {
case noCameraPermission
case connectionNotSecure
case cancelled
@@ -77,21 +76,23 @@ enum QRCodeLoginState: Equatable {
case unknown
}
enum QRCodeLoginScanningState: Equatable {
/// the QR code is scanning
enum ScanningState: Equatable {
/// The QR code is scanning.
case scanning
/// the QR code has been detected and is being processed
/// The QR code has been detected and is being processed.
case connecting
/// the QR code was scanned, but an error occurred.
/// The QR code was scanned, but an error occurred.
case scanFailed(Error)
enum Error: Equatable {
/// the QR code has been processed and is invalid
/// The QR code has been processed and is invalid.
case invalid
/// the QR code has been processed but it is for an account provider that isn't allowed.
/// The QR code has been processed but it is for an account provider that isn't allowed.
case notAllowed(scannedProvider: String, allowedProviders: [String])
/// the QR code has been processed but it belongs to a device not signed in
/// The QR code has been processed but it belongs to a device not signed in.
case deviceNotSignedIn
/// Expected a QR code for a new device, however the processed code belongs to a device that is already signed in.
case deviceAlreadySignedIn
var title: String {
switch self {
@@ -101,6 +102,8 @@ enum QRCodeLoginState: Equatable {
L10n.screenChangeServerErrorUnauthorizedHomeserverTitle(scannedProvider)
case .deviceNotSignedIn:
L10n.screenQrCodeLoginDeviceNotSignedInScanStateSubtitle
case .deviceAlreadySignedIn:
L10n.screenQrCodeLoginDeviceNotSignedInScanStateSubtitle // FIXME: Update string
}
}
@@ -112,6 +115,8 @@ enum QRCodeLoginState: Equatable {
L10n.screenChangeServerErrorUnauthorizedHomeserverContent(allowedProviders.formatted(.list(type: .and)))
case .deviceNotSignedIn:
L10n.screenQrCodeLoginDeviceNotSignedInScanStateDescription
case .deviceAlreadySignedIn:
L10n.screenQrCodeLoginDeviceNotSignedInScanStateDescription // FIXME: Update string
}
}
}
@@ -35,13 +35,13 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
override func process(viewAction: QRCodeLoginScreenViewAction) {
switch viewAction {
case .cancel:
case .cancel, .errorAction(.cancel):
actionsSubject.send(.cancel)
case .startScan:
case .startScan, .errorAction(.startScan):
Task { await startScanIfPossible() }
case .openSettings:
case .errorAction(.openSettings):
appMediator.openAppSettings()
case .signInManually:
case .errorAction(.signInManually):
actionsSubject.send(.signInManually)
}
}
@@ -128,6 +128,8 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
state.state = .scan(.scanFailed(.notAllowed(scannedProvider: scannedProvider, allowedProviders: allowedProviders)))
case .deviceNotSignedIn:
state.state = .scan(.scanFailed(.deviceNotSignedIn))
case .deviceAlreadySignedIn:
state.state = .scan(.scanFailed(.deviceAlreadySignedIn))
case .cancelled:
state.state = .error(.cancelled)
case .connectionInsecure:
@@ -140,7 +142,7 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
state.state = .error(.expired)
case .deviceNotSupported:
state.state = .error(.deviceNotSupported)
case .deviceAlreadySignedIn, .unknown:
case .unknown:
state.state = .error(.unknown)
}
}
@@ -0,0 +1,220 @@
//
// 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 SwiftUI
struct QRCodeErrorView: View {
let errorState: QRCodeLoginState.ErrorState
let canSignInManually: Bool
enum Action { case openSettings, startScan, signInManually, cancel }
let action: (Action) -> Void
var title: String {
switch errorState {
case .noCameraPermission:
L10n.screenQrCodeLoginNoCameraPermissionStateTitle
case .connectionNotSecure:
L10n.screenQrCodeLoginConnectionNoteSecureStateTitle
case .cancelled:
L10n.screenQrCodeLoginErrorCancelledTitle
case .declined:
L10n.screenQrCodeLoginErrorDeclinedTitle
case .expired:
L10n.screenQrCodeLoginErrorExpiredTitle
case .linkingNotSupported:
L10n.screenQrCodeLoginErrorLinkingNotSuportedTitle
case .deviceNotSupported:
L10n.screenQrCodeLoginErrorSlidingSyncNotSupportedTitle(InfoPlistReader.main.bundleDisplayName)
case .unknown:
L10n.commonSomethingWentWrong
}
}
var subtitle: String {
switch errorState {
case .noCameraPermission:
L10n.screenQrCodeLoginNoCameraPermissionStateDescription(InfoPlistReader.main.productionAppName)
case .connectionNotSecure:
L10n.screenQrCodeLoginConnectionNoteSecureStateDescription
case .cancelled:
L10n.screenQrCodeLoginErrorCancelledSubtitle
case .declined:
L10n.screenQrCodeLoginErrorDeclinedSubtitle
case .expired:
L10n.screenQrCodeLoginErrorExpiredSubtitle
case .linkingNotSupported:
L10n.screenQrCodeLoginErrorLinkingNotSuportedSubtitle(InfoPlistReader.main.bundleDisplayName)
case .deviceNotSupported:
L10n.screenQrCodeLoginErrorSlidingSyncNotSupportedSubtitle(InfoPlistReader.main.bundleDisplayName)
case .unknown:
L10n.screenQrCodeLoginUnknownErrorDescription
}
}
var body: some View {
FullscreenDialog {
header
} bottomContent: {
footer
}
.padding(.horizontal, 24)
}
@ViewBuilder
private var header: some View {
switch errorState {
case .noCameraPermission:
VStack(spacing: 16) {
BigIcon(icon: \.takePhotoSolid, style: .defaultSolid)
VStack(spacing: 8) {
Text(title)
.foregroundColor(.compound.textPrimary)
.font(.compound.headingMDBold)
.multilineTextAlignment(.center)
Text(subtitle)
.foregroundColor(.compound.textSecondary)
.font(.compound.bodyMD)
.multilineTextAlignment(.center)
}
}
case .connectionNotSecure:
VStack(spacing: 40) {
VStack(spacing: 16) {
BigIcon(icon: \.errorSolid, style: .alertSolid)
VStack(spacing: 8) {
Text(title)
.foregroundColor(.compound.textPrimary)
.font(.compound.headingMDBold)
.multilineTextAlignment(.center)
Text(subtitle)
.foregroundColor(.compound.textSecondary)
.font(.compound.bodyMD)
.multilineTextAlignment(.center)
}
}
VStack(spacing: 24) {
Text(L10n.screenQrCodeLoginConnectionNoteSecureStateListHeader)
.foregroundColor(.compound.textPrimary)
.font(.compound.bodyLGSemibold)
.multilineTextAlignment(.center)
SFNumberedListView(items: [
AttributedString(L10n.screenQrCodeLoginConnectionNoteSecureStateListItem1),
AttributedString(L10n.screenQrCodeLoginConnectionNoteSecureStateListItem2),
AttributedString(L10n.screenQrCodeLoginConnectionNoteSecureStateListItem3)
])
}
}
default:
simpleErrorStack
}
}
@ViewBuilder
private var simpleErrorStack: some View {
VStack(spacing: 16) {
BigIcon(icon: \.errorSolid, style: .alertSolid)
VStack(spacing: 8) {
Text(title)
.foregroundColor(.compound.textPrimary)
.font(.compound.headingMDBold)
.multilineTextAlignment(.center)
Text(subtitle)
.foregroundColor(.compound.textSecondary)
.font(.compound.bodyMD)
.multilineTextAlignment(.center)
}
}
}
@ViewBuilder
private var footer: some View {
switch errorState {
case .noCameraPermission:
Button(L10n.screenQrCodeLoginNoCameraPermissionButton) {
action(.openSettings)
}
.buttonStyle(.compound(.primary))
case .connectionNotSecure, .unknown, .expired, .declined, .deviceNotSupported:
Button(L10n.screenQrCodeLoginStartOverButton) {
action(.startScan)
}
.buttonStyle(.compound(.primary))
case .cancelled:
Button(L10n.actionTryAgain) {
action(.startScan)
}
.buttonStyle(.compound(.primary))
case .linkingNotSupported:
VStack(spacing: 16) {
if canSignInManually {
Button(L10n.screenOnboardingSignInManually) {
action(.signInManually)
}
.buttonStyle(.compound(.primary))
}
Button(L10n.actionCancel) {
action(.cancel)
}
.buttonStyle(.compound(.tertiary))
}
}
}
}
// MARK: - Previews
struct QRCodeErrorView_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
QRCodeErrorViewSnapshot(errorState: .noCameraPermission, canSignInManually: false)
.previewDisplayName("No Camera Permission")
QRCodeErrorViewSnapshot(errorState: .connectionNotSecure, canSignInManually: false)
.previewDisplayName("Connection not secure")
QRCodeErrorViewSnapshot(errorState: .linkingNotSupported, canSignInManually: true)
.previewDisplayName("Linking unsupported")
QRCodeErrorViewSnapshot(errorState: .linkingNotSupported, canSignInManually: false)
.previewDisplayName("Linking unsupported restricted flow")
QRCodeErrorViewSnapshot(errorState: .cancelled, canSignInManually: false)
.previewDisplayName("Cancelled")
QRCodeErrorViewSnapshot(errorState: .declined, canSignInManually: false)
.previewDisplayName("Declined")
QRCodeErrorViewSnapshot(errorState: .expired, canSignInManually: false)
.previewDisplayName("Expired")
QRCodeErrorViewSnapshot(errorState: .deviceNotSupported, canSignInManually: false)
.previewDisplayName("Device not supported")
QRCodeErrorViewSnapshot(errorState: .unknown, canSignInManually: false)
.previewDisplayName("Unknown error")
}
}
private struct QRCodeErrorViewSnapshot: View {
let errorState: QRCodeLoginState.ErrorState
let canSignInManually: Bool
var body: some View {
NavigationStack {
QRCodeErrorView(errorState: errorState, canSignInManually: canSignInManually) { _ in }
.toolbar(.visible, for: .navigationBar)
}
}
}
@@ -13,13 +13,21 @@ struct QRCodeLoginScreen: View {
@ObservedObject var context: QRCodeLoginScreenViewModel.Context
@State private var qrFrame = CGRect.zero
var backgroundStyle: Color {
if case .error = context.viewState.state {
.compound.bgCanvasDefault
} else {
.compound.bgSubtleSecondary
}
}
var body: some View {
NavigationStack {
mainContent
.toolbar { toolbar }
.toolbar(.visible, for: .navigationBar)
.background()
.backgroundStyle(.compound.bgSubtleSecondary)
.backgroundStyle(backgroundStyle)
.interactiveDismissDisabled()
}
}
@@ -33,8 +41,10 @@ struct QRCodeLoginScreen: View {
qrScanContent
case .displayCode:
displayCodeContent
case .error:
errorContent
case .error(let errorState):
QRCodeErrorView(errorState: errorState, canSignInManually: context.viewState.canSignInManually) { action in
context.send(viewAction: .errorAction(action))
}
}
}
@@ -218,157 +228,6 @@ struct QRCodeLoginScreen: View {
}
}
}
@ViewBuilder
private var errorContent: some View {
if case let .error(errorState) = context.viewState.state {
FullscreenDialog {
errorContentHeader(errorState: errorState)
} bottomContent: {
errorContentFooter(errorState: errorState)
}
.padding(.horizontal, 24)
}
}
@ViewBuilder
private func errorContentHeader(errorState: QRCodeLoginState.QRCodeLoginErrorState) -> some View {
switch errorState {
case .noCameraPermission:
VStack(spacing: 16) {
BigIcon(icon: \.takePhotoSolid, style: .default)
VStack(spacing: 8) {
Text(L10n.screenQrCodeLoginNoCameraPermissionStateTitle)
.foregroundColor(.compound.textPrimary)
.font(.compound.headingMDBold)
.multilineTextAlignment(.center)
Text(L10n.screenQrCodeLoginNoCameraPermissionStateDescription(InfoPlistReader.main.productionAppName))
.foregroundColor(.compound.textSecondary)
.font(.compound.bodyMD)
.multilineTextAlignment(.center)
}
}
case .connectionNotSecure:
VStack(spacing: 40) {
VStack(spacing: 16) {
BigIcon(icon: \.errorSolid, style: .alert)
VStack(spacing: 8) {
Text(L10n.screenQrCodeLoginConnectionNoteSecureStateTitle)
.foregroundColor(.compound.textPrimary)
.font(.compound.headingMDBold)
.multilineTextAlignment(.center)
Text(L10n.screenQrCodeLoginConnectionNoteSecureStateDescription)
.foregroundColor(.compound.textSecondary)
.font(.compound.bodyMD)
.multilineTextAlignment(.center)
}
}
VStack(spacing: 24) {
Text(L10n.screenQrCodeLoginConnectionNoteSecureStateListHeader)
.foregroundColor(.compound.textPrimary)
.font(.compound.bodyLGSemibold)
.multilineTextAlignment(.center)
SFNumberedListView(items: context.viewState.connectionNotSecureListItems)
}
}
default:
simpleErrorStack(errorState: errorState)
}
}
@ViewBuilder
private func simpleErrorStack(errorState: QRCodeLoginState.QRCodeLoginErrorState) -> some View {
let title = switch errorState {
case .cancelled:
L10n.screenQrCodeLoginErrorCancelledTitle
case .declined:
L10n.screenQrCodeLoginErrorDeclinedTitle
case .expired:
L10n.screenQrCodeLoginErrorExpiredTitle
case .linkingNotSupported:
L10n.screenQrCodeLoginErrorLinkingNotSuportedTitle
case .deviceNotSupported:
L10n.screenQrCodeLoginErrorSlidingSyncNotSupportedTitle(InfoPlistReader.main.bundleDisplayName)
case .unknown:
L10n.commonSomethingWentWrong
default:
fatalError("This should not be displayed")
}
let subtitle: String = switch errorState {
case .cancelled:
L10n.screenQrCodeLoginErrorCancelledSubtitle
case .declined:
L10n.screenQrCodeLoginErrorDeclinedSubtitle
case .expired:
L10n.screenQrCodeLoginErrorExpiredSubtitle
case .linkingNotSupported:
L10n.screenQrCodeLoginErrorLinkingNotSuportedSubtitle(InfoPlistReader.main.bundleDisplayName)
case .deviceNotSupported:
L10n.screenQrCodeLoginErrorSlidingSyncNotSupportedSubtitle(InfoPlistReader.main.bundleDisplayName)
case .unknown:
L10n.screenQrCodeLoginUnknownErrorDescription
default:
fatalError("This should not be displayed")
}
VStack(spacing: 16) {
BigIcon(icon: \.errorSolid, style: .alert)
VStack(spacing: 8) {
Text(title)
.foregroundColor(.compound.textPrimary)
.font(.compound.headingMDBold)
.multilineTextAlignment(.center)
Text(subtitle)
.foregroundColor(.compound.textSecondary)
.font(.compound.bodyMD)
.multilineTextAlignment(.center)
}
}
}
@ViewBuilder
private func errorContentFooter(errorState: QRCodeLoginState.QRCodeLoginErrorState) -> some View {
switch errorState {
case .noCameraPermission:
Button(L10n.screenQrCodeLoginNoCameraPermissionButton) {
context.send(viewAction: .openSettings)
}
.buttonStyle(.compound(.primary))
case .connectionNotSecure, .unknown, .expired, .declined, .deviceNotSupported:
Button(L10n.screenQrCodeLoginStartOverButton) {
context.send(viewAction: .startScan)
}
.buttonStyle(.compound(.primary))
case .cancelled:
Button(L10n.actionTryAgain) {
context.send(viewAction: .startScan)
}
.buttonStyle(.compound(.primary))
case .linkingNotSupported:
VStack(spacing: 16) {
if context.viewState.canSignInManually {
Button(L10n.screenOnboardingSignInManually) {
context.send(viewAction: .signInManually)
}
.buttonStyle(.compound(.primary))
}
Button(L10n.actionCancel) {
context.send(viewAction: .cancel)
}
.buttonStyle(.compound(.tertiary))
}
}
}
}
private struct QRScannerViewOverlay: View {
@@ -420,23 +279,8 @@ struct QRCodeLoginScreen_Previews: PreviewProvider, TestablePreview {
static let verificationCodeStateViewModel = QRCodeLoginScreenViewModel.mock(state: .displayCode(.verificationCode("123456")))
// Errors
static let noCameraPermissionStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.noCameraPermission))
static let connectionNotSecureStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.connectionNotSecure))
static let linkingUnsupportedStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.linkingNotSupported))
static let linkingUnsupportedRestrictedFlowViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.linkingNotSupported), canSignInManually: false)
static let cancelledStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.cancelled))
static let declinedStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.declined))
static let expiredStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.expired))
static let deviceNoSupportedViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.deviceNotSupported))
static let unknownErrorStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.unknown))
// Errors (no need to test them all QRCodeErrorView covers that).
static let errorStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.declined))
static var previews: some View {
QRCodeLoginScreen(context: initialStateViewModel.context)
@@ -463,30 +307,7 @@ struct QRCodeLoginScreen_Previews: PreviewProvider, TestablePreview {
QRCodeLoginScreen(context: verificationCodeStateViewModel.context)
.previewDisplayName("Verification code")
QRCodeLoginScreen(context: noCameraPermissionStateViewModel.context)
.previewDisplayName("No Camera Permission")
QRCodeLoginScreen(context: connectionNotSecureStateViewModel.context)
.previewDisplayName("Connection not secure")
QRCodeLoginScreen(context: linkingUnsupportedStateViewModel.context)
.previewDisplayName("Linking unsupported")
QRCodeLoginScreen(context: linkingUnsupportedRestrictedFlowViewModel.context)
.previewDisplayName("Linking unsupported restricted flow")
QRCodeLoginScreen(context: cancelledStateViewModel.context)
.previewDisplayName("Cancelled")
QRCodeLoginScreen(context: declinedStateViewModel.context)
.previewDisplayName("Declined")
QRCodeLoginScreen(context: expiredStateViewModel.context)
.previewDisplayName("Expired")
QRCodeLoginScreen(context: deviceNoSupportedViewModel.context)
.previewDisplayName("Device not supported")
QRCodeLoginScreen(context: unknownErrorStateViewModel.context)
.previewDisplayName("Unknown error")
QRCodeLoginScreen(context: errorStateViewModel.context)
.previewDisplayName("Error")
}
}
@@ -6,8 +6,9 @@
//
import Combine
import Foundation
import CoreImage.CIFilterBuiltins
import MatrixRustSDK
import SwiftUI
class LinkNewDeviceService {
typealias GenerateProgressPublisher = CurrentValuePublisher<GenerateProgress, QRCodeLoginError>
@@ -15,9 +16,9 @@ class LinkNewDeviceService {
enum GenerateProgress {
case starting
case qrReady(QrCodeData)
case qrReady(UIImage)
case qrScanned(CheckCodeSenderProtocol)
case waitingForAuthorisation(verificationURI: String)
case waitingForAuthorisation(verificationURL: URL)
case syncingSecrets
case done
}
@@ -25,7 +26,7 @@ class LinkNewDeviceService {
enum ScanProgress {
case starting
case establishingSecureChannel(checkCode: UInt8, checkCodeString: String)
case waitingForAuth(verificationURI: String)
case waitingForAuthorisation(verificationURL: URL)
case syncingSecrets
case done
}
@@ -38,7 +39,14 @@ class LinkNewDeviceService {
func generateQRCode() -> GenerateProgressPublisher {
let progressSubject = CurrentValueSubject<GenerateProgress, QRCodeLoginError>(.starting)
let listener = SDKListener { progressSubject.send(.init(rustProgress: $0)) }
let listener = SDKListener {
do {
try progressSubject.send(.init(rustProgress: $0))
} catch {
MXLog.error("Invalid GenerateProgress")
progressSubject.send(completion: .failure(.unknown))
}
}
Task {
do {
@@ -58,7 +66,14 @@ class LinkNewDeviceService {
func scanQRCode(_ scannedQRData: Data) -> ScanProgressPublisher {
let progressSubject = CurrentValueSubject<ScanProgress, QRCodeLoginError>(.starting)
let listener = SDKListener { progressSubject.send(.init(rustProgress: $0)) }
let listener = SDKListener {
do {
try progressSubject.send(.init(rustProgress: $0))
} catch {
MXLog.error("Invalid ScanProgress")
progressSubject.send(completion: .failure(.unknown))
}
}
let qrCodeData: QrCodeData
do {
@@ -89,12 +104,28 @@ class LinkNewDeviceService {
}
extension LinkNewDeviceService.GenerateProgress: CustomStringConvertible {
init(rustProgress: GrantGeneratedQrLoginProgress) {
enum Error: Swift.Error {
case invalidQRCodeData
case invalidVerificationURI(String)
}
init(rustProgress: GrantGeneratedQrLoginProgress) throws {
self = switch rustProgress {
case .starting: .starting
case .qrReady(let qrCode): .qrReady(qrCode)
case .qrReady(let qrCode):
if let image = UIImage(qrCodeData: qrCode.toBytes()) {
.qrReady(image)
} else {
throw Error.invalidQRCodeData
}
case .qrScanned(let checkCodeSender): .qrScanned(checkCodeSender)
case .waitingForAuth(let verificationUri): .waitingForAuthorisation(verificationURI: verificationUri)
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
}
@@ -113,11 +144,19 @@ extension LinkNewDeviceService.GenerateProgress: CustomStringConvertible {
}
extension LinkNewDeviceService.ScanProgress: CustomStringConvertible {
init(rustProgress: GrantQrLoginProgress) {
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 .waitingForAuth(let verificationUri): .waitingForAuth(verificationURI: verificationUri)
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
}
@@ -127,7 +166,7 @@ extension LinkNewDeviceService.ScanProgress: CustomStringConvertible {
switch self {
case .starting: "starting"
case .establishingSecureChannel: "establishingSecureChannel"
case .waitingForAuth: "waitingForAuth"
case .waitingForAuthorisation: "waitingForAuthorisation"
case .syncingSecrets: "syncingSecrets"
case .done: "done"
}
@@ -146,3 +185,21 @@ private extension QRCodeLoginError {
}
}
}
private 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)
}
}