Implemented securing app access with password, FaceID or TouchID
This commit is contained in:
parent
41b60a07a4
commit
5e07a900a9
|
@ -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 */,
|
||||
);
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import Foundation
|
||||
|
||||
|
||||
enum AuthenticationType: String {
|
||||
|
||||
case unset
|
||||
|
||||
case none
|
||||
|
||||
case password
|
||||
|
||||
case touchID
|
||||
|
||||
case faceID
|
||||
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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 sample’s 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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 { }
|
||||
}
|
||||
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 { }
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue