Skip to content

Commit a8d7b2f

Browse files
authored
Merge pull request #256 from TaskarCenterAtUW/feature-biometric-login
Biometric login
2 parents 8c8ff4a + abd1bfc commit a8d7b2f

16 files changed

+563
-85
lines changed

GoInfoGame/GoInfoGame.xcodeproj/project.pbxproj

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,6 @@
187187
FA18CAE92CCFD212008247F2 /* FinishUploadingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA18CAE82CCFD212008247F2 /* FinishUploadingModel.swift */; };
188188
FA18CAEB2CD0F718008247F2 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA18CAEA2CD0F718008247F2 /* CameraView.swift */; };
189189
FA1992ED2BB1A78C003B4719 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1992EC2BB1A78C003B4719 /* KeychainManager.swift */; };
190-
FA2EC4822BBECE38003B8B64 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1992EC2BB1A78C003B4719 /* KeychainManager.swift */; };
191190
FA50718C2D54E08800CE9798 /* AddFeatureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA50718B2D54E08800CE9798 /* AddFeatureView.swift */; };
192191
FA50718E2D54E1D200CE9798 /* BingTileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA50718D2D54E1D200CE9798 /* BingTileOverlay.swift */; };
193192
FA5071902D5B31CC00CE9798 /* CreateNoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA50718F2D5B31CC00CE9798 /* CreateNoteView.swift */; };
@@ -196,6 +195,8 @@
196195
FA50719A2D7263F800CE9798 /* LongFormImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA5071992D7263F800CE9798 /* LongFormImageView.swift */; };
197196
FA51484E2D76ED6C00C0D35B /* NotesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA51484D2D76ED6300C0D35B /* NotesViewModel.swift */; };
198197
FA5853C12B21F17F00301CDA /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA5853C02B21F17F00301CDA /* OnboardingView.swift */; };
198+
FA633CB42DD4843B00324404 /* BiometricAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA633CB32DD4842D00324404 /* BiometricAuthManager.swift */; };
199+
FA633CB62DD48C4400324404 /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA633CB52DD48C3F00324404 /* SessionManager.swift */; };
199200
FA6DB8B32D8853DD0070CCD3 /* UserSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA6DB8B22D8853D00070CCD3 /* UserSettingsView.swift */; };
200201
FA6DB8B52D96BE510070CCD3 /* HiddenQuestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA6DB8B42D96BE510070CCD3 /* HiddenQuestManager.swift */; };
201202
FA87A8102B68142F000A6BEA /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA87A80F2B68142F000A6BEA /* LoadingView.swift */; };
@@ -231,6 +232,9 @@
231232
FAD5C5162AFCBE720040C61A /* GoInfoGameUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD5C5152AFCBE720040C61A /* GoInfoGameUITestsLaunchTests.swift */; };
232233
FAE2584F2D39819300D2BB12 /* ManageQuestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE2584E2D39819300D2BB12 /* ManageQuestsView.swift */; };
233234
FAE258522D3A780F00D2BB12 /* FloatingActionButtonStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE258512D3A780F00D2BB12 /* FloatingActionButtonStack.swift */; };
235+
FAE481A22DD5EF3800149A48 /* PasswordAuthenticationPopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE481A12DD5EF2C00149A48 /* PasswordAuthenticationPopupView.swift */; };
236+
FAE481A42DD5F97800149A48 /* PasswordAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE481A32DD5F96E00149A48 /* PasswordAuthenticationViewModel.swift */; };
237+
FAE481A62DDB22DB00149A48 /* BiometricToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE481A52DDB22D400149A48 /* BiometricToggleView.swift */; };
234238
FAEE21DB2DCA1963002F9BEC /* UndoButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEE21DA2DCA1963002F9BEC /* UndoButton.swift */; };
235239
FAF44FBD2B3084EC004FE664 /* OnboardingView1.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF44FBC2B3084EC004FE664 /* OnboardingView1.swift */; };
236240
FAF44FBF2B3084FB004FE664 /* OnboardingView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF44FBE2B3084FB004FE664 /* OnboardingView2.swift */; };
@@ -518,6 +522,8 @@
518522
FA5071992D7263F800CE9798 /* LongFormImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongFormImageView.swift; sourceTree = "<group>"; };
519523
FA51484D2D76ED6300C0D35B /* NotesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesViewModel.swift; sourceTree = "<group>"; };
520524
FA5853C02B21F17F00301CDA /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
525+
FA633CB32DD4842D00324404 /* BiometricAuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricAuthManager.swift; sourceTree = "<group>"; };
526+
FA633CB52DD48C3F00324404 /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = "<group>"; };
521527
FA6DB8B22D8853D00070CCD3 /* UserSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsView.swift; sourceTree = "<group>"; };
522528
FA6DB8B42D96BE510070CCD3 /* HiddenQuestManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenQuestManager.swift; sourceTree = "<group>"; };
523529
FA87A80F2B68142F000A6BEA /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
@@ -560,6 +566,9 @@
560566
FAD5C5152AFCBE720040C61A /* GoInfoGameUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoInfoGameUITestsLaunchTests.swift; sourceTree = "<group>"; };
561567
FAE2584E2D39819300D2BB12 /* ManageQuestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageQuestsView.swift; sourceTree = "<group>"; };
562568
FAE258512D3A780F00D2BB12 /* FloatingActionButtonStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingActionButtonStack.swift; sourceTree = "<group>"; };
569+
FAE481A12DD5EF2C00149A48 /* PasswordAuthenticationPopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordAuthenticationPopupView.swift; sourceTree = "<group>"; };
570+
FAE481A32DD5F96E00149A48 /* PasswordAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordAuthenticationViewModel.swift; sourceTree = "<group>"; };
571+
FAE481A52DDB22D400149A48 /* BiometricToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricToggleView.swift; sourceTree = "<group>"; };
563572
FAEE21DA2DCA1963002F9BEC /* UndoButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UndoButton.swift; sourceTree = "<group>"; };
564573
FAF44FBC2B3084EC004FE664 /* OnboardingView1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView1.swift; sourceTree = "<group>"; };
565574
FAF44FBE2B3084FB004FE664 /* OnboardingView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView2.swift; sourceTree = "<group>"; };
@@ -1517,6 +1526,11 @@
15171526
FAFDA1FB2C6D321500ECEAE9 /* Login */ = {
15181527
isa = PBXGroup;
15191528
children = (
1529+
FA633CB52DD48C3F00324404 /* SessionManager.swift */,
1530+
FA633CB32DD4842D00324404 /* BiometricAuthManager.swift */,
1531+
FAE481A12DD5EF2C00149A48 /* PasswordAuthenticationPopupView.swift */,
1532+
FAE481A52DDB22D400149A48 /* BiometricToggleView.swift */,
1533+
FAE481A32DD5F96E00149A48 /* PasswordAuthenticationViewModel.swift */,
15201534
FAFDA2002C6D338400ECEAE9 /* ViewModel */,
15211535
FAFDA1FD2C6D335300ECEAE9 /* Model */,
15221536
FAFDA1FC2C6D322800ECEAE9 /* View */,
@@ -2006,7 +2020,6 @@
20062020
A475C7CE2B5D4FE200BABEDA /* OSMChangeset.swift in Sources */,
20072021
A40E728B2B5D48A800328848 /* OSMConnection.swift in Sources */,
20082022
A475C7D32B5E22C800BABEDA /* OSMNode.swift in Sources */,
2009-
FA2EC4822BBECE38003B8B64 /* KeychainManager.swift in Sources */,
20102023
A475C7D92B5E264700BABEDA /* OSMWay.swift in Sources */,
20112024
A475C7DD2B5E3E7C00BABEDA /* OSMPayload.swift in Sources */,
20122025
97A014BF2B7B580700F7C277 /* OSMMapData.swift in Sources */,
@@ -2123,6 +2136,7 @@
21232136
973FC02D2B58F79B00878269 /* StepsRampForm.swift in Sources */,
21242137
FA5071902D5B31CC00CE9798 /* CreateNoteView.swift in Sources */,
21252138
FA6DB8B32D8853DD0070CCD3 /* UserSettingsView.swift in Sources */,
2139+
FA633CB62DD48C4400324404 /* SessionManager.swift in Sources */,
21262140
059D47312B2C81A9000987FA /* Style.swift in Sources */,
21272141
A4E711A82B57CA4300C9DE08 /* QuestsRepository.swift in Sources */,
21282142
97AC1C112B70CF03004F0BF4 /* SidewalkSurfaceForm.swift in Sources */,
@@ -2135,6 +2149,7 @@
21352149
973FC0012B46CD5D00878269 /* QuestsListUIView.swift in Sources */,
21362150
973FC01F2B50DE7E00878269 /* ImageGridItemView.swift in Sources */,
21372151
97AC1C212B726AC9004F0BF4 /* TactilePavingKerb.swift in Sources */,
2152+
FA633CB42DD4843B00324404 /* BiometricAuthManager.swift in Sources */,
21382153
A4E711A22B57A3B400C9DE08 /* SideWalkWidth.swift in Sources */,
21392154
05CB71E02B0FAFD200DED821 /* GenericUIViewControllerRepresentable.swift in Sources */,
21402155
971F9FE42B84CDBF005397CC /* CrossingType.swift in Sources */,
@@ -2175,6 +2190,7 @@
21752190
973FC04A2B5A478C00878269 /* CrossMarkingForm.swift in Sources */,
21762191
FAE2584F2D39819300D2BB12 /* ManageQuestsView.swift in Sources */,
21772192
973FC03F2B59418B00878269 /* WayLit.swift in Sources */,
2193+
FAE481A42DD5F97800149A48 /* PasswordAuthenticationViewModel.swift in Sources */,
21782194
971342772BBD415600174EBF /* InitialViewController.swift in Sources */,
21792195
A40004EC2B62338400AF21FB /* AppQuestManager.swift in Sources */,
21802196
FA51484E2D76ED6C00C0D35B /* NotesViewModel.swift in Sources */,
@@ -2183,6 +2199,7 @@
21832199
A4B83AF32B5F9385006684CA /* StoredWay.swift in Sources */,
21842200
FA18CAE52CC7D0DD008247F2 /* SequenceModel.swift in Sources */,
21852201
971F9FDF2B847349005397CC /* CrossingIslandForm.swift in Sources */,
2202+
FAE481A22DD5EF3800149A48 /* PasswordAuthenticationPopupView.swift in Sources */,
21862203
05DBBB5F2B1263FF00B6F110 /* DatabaseConnector.swift in Sources */,
21872204
FAF44FC12B30850A004FE664 /* OnboardingView3.swift in Sources */,
21882205
97439F7A2B87447400DA43E1 /* CrossingKerbHeight.swift in Sources */,
@@ -2230,6 +2247,7 @@
22302247
B0CCB98C2B8626AE00AA73DE /* ProfileView.swift in Sources */,
22312248
FAF44FBD2B3084EC004FE664 /* OnboardingView1.swift in Sources */,
22322249
973FC01D2B4FEE1B00878269 /* YesNoView.swift in Sources */,
2250+
FAE481A62DDB22DB00149A48 /* BiometricToggleView.swift in Sources */,
22332251
);
22342252
runOnlyForDeploymentPostprocessing = 0;
22352253
};
@@ -2345,7 +2363,7 @@
23452363
CODE_SIGN_STYLE = Automatic;
23462364
CURRENT_PROJECT_VERSION = 1;
23472365
DEFINES_MODULE = YES;
2348-
DEVELOPMENT_TEAM = NPCMG529DV;
2366+
DEVELOPMENT_TEAM = G8MQVE5WWW;
23492367
DYLIB_COMPATIBILITY_VERSION = 1;
23502368
DYLIB_CURRENT_VERSION = 1;
23512369
DYLIB_INSTALL_NAME_BASE = "@rpath";
@@ -2470,7 +2488,7 @@
24702488
CODE_SIGN_STYLE = Automatic;
24712489
CURRENT_PROJECT_VERSION = 1;
24722490
DEFINES_MODULE = YES;
2473-
DEVELOPMENT_TEAM = NPCMG529DV;
2491+
DEVELOPMENT_TEAM = G8MQVE5WWW;
24742492
DYLIB_COMPATIBILITY_VERSION = 1;
24752493
DYLIB_CURRENT_VERSION = 1;
24762494
DYLIB_INSTALL_NAME_BASE = "@rpath";

