MESSENGER-3895 qr code login

This commit is contained in:
Arnfried Griesert
2023-02-08 12:19:07 +00:00
committed by Frank Rotermund
parent fd8ab8e648
commit d7a23d53dc
15 changed files with 294 additions and 20 deletions

View File

@@ -542,6 +542,9 @@ class BWIBuildSettings: NSObject {
var netiquetteEnHTML = "netiquette_en"
var netiquetteDeHTML = "netiquette_de"
// MARK: - Scan server qr code
var scanServerQRCode = true
var allowLoginWithQR = false // should be set by the server but we disable it with false also in the app
// MARK: - Maintenance
var enableMaintenanceInfoOnWelcomeScreen = false

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "60.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "120-1.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "180.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -516,7 +516,10 @@
// MARK: - Onboarding
"onboarding_splash_login_button_title" = "Loslegen";
"authentication_server_selection_server_url" = "Serveradresse";
"authentication_server_selection_scan_code_button_title" = "QR-Code einlesen";
"authentication_server_selection_submit_button_title" = "Anmelden";
"authentication_server_selection_qr_missing_authorization_title" = "Scannen nicht möglich";
"authentication_server_selection_qr_missing_authorization_message" = "Gehe in die Einstellungen deines iPhones, um der App den Zugriff auf die Kamera zu erlauben.";
// MARK: - Login Protection
"bwi_login_protection_error_message" = "Der angegebene Server ist nicht für die Nutzung mit dem %@ vorgesehen";
@@ -555,7 +558,9 @@
"authentication_login_description" = "Mit deinem Konto anmelden";
"authentication_dataprivacy_text" = "Hinweise zur Verarbeitung Ihrer Daten und Ihrer Rechte erhalten Sie in unseren ";
"authentication_dataprivacy_link" = "Datenschutzhinweisen";
"authentication_server_selection_server_url" = "Homeserver URL";
"authentication_server_selection_server_url" = "Server-URL";
"authentication_server_selection_server_denied_title" = "Unbekannte URL";
"authentication_server_selection_server_denied_message" = "Dein Server ist leider noch nicht für den BundesMessenger eingerichtet. Wenn Du aus der Öffentlichen Verwaltung bist und Fragen hast, wie du den BundesMessenger nutzen kannst, besuche unsere Webseite.";
// MARK: - Netiquette Menu

View File

@@ -1,5 +1,5 @@
// Permissions usage explanations
"NSCameraUsageDescription" = "Die Kamera wird verwendet, um Fotos und Videos aufzunehmen sowie Videoanrufe durchzuführen.";
"NSCameraUsageDescription" = "Die Kamera wird verwendet, um Fotos und Videos aufzunehmen sowie QR-Codes zu scannen.";
"NSPhotoLibraryUsageDescription" = "Die Fotobibliothek wird verwendet, um Fotos und Videos zu versenden.";
"NSMicrophoneUsageDescription" = "BundesMessenger benötigt Zugriff auf das Mikrofon um Videos oder Sprachnachrichten aufzunehmen.";

View File

@@ -429,10 +429,17 @@
// MARK: - new login flow
"authentication_server_selection_login_title" = "Welcome!";
"authentication_server_selection_scan_code_button_title" = "Scan QR Code";
"authentication_server_selection_submit_button_title" = "Login";
"authentication_login_username" = "Username";
"authentication_login_description" = "Login to your account";
"authentication_dataprivacy_text" = "You can see our data usage and your rights in our ";
"authentication_dataprivacy_link" = "privacy agreement";
"authentication_server_selection_server_url" = "Server-URL";
"authentication_server_selection_qr_missing_authorization_title" = "Missing Permissions";
"authentication_server_selection_qr_missing_authorization_message" = "Switch to your iPhone settings app to allow the app to use the camera.";
"authentication_server_selection_server_denied_title" = "Invalid URL";
"authentication_server_selection_server_denied_message" = "Your server is not configured for the BundesMessenger. If you are part of the Öffentlichen Verwaltung and want more information how to use the BundesMessenger in your organistation then visit our website.";
// MARK: - Netiquette Menu

View File

@@ -36,7 +36,7 @@ struct AuthenticationLoginScreen: View {
ScrollView {
VStack(spacing: 0) {
if BWIBuildSettings.shared.bumLoginFlowLayout {
ServerIcon(image: Asset.Images.welcomeExperience1, size: OnboardingMetrics.iconSize-18)
ServerIcon(image: Asset.SharedImages.loginFlowLogo, size: OnboardingMetrics.iconSize)
.padding(.top, OnboardingMetrics.topPaddingToNavigationBar)
.padding(.bottom, 16)
} else {
@@ -63,7 +63,7 @@ struct AuthenticationLoginScreen: View {
loginForm
}
if viewModel.viewState.homeserver.showQRLogin {
if viewModel.viewState.homeserver.showQRLogin && BWIBuildSettings.shared.allowLoginWithQR {
qrLoginButton
}

View File

@@ -46,10 +46,15 @@ struct AuthenticationServerSelectionViewState: BindableState {
}
/// The title shown on the confirm button.
var buttonTitle: String {
hasModalPresentation ? VectorL10n.confirm : VectorL10n.next
var scanCodeButtonTitle: String {
BWIL10n.authenticationServerSelectionScanCodeButtonTitle
}
/// The title shown on the confirm button.
var buttonTitle: String {
hasModalPresentation ? VectorL10n.confirm : BWIL10n.authenticationServerSelectionSubmitButtonTitle
}
/// The text field is showing an error.
var isShowingFooterError: Bool {
footerErrorMessage != nil

View File

@@ -15,6 +15,7 @@
//
import SwiftUI
import AVKit
struct AuthenticationServerSelectionScreen: View {
// MARK: - Properties
@@ -23,8 +24,12 @@ struct AuthenticationServerSelectionScreen: View {
@Environment(\.theme) private var theme
@State private var showAlertForMissingCameraAuthorization = false
@State private var showAlertForInvalidServer = false
@State private var isEditingTextField = false
@State private var presentQRCodeScanner = false
@State private var qrCode = ""
private var textFieldFooterColor: Color {
viewModel.viewState.hasValidationError ? theme.colors.alert : theme.colors.tertiaryContent
}
@@ -38,31 +43,67 @@ struct AuthenticationServerSelectionScreen: View {
var body: some View {
GeometryReader { _ in
ScrollView {
VStack(spacing: 0) {
VStack(spacing: 8) {
header
.padding(.top, OnboardingMetrics.topPaddingToNavigationBar)
.padding(.bottom, 36)
.padding(.bottom, 16)
if BWIBuildSettings.shared.scanServerQRCode {
scanButton
}
serverForm
}
.readableFrame()
.padding(.horizontal, 16)
}
.alert(isPresented: $showAlertForInvalidServer) {
Alert(
title: Text(BWIL10n.authenticationServerSelectionServerDeniedTitle),
message: Text(BWIL10n.authenticationServerSelectionServerDeniedMessage),
dismissButton: .default(Text(VectorL10n.ok)))
}
}
.background(theme.colors.background.ignoresSafeArea())
.toolbar { toolbar }
.alert(item: $viewModel.alertInfo) { $0.alert }
.alert(isPresented: $showAlertForMissingCameraAuthorization) {
Alert(
title: Text(BWIL10n.authenticationServerSelectionQrMissingAuthorizationTitle),
message: Text(BWIL10n.authenticationServerSelectionQrMissingAuthorizationMessage),
primaryButton: .default(Text(VectorL10n.settingsTitle), action: openSettingsApp),
secondaryButton: .cancel())
}
.alert(isPresented: $showAlertForInvalidServer) {
Alert(
title: Text(BWIL10n.authenticationServerSelectionServerDeniedTitle),
message: Text(BWIL10n.authenticationServerSelectionServerDeniedMessage),
dismissButton: .default(Text(VectorL10n.ok)))
}
.accentColor(theme.colors.accent)
.sheet(isPresented: $presentQRCodeScanner) {
AuthenticationServerSelectionQRCodeScanner(qrCode: $qrCode)
}
.onChange(of: qrCode) { newValue in
if !qrCode.isEmpty {
viewModel.homeserverAddress = qrCode
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.submit()
}
}
}
}
/// The title, message and icon at the top of the screen.
var header: some View {
VStack(spacing: 8) {
if BWIBuildSettings.shared.bumLoginFlowLayout {
ServerIcon(image: Asset.Images.welcomeExperience1, size: OnboardingMetrics.iconSize)
ServerIcon(image: Asset.SharedImages.loginFlowLogo, size: OnboardingMetrics.iconSize)
.padding(.bottom, 16)
} else {
OnboardingIconImage(image: Asset.Images.welcomeExperience1)
.padding(.bottom, 8)
.padding(.bottom, 16)
}
Text(viewModel.viewState.headerTitle)
@@ -79,6 +120,28 @@ struct AuthenticationServerSelectionScreen: View {
}
}
var scanButton: some View {
VStack(spacing: 8) {
Button {
if AVCaptureDevice.authorizationStatus(for: .video) == .denied {
showAlertForMissingCameraAuthorization = true
} else {
qrCode = ""
presentQRCodeScanner = true
}
} label: {
Text(viewModel.viewState.scanCodeButtonTitle)
}
.buttonStyle(PrimaryActionButtonStyle())
.accessibilityIdentifier("qrCodeButton")
Text(VectorL10n.or)
.foregroundColor(theme.colors.secondaryContent)
.padding(5)
}
}
/// The text field and confirm button where the user enters a server URL.
var serverForm: some View {
VStack(alignment: .leading, spacing: 12) {
@@ -136,10 +199,34 @@ struct AuthenticationServerSelectionScreen: View {
}
/// Sends the `confirm` view action so long as the text field input is valid.
func submit() {
private func submit() {
guard !viewModel.viewState.hasValidationError else { return }
viewModel.send(viewAction: .confirm)
if isHomeserverAddressValid(viewModel.homeserverAddress) {
viewModel.send(viewAction: .confirm)
} else {
showAlertForInvalidServer = true
}
}
private func isHomeserverAddressValid(_ homeserverAddress: String) -> Bool {
if BWIBuildSettings.shared.bwiEnableLoginProtection {
let protectionService = LoginProtectionService()
protectionService.hashes = BWIBuildSettings.shared.bwiHashes
return protectionService.isValid(homeserverAddress)
} else {
return true
}
}
/// bwi: jump directly into the iOS settings app to allow camera access
private func openSettingsApp() {
if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
}
// MARK: - Previews

View File

@@ -33,11 +33,11 @@ struct BorderedInputFieldStyle: TextFieldStyle {
if isError {
return theme.colors.alert
} else if isEditing {
return theme.colors.accent
return BWIBuildSettings.shared.bwiEnableBuMUI ? Color(hex: 0x108194) : theme.colors.accent
}
return theme.colors.quinaryContent
}
private var accentColor: Color {
if isError {
return theme.colors.alert

View File

@@ -99,7 +99,7 @@ struct RoundedBorderTextField: View {
/// The text field's border color.
private var borderColor: Color {
if isEditing {
return theme.colors.accent
return BWIBuildSettings.shared.bwiEnableBuMUI ? Color(hex: 0x108194) : theme.colors.accent
} else if footerText != nil, isError {
return theme.colors.alert
} else {

View File

@@ -26,9 +26,8 @@ struct ServerIcon: View {
var body: some View {
Image(image.name)
.resizable()
.frame(width: size, height: size * 1.3)
.padding()
.background(Color(.white))
.frame(width: size, height: size)
.background(Color.white)
.cornerRadius(20)
.accessibilityHidden(true)
}

View File

@@ -0,0 +1,145 @@
//
/*
* Copyright (c) 2022 BWI GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import SwiftUI
import UIKit
import AVFoundation
struct AuthenticationServerSelectionQRCodeScanner: View {
@Environment(\.presentationMode) var presentationMode
@Binding var qrCode: String
@State var scanCompleted = false
var body: some View {
NavigationView {
ScannerView(qrCode: $qrCode, scanCompleted: $scanCompleted)
.navigationTitle(BWIL10n.authenticationServerSelectionScanCodeButtonTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(VectorL10n.close) {
presentationMode.wrappedValue.dismiss()
}
}
}
}
.onChange(of: scanCompleted) { newValue in
if newValue {
presentationMode.wrappedValue.dismiss()
}
}
}
}
struct ScannerView: UIViewControllerRepresentable {
@Binding var qrCode: String
@Binding var scanCompleted: Bool
class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
var parent: ScannerView
init(_ parent: ScannerView) {
self.parent = parent
}
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
if let metadataObject = metadataObjects.first {
guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
guard let stringValue = readableObject.stringValue else { return }
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
DispatchQueue.main.async {
self.parent.qrCode = stringValue
self.parent.scanCompleted = true
}
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> some ScannerViewController {
let controller = ScannerViewController()
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
class ScannerViewController: UIViewController {
var captureSession: AVCaptureSession?
var previewLayer: AVCaptureVideoPreviewLayer!
let metadataOutput = AVCaptureMetadataOutput()
weak var delegate: AVCaptureMetadataOutputObjectsDelegate?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black
captureSession = AVCaptureSession()
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video), let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice) else {
captureSession = nil
return
}
if let captureSession = captureSession {
guard captureSession.canAddInput(videoInput) && captureSession.canAddOutput(metadataOutput) else {
self.captureSession = nil
return
}
captureSession.addInput(videoInput)
captureSession.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(delegate, queue: .main)
metadataOutput.metadataObjectTypes = [.qr]
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.frame = view.layer.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let dispatchQueue = DispatchQueue(label: "AVCapturesession.startRunning", qos: .background)
dispatchQueue.async{
if (self.captureSession?.isRunning == false) {
self.captureSession?.startRunning()
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
let dispatchQueue = DispatchQueue(label: "AVCapturesession.stopRunning", qos: .background)
dispatchQueue.async{
if (self.captureSession?.isRunning == true) {
self.captureSession?.stopRunning()
}
}
}
}