mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user