GoInfoGame/GoInfoGame/Helpers/Utils.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ struct Utilities {
4040
@AppStorage("loggedIn") var loggedIn: Bool = false
4141
_ = KeychainManager.delete(key: "accessToken")
4242
_ = KeychainManager.delete(key: "refreshToken")
43-
_ = KeychainManager.delete(key: "username")
43+
// _ = KeychainManager.delete(key: "username")
4444
loggedIn = false
4545
UserProfileCache.shared.clearUserProfile()
4646
UserDefaults.standard.removeObject(forKey: "accessToken_Generate")

GoInfoGame/GoInfoGame/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
</array>
1616
</dict>
1717
</array>
18+
<key>NSFaceIDUsageDescription</key>
19+
<string>Used to securely log in using Face ID</string>
1820
<key>UIApplicationSceneManifest</key>
1921
<dict>
2022
<key>UIApplicationSupportsMultipleScenes</key>

GoInfoGame/GoInfoGame/KeychainManager.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,29 @@ struct KeychainManager {
5656
return status == errSecSuccess
5757
}
5858
}
59+
60+
extension KeychainManager {
61+
enum Key: String {
62+
case username
63+
case password
64+
65+
func namespaced(for environment: APIEnvironment) -> String {
66+
return "\(rawValue)_\(environment.rawValue)"
67+
}
68+
}
69+
70+
static func save(_ key: Key, value: String, for environment: APIEnvironment) -> Bool {
71+
let namespacedKey = key.namespaced(for: environment)
72+
return save(key: namespacedKey, data: value)
73+
}
74+
75+
static func load(_ key: Key, for environment: APIEnvironment) -> String? {
76+
let namespacedKey = key.namespaced(for: environment)
77+
return load(key: namespacedKey)
78+
}
79+
80+
static func delete(_ key: Key, for environment: APIEnvironment) -> Bool {
81+
let namespacedKey = key.namespaced(for: environment)
82+
return delete(key: namespacedKey)
83+
}
84+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//
2+
// BiometricAuthenticator.swift
3+
// GoInfoGame
4+
//
5+
// Created by Achyut Kumar Maddela on 14/05/25.
6+
//
7+
8+
import Foundation
9+
import LocalAuthentication
10+
11+
struct BiometricAuthManager {
12+
13+
enum BiometricAuthResult {
14+
case success
15+
case failure(String)
16+
case unavailable(String)
17+
}
18+
19+
static func authenticate(reason: String = "Authenticate to proceed", completion: @escaping (BiometricAuthResult) -> Void) {
20+
let context = LAContext()
21+
var error: NSError?
22+
23+
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
24+
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authError in
25+
DispatchQueue.main.async {
26+
if success {
27+
completion(.success)
28+
} else {
29+
let message = authError?.localizedDescription ?? "Authentication failed."
30+
completion(.failure(message))
31+
}
32+
}
33+
}
34+
} else {
35+
let message = error?.localizedDescription ?? "Biometric authentication not available."
36+
DispatchQueue.main.async {
37+
completion(.unavailable(message))
38+
}
39+
}
40+
}
41+
42+
static func biometricType() -> LABiometryType {
43+
let context = LAContext()
44+
_ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
45+
return context.biometryType
46+
}
47+
48+
static func biometricLabelText() -> String {
49+
switch biometricType() {
50+
case .faceID:
51+
return "Login with Face ID"
52+
case .touchID:
53+
return "Login with Touch ID"
54+
default:
55+
return "Login with Biometrics"
56+
}
57+
}
58+
59+
static func biometricIcon() -> String {
60+
switch biometricType() {
61+
case .faceID:
62+
return "faceid"
63+
case .touchID:
64+
return "lock"
65+
default:
66+
return "lock"
67+
}
68+
}
69+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// BiometricToggleView.swift
3+
// GoInfoGame
4+
//
5+
// Created by Achyut Kumar M on 19/05/25.
6+
//
7+
8+
import SwiftUI
9+
import LocalAuthentication
10+
11+
struct BiometricToggleView: View {
12+
@Binding var isEnabled: Bool
13+
let onToggleOn: () -> Void
14+
15+
private var biometricToggleText: String {
16+
let context = LAContext()
17+
_ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
18+
return context.biometryType == .faceID ? "Use Face ID for Login" :
19+
context.biometryType == .touchID ? "Use Touch ID for Login" :
20+
"Use Biometric Login"
21+
}
22+
23+
var body: some View {
24+
Toggle(isOn: Binding(
25+
get: { isEnabled },
26+
set: { newValue in
27+
let wasOff = !isEnabled
28+
isEnabled = newValue
29+
if newValue && wasOff {
30+
// User turned it on manually
31+
onToggleOn()
32+
}
33+
}
34+
)) {
35+
Text(biometricToggleText)
36+
}
37+
}
38+
}
39+
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//
2+
// PasswordAuthenticationPopupView.swift
3+
// GoInfoGame
4+
//
5+
// Created by Achyut Kumar M on 15/05/25.
6+
//
7+
8+
import SwiftUI
9+
import LocalAuthentication
10+
11+
struct PasswordAuthenticationPopupView: View {
12+
@ObservedObject var viewModel: PasswordAuthenticationViewModel
13+
let onSuccess: () -> Void
14+
let onCancel: () -> Void
15+
let onFailure: (String) -> Void
16+
17+
var body: some View {
18+
ZStack {
19+
Color.black.opacity(0.4).ignoresSafeArea()
20+
21+
VStack(spacing: 16) {
22+
Text("Enable Biometric Login")
23+
.font(.headline)
24+
25+
SecureField("Enter your password", text: $viewModel.password)
26+
.textContentType(.password)
27+
.padding()
28+
.background(Color(.systemGray6))
29+
.cornerRadius(8)
30+
31+
if !viewModel.errorMessage.isEmpty {
32+
Text(viewModel.errorMessage)
33+
.foregroundColor(.red)
34+
.font(.caption)
35+
}
36+
37+
Button("Continue") {
38+
handleContinue()
39+
}
40+
.padding()
41+
.frame(maxWidth: .infinity)
42+
.background(Color.blue)
43+
.foregroundColor(.white)
44+
.cornerRadius(8)
45+
46+
Button("Cancel") {
47+
onCancel()
48+
}
49+
.padding(.top, 4)
50+
}
51+
.padding()
52+
.background(Color.white)
53+
.cornerRadius(16)
54+
.padding(40)
55+
56+
if viewModel.isLoading {
57+
ProgressView("Autheticating...")
58+
.frame(maxWidth: .infinity, maxHeight: .infinity)
59+
.background(Color.black.opacity(0.3).ignoresSafeArea())
60+
}
61+
}
62+
}
63+
64+
private func handleContinue() {
65+
let environment = APIConfiguration.shared.environment
66+
let username = KeychainManager.load(.username, for: environment) ?? ""
67+
68+
viewModel.performBiometricEnrollment(
69+
username: username,
70+
environment: environment,
71+
onSuccess: {
72+
onSuccess()
73+
},
74+
onFailure: { error in
75+
viewModel.errorMessage = error
76+
onFailure(error)
77+
}
78+
)
79+
}
80+
}
81+
82+
83+
84+
85+
//#Preview {
86+
// PasswordAuthenticationPopupView(
87+
// viewModel: <#PasswordAuthenticationViewModel#>, onSuccess: {
88+
// print("Biometric setup successful")
89+
// },
90+
// onCancel: {
91+
// print("Biometric setup cancelled")
92+
// }
93+
// )
94+
//}

0 commit comments

Comments
 (0)