mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
QR Code Login Flow (#2767)
Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com> Co-authored-by: Hugh Nimmo-Smith <hughns@element.io>
This commit is contained in:
+7
-3
@@ -31,9 +31,13 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
|
||||
self.appSettings = appSettings
|
||||
super.init(initialViewState: AuthenticationStartScreenViewState())
|
||||
if !ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
appSettings.$qrCodeLoginEnabled
|
||||
.weakAssign(to: \.state.isQRCodeLoginEnabled, on: self)
|
||||
.store(in: &cancellables)
|
||||
if appSettings.isDevelopmentBuild {
|
||||
state.isQRCodeLoginEnabled = true
|
||||
} else {
|
||||
appSettings.$qrCodeLoginEnabled
|
||||
.weakAssign(to: \.state.isQRCodeLoginEnabled, on: self)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ struct QRCodeLoginScreenCoordinatorParameters {
|
||||
|
||||
enum QRCodeLoginScreenCoordinatorAction {
|
||||
case cancel
|
||||
case signInManually
|
||||
case done(userSession: UserSessionProtocol)
|
||||
}
|
||||
|
||||
final class QRCodeLoginScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -52,8 +54,12 @@ final class QRCodeLoginScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .signInManually:
|
||||
self.actionsSubject.send(.signInManually)
|
||||
case .cancel:
|
||||
self.actionsSubject.send(.cancel)
|
||||
case .done(let userSession):
|
||||
self.actionsSubject.send(.done(userSession: userSession))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -18,6 +18,8 @@ import Foundation
|
||||
|
||||
enum QRCodeLoginScreenViewModelAction {
|
||||
case cancel
|
||||
case signInManually
|
||||
case done(userSession: UserSessionProtocol)
|
||||
}
|
||||
|
||||
struct QRCodeLoginScreenViewState: BindableState {
|
||||
@@ -44,11 +46,18 @@ struct QRCodeLoginScreenViewState: BindableState {
|
||||
AttributedString(L10n.screenQrCodeLoginConnectionNoteSecureStateListItem2),
|
||||
AttributedString(L10n.screenQrCodeLoginConnectionNoteSecureStateListItem3)
|
||||
]
|
||||
|
||||
var bindings = QRCodeLoginScreenViewStateBindings()
|
||||
}
|
||||
|
||||
struct QRCodeLoginScreenViewStateBindings {
|
||||
var qrResult: Data?
|
||||
}
|
||||
|
||||
enum QRCodeLoginScreenViewAction {
|
||||
case cancel
|
||||
case startScan
|
||||
case signInManually
|
||||
case openSettings
|
||||
}
|
||||
|
||||
@@ -65,6 +74,11 @@ enum QRCodeLoginState: Equatable {
|
||||
enum QRCodeLoginErrorState: Equatable {
|
||||
case noCameraPermission
|
||||
case connectionNotSecure
|
||||
case cancelled
|
||||
case declined
|
||||
case expired
|
||||
case linkingNotSupported
|
||||
case deviceNotSupported
|
||||
case unknown
|
||||
}
|
||||
|
||||
@@ -75,6 +89,8 @@ enum QRCodeLoginState: Equatable {
|
||||
case connecting
|
||||
/// the qr code has been processed and is invalid
|
||||
case invalid
|
||||
/// the qr code has been processed but it belongs to a device not signed in,
|
||||
case deviceNotSignedIn
|
||||
}
|
||||
|
||||
enum QRCodeLoginDisplayCodeState: Equatable {
|
||||
@@ -99,4 +115,42 @@ enum QRCodeLoginState: Equatable {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isScanning: Bool {
|
||||
switch self {
|
||||
case .scan(let state):
|
||||
return state == .scanning
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isError: Bool {
|
||||
switch self {
|
||||
case .error:
|
||||
return true
|
||||
case let .scan(state):
|
||||
switch state {
|
||||
case .invalid, .deviceNotSignedIn:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var shouldDisplayCancelButton: Bool {
|
||||
switch self {
|
||||
case .initial:
|
||||
return true
|
||||
case .scan:
|
||||
return true
|
||||
case .error(let error):
|
||||
return error == .noCameraPermission
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,8 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Combine
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
typealias QRCodeLoginScreenViewModelType = StateStoreViewModel<QRCodeLoginScreenViewState, QRCodeLoginScreenViewAction>
|
||||
|
||||
@@ -28,12 +27,15 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
|
||||
var actionsPublisher: AnyPublisher<QRCodeLoginScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private var scanTask: Task<Void, Never>?
|
||||
|
||||
init(qrCodeLoginService: QRCodeLoginServiceProtocol,
|
||||
appMediator: AppMediatorProtocol) {
|
||||
self.qrCodeLoginService = qrCodeLoginService
|
||||
self.appMediator = appMediator
|
||||
super.init(initialViewState: QRCodeLoginScreenViewState())
|
||||
setupSubscriptions()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -46,16 +48,109 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
|
||||
Task { await startScanIfPossible() }
|
||||
case .openSettings:
|
||||
appMediator.openAppSettings()
|
||||
case .signInManually:
|
||||
actionsSubject.send(.signInManually)
|
||||
}
|
||||
}
|
||||
|
||||
private func startScanIfPossible() async {
|
||||
state.state = await qrCodeLoginService.requestAuthorizationIfNeeded() ? .scan(.scanning) : .error(.noCameraPermission)
|
||||
// MARK: - Private
|
||||
|
||||
private func setupSubscriptions() {
|
||||
context.$viewState
|
||||
// not using compactMap before remove duplicates because if there is an error, and the same code needs to be rescanned the transition to nil to clean the state would get ignored.
|
||||
.map(\.bindings.qrResult)
|
||||
.removeDuplicates()
|
||||
.compactMap { $0 }
|
||||
// this needs to be received on the main actor or the state change for connecting won't work properly
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] qrData in
|
||||
self?.handleScan(qrData: qrData)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
qrCodeLoginService.qrLoginProgressPublisher
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] progress in
|
||||
MXLog.info("QR Login Progress changed to: \(progress)")
|
||||
|
||||
guard let self,
|
||||
// Let's not advance the state if the current state is already invalid
|
||||
!state.state.isError else {
|
||||
return
|
||||
}
|
||||
|
||||
switch progress {
|
||||
case .establishingSecureChannel(_, let stringCode):
|
||||
state.state = .displayCode(.deviceCode(stringCode))
|
||||
case .waitingForToken(let code):
|
||||
state.state = .displayCode(.verificationCode(code))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func startScanIfPossible() async {
|
||||
state.bindings.qrResult = nil
|
||||
state.state = await appMediator.requestAuthorizationIfNeeded() ? .scan(.scanning) : .error(.noCameraPermission)
|
||||
}
|
||||
|
||||
private func handleScan(qrData: Data) {
|
||||
guard scanTask == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
state.state = .scan(.connecting)
|
||||
|
||||
scanTask = Task { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
defer {
|
||||
scanTask = nil
|
||||
}
|
||||
|
||||
MXLog.info("Scanning QR code: \(qrData)")
|
||||
switch await qrCodeLoginService.loginWithQRCode(data: qrData) {
|
||||
case let .success(session):
|
||||
MXLog.info("QR Login completed")
|
||||
actionsSubject.send(.done(userSession: session))
|
||||
case .failure(let error):
|
||||
handleError(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleError(error: QRCodeLoginServiceError) {
|
||||
MXLog.error("Failed to scan the QR code: \(error)")
|
||||
switch error {
|
||||
case .invalidQRCode:
|
||||
state.state = .scan(.invalid)
|
||||
case .deviceNotSignedIn:
|
||||
state.state = .scan(.deviceNotSignedIn)
|
||||
case .cancelled:
|
||||
state.state = .error(.cancelled)
|
||||
case .connectionInsecure:
|
||||
state.state = .error(.connectionNotSecure)
|
||||
case .declined:
|
||||
state.state = .error(.declined)
|
||||
case .linkingNotSupported:
|
||||
state.state = .error(.linkingNotSupported)
|
||||
case .expired:
|
||||
state.state = .error(.expired)
|
||||
case .deviceNotSupported:
|
||||
state.state = .error(.deviceNotSupported)
|
||||
case .failedLoggingIn, .unknown:
|
||||
state.state = .error(.unknown)
|
||||
}
|
||||
}
|
||||
|
||||
/// Only for mocking initial states
|
||||
fileprivate init(state: QRCodeLoginState) {
|
||||
qrCodeLoginService = QRCodeLoginServiceMock(configuration: .init())
|
||||
qrCodeLoginService = QRCodeLoginServiceMock()
|
||||
appMediator = AppMediatorMock.default
|
||||
super.init(initialViewState: .init(state: state))
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ struct QRCodeLoginScreen: View {
|
||||
SFNumberedListView(items: context.viewState.initialStateListItems)
|
||||
}
|
||||
} bottomContent: {
|
||||
Button(L10n.actionContinue) {
|
||||
Button(L10n.screenQrCodeLoginInitialStateButtonTitle) {
|
||||
context.send(viewAction: .startScan)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
@@ -178,8 +178,11 @@ struct QRCodeLoginScreen: View {
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Label(L10n.screenQrCodeLoginInvalidScanStateSubtitle, icon: \.error, iconSize: .medium, relativeTo: .compound.bodyMDSemibold)
|
||||
VStack(spacing: 4) {
|
||||
Label(L10n.screenQrCodeLoginInvalidScanStateSubtitle,
|
||||
icon: \.error,
|
||||
iconSize: .medium,
|
||||
relativeTo: .compound.bodyMDSemibold)
|
||||
.labelStyle(.custom(spacing: 10))
|
||||
.font(.compound.bodyMDSemibold)
|
||||
.foregroundColor(.compound.textCriticalPrimary)
|
||||
@@ -187,6 +190,29 @@ struct QRCodeLoginScreen: View {
|
||||
Text(L10n.screenQrCodeLoginInvalidScanStateDescription)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.font(.compound.bodySM)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
case .deviceNotSignedIn:
|
||||
VStack(spacing: 16) {
|
||||
Button(L10n.screenQrCodeLoginInvalidScanStateRetryButton) {
|
||||
context.send(viewAction: .startScan)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Label(L10n.screenQrCodeLoginDeviceNotSignedInScanStateSubtitle,
|
||||
icon: \.error,
|
||||
iconSize: .medium,
|
||||
relativeTo: .compound.bodyMDSemibold)
|
||||
.labelStyle(.custom(spacing: 10))
|
||||
.font(.compound.bodyMDSemibold)
|
||||
.foregroundColor(.compound.textCriticalPrimary)
|
||||
|
||||
Text(L10n.screenQrCodeLoginDeviceNotSignedInScanStateDescription)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.font(.compound.bodySM)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,7 +220,7 @@ struct QRCodeLoginScreen: View {
|
||||
}
|
||||
|
||||
private var qrScanner: some View {
|
||||
QRCodeScannerView()
|
||||
QRCodeScannerView(result: $context.qrResult, isScanning: context.viewState.state.isScanning)
|
||||
.aspectRatio(1.0, contentMode: .fill)
|
||||
.frame(maxWidth: 312)
|
||||
.readFrame($qrFrame)
|
||||
@@ -207,7 +233,7 @@ struct QRCodeLoginScreen: View {
|
||||
@ToolbarContentBuilder
|
||||
private var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
if !context.viewState.state.isDisplayingCode {
|
||||
if context.viewState.state.shouldDisplayCancelButton {
|
||||
Button(L10n.actionCancel) {
|
||||
context.send(viewAction: .cancel)
|
||||
}
|
||||
@@ -273,25 +299,65 @@ struct QRCodeLoginScreen: View {
|
||||
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:
|
||||
VStack(spacing: 16) {
|
||||
HeroImage(icon: \.error, style: .critical)
|
||||
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) {
|
||||
HeroImage(icon: \.error, style: .critical)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(title)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(L10n.commonSomethingWentWrong)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(L10n.screenQrCodeLoginUnknownErrorDescription)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.font(.compound.bodyMD)
|
||||
.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:
|
||||
@@ -299,11 +365,30 @@ struct QRCodeLoginScreen: View {
|
||||
context.send(viewAction: .openSettings)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
case .connectionNotSecure, .unknown:
|
||||
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) {
|
||||
Button(L10n.screenOnboardingSignInManually) {
|
||||
context.send(viewAction: .signInManually)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
|
||||
Button(L10n.actionCancel) {
|
||||
context.send(viewAction: .cancel)
|
||||
}
|
||||
.padding(.vertical, 13)
|
||||
.frame(maxWidth: .infinity)
|
||||
.buttonStyle(.compound(.plain))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,24 +421,40 @@ private struct QRScannerViewOverlay: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct QRCodeLoginScreen_Previews: PreviewProvider, TestablePreview {
|
||||
// Initial
|
||||
static let initialStateViewModel = QRCodeLoginScreenViewModel.mock(state: .initial)
|
||||
|
||||
// Scanning
|
||||
static let scanningStateViewModel = QRCodeLoginScreenViewModel.mock(state: .scan(.scanning))
|
||||
|
||||
static let connectingStateViewModel = QRCodeLoginScreenViewModel.mock(state: .scan(.connecting))
|
||||
|
||||
static let invalidStateViewModel = QRCodeLoginScreenViewModel.mock(state: .scan(.invalid))
|
||||
|
||||
static let deviceNotSignedInStateViewModel = QRCodeLoginScreenViewModel.mock(state: .scan(.deviceNotSignedIn))
|
||||
|
||||
// Display Code
|
||||
static let deviceCodeStateViewModel = QRCodeLoginScreenViewModel.mock(state: .displayCode(.deviceCode("12")))
|
||||
|
||||
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 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))
|
||||
|
||||
static let deviceCodeStateViewModel = QRCodeLoginScreenViewModel.mock(state: .displayCode(.deviceCode("12")))
|
||||
|
||||
static let verificationCodeStateViewModel = QRCodeLoginScreenViewModel.mock(state: .displayCode(.verificationCode("123456")))
|
||||
|
||||
static var previews: some View {
|
||||
QRCodeLoginScreen(context: initialStateViewModel.context)
|
||||
.previewDisplayName("Initial")
|
||||
@@ -367,19 +468,37 @@ struct QRCodeLoginScreen_Previews: PreviewProvider, TestablePreview {
|
||||
QRCodeLoginScreen(context: invalidStateViewModel.context)
|
||||
.previewDisplayName("Invalid")
|
||||
|
||||
QRCodeLoginScreen(context: noCameraPermissionStateViewModel.context)
|
||||
.previewDisplayName("No Camera Permission")
|
||||
|
||||
QRCodeLoginScreen(context: connectionNotSecureStateViewModel.context)
|
||||
.previewDisplayName("Connection not secure")
|
||||
|
||||
QRCodeLoginScreen(context: unknownErrorStateViewModel.context)
|
||||
.previewDisplayName("Unknown error")
|
||||
QRCodeLoginScreen(context: deviceNotSignedInStateViewModel.context)
|
||||
.previewDisplayName("Device not signed in")
|
||||
|
||||
QRCodeLoginScreen(context: deviceCodeStateViewModel.context)
|
||||
.previewDisplayName("Device code")
|
||||
|
||||
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: 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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,48 @@ import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct QRCodeScannerView: UIViewControllerRepresentable {
|
||||
@Binding var result: Data?
|
||||
var isScanning: Bool
|
||||
|
||||
func makeUIViewController(context: Context) -> QRScannerController {
|
||||
let controller = QRScannerController()
|
||||
|
||||
controller.delegate = context.coordinator
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: QRScannerController, context: Context) { }
|
||||
func updateUIViewController(_ uiViewController: QRScannerController, context: Context) {
|
||||
if isScanning {
|
||||
uiViewController.startScan()
|
||||
} else {
|
||||
uiViewController.stopScan()
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator($result)
|
||||
}
|
||||
|
||||
final class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
|
||||
@Binding var scanResult: Data?
|
||||
|
||||
init(_ scanResult: Binding<Data?>) {
|
||||
_scanResult = scanResult
|
||||
}
|
||||
|
||||
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
|
||||
// Check if the metadataObjects array is not nil and it contains at least one object.
|
||||
guard metadataObjects.count > 0,
|
||||
let metadataObj = metadataObjects[0] as? AVMetadataMachineReadableCodeObject,
|
||||
metadataObj.type == AVMetadataObject.ObjectType.qr,
|
||||
let data = metadataObj.binaryValue else {
|
||||
MXLog.info("QRCodeScannerView: invalid qr scan")
|
||||
return
|
||||
}
|
||||
|
||||
scanResult = data
|
||||
MXLog.info("QRCodeScannerView: scanned data")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class QRScannerController: UIViewController {
|
||||
@@ -33,8 +68,8 @@ final class QRScannerController: UIViewController {
|
||||
private var videoPreviewLayer: AVCaptureVideoPreviewLayer?
|
||||
private var qrCodeFrameView: UIView?
|
||||
|
||||
var delegate: AVCaptureMetadataOutputObjectsDelegate?
|
||||
|
||||
weak var delegate: AVCaptureMetadataOutputObjectsDelegate?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
@@ -73,15 +108,26 @@ final class QRScannerController: UIViewController {
|
||||
videoPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
|
||||
videoPreviewLayer?.frame = view.layer.bounds
|
||||
view.layer.addSublayer(previewLayer)
|
||||
|
||||
// Start video capture.
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.captureSession.startRunning()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillLayoutSubviews() {
|
||||
super.viewWillLayoutSubviews()
|
||||
videoPreviewLayer?.frame = view.layer.bounds
|
||||
}
|
||||
|
||||
func startScan() {
|
||||
// Start video capture.
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.captureSession.startRunning()
|
||||
MXLog.info("QRCodeScannerView: capture session started")
|
||||
}
|
||||
}
|
||||
|
||||
func stopScan() {
|
||||
// Stop video capture.
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.captureSession.stopRunning()
|
||||
MXLog.info("QRCodeScannerView: capture session stopped")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-6
@@ -39,12 +39,6 @@ struct DeveloperOptionsScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section("QR Code") {
|
||||
Toggle(isOn: $context.qrCodeLoginEnabled) {
|
||||
Text("QR code login")
|
||||
}
|
||||
}
|
||||
|
||||
Section("Room") {
|
||||
Toggle(isOn: $context.shouldCollapseRoomStateEvents) {
|
||||
Text("Collapse room state events")
|
||||
|
||||
Reference in New Issue
Block a user