Skip to content

Commit 06a0734

Browse files
committed
implemented enabling biometric login feature in login screen
- removed biometric login feature in InitialView screen (work spaces list screen) - Handled biometric login edge cases - implemented prompting biometric login if the new user login with the same environment
1 parent bdbba42 commit 06a0734

File tree

6 files changed

+71
-49
lines changed

6 files changed

+71
-49
lines changed

GoInfoGame/GoInfoGame/Login/BiometricAuthManager.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ struct BiometricAuthManager {
1515
case failure(String)
1616
case unavailable(String)
1717
}
18+
19+
static func canEvaluateBiometrics() -> Bool {
20+
return LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
21+
}
1822

1923
static func authenticate(reason: String = "Authenticate to proceed", completion: @escaping (BiometricAuthResult) -> Void) {
2024
let context = LAContext()
@@ -33,9 +37,7 @@ struct BiometricAuthManager {
3337
}
3438
} else {
3539
let message = error?.localizedDescription ?? "Biometric authentication not available."
36-
DispatchQueue.main.async {
37-
completion(.unavailable(message))
38-
}
40+
completion(.unavailable(message))
3941
}
4042
}
4143

GoInfoGame/GoInfoGame/Login/SessionManager.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,14 @@ final class SessionManager: ObservableObject {
5555
}
5656

