Skip to content

Commit ccfdbd4

Browse files
authored
Merge pull request #273 from TaskarCenterAtUW/bug-2050-Biometric-login-flow-issues
Bug 2050 biometric login flow issues
2 parents d89ff00 + de8c319 commit ccfdbd4

File tree

11 files changed

+101
-79
lines changed

11 files changed

+101
-79
lines changed

GoInfoGame/GoInfoGame.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2674,7 +2674,7 @@
26742674
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
26752675
CODE_SIGN_IDENTITY = "Apple Development";
26762676
CODE_SIGN_STYLE = Automatic;
2677-
CURRENT_PROJECT_VERSION = 37;
2677+
CURRENT_PROJECT_VERSION = 38;
26782678
DEVELOPMENT_TEAM = G8MQVE5WWW;
26792679
GENERATE_INFOPLIST_FILE = YES;
26802680
INFOPLIST_FILE = GoInfoGame/Info.plist;
@@ -2712,7 +2712,7 @@
27122712
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
27132713
CODE_SIGN_IDENTITY = "Apple Development";
27142714
CODE_SIGN_STYLE = Automatic;
2715-
CURRENT_PROJECT_VERSION = 37;
2715+
CURRENT_PROJECT_VERSION = 38;
27162716
DEVELOPMENT_TEAM = G8MQVE5WWW;
27172717
GENERATE_INFOPLIST_FILE = YES;
27182718
INFOPLIST_FILE = GoInfoGame/Info.plist;

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/BiometricToggleView.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import LocalAuthentication
1010

