Przeglądaj źródła

Simplified and DRYed bolus auth

Jonas Björkert 10 miesięcy temu
rodzic
commit
3829a74815

+ 4 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -77,6 +77,7 @@
 		DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE42ACF2383009A6922 /* Treatments.swift */; };
 		DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */; };
 		DD493AE92ACF2445009A6922 /* BGData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE82ACF2445009A6922 /* BGData.swift */; };
+		DD4A407E2E6AFEE6007B318B /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A407D2E6AFEE6007B318B /* AuthService.swift */; };
 		DD4AFB3B2DB55CB600BB593F /* TimeOfDay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */; };
 		DD4AFB3D2DB55D2900BB593F /* AlarmConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */; };
 		DD4AFB492DB576C200BB593F /* AlarmSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */; };
@@ -461,6 +462,7 @@
 		DD493AE42ACF2383009A6922 /* Treatments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Treatments.swift; sourceTree = "<group>"; };
 		DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatus.swift; sourceTree = "<group>"; };
 		DD493AE82ACF2445009A6922 /* BGData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGData.swift; sourceTree = "<group>"; };
+		DD4A407D2E6AFEE6007B318B /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = "<group>"; };
 		DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeOfDay.swift; sourceTree = "<group>"; };
 		DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmConfiguration.swift; sourceTree = "<group>"; };
 		DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingsView.swift; sourceTree = "<group>"; };
@@ -1484,6 +1486,7 @@
 		FCC688542489367300A0279D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				DD4A407D2E6AFEE6007B318B /* AuthService.swift */,
 				DD1D52B82E1EB5DC00432050 /* TabPosition.swift */,
 				DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */,
 				DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */,
@@ -2013,6 +2016,7 @@
 				DD493AD72ACF2139009A6922 /* SuspendPump.swift in Sources */,
 				DDB9FC7F2DDB584500EFAA76 /* BolusEntry.swift in Sources */,
 				FC9788182485969B00A7906C /* AppDelegate.swift in Sources */,
+				DD4A407E2E6AFEE6007B318B /* AuthService.swift in Sources */,
 				654134182E1DC09700BDBE08 /* OverridePresetsView.swift in Sources */,
 				DDDC01DD2E244B3100D9975C /* JWTManager.swift in Sources */,
 				DDD10F072C529DE800D76A8E /* Observable.swift in Sources */,

+ 55 - 0
LoopFollow/Helpers/AuthService.swift

@@ -0,0 +1,55 @@
+// LoopFollow
+// AuthService.swift
+
+import Foundation
+import LocalAuthentication
+
+public enum AuthResult {
+    case success
+    case canceled
+    case unavailable
+    case failed
+}
+
+public enum AuthService {
+    /// Unified authentication that prefers biometrics and falls back to passcode automatically.
+    /// - Parameters:
+    ///   - reason: Shown in the system auth prompt.
+    ///   - reuseDuration: Optional Touch ID/Face ID reuse window (seconds). 0 disables reuse.
+    ///   - completion: Returns an `AuthResult` representing the outcome.
+    public static func authenticate(reason: String,
+                                    reuseDuration: TimeInterval = 0,
+                                    completion: @escaping (AuthResult) -> Void)
+    {
+        let context = LAContext()
+        context.localizedFallbackTitle = "Enter Passcode"
+        if reuseDuration > 0 {
+            context.touchIDAuthenticationAllowableReuseDuration = reuseDuration
+        }
+
+        var error: NSError?
+        guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
+            completion(.unavailable)
+            return
+        }
+
+        context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, err in
+            DispatchQueue.main.async {
+                if success {
+                    completion(.success)
+                    return
+                }
+                if let e = err as? LAError {
+                    switch e.code {
+                    case .userCancel, .systemCancel, .appCancel:
+                        completion(.canceled)
+                    default:
+                        completion(.failed)
+                    }
+                } else {
+                    completion(.failed)
+                }
+            }
+        }
+    }
+}

+ 15 - 47
LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift

@@ -315,54 +315,22 @@ struct LoopAPNSBolusView: View {
     }
 
     private func authenticateAndSendInsulin() {
-        let context = LAContext()
-        var error: NSError?
-
-        let reason = "Confirm your identity to send insulin."
-
-        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
-            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, _ in
-                DispatchQueue.main.async {
-                    if success {
-                        sendInsulinConfirmed()
-                    } else {
-                        // Biometric authentication failed, try passcode fallback
-                        self.authenticateWithPasscode()
-                    }
-                }
-            }
-        } else if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
-            // No biometrics available, go directly to passcode
-            authenticateWithPasscode()
-        } else {
-            alertMessage = "Authentication not available"
-            alertType = .error
-            showAlert = true
-        }
-    }
-
-    private func authenticateWithPasscode() {
-        let context = LAContext()
-        var error: NSError?
-
-        let reason = "Confirm your identity to send insulin."
-
-        if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
-            context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in
-                DispatchQueue.main.async {
-                    if success {
-                        sendInsulinConfirmed()
-                    } else {
-                        alertMessage = "Authentication failed"
-                        alertType = .error
-                        showAlert = true
-                    }
-                }
+        AuthService.authenticate(reason: "Confirm your identity to send insulin.") { result in
+            switch result {
+            case .success:
+                sendInsulinConfirmed()
+            case .unavailable:
+                alertMessage = "Authentication not available"
+                alertType = .error
+                showAlert = true
+            case .failed:
+                alertMessage = "Authentication failed"
+                alertType = .error
+                showAlert = true
+            case .canceled:
+                // User canceled: no alert to avoid spammy UX
+                break
             }
-        } else {
-            alertMessage = "Authentication not available"
-            alertType = .error
-            showAlert = true
         }
     }
 

+ 2 - 42
LoopFollow/Remote/TRC/BolusView.swift

@@ -71,8 +71,8 @@ struct BolusView: View {
                         title: Text("Confirm Bolus"),
                         message: Text("Are you sure you want to send \(bolusAmount.doubleValue(for: HKUnit.internationalUnit()), specifier: "%.2f") U?"),
                         primaryButton: .default(Text("Confirm"), action: {
-                            authenticateUser { success in
-                                if success {
+                            AuthService.authenticate(reason: "Confirm your identity to send bolus.") { result in
+                                if case .success = result {
                                     sendBolus()
                                 }
                             }
@@ -127,46 +127,6 @@ struct BolusView: View {
         }
     }
 
-    private func authenticateUser(completion: @escaping (Bool) -> Void) {
-        let context = LAContext()
-        var error: NSError?
-        let reason = "Confirm your identity to send bolus."
-
-        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
-            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, _ in
-                DispatchQueue.main.async {
-                    if success {
-                        completion(true)
-                    } else {
-                        // Biometric failed, try passcode
-                        self.tryPasscode(completion: completion)
-                    }
-                }
-            }
-        } else {
-            // No biometrics available, try passcode directly
-            tryPasscode(completion: completion)
-        }
-    }
-    
-    private func tryPasscode(completion: @escaping (Bool) -> Void) {
-        let context = LAContext()
-        var error: NSError?
-        let reason = "Confirm your identity to send bolus."
-
-        if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
-            context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in
-                DispatchQueue.main.async {
-                    completion(success)
-                }
-            }
-        } else {
-            DispatchQueue.main.async {
-                completion(false)
-            }
-        }
-    }
-
     private func handleValidationError(_ message: String) {
         alertMessage = message
         alertType = .validation

+ 2 - 42
LoopFollow/Remote/TRC/MealView.swift

@@ -195,8 +195,8 @@ struct MealView: View {
                         primaryButton: .default(Text("Confirm"), action: {
                             DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                                 if bolusAmount > 0 {
-                                    authenticateUser { success in
-                                        if success {
+                                    AuthService.authenticate(reason: "Confirm your identity to send bolus.") { result in
+                                        if case .success = result {
                                             sendMealCommand()
                                         }
                                     }
@@ -300,44 +300,4 @@ struct MealView: View {
         alertType = .validationError
         showAlert = true
     }
-
-    private func authenticateUser(completion: @escaping (Bool) -> Void) {
-        let context = LAContext()
-        var error: NSError?
-        let reason = "Confirm your identity to send bolus."
-
-        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
-            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, _ in
-                DispatchQueue.main.async {
-                    if success {
-                        completion(true)
-                    } else {
-                        // Biometric failed, try passcode
-                        self.tryPasscode(completion: completion)
-                    }
-                }
-            }
-        } else {
-            // No biometrics available, try passcode directly
-            tryPasscode(completion: completion)
-        }
-    }
-    
-    private func tryPasscode(completion: @escaping (Bool) -> Void) {
-        let context = LAContext()
-        var error: NSError?
-        let reason = "Confirm your identity to send bolus."
-
-        if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
-            context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in
-                DispatchQueue.main.async {
-                    completion(success)
-                }
-            }
-        } else {
-            DispatchQueue.main.async {
-                completion(false)
-            }
-        }
-    }
 }