5757
func savePasswordForBiometric(for environment: APIEnvironment) {
58-
if let password = lastLoginPassword {
58+
if let password = lastLoginPassword,
59+
let username = username {
5960
_ = KeychainManager.save(.password, value: password, for: environment)
61+
_ = KeychainManager.save(.username, value: username, for: environment)
62+
setBiometricEnabled(true, for: environment)
63+
} else {
64+
print("❌ not able to save the biometric credentials.")
6065
}
61-
setBiometricEnabled(true, for: environment)
6266
}
6367

6468
func logout(environment: APIEnvironment, clearBiometricCreds: Bool = false) {

GoInfoGame/GoInfoGame/Login/View/PosmLogin.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,16 @@ struct PosmLoginView: View {
132132
.alert(viewModel.errorMessage ?? "Invalid Credentials", isPresented: $viewModel.shouldShowValidationAlert) {
133133
Button("OK", role: .cancel) { }
134134
}
135+
.alert("Enable Biometric Login?", isPresented: $viewModel.showBiometricPrompt) {
136+
Button("Enable") {
137+
viewModel.enableBiometrics(enable: true)
138+
}
139+
Button("Not Now", role: .cancel) {
140+
viewModel.enableBiometrics(enable: false)
141+
}
142+
} message: {
143+
Text("Would you like to use Face ID or Touch ID for faster logins?")
144+
}
135145
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("SessionExpired"))) { notification in
136146
showAlert = true
137147
}

GoInfoGame/GoInfoGame/Login/ViewModel/PosmLoginViewModel.swift

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
import Foundation
99
import SwiftUI
10+
import LocalAuthentication
1011

11-
@MainActor
1212
class PosmLoginViewModel: ObservableObject {
1313
@Published var username: String = ""
1414
@Published var password: String = ""
@@ -18,6 +18,7 @@ class PosmLoginViewModel: ObservableObject {
1818
@Published var errorMessage: String?
1919
@Published var isLoading = false
2020
@Published var shouldShowValidationAlert: Bool = false
21+
@Published var showBiometricPrompt = false
2122

2223
@AppStorage("loggedIn") private var loggedIn: Bool = false
2324

@@ -45,13 +46,57 @@ class PosmLoginViewModel: ObservableObject {
4546
SessionManager.shared.performLogin(username: username, password: password, environment: environment) { [weak self] success, error in
4647
guard let self = self else { return }
4748

48-
DispatchQueue.main.async {
49-
self.isLoading = false
49+
self.isLoading = false
50+
self.loggedIn = success
51+
let env = APIConfiguration.shared.environment
52+
if success {
53+
if canEvaluateBiometrics() {
54+
if !SessionManager.shared.hasDeclinedBiometric(for: env) {
55+
if !SessionManager.shared.isBiometricEnabled(for: env) ||
56+
(SessionManager.shared.isBiometricEnabled(for: env) && self.username != KeychainManager.load(.username, for: env)){
57+
self.showBiometricPrompt = true
58+
return
59+
}
60+
}
61+
}
5062
self.isLoginSuccess = success
63+
} else {
5164
self.hasLoginFailed = !success
52-
self.loggedIn = success
5365
}
5466
}
5567
}
68+
69+
private func canEvaluateBiometrics() -> Bool {
70+
BiometricAuthManager.canEvaluateBiometrics()
71+
}
72+
73+
func enableBiometrics(enable: Bool) {
74+
guard self.loggedIn else {
75+
return
76+
}
77+
if enable, canEvaluateBiometrics() {
78+
BiometricAuthManager.authenticate { [weak self] result in
79+
switch result {
80+
case .success:
81+
if let username = self?.username,
82+
let password = self?.password {
83+
let env = APIConfiguration.shared.environment
84+
_ = KeychainManager.save(.username, value: username, for: env)
85+
_ = KeychainManager.save(.password, value: password, for: env)
86+
SessionManager.shared.setBiometricEnabled(true, for: env)
87+
}
88+
89+
case .failure(let message):
90+
print(message)
91+
case .unavailable(let message):
92+
print(message)
93+
}
94+
self?.isLoginSuccess = true
95+
}
96+
} else {
97+
SessionManager.shared.setDeclinedBiometric(true, for: APIConfiguration.shared.environment)
98+
self.isLoginSuccess = true
99+
}
100+
}
56101
}
57102

GoInfoGame/GoInfoGame/UI/InitialView/InitialView.swift

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ struct InitialView: View {
1313
@State private var selectedWorkspace: Workspace? = nil
1414
@StateObject private var locManagerDelegate = LocationManagerDelegate()
1515
@AppStorage("loggedIn") private var loggedIn: Bool = false
16-
@State private var showBiometricPrompt = false
1716

1817
var body: some View {
1918
NavigationStack {
@@ -60,20 +59,6 @@ struct InitialView: View {
6059
}
6160
}
6261
}
63-
.onAppear {
64-
viewModel.checkBiometricOptInCondition(for: APIConfiguration.shared.environment)
65-
showBiometricPrompt = viewModel.shouldShowBiometricOptInPrompt
66-
}
67-
.alert("Enable Biometric Login?", isPresented: $showBiometricPrompt) {
68-
Button("Enable") {
69-
viewModel.userAcceptedBiometricOptIn(for: APIConfiguration.shared.environment)
70-
}
71-
Button("Not Now", role: .cancel) {
72-
viewModel.userDeclinedBiometricOptIn(for: APIConfiguration.shared.environment)
73-
}
74-
} message: {
75-
Text("Would you like to use Face ID or Touch ID for faster logins?")
76-
}
7762
.toolbar(.hidden)
7863
}
7964
}

GoInfoGame/GoInfoGame/UI/InitialView/InitialViewModel.swift

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@ import LocalAuthentication
1414
@MainActor
1515
class InitialViewModel: ObservableObject {
1616
let locationManagerDelegate = LocationManagerDelegate()
17-
@Published var workspaces: [Workspace] = []
17+
@Published var workspaces: [Workspace] = []
1818
@Published var longQuests: [LongFormElement] = []
1919
@Published var isLoading: Bool = false
2020

21-
@Published var shouldShowBiometricOptInPrompt = false
2221
@Published var showBiometricIDError: Bool = false
2322
@Published var biometricIDErrorMessage: String?
2423

@@ -32,30 +31,7 @@ class InitialViewModel: ObservableObject {
3231
guard let self = self else { return }
3332
fetchWorkspacesList()
3433
}
35-
36-
checkBiometricOptInCondition(for: APIConfiguration.shared.environment)
3734
}
38-
39-
func checkBiometricOptInCondition(for env: APIEnvironment) {
40-
if !SessionManager.shared.isBiometricEnabled(for: env) &&
41-
!SessionManager.shared.hasDeclinedBiometric(for: env) &&
42-
KeychainManager.load(.username, for: env) != nil &&
43-
SessionManager.shared.lastLoginPassword != nil {
44-
shouldShowBiometricOptInPrompt = true
45-
} else {
46-
shouldShowBiometricOptInPrompt = false
47-
}
48-
}
49-
50-
func userAcceptedBiometricOptIn(for env: APIEnvironment) {
51-
SessionManager.shared.setBiometricEnabled(true, for: env)
52-
SessionManager.shared.setDeclinedBiometric(false, for: env)
53-
SessionManager.shared.savePasswordForBiometric(for: env)
54-
}
55-
56-
func userDeclinedBiometricOptIn(for env: APIEnvironment) {
57-
SessionManager.shared.setDeclinedBiometric(true, for: env)
58-
}
5935

6036
// fetch workspaces list
6137
func fetchWorkspacesList() {

0 commit comments

Comments
 (0)