Implemented securing app access with password, FaceID or TouchID

This commit is contained in:
dankito 2020-09-07 00:07:43 +02:00
parent 41b60a07a4
commit 5e07a900a9
21 changed files with 1134 additions and 76 deletions

View File

@ -19,6 +19,9 @@
360782D324F429F80098FEFE /* FlickerCodeStripe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 360782D224F429F70098FEFE /* FlickerCodeStripe.swift */; };
3608D6C224FBA9C6006C93A8 /* TrianglePointingDown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3608D6C124FBA9C6006C93A8 /* TrianglePointingDown.swift */; };
3608D6C624FBAB41006C93A8 /* TanGeneratorPositionMarker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3608D6C524FBAB41006C93A8 /* TanGeneratorPositionMarker.swift */; };
361116A62505430500315620 /* KeychainPasswordItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 361116A52505430400315620 /* KeychainPasswordItem.swift */; };
361116A8250562BE00315620 /* FaceIDButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 361116A7250562BE00315620 /* FaceIDButton.swift */; };
361116AA250562CF00315620 /* TouchIDButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 361116A9250562CF00315620 /* TouchIDButton.swift */; };
3642F00A2500F5AE005186FE /* Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3642F0092500F5AE005186FE /* Divider.swift */; };
3642F00C25010021005186FE /* UIKitActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3642F00B25010021005186FE /* UIKitActivityIndicator.swift */; };
3642F01425018BA9005186FE /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3642F01325018BA9005186FE /* TabBarController.swift */; };
@ -31,6 +34,14 @@
366FA4E024C4924A0094F009 /* RemitteeListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 366FA4DF24C4924A0094F009 /* RemitteeListItem.swift */; };
366FA4E224C4ED6C0094F009 /* EnterTanDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 366FA4E124C4ED6C0094F009 /* EnterTanDialog.swift */; };
366FA4E624C6EBF40094F009 /* EnterTanState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 366FA4E524C6EBF40094F009 /* EnterTanState.swift */; };
36B8A4482503D12100C15359 /* ProtectAppSettingsDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B8A4472503D12100C15359 /* ProtectAppSettingsDialog.swift */; };
36B8A44B2503D1E800C15359 /* BiometricAuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B8A44A2503D1E800C15359 /* BiometricAuthenticationService.swift */; };
36B8A44D2503D96D00C15359 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B8A44C2503D96D00C15359 /* AuthenticationService.swift */; };
36B8A44F2503D97D00C15359 /* AuthenticationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B8A44E2503D97D00C15359 /* AuthenticationType.swift */; };
36B8A4512503DE1800C15359 /* LoginDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B8A4502503DE1800C15359 /* LoginDialog.swift */; };
36B8A4542503E93B00C15359 /* UIAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B8A4532503E93B00C15359 /* UIAlert.swift */; };
36B8A4562503E9B200C15359 /* UIAlertBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B8A4552503E9B200C15359 /* UIAlertBase.swift */; };
36B8A4582503EEB600C15359 /* ActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B8A4572503EEB600C15359 /* ActionSheet.swift */; };
36BCF85424BA0C54005BEC29 /* BankList.json in Resources */ = {isa = PBXBuildFile; fileRef = 36BCF85324BA0C54005BEC29 /* BankList.json */; };
36BCF85824BA4274005BEC29 /* BankingUiCommon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 36BCF85524BA41EE005BEC29 /* BankingUiCommon.framework */; };
36BCF85924BA4274005BEC29 /* BankingUiCommon.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 36BCF85524BA41EE005BEC29 /* BankingUiCommon.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@ -155,6 +166,9 @@
360782D224F429F70098FEFE /* FlickerCodeStripe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlickerCodeStripe.swift; sourceTree = "<group>"; };
3608D6C124FBA9C6006C93A8 /* TrianglePointingDown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrianglePointingDown.swift; sourceTree = "<group>"; };
3608D6C524FBAB41006C93A8 /* TanGeneratorPositionMarker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TanGeneratorPositionMarker.swift; sourceTree = "<group>"; };
361116A52505430400315620 /* KeychainPasswordItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainPasswordItem.swift; sourceTree = "<group>"; };
361116A7250562BE00315620 /* FaceIDButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaceIDButton.swift; sourceTree = "<group>"; };
361116A9250562CF00315620 /* TouchIDButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchIDButton.swift; sourceTree = "<group>"; };
3642F0092500F5AE005186FE /* Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Divider.swift; sourceTree = "<group>"; };
3642F00B25010021005186FE /* UIKitActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitActivityIndicator.swift; sourceTree = "<group>"; };
3642F01325018BA9005186FE /* TabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = "<group>"; };
@ -167,6 +181,14 @@
366FA4DF24C4924A0094F009 /* RemitteeListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemitteeListItem.swift; sourceTree = "<group>"; };
366FA4E124C4ED6C0094F009 /* EnterTanDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterTanDialog.swift; sourceTree = "<group>"; };
366FA4E524C6EBF40094F009 /* EnterTanState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterTanState.swift; sourceTree = "<group>"; };
36B8A4472503D12100C15359 /* ProtectAppSettingsDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtectAppSettingsDialog.swift; sourceTree = "<group>"; };
36B8A44A2503D1E800C15359 /* BiometricAuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricAuthenticationService.swift; sourceTree = "<group>"; };
36B8A44C2503D96D00C15359 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
36B8A44E2503D97D00C15359 /* AuthenticationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationType.swift; sourceTree = "<group>"; };
36B8A4502503DE1800C15359 /* LoginDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginDialog.swift; sourceTree = "<group>"; };
36B8A4532503E93B00C15359 /* UIAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlert.swift; sourceTree = "<group>"; };
36B8A4552503E9B200C15359 /* UIAlertBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertBase.swift; sourceTree = "<group>"; };
36B8A4572503EEB600C15359 /* ActionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionSheet.swift; sourceTree = "<group>"; };
36BCF85324BA0C54005BEC29 /* BankList.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = BankList.json; path = ../../../tools/BankFinder/src/commonMain/resources/BankList.json; sourceTree = "<group>"; };
36BCF85524BA41EE005BEC29 /* BankingUiCommon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = BankingUiCommon.framework; path = "../BankingUiCommon/build/xcode-frameworks/BankingUiCommon.framework"; sourceTree = "<group>"; };
36BCF85D24BA4DA8005BEC29 /* MultiplatformUtils.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MultiplatformUtils.framework; path = "../../common/build/xcode-frameworks/MultiplatformUtils.framework"; sourceTree = "<group>"; };
@ -290,6 +312,27 @@
path = fints4k;
sourceTree = "<group>";
};
36B8A4492503D15300C15359 /* Security */ = {
isa = PBXGroup;
children = (
36B8A44A2503D1E800C15359 /* BiometricAuthenticationService.swift */,
36B8A44C2503D96D00C15359 /* AuthenticationService.swift */,
36B8A44E2503D97D00C15359 /* AuthenticationType.swift */,
361116A52505430400315620 /* KeychainPasswordItem.swift */,
);
path = Security;
sourceTree = "<group>";
};
36B8A4522503E92300C15359 /* UIKit */ = {
isa = PBXGroup;
children = (
36B8A4552503E9B200C15359 /* UIAlertBase.swift */,
36B8A4532503E93B00C15359 /* UIAlert.swift */,
36B8A4572503EEB600C15359 /* ActionSheet.swift */,
);
path = UIKit;
sourceTree = "<group>";
};
36BCF87924BFA679005BEC29 /* persistence */ = {
isa = PBXGroup;
children = (
@ -350,6 +393,7 @@
children = (
36FC92D424B3A389002B12E9 /* fints4k */,
36BCF87924BFA679005BEC29 /* persistence */,
36B8A4492503D15300C15359 /* Security */,
36BE06B624D077B400CBBB68 /* BankIconFinder */,
36FC92D924B3A479002B12E9 /* ui */,
36FC929B24B39A05002B12E9 /* AppDelegate.swift */,
@ -425,6 +469,7 @@
36FC92D924B3A479002B12E9 /* ui */ = {
isa = PBXGroup;
children = (
36B8A4522503E92300C15359 /* UIKit */,
36FC92DA24B3A485002B12E9 /* views */,
36E7BA1324B3D05C00757859 /* ViewExtensions.swift */,
36E21ECA24D88DF000649DC8 /* UIKitExtensions.swift */,
@ -453,6 +498,7 @@
36FC92DA24B3A485002B12E9 /* views */ = {
isa = PBXGroup;
children = (
36B8A4502503DE1800C15359 /* LoginDialog.swift */,
36FC92DB24B3A4A0002B12E9 /* AccountsTab.swift */,
360782C024E18D5E0098FEFE /* AddAccountButtonView.swift */,
36FC92EE24B3BB81002B12E9 /* AddAccountDialog.swift */,
@ -473,6 +519,7 @@
36E21ED024DC540400649DC8 /* SettingsDialog.swift */,
36E21ED424DC549800649DC8 /* BankSettingsDialog.swift */,
36E21ED624DC617200649DC8 /* BankAccountSettingsDialog.swift */,
36B8A4472503D12100C15359 /* ProtectAppSettingsDialog.swift */,
36BE065624C9E04800CBBB68 /* UIKitImageView.swift */,
36BE065824CA3CAB00CBBB68 /* UIKitSearchBar.swift */,
36BE065A24CA4B3500CBBB68 /* SelectBankDialog.swift */,
@ -486,6 +533,8 @@
3642F0092500F5AE005186FE /* Divider.swift */,
3642F00B25010021005186FE /* UIKitActivityIndicator.swift */,
3642F0172502723A005186FE /* UIKitButton.swift */,
361116A7250562BE00315620 /* FaceIDButton.swift */,
361116A9250562CF00315620 /* TouchIDButton.swift */,
);
path = views;
sourceTree = "<group>";
@ -662,6 +711,7 @@
36E21ECB24D88DF000649DC8 /* UIKitExtensions.swift in Sources */,
360782C524E541970098FEFE /* ScaleImageView.swift in Sources */,
366FA4E224C4ED6C0094F009 /* EnterTanDialog.swift in Sources */,
361116AA250562CF00315620 /* TouchIDButton.swift in Sources */,
36FC92DC24B3A4A0002B12E9 /* AccountsTab.swift in Sources */,
36BCF86E24BA691B005BEC29 /* DependencyInjector.swift in Sources */,
36BE06C224D07FB100CBBB68 /* Favicon.swift in Sources */,
@ -679,8 +729,10 @@
36BE06BA24D0783900CBBB68 /* FaviconFinder.swift in Sources */,
36BCF89524C31F02005BEC29 /* AppData.swift in Sources */,
3642F01A2502931F005186FE /* InstantPaymentInfoView.swift in Sources */,
36B8A44F2503D97D00C15359 /* AuthenticationType.swift in Sources */,
36E21EDD24DCA89100649DC8 /* TanProcedurePicker.swift in Sources */,
3608D6C624FBAB41006C93A8 /* TanGeneratorPositionMarker.swift in Sources */,
36B8A4542503E93B00C15359 /* UIAlert.swift in Sources */,
36BE065B24CA4B3500CBBB68 /* SelectBankDialog.swift in Sources */,
36E21ED524DC549800649DC8 /* BankSettingsDialog.swift in Sources */,
36BE068924CE288800CBBB68 /* CollapsibleText.swift in Sources */,
@ -693,12 +745,16 @@
36BCF88924C0A7D7005BEC29 /* Message.swift in Sources */,
366FA4E024C4924A0094F009 /* RemitteeListItem.swift in Sources */,
3608D6C224FBA9C6006C93A8 /* TrianglePointingDown.swift in Sources */,
36B8A4582503EEB600C15359 /* ActionSheet.swift in Sources */,
36BE068B24CE3B0400CBBB68 /* SwiftExtensions.swift in Sources */,
360782C724E544170098FEFE /* FlickerCodeTanView.swift in Sources */,
36B8A4482503D12100C15359 /* ProtectAppSettingsDialog.swift in Sources */,
36BE065D24CB08FC00CBBB68 /* LazyView.swift in Sources */,
360782C124E18D5E0098FEFE /* AddAccountButtonView.swift in Sources */,
36BCF86C24BA5E72005BEC29 /* DispatchQueueAsyncRunner.swift in Sources */,
36BCF86324BA5097005BEC29 /* SwiftUiRouter.swift in Sources */,
36B8A44D2503D96D00C15359 /* AuthenticationService.swift in Sources */,
36B8A4512503DE1800C15359 /* LoginDialog.swift in Sources */,
36FC929C24B39A05002B12E9 /* AppDelegate.swift in Sources */,
36BCF88B24C0BD2D005BEC29 /* AccountTransactionsDialog.swift in Sources */,
36BCF87624BF114F005BEC29 /* UrlSessionWebClient.swift in Sources */,
@ -707,6 +763,7 @@
360782CF24F3D6610098FEFE /* InfoLabel.swift in Sources */,
3642F04B25031157005186FE /* SectionHeaderWithRightAlignedEditButton.swift in Sources */,
36C4009B24D2F9E4005227AD /* IconedTitleView.swift in Sources */,
361116A62505430500315620 /* KeychainPasswordItem.swift in Sources */,
36BE065724C9E04800CBBB68 /* UIKitImageView.swift in Sources */,
3642F01625018DA1005186FE /* InterceptTabClickViewController.swift in Sources */,
36BCF88724C0A310005BEC29 /* PreviewData.swift in Sources */,
@ -717,13 +774,16 @@
36BE069124CEF52800CBBB68 /* UpdateButton.swift in Sources */,
36E21ED124DC540400649DC8 /* SettingsDialog.swift in Sources */,
366FA4DC24C479120094F009 /* BankInfoListItem.swift in Sources */,
36B8A44B2503D1E800C15359 /* BiometricAuthenticationService.swift in Sources */,
36FC929E24B39A05002B12E9 /* SceneDelegate.swift in Sources */,
3607829924E148D40098FEFE /* AdaptsToKeyboard.swift in Sources */,
36E21ECF24DA0EEE00649DC8 /* IconView.swift in Sources */,
3642F0182502723A005186FE /* UIKitButton.swift in Sources */,
36B8A4562503E9B200C15359 /* UIAlertBase.swift in Sources */,
3642F01425018BA9005186FE /* TabBarController.swift in Sources */,
36BCF88524C098C8005BEC29 /* BankAccountListItem.swift in Sources */,
36FC92EF24B3BB81002B12E9 /* AddAccountDialog.swift in Sources */,
361116A8250562BE00315620 /* FaceIDButton.swift in Sources */,
36C4009D24D3236B005227AD /* UrlUtil.swift in Sources */,
36BE066524CDE62800CBBB68 /* AccountTransactionListItem.swift in Sources */,
);

View File

@ -27,6 +27,19 @@
"Settings" = "Settings";
/* LoginDialog */
"Login Dialog title" = "Login";
"Authenticate to change app protection settings" = "Authenticate to change app protection settings";
"Authenticate with biometrics to unlock app reason" = "Authenticate to unlock your data";
"To unlock app please authenticate with FaceID" = "To unlock app please authenticate with FaceID";
"To unlock app please authenticate with TouchID" = "To unlock app please authenticate with TouchID";
"Enter your password" = "Enter your password";
"Login" = "Login";
"Authentication failed" = "Authentication failed";
"Incorrect password entered" = "Incorrect password entered";
/* SelectBankDialog */
"Select Bank Dialog Title" = "Select your bank ...";
@ -52,6 +65,9 @@
"Could not add account" = "Could not add account";
"Error message from your bank %@" = "Error message from your bank:\n\n%@";
"Secure data?" = "Secure data?";
"Secure data with?" = "Adding account was successful.\n\nDo you want to secure your data and login passwords by password, TouchID or FaceID (if available)?\n\nYou can also do this later in the settings.";
/* AccountTransactionsDialog */
@ -113,6 +129,11 @@ Unfortunately, Bankmeister cannot know whether a bank charges for instant paymen
"Could not change TAN medium to %@. Error: %@." = "Could not change TAN medium to '%@'.\n\nError message from your bank:\n\n%@.";
/* SettingsDialog */
"Secure app data" = "Secure app data";
/* BankSettingsDialog */
"Credentials" = "Credentials";
@ -141,3 +162,15 @@ Unfortunately, Bankmeister cannot know whether a bank charges for instant paymen
"Supports Retrieving Account Transactions" = "Retrieve transactions";
"Supports Transferring Money" = "Transfer money";
"Supports Instant payment transfer" = "Instant payment transfer";
/* ProtectAppSettingsDialog */
"Protect App Settings Dialog title" = "Security settings";
"FaceID" = "FaceID";
"TouchID" = "TouchID";
"Password" = "Password";
"Authenticate with TouchID" = "Authenticate with TouchID";
"Enter new password" = "Enter new password";
"Confirm password" = "Confirm password";
"Confirm new password" = "Confirm new password";

View File

@ -20,6 +20,8 @@
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSFaceIDUsageDescription</key>
<string>On user demand we use FaceID to unlock the app</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>

View File

@ -20,12 +20,21 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
setupBankingUi(context: context)
// Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath.
// Add `@Environment(\.managedObjectContext)` in the views that will need the context.
let authenticationService = AuthenticationService()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UINavigationController(rootViewController: TabBarController())
self.window = window
if authenticationService.needsAuthenticationToUnlockApp {
window.rootViewController = UIHostingController(rootView: LoginDialog(authenticationService) { _ in
self.showApplicationMainView(window: window)
} )
}
else {
showApplicationMainView(window: window)
}
window.makeKeyAndVisible()
}
}
@ -45,6 +54,17 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
func showApplicationMainView() {
if let window = window {
showApplicationMainView(window: window)
}
}
private func showApplicationMainView(window: UIWindow) {
window.rootViewController = UINavigationController(rootViewController: TabBarController())
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.

View File

@ -0,0 +1,133 @@
import SwiftUI
class AuthenticationService {
static private let AuthenticationTypeUserDefaultsKey = "AuthenticationType"
static private let KeychainAccountName = "LoginPassword"
private let biometricAuthenticationService = BiometricAuthenticationService()
var authenticationType: AuthenticationType {
let authenticationTypeString = UserDefaults.standard.string(forKey: Self.AuthenticationTypeUserDefaultsKey, defaultValue: AuthenticationType.unset.rawValue)
return AuthenticationType.init(rawValue: authenticationTypeString) ?? .unset
}
var needsAuthenticationToUnlockApp: Bool {
let authenticationType = self.authenticationType
return authenticationType != .unset && authenticationType != .none
}
var needsBiometricAuthenticationToUnlockApp: Bool {
let authenticationType = self.authenticationType
return authenticationType == .faceID || authenticationType == .touchID
}
var needsFaceIDToUnlockApp: Bool {
return self.authenticationType == .faceID
}
var needsTouchIDToUnlockApp: Bool {
return self.authenticationType == .touchID
}
var needsPasswordToUnlockApp: Bool {
return self.authenticationType == .password
}
var deviceSupportsFaceID: Bool {
return biometricAuthenticationService.isFaceIDSupported
}
var deviceSupportsTouchID: Bool {
return biometricAuthenticationService.isTouchIDSupported
}
func setAuthenticationType(_ type: AuthenticationType) {
if type != .unset { // it's not allowed to unset authentication type
if needsPasswordToUnlockApp {
deleteLoginPassword()
}
UserDefaults.standard.set(type.rawValue, forKey: Self.AuthenticationTypeUserDefaultsKey)
}
else {
// TODO: what to do in this case, throw an exception?
}
}
func setAuthenticationTypeToPassword(_ newPassword: String) {
setAuthenticationType(.password)
setLoginPassword(newPassword)
}
@discardableResult
private func setLoginPassword(_ newPassword: String) -> Bool {
do {
let passwordItem = createKeychainPasswordItem()
try passwordItem.savePassword(newPassword)
return true
} catch {
NSLog("Could not save login password: \(error)")
}
return false
}
@discardableResult
private func deleteLoginPassword() -> Bool {
do {
let passwordItem = createKeychainPasswordItem()
try passwordItem.deleteItem()
return true
} catch {
NSLog("Could not delete login password: \(error)")
}
return false
}
private func retrieveLoginPassword() -> String? {
do {
let passwordItem = createKeychainPasswordItem()
return try passwordItem.readPassword()
} catch {
NSLog("Could not read login password: \(error)")
}
return nil
}
private func createKeychainPasswordItem() -> KeychainPasswordItem {
return KeychainPasswordItem(Self.KeychainAccountName)
}
func loginWithBiometricAuthentication(_ authenticationResult: @escaping (Bool, String?) -> Void) {
biometricAuthenticationService.authenticate("Authenticate with biometrics to unlock app reason", authenticationResult)
}
func loginWithPassword(_ enteredPassword: String, _ authenticationResult: @escaping (Bool, String?) -> Void) {
if retrieveLoginPassword() == enteredPassword {
authenticationResult(true, nil)
}
else {
authenticationResult(false, "Incorrect password entered".localize())
}
}
}

View File

@ -0,0 +1,16 @@
import Foundation
enum AuthenticationType: String {
case unset
case none
case password
case touchID
case faceID
}

View File

@ -0,0 +1,80 @@
import LocalAuthentication
class BiometricAuthenticationService {
private let localAuthenticationContext = LAContext()
var biometryType: LABiometryType {
localAuthenticationContext.biometryType
}
var isFaceIDSupported: Bool {
biometryType == .faceID
}
var isTouchIDSupported: Bool {
biometryType == .touchID
}
var isBiometricAuthenticationAvailable: Bool {
var authorizationError: NSError?
return localAuthenticationContext.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &authorizationError)
}
func authenticate(_ authenticationReason: String, _ authenticationResult: @escaping (Bool, String?) -> Void) {
localAuthenticationContext.evaluatePolicy(LAPolicy.deviceOwnerAuthentication, localizedReason: authenticationReason.localize()) { (success, evaluationError) in
var errorMessage: String? = nil
if let errorObj = evaluationError {
let (showMessageToUser, untranslatedErrorMessage) = self.getErrorMessageKey(errorCode: errorObj._code)
if showMessageToUser {
errorMessage = untranslatedErrorMessage.localize()
}
}
DispatchQueue.main.async { // handler is called off main thread, so dispatch result back to main thread
authenticationResult(success, errorMessage)
}
}
}
// copied from https://www.appsdeveloperblog.com/touch-id-or-face-id-authentication-in-swift/
func getErrorMessageKey(errorCode: Int) -> (Bool, String) {
switch errorCode {
case LAError.authenticationFailed.rawValue:
return (true, "Authentication was not successful, because user failed to provide valid credentials.")
case LAError.appCancel.rawValue:
return (true, "Authentication was canceled by application (e.g. invalidate was called while authentication was in progress).")
case LAError.invalidContext.rawValue:
return (true, "LAContext passed to this call has been previously invalidated.")
case LAError.notInteractive.rawValue:
return (true, "Authentication failed, because it would require showing UI which has been forbidden by using interactionNotAllowed property.")
case LAError.passcodeNotSet.rawValue:
return (true, "Authentication could not start, because passcode is not set on the device.")
case LAError.systemCancel.rawValue:
return (true, "Authentication was canceled by system (e.g. another application went to foreground).")
case LAError.userCancel.rawValue:
return (false, "Authentication was canceled by user (e.g. tapped Cancel button).")
case LAError.userFallback.rawValue:
return (false, "Authentication was canceled, because the user tapped the fallback button (Enter Password).")
default:
return (true, "Error code \(errorCode) not found")
}
}
}

View File

@ -0,0 +1,224 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt (https://developer.apple.com/library/archive/samplecode/GenericKeychain/Listings/LICENSE_txt.html#//apple_ref/doc/uid/DTS40007797-LICENSE_txt-DontLinkElementID_8) for this samples licensing information
Abstract:
A struct for accessing generic password keychain items.
License:
Sample code project: GenericKeychain
Version: 4.0
IMPORTANT: This Apple software is supplied to you by Apple
Inc. ("Apple") in consideration of your agreement to the following
terms, and your use, installation, modification or redistribution of
this Apple software constitutes acceptance of these terms. If you do
not agree with these terms, please do not use, install, modify or
redistribute this Apple software.
In consideration of your agreement to abide by the following terms, and
subject to these terms, Apple grants you a personal, non-exclusive
license, under Apple's copyrights in this original Apple software (the
"Apple Software"), to use, reproduce, modify and redistribute the Apple
Software, with or without modifications, in source and/or binary forms;
provided that if you redistribute the Apple Software in its entirety and
without modifications, you must retain this notice and the following
text and disclaimers in all such redistributions of the Apple Software.
Neither the name, trademarks, service marks or logos of Apple Inc. may
be used to endorse or promote products derived from the Apple Software
without specific prior written permission from Apple. Except as
expressly stated in this notice, no other rights or licenses, express or
implied, are granted by Apple herein, including but not limited to any
patent rights that may be infringed by your derivative works or by other
works in which the Apple Software may be incorporated.
The Apple Software is provided by Apple on an "AS IS" basis. APPLE
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
Copyright (C) 2016 Apple Inc. All Rights Reserved.
*/
import Foundation
struct KeychainPasswordItem {
// MARK: Types
enum KeychainError: Error {
case noPassword
case unexpectedPasswordData
case unexpectedItemData
case unhandledError(status: OSStatus)
}
// MARK: Properties
let service: String
private(set) var account: String
let accessGroup: String?
// MARK: Intialization
init(service: String, account: String, accessGroup: String? = nil) {
self.service = service
self.account = account
self.accessGroup = accessGroup
}
// MARK: Keychain access
func readPassword() throws -> String {
/*
Build a query to find the item that matches the service, account and
access group.
*/
var query = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
query[kSecMatchLimit as String] = kSecMatchLimitOne
query[kSecReturnAttributes as String] = kCFBooleanTrue
query[kSecReturnData as String] = kCFBooleanTrue
// Try to fetch the existing keychain item that matches the query.
var queryResult: AnyObject?
let status = withUnsafeMutablePointer(to: &queryResult) {
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
}
// Check the return status and throw an error if appropriate.
guard status != errSecItemNotFound else { throw KeychainError.noPassword }
guard status == noErr else { throw KeychainError.unhandledError(status: status) }
// Parse the password string from the query result.
guard let existingItem = queryResult as? [String : AnyObject],
let passwordData = existingItem[kSecValueData as String] as? Data,
let password = String(data: passwordData, encoding: String.Encoding.utf8)
else {
throw KeychainError.unexpectedPasswordData
}
return password
}
func savePassword(_ password: String) throws {
// Encode the password into an Data object.
let encodedPassword = password.data(using: String.Encoding.utf8)!
do {
// Check for an existing item in the keychain.
try _ = readPassword()
// Update the existing item with the new password.
var attributesToUpdate = [String : AnyObject]()
attributesToUpdate[kSecValueData as String] = encodedPassword as AnyObject?
let query = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
// Throw an error if an unexpected status was returned.
guard status == noErr else { throw KeychainError.unhandledError(status: status) }
}
catch KeychainError.noPassword {
/*
No password was found in the keychain. Create a dictionary to save
as a new keychain item.
*/
var newItem = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
newItem[kSecValueData as String] = encodedPassword as AnyObject?
// Add a the new item to the keychain.
let status = SecItemAdd(newItem as CFDictionary, nil)
// Throw an error if an unexpected status was returned.
guard status == noErr else { throw KeychainError.unhandledError(status: status) }
}
}
mutating func renameAccount(_ newAccountName: String) throws {
// Try to update an existing item with the new account name.
var attributesToUpdate = [String : AnyObject]()
attributesToUpdate[kSecAttrAccount as String] = newAccountName as AnyObject?
let query = KeychainPasswordItem.keychainQuery(withService: service, account: self.account, accessGroup: accessGroup)
let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
// Throw an error if an unexpected status was returned.
guard status == noErr || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) }
self.account = newAccountName
}
func deleteItem() throws {
// Delete the existing item from the keychain.
let query = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
let status = SecItemDelete(query as CFDictionary)
// Throw an error if an unexpected status was returned.
guard status == noErr || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) }
}
static func passwordItems(forService service: String, accessGroup: String? = nil) throws -> [KeychainPasswordItem] {
// Build a query for all items that match the service and access group.
var query = KeychainPasswordItem.keychainQuery(withService: service, accessGroup: accessGroup)
query[kSecMatchLimit as String] = kSecMatchLimitAll
query[kSecReturnAttributes as String] = kCFBooleanTrue
query[kSecReturnData as String] = kCFBooleanFalse
// Fetch matching items from the keychain.
var queryResult: AnyObject?
let status = withUnsafeMutablePointer(to: &queryResult) {
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
}
// If no items were found, return an empty array.
guard status != errSecItemNotFound else { return [] }
// Throw an error if an unexpected status was returned.
guard status == noErr else { throw KeychainError.unhandledError(status: status) }
// Cast the query result to an array of dictionaries.
guard let resultData = queryResult as? [[String : AnyObject]] else { throw KeychainError.unexpectedItemData }
// Create a `KeychainPasswordItem` for each dictionary in the query result.
var passwordItems = [KeychainPasswordItem]()
for result in resultData {
guard let account = result[kSecAttrAccount as String] as? String else { throw KeychainError.unexpectedItemData }
let passwordItem = KeychainPasswordItem(service: service, account: account, accessGroup: accessGroup)
passwordItems.append(passwordItem)
}
return passwordItems
}
// MARK: Convenience
private static func keychainQuery(withService service: String, account: String? = nil, accessGroup: String? = nil) -> [String : AnyObject] {
var query = [String : AnyObject]()
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrService as String] = service as AnyObject?
if let account = account {
query[kSecAttrAccount as String] = account as AnyObject?
}
if let accessGroup = accessGroup {
query[kSecAttrAccessGroup as String] = accessGroup as AnyObject?
}
return query
}
}

View File

@ -88,27 +88,15 @@ class TabBarController : UITabBarController, UITabBarControllerDelegate {
private func showNewOptionsActionSheet() {
let transferMoneyAction = UIAlertAction.default("Show transfer money dialog".localize()) { self.showView(TransferMoneyDialog()) }
let transferMoneyAction = UIAlertAction.default("Show transfer money dialog".localize()) { SceneDelegate.navigateToView(TransferMoneyDialog()) }
transferMoneyAction.isEnabled = data.hasAccountsThatSupportTransferringMoney
alert.addAction(transferMoneyAction)
alert.addAction(UIAlertAction(title: "Add account".localize(), style: .default, handler: { _ in self.showView(AddAccountDialog()) } ))
alert.addAction(UIAlertAction(title: "Cancel".localize(), style: .cancel, handler: nil))
if let popoverController = alert.popoverPresentationController {
popoverController.sourceView = self.tabBar
popoverController.sourceRect = CGRect(x: self.tabBar.bounds.midX, y: 0, width: 0, height: 0)
}
self.present(alert, animated: true, completion: nil)
}
private func showView<Content: View>(_ view: Content) {
showViewController(UIHostingController(rootView: view))
}
private func showViewController(_ viewController: UIViewController) {
self.navigationController?.pushViewController(viewController, animated: true)
ActionSheet(
nil,
transferMoneyAction,
UIAlertAction.default("Add account") { SceneDelegate.navigateToView(AddAccountDialog()) },
UIAlertAction.cancel()
).show(self.tabBar, self.tabBar.bounds.midX, 0)
}
}

View File

@ -27,6 +27,19 @@
"Settings" = "Einstellungen";
/* LoginDialog */
"Login Dialog title" = "Anmelden";
"Authenticate to change app protection settings" = "Authentifizieren Sie sich um die Einstellungen zum Schutz der Appdaten zu ändern";
"Authenticate with biometrics to unlock app reason" = "Authentifizieren Sie sich bitte um ihre Daten zu entsperren";
"To unlock app please authenticate with FaceID" = "Authentifizieren Sich sich bitte mit FaceID um die App zu entsperren";
"To unlock app please authenticate with TouchID" = "Authentifizieren Sich sich bitte mit TouchID um die App zu entsperren";
"Enter your password" = "Geben Sie Ihr Passwort ein";
"Login" = "Anmelden";
"Authentication failed" = "Authentifizierung fehlgeschlagen";
"Incorrect password entered" = "Falsches Passwort eingegeben";
/* SelectBankDialog */
"Select Bank Dialog Title" = "Bank auswählen";
@ -52,6 +65,9 @@
"Could not add account" = "Konto konnte nicht hinzugefügt werden.";
"Error message from your bank %@" = "Fehlermeldung Ihrer Bank:\n\n%@";
"Secure data?" = "Daten schützen?";
"Secure data with %@?" = "Hinuzufügen war erfolgreich.\n\nMöchten Sie Ihre Daten und Anmeldekennwörter durch Passwort, TouchID oder FaceID (falls vorhanden= schützen?\n\nSie können dies auch später in den Einstellungen vornehmen.";
/* AccountTransactionsDialog */
@ -113,6 +129,11 @@ Ob eine Bank Gebühren für Echtzeitüberweisungen erhebt, kann Bankmeister leid
"Could not change TAN medium to %@. Error: %@." = "TAN medium konnte nicht zu '%@' geändert werden.\n\nFehlermeldung Ihrer Bank:\n\n%@.";
/* SettingsDialog */
"Secure app data" = "Appdaten schützen";
/* BankSettingsDialog */
"Credentials" = "Zugangsdaten";
@ -141,3 +162,15 @@ Ob eine Bank Gebühren für Echtzeitüberweisungen erhebt, kann Bankmeister leid
"Supports Retrieving Account Transactions" = "Kontoumsätze abrufen";
"Supports Transferring Money" = "Überweisen";
"Supports Instant payment transfer" = "Echtzeitüberweisung";
/* ProtectAppSettingsDialog */
"Protect App Settings Dialog title" = "Sicherheitseinstellungen"; // TODO: find a better title
"FaceID" = "FaceID";
"TouchID" = "TouchID";
"Password" = "Passwort";
"Authenticate with TouchID" = "Mit TouchID authentifizieren";
"Enter new password" = "Neues Passwort eingeben";
"Confirm password" = "Bestätigen";
"Confirm new password" = "Neues Passwort bestätigen";

View File

@ -11,14 +11,16 @@ extension AppDelegate {
extension SceneDelegate {
public static var currentWindow: UIWindow? {
UIApplication.shared.windows.first(where: { (window) -> Bool in window.isKeyWindow})
public static var currentWindow: UIWindow {
UIApplication.shared.windows.first(where: { (window) -> Bool in window.isKeyWindow})!
}
public static var currentScene: UIWindowScene? { currentWindow?.windowScene }
public static var currentScene: UIWindowScene { currentWindow.windowScene! }
public static var current: SceneDelegate { currentScene.delegate as! SceneDelegate }
public static var rootViewController: UIViewController? {
currentWindow?.rootViewController
currentWindow.rootViewController
}
public static var rootNavigationController: UINavigationController? {
@ -43,6 +45,24 @@ extension SceneDelegate {
currentViewController?.navigationItem
}
public static func navigateToView<Content: View>(_ view: Content) {
navigateToViewController(UIHostingController(rootView: view))
}
public static func navigateToViewController(_ viewController: UIViewController) {
rootNavigationController?.pushViewController(viewController, animated: true)
}
}
extension KeychainPasswordItem {
init(_ accountName: String) {
self.init(service: "Bankmeister", account: accountName, accessGroup: nil)
}
}

View File

@ -2,7 +2,7 @@ import SwiftUI
import CoreData
extension String {
extension StringProtocol {
var isNotEmpty: Bool {
return !isEmpty
@ -17,24 +17,6 @@ extension String {
}
func localize() -> String {
return NSLocalizedString(self, comment: "")
}
// TODO: implement passing multiple arguments to localize()
// func localize(_ arguments: CVarArg...) -> String {
// return localize(arguments)
// }
//
// func localize(_ arguments: [CVarArg]) -> String {
// return String(format: NSLocalizedString(self, comment: ""), arguments)
// }
func localize(_ arguments: CVarArg) -> String {
return String(format: NSLocalizedString(self, comment: ""), arguments)
}
subscript(_ i: Int) -> String {
let idx1 = index(startIndex, offsetBy: i)
let idx2 = index(idx1, offsetBy: 1)
@ -54,6 +36,11 @@ extension String {
}
var firstLetterUppercased: String {
prefix(1).uppercased() + dropFirst()
}
///
/// Be aware that this crashes when used in a View's body method. So use it only e.g. in init() method
///
@ -79,6 +66,30 @@ extension String {
}
extension String {
func localize() -> String {
return NSLocalizedString(self, comment: "")
}
// TODO: implement passing multiple arguments to localize()
// func localize(_ arguments: CVarArg...) -> String {
// return localize(arguments)
// }
//
// func localize(_ arguments: [CVarArg]) -> String {
// return String(format: NSLocalizedString(self, comment: ""), arguments)
// return String(format: NSLocalizedString(self, comment: ""), getVaList(arguments))
// }
func localize(_ arguments: CVarArg) -> String {
return String(format: NSLocalizedString(self, comment: ""), arguments)
}
}
extension Optional where Wrapped == String {
var isNullOrEmpty: Bool {

View File

@ -10,13 +10,9 @@ class SwiftUiRouter : IRouter {
}
func getTanFromUserFromNonUiThread(customer: Customer, tanChallenge: TanChallenge, presenter: BankingPresenter, callback: @escaping (EnterTanResult) -> Void) {
if let rootViewController = UIApplication.shared.windows.first(where: { (window) -> Bool in window.isKeyWindow})?.rootViewController as? UINavigationController {
let enterTanState = EnterTanState(customer, tanChallenge, callback)
let enterTanState = EnterTanState(customer, tanChallenge, callback)
let enterTanDialogController = UIHostingController(rootView: EnterTanDialog(enterTanState))
rootViewController.pushViewController(enterTanDialogController, animated: true)
}
SceneDelegate.navigateToView(EnterTanDialog(enterTanState))
}
func getAtcFromUserFromNonUiThread(tanMedium: TanGeneratorTanMedium, callback: @escaping (EnterTanGeneratorAtcResult) -> Void) {

View File

@ -30,28 +30,20 @@ extension UIResponder {
extension UIDevice {
static var deviceType: UIUserInterfaceIdiom {
get {
return UIDevice.current.deviceType
}
UIDevice.current.deviceType
}
var deviceType: UIUserInterfaceIdiom {
get {
return self.userInterfaceIdiom
}
self.userInterfaceIdiom
}
static var isRunningOniPad: Bool {
get {
return UIDevice.current.userInterfaceIdiom == .pad
}
UIDevice.current.userInterfaceIdiom == .pad
}
var isRunningOniPad: Bool {
get {
return self.userInterfaceIdiom == .pad
}
self.userInterfaceIdiom == .pad
}
}

View File

@ -100,13 +100,29 @@ struct AddAccountDialog: View {
if (response.isSuccessful) {
DispatchQueue.main.async { // dispatch async as may EnterTanDialog is still displayed so dismiss() won't dismiss this view
self.presentation.wrappedValue.dismiss()
self.closeDialog()
DispatchQueue.main.async {
let authenticationService = AuthenticationService()
if self.presenter.customers.count == 1 && authenticationService.authenticationType == .unset {
authenticationService.setAuthenticationType(.none)
UIAlert("Secure data?", "Secure data with?",
UIAlertAction.ok { SceneDelegate.navigateToView(ProtectAppSettingsDialog()) },
UIAlertAction.cancel(self.closeDialog))
.show()
}
}
}
}
else {
self.errorMessage = Message(title: Text("Could not add account"), message: Text("Error message from your bank \(response.errorToShowToUser ?? "")"))
}
}
private func closeDialog() {
self.presentation.wrappedValue.dismiss()
}
}

View File

@ -3,9 +3,9 @@ import SwiftUI
struct Divider: View {
let height: CGFloat = 1
var height: CGFloat = 1
let color: Color = Color.black
var color: Color = Color.black
var body: some View {

View File

@ -0,0 +1,38 @@
import SwiftUI
struct FaceIDButton: View {
private let widthAndHeight: CGFloat
private let action: () -> Void
init(_ action: @escaping () -> Void) {
self.init(34, action)
}
init(_ widthAndHeight: CGFloat, _ action: @escaping () -> Void) {
self.widthAndHeight = widthAndHeight
self.action = action
}
var body: some View {
Button(action: self.action) {
Image(systemName: "faceid")
.resizable()
.frame(width: widthAndHeight, height: widthAndHeight)
}
}
}
struct FaceIDButton_Previews: PreviewProvider {
static var previews: some View {
FaceIDButton { }
}
}

View File

@ -0,0 +1,129 @@
import SwiftUI
struct LoginDialog: View {
private let authenticationService: AuthenticationService
private let allowCancellingLogin: Bool
private let loginReason: LocalizedStringKey?
private let loginResult: (Bool) -> Void
@State private var enteredPassword: String = ""
init(_ authenticationService: AuthenticationService = AuthenticationService(), allowCancellingLogin: Bool = false, loginReason: LocalizedStringKey? = nil, loginResult: @escaping (Bool) -> Void) {
self.authenticationService = authenticationService
self.allowCancellingLogin = allowCancellingLogin
self.loginReason = loginReason
self.loginResult = loginResult
if authenticationService.needsBiometricAuthenticationToUnlockApp {
self.loginWithBiometricAuthentication()
}
}
var body: some View {
VStack {
if authenticationService.needsFaceIDToUnlockApp {
VStack {
Spacer()
Text(loginReason ?? "To unlock app please authenticate with FaceID")
.multilineTextAlignment(.center)
.padding()
.padding(.bottom, 30)
FaceIDButton(50, self.loginWithBiometricAuthentication)
Spacer()
}
}
else if authenticationService.needsTouchIDToUnlockApp {
VStack {
Spacer()
Text(loginReason ?? "To unlock app please authenticate with TouchID")
.multilineTextAlignment(.center)
.padding()
.padding(.bottom, 35)
TouchIDButton(self.loginWithBiometricAuthentication)
Spacer()
}
}
else {
loginReason.map { loginReason in
Text(loginReason)
.multilineTextAlignment(.center)
.padding()
.padding(.bottom, 0)
}
Form {
Section {
LabelledUIKitTextField(label: "Password", text: $enteredPassword, placeholder: "Enter your password", isPasswordField: true, focusOnStart: true,
actionOnReturnKeyPress: { self.loginWithPasswordOnReturnKeyPress() })
}
Section {
Button("Login") { self.loginWithPassword() }
.alignVertically(.center)
}
}
}
}
.navigationBarTitle("Login Dialog title")
.navigationBarItems(leading: allowCancellingLogin == false ? nil : createCancelButton {
self.closeDialogAndDispatchLoginResult(false)
})
}
private func loginWithBiometricAuthentication() {
authenticationService.loginWithBiometricAuthentication(self.handleAuthenticationResult)
}
private func loginWithPasswordOnReturnKeyPress() -> Bool {
loginWithPassword()
return true
}
private func loginWithPassword() {
authenticationService.loginWithPassword(enteredPassword, self.handleAuthenticationResult)
}
private func handleAuthenticationResult(success: Bool, errorMessage: String?) {
// as .alert() didn't work (why SwiftUI?), displaying Alert now manually
if let errorMessage = errorMessage {
UIAlert("Authentication failed", errorMessage, UIAlertAction.ok())
.show()
}
else if success {
closeDialogAndDispatchLoginResult(true)
}
}
private func closeDialogAndDispatchLoginResult(_ authenticationSuccess: Bool) {
SceneDelegate.rootNavigationController?.popViewController(animated: false)
self.loginResult(authenticationSuccess)
}
}
struct LoginDialog_Previews: PreviewProvider {
static var previews: some View {
LoginDialog { _ in }
}
}

View File

@ -0,0 +1,198 @@
import SwiftUI
struct ProtectAppSettingsDialog: View {
@Environment(\.presentationMode) var presentation
private let authenticationService = AuthenticationService()
private let supportedAuthenticationTypes: [AuthenticationType]
@State private var isFaceIDSelected: Bool = false
@State private var isTouchIDSelected: Bool = false
@State private var isPasswordSelected: Bool = false
@State private var selectedAuthenticationTypeIndex = 0
private var selectedAuthenticationTypeIndexBinding: Binding<Int> {
Binding<Int>(
get: { self.selectedAuthenticationTypeIndex },
set: {
if (self.selectedAuthenticationTypeIndex != $0) { // only if authentication type really changed
self.selectedAuthenticationTypeIndex = $0
self.selectedAuthenticationTypeChanged(self.supportedAuthenticationTypes[$0])
}
})
}
@State private var newPassword: String = ""
@State private var confirmedNewPassword: String = ""
@State private var successfullyAuthenticatedWithBiometricAuthentication = false
@State private var successfullyAuthenticatedWithPassword = false
init() {
var authenticationTypes = [AuthenticationType]()
if authenticationService.deviceSupportsFaceID {
authenticationTypes.append(.faceID)
}
if authenticationService.deviceSupportsTouchID {
authenticationTypes.append(.touchID)
}
authenticationTypes.append(.password)
self.supportedAuthenticationTypes = authenticationTypes
let currentAuthenticationType = authenticationService.authenticationType
if currentAuthenticationType == .faceID || (currentAuthenticationType != .password && authenticationService.deviceSupportsFaceID) {
_isFaceIDSelected = State(initialValue: true)
_selectedAuthenticationTypeIndex = State(initialValue: 0)
}
else if currentAuthenticationType == .touchID || (currentAuthenticationType != .password && authenticationService.deviceSupportsTouchID) {
_isTouchIDSelected = State(initialValue: true)
_selectedAuthenticationTypeIndex = State(initialValue: 0)
}
else {
_isPasswordSelected = State(initialValue: true)
_selectedAuthenticationTypeIndex = State(initialValue: supportedAuthenticationTypes.firstIndex(of: .password)!)
}
}
var body: some View {
Form {
if supportedAuthenticationTypes.count > 1 {
Section {
Picker("", selection: selectedAuthenticationTypeIndexBinding) {
ForEach(0..<supportedAuthenticationTypes.count) { index in
Text(self.supportedAuthenticationTypes[index].rawValue.firstLetterUppercased.localize())
.tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
.alignVertically(.center)
}
}
if isFaceIDSelected {
Section {
FaceIDButton(self.doBiometricAuthentication)
.alignVertically(.center)
}
}
if isTouchIDSelected {
Section {
TouchIDButton(self.doBiometricAuthentication)
.alignVertically(.center)
}
}
if isPasswordSelected {
Section {
LabelledUIKitTextField(label: "Password", text: $newPassword.didSet(self.enteredPasswordChanged), placeholder: "Enter new password", isPasswordField: true,
focusOnStart: true, focusNextTextFieldOnReturnKeyPress: true, actionOnReturnKeyPress: handleReturnKeyPress)
LabelledUIKitTextField(label: "Confirm password", text: $confirmedNewPassword.didSet(self.enteredPasswordChanged), placeholder: "Confirm new password",
isPasswordField: true, actionOnReturnKeyPress: handleReturnKeyPress)
}
}
Section {
HStack {
Spacer()
Button("OK") { self.setAuthenticationType() }
.alignVertically(.center)
.disabled( !self.authenticatedWithNewAuthenticationType)
Spacer()
}
.frame(maxWidth: .infinity, minHeight: 40)
}
}
.fixKeyboardCoversLowerPart()
.navigationBarTitle("Protect App Settings Dialog title")
}
private func selectedAuthenticationTypeChanged(_ type: AuthenticationType) {
isFaceIDSelected = false
isTouchIDSelected = false
isPasswordSelected = false
if type == .faceID {
isFaceIDSelected = true
}
else if type == .touchID {
isTouchIDSelected = true
}
else {
isPasswordSelected = true
}
if isPasswordSelected == false {
UIApplication.hideKeyboard()
}
}
private func doBiometricAuthentication() {
authenticationService.loginWithBiometricAuthentication { success, errorMessage in
self.successfullyAuthenticatedWithBiometricAuthentication = success
}
}
private func enteredPasswordChanged(_ oldValue: String, _ newValue: String) {
successfullyAuthenticatedWithPassword = newPassword.isNotBlank && newPassword == confirmedNewPassword
}
private var authenticatedWithNewAuthenticationType: Bool {
((isFaceIDSelected || isTouchIDSelected) && successfullyAuthenticatedWithBiometricAuthentication) ||
(isPasswordSelected && successfullyAuthenticatedWithPassword)
}
func handleReturnKeyPress() -> Bool {
if authenticatedWithNewAuthenticationType {
self.setAuthenticationType()
return true
}
return false
}
private func setAuthenticationType() {
if isFaceIDSelected {
authenticationService.setAuthenticationType(.faceID)
}
else if isTouchIDSelected {
authenticationService.setAuthenticationType(.touchID)
}
else {
authenticationService.setAuthenticationTypeToPassword(newPassword)
}
presentation.wrappedValue.dismiss()
}
}
struct ProtectAppSettingsDIalog_Previews: PreviewProvider {
static var previews: some View {
ProtectAppSettingsDialog()
}
}

View File

@ -4,6 +4,8 @@ import BankingUiSwift
struct SettingsDialog: View {
@Environment(\.editMode) var editMode
@ObservedObject var data: AppData
@Inject var presenter: BankingPresenterSwift
@ -24,6 +26,17 @@ struct SettingsDialog: View {
.onMove(perform: reorderBanks)
.onDelete(perform: deleteBanks)
}
Section {
NavigationLink(destination: EmptyView(), isActive: .constant(false)) { // we need custom navigation handling, so disable that NavigationLink takes care of navigating
Text("Secure app data")
.frame(maxWidth: .infinity, alignment: .leading) // stretch over full width
.background(Color.systemBackground) // make background tapable
}
}
.onTapGesture {
self.navigateToProtectAppSettingsDialog()
}
}
.alert(item: $askToDeleteAccountMessage) { message in
Alert(title: message.title, message: message.message, primaryButton: message.primaryButton, secondaryButton: message.secondaryButton!)
@ -32,13 +45,11 @@ struct SettingsDialog: View {
}
private var footer: some View {
get {
HStack {
Spacer()
HStack {
Spacer()
NavigationLink(destination: LazyView(AddAccountDialog())) {
Text("Add")
}
NavigationLink(destination: LazyView(AddAccountDialog())) {
Text("Add")
}
}
}
@ -60,13 +71,44 @@ struct SettingsDialog: View {
}
func askUserToDeleteAccount(_ bankToDelete: Customer) {
self.askToDeleteAccountMessage = Message.createAskUserToDeleteAccountMessage(bankToDelete, self.deleteAccount)
self.askToDeleteAccountMessage = Message.createAskUserToDeleteAccountMessage(bankToDelete, self.deleteAccountWithSecurityChecks)
}
func deleteAccount(_ bankToDelete: Customer) {
func deleteAccountWithSecurityChecks(_ bankToDelete: Customer) {
// don't know why but when deleting last bank application crashes if we don't delete bank async
DispatchQueue.main.async {
self.presenter.deleteAccount(customer: bankToDelete)
if self.presenter.customers.count == 1 {
self.editMode?.wrappedValue = .inactive
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.deleteAccount(bankToDelete)
}
}
else {
self.deleteAccount(bankToDelete)
}
}
}
private func deleteAccount(_ bankToDelete: Customer) {
self.presenter.deleteAccount(customer: bankToDelete)
}
private func navigateToProtectAppSettingsDialog() {
let authenticationService = AuthenticationService()
if authenticationService.needsAuthenticationToUnlockApp == false {
SceneDelegate.navigateToView(ProtectAppSettingsDialog())
}
else {
let loginDialog = LoginDialog(authenticationService, allowCancellingLogin: true, loginReason: "Authenticate to change app protection settings") { authenticationSuccess in
if authenticationSuccess {
SceneDelegate.navigateToView(ProtectAppSettingsDialog())
}
}
SceneDelegate.navigateToView(loginDialog)
}
}

View File

@ -0,0 +1,27 @@
import SwiftUI
struct TouchIDButton: View {
private let action: () -> Void
init(_ action: @escaping () -> Void) {
self.action = action
}
var body: some View {
Button("Authenticate with TouchID", action: self.action)
}
}
struct TouchIDButton_Previews: PreviewProvider {
static var previews: some View {
TouchIDButton { }
}
}