mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-17 23:18:27 +02:00
Merge branch 'feature/3895_qr_code_login' into 'develop'
MESSENGER-3895 qr code login See merge request bwmessenger/bundesmessenger/bundesmessenger-ios!82
This commit is contained in:
@@ -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
|
||||
|
||||
BIN
Riot/Assets/SharedImages.xcassets/login_flow_logo.imageset/120-1.png
vendored
Normal file
BIN
Riot/Assets/SharedImages.xcassets/login_flow_logo.imageset/120-1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
BIN
Riot/Assets/SharedImages.xcassets/login_flow_logo.imageset/180.png
vendored
Normal file
BIN
Riot/Assets/SharedImages.xcassets/login_flow_logo.imageset/180.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
BIN
Riot/Assets/SharedImages.xcassets/login_flow_logo.imageset/60.png
vendored
Normal file
BIN
Riot/Assets/SharedImages.xcassets/login_flow_logo.imageset/60.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
23
Riot/Assets/SharedImages.xcassets/login_flow_logo.imageset/Contents.json
vendored
Normal file
23
Riot/Assets/SharedImages.xcassets/login_flow_logo.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
145
bwi/QRCode/AuthenticationServerSelectionQRCodeScanner.swift
Normal file
145
bwi/QRCode/AuthenticationServerSelectionQRCodeScanner.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user