1111
struct BiometricToggleView: View {
1212
@Binding var isEnabled: Bool
13-
let onToggleOn: () -> Void
13+
let onToggleOn: (_ status: Bool) -> Void
1414

1515
private var biometricToggleText: String {
1616
let context = LAContext()
@@ -24,12 +24,8 @@ struct BiometricToggleView: View {
2424
Toggle(isOn: Binding(
2525
get: { isEnabled },
2626
set: { newValue in
27-
let wasOff = !isEnabled
2827
isEnabled = newValue
29-
if newValue && wasOff {
30-
// User turned it on manually
31-
onToggleOn()
32-
}
28+
onToggleOn(newValue)
3329
}
3430
)) {
3531
Text(biometricToggleText)

GoInfoGame/GoInfoGame/Login/PasswordAuthenticationPopupView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ struct PasswordAuthenticationPopupView: View {
6363

6464
private func handleContinue() {
6565
let environment = APIConfiguration.shared.environment
66-
let username = KeychainManager.load(.username, for: environment) ?? ""
66+
let username = SessionManager.shared.username ?? ""
6767

6868
viewModel.performBiometricEnrollment(
6969
username: username,

GoInfoGame/GoInfoGame/Login/SessionManager.swift

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77

88
import Foundation
99

10-
@MainActor
10+
//@MainActor
1111
final class SessionManager: ObservableObject {
1212
static let shared = SessionManager()
13-
private init() {}
13+
private init() {
14+
username = KeychainManager.load(.username, for: APIConfiguration.shared.environment)
15+
}
1416

1517
@Published var isLoginSuccessful: Bool = false
1618
@Published var hasLoginFailed: Bool = false
19+
private(set) var username: String? = nil
1720

1821
var lastLoginPassword: String?
1922

@@ -29,37 +32,40 @@ final class SessionManager: ObservableObject {
2932
setupType: .login,
3033
modelType: PosmLoginSuccessResponse.self
3134
) { result in
32-
DispatchQueue.main.async {
35+
DispatchQueue.main.async { [weak self] in
3336
switch result {
3437
case .success(let response):
35-
_ = KeychainManager.save(.username, value: username, for: environment)
38+
self?.username = username
3639
_ = KeychainManager.save(key: "accessToken", data: response.accessToken)
37-
self.lastLoginPassword = password
40+
self?.lastLoginPassword = password
3841

39-
self.isLoginSuccessful = true
40-
self.hasLoginFailed = false
42+
self?.isLoginSuccessful = true
43+
self?.hasLoginFailed = false
4144
completion(true, "")
4245

4346
case .failure(let error):
4447
print("Login failed:", error)
45-
self.isLoginSuccessful = false
46-
self.hasLoginFailed = true
48+
self?.isLoginSuccessful = false
49+
self?.hasLoginFailed = true
4750
completion(false, "Invalid credentials")
4851
}
4952
}
5053
}
5154
}
5255

5356
func savePasswordForBiometric(for environment: APIEnvironment) {
54-
if let password = lastLoginPassword {
57+
if let password = lastLoginPassword,
58+
let username = username {
5559
_ = KeychainManager.save(.password, value: password, for: environment)
60+
_ = KeychainManager.save(.username, value: username, for: environment)
61+
setBiometricEnabled(true, for: environment)
62+
} else {
63+
print("❌ not able to save the biometric credentials.")
5664
}
57-
setBiometricEnabled(true, for: environment)
5865
}
5966

6067
func logout(environment: APIEnvironment, clearBiometricCreds: Bool = false) {
6168
if clearBiometricCreds {
62-
_ = KeychainManager.delete(.username, for: environment)
6369
_ = KeychainManager.delete(.password, for: environment)
6470
setBiometricEnabled(false, for: environment)
6571
}

GoInfoGame/GoInfoGame/Login/View/PosmLogin.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,13 @@ struct PosmLoginView: View {
9494
} else {
9595
print("Missing credentials in Keychain")
9696
viewModel.hasLoginFailed = true
97+
viewModel.loginFailedMessage = "Invalid Credentials"
9798
}
9899

99100
case .failure(let message), .unavailable(let message):
100101
print("Biometric login failed: \(message)")
101102
viewModel.hasLoginFailed = true
103+
viewModel.loginFailedMessage = message
102104
}
103105
}
104106
}) {
@@ -109,7 +111,7 @@ struct PosmLoginView: View {
109111
}
110112

111113
if viewModel.hasLoginFailed {
112-
Text("Invalid Credentials")
114+
Text(viewModel.loginFailedMessage ?? "Invalid Credentials")
113115
.foregroundColor(.red)
114116
.padding(.top, 10)
115117
}
@@ -127,9 +129,19 @@ struct PosmLoginView: View {
127129
InitialView()
128130
}
129131
}
130-
.alert("Invalid Credentials", isPresented: $viewModel.shouldShowValidationAlert) {
132+
.alert(viewModel.errorMessage ?? "Invalid Credentials", isPresented: $viewModel.shouldShowValidationAlert) {
131133
Button("OK", role: .cancel) { }
132134
}
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+
}
133145
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("SessionExpired"))) { notification in
134146
showAlert = true
135147
}

GoInfoGame/GoInfoGame/Login/ViewModel/PosmLoginViewModel.swift

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@
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 = ""
1515
@Published var hasLoginFailed: Bool = false
16+
@Published var loginFailedMessage: String? = nil
1617
@Published var isLoginSuccess: Bool = false
1718
@Published var errorMessage: String?
1819
@Published var isLoading = false
1920
@Published var shouldShowValidationAlert: Bool = false
21+
@Published var showBiometricPrompt = false
2022

2123
@AppStorage("loggedIn") private var loggedIn: Bool = false
2224

@@ -28,6 +30,7 @@ class PosmLoginViewModel: ObservableObject {
2830
} else if password.isEmpty {
2931
errorMessage = "Password is required."
3032
} else {
33+
errorMessage = nil
3134
return true
3235
}
3336

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

46-
DispatchQueue.main.async {
47-
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+
}
4862
self.isLoginSuccess = success
63+
} else {
4964
self.hasLoginFailed = !success
50-
self.loggedIn = success
5165
}
5266
}
5367
}
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+
}
54101
}
55102

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() {

GoInfoGame/GoInfoGame/UserProfile/View/UserProfileView.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,12 @@ struct UserProfileView: View {
3939
}
4040
.padding([.bottom], 200)
4141

42-
BiometricToggleView(isEnabled: $useBiometricID) {
43-
showPasswordAuthenticationView = true
42+
BiometricToggleView(isEnabled: $useBiometricID) {status in
43+
if status {
44+
showPasswordAuthenticationView = true
45+
} else {
46+
SessionManager.shared.logout(environment: APIConfiguration.shared.environment, clearBiometricCreds: true)
47+
}
4448
}
4549
.padding([.bottom], 30)
4650

@@ -95,10 +99,6 @@ struct UserProfileView: View {
9599
Button {
96100
Utilities.clearAllData()
97101

98-
if useBiometricID == false {
99-
SessionManager.shared.logout(environment: APIConfiguration.shared.environment, clearBiometricCreds: true)
100-
}
101-
102102
if let window = UIApplication.window() {
103103
window.rootViewController = UIHostingController(rootView: PosmLoginView())
104104
}

0 commit comments

Comments
 (0)