Skip to content

Commit 3829a74

Browse files
committed
Simplified and DRYed bolus auth
1 parent 613ccb3 commit 3829a74

File tree

5 files changed

+78
-131
lines changed

5 files changed

+78
-131
lines changed

LoopFollow.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE42ACF2383009A6922 /* Treatments.swift */; };
7878
DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */; };
7979
DD493AE92ACF2445009A6922 /* BGData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE82ACF2445009A6922 /* BGData.swift */; };
80+
DD4A407E2E6AFEE6007B318B /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A407D2E6AFEE6007B318B /* AuthService.swift */; };
8081
DD4AFB3B2DB55CB600BB593F /* TimeOfDay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */; };
8182
DD4AFB3D2DB55D2900BB593F /* AlarmConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */; };
8283
DD4AFB492DB576C200BB593F /* AlarmSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */; };
@@ -461,6 +462,7 @@
461462
DD493AE42ACF2383009A6922 /* Treatments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Treatments.swift; sourceTree = "<group>"; };
462463
DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatus.swift; sourceTree = "<group>"; };
463464
DD493AE82ACF2445009A6922 /* BGData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGData.swift; sourceTree = "<group>"; };
465+
DD4A407D2E6AFEE6007B318B /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = "<group>"; };
464466
DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeOfDay.swift; sourceTree = "<group>"; };
465467
DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmConfiguration.swift; sourceTree = "<group>"; };
466468
DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingsView.swift; sourceTree = "<group>"; };
@@ -1484,6 +1486,7 @@
14841486
FCC688542489367300A0279D /* Helpers */ = {
14851487
isa = PBXGroup;
14861488
children = (
1489+
DD4A407D2E6AFEE6007B318B /* AuthService.swift */,
14871490
DD1D52B82E1EB5DC00432050 /* TabPosition.swift */,
14881491
DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */,
14891492
DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */,
@@ -2013,6 +2016,7 @@
20132016
DD493AD72ACF2139009A6922 /* SuspendPump.swift in Sources */,
20142017
DDB9FC7F2DDB584500EFAA76 /* BolusEntry.swift in Sources */,
20152018
FC9788182485969B00A7906C /* AppDelegate.swift in Sources */,
2019+
DD4A407E2E6AFEE6007B318B /* AuthService.swift in Sources */,
20162020
654134182E1DC09700BDBE08 /* OverridePresetsView.swift in Sources */,
20172021
DDDC01DD2E244B3100D9975C /* JWTManager.swift in Sources */,
20182022
DDD10F072C529DE800D76A8E /* Observable.swift in Sources */,
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// LoopFollow
2+
// AuthService.swift
3+
4+
import Foundation
5+
import LocalAuthentication
6+
7+
public enum AuthResult {
8+
case success
9+
case canceled
10+
case unavailable
11+
case failed
12+
}
13+
14+
public enum AuthService {
15+
/// Unified authentication that prefers biometrics and falls back to passcode automatically.
16+
/// - Parameters:
17+
/// - reason: Shown in the system auth prompt.
18+
/// - reuseDuration: Optional Touch ID/Face ID reuse window (seconds). 0 disables reuse.
19+
/// - completion: Returns an `AuthResult` representing the outcome.
20+
public static func authenticate(reason: String,
21+
reuseDuration: TimeInterval = 0,
22+
completion: @escaping (AuthResult) -> Void)
23+
{
24+
let context = LAContext()
25+
context.localizedFallbackTitle = "Enter Passcode"
26+
if reuseDuration > 0 {
27+
context.touchIDAuthenticationAllowableReuseDuration = reuseDuration
28+
}
29+
30+
var error: NSError?
31+
guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
32+
completion(.unavailable)
33+
return
34+
}
35+
36+
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, err in
37+
DispatchQueue.main.async {
38+
if success {
39+
completion(.success)
40+
return
41+
}
42+
if let e = err as? LAError {
43+
switch e.code {
44+
case .userCancel, .systemCancel, .appCancel:
45+
completion(.canceled)
46+
default:
47+
completion(.failed)
48+
}
49+
} else {
50+
completion(.failed)
51+
}
52+
}
53+
}
54+
}
55+
}

LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift

Lines changed: 15 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -315,54 +315,22 @@ struct LoopAPNSBolusView: View {
315315
}
316316

317317
private func authenticateAndSendInsulin() {
318-
let context = LAContext()
319-
var error: NSError?
320-
321-
let reason = "Confirm your identity to send insulin."
322-
323-
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
324-
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, _ in
325-
DispatchQueue.main.async {
326-
if success {
327-
sendInsulinConfirmed()
328-
} else {
329-
// Biometric authentication failed, try passcode fallback
330-
self.authenticateWithPasscode()
331-
}
332-
}
333-
}
334-
} else if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
335-
// No biometrics available, go directly to passcode
336-
authenticateWithPasscode()
337-
} else {
338-
alertMessage = "Authentication not available"
339-
alertType = .error
340-
showAlert = true
341-
}
342-
}
343-
344-
private func authenticateWithPasscode() {
345-
let context = LAContext()
346-
var error: NSError?
347-
348-
let reason = "Confirm your identity to send insulin."
349-
350-
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
351-
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in
352-
DispatchQueue.main.async {
353-
if success {
354-
sendInsulinConfirmed()
355-
} else {
356-
alertMessage = "Authentication failed"
357-
alertType = .error
358-
showAlert = true
359-
}
360-
}
318+
AuthService.authenticate(reason: "Confirm your identity to send insulin.") { result in
319+
switch result {
320+
case .success:
321+
sendInsulinConfirmed()
322+
case .unavailable:
323+
alertMessage = "Authentication not available"
324+
alertType = .error
325+
showAlert = true
326+
case .failed:
327+
alertMessage = "Authentication failed"
328+
alertType = .error
329+
showAlert = true
330+
case .canceled:
331+
// User canceled: no alert to avoid spammy UX
332+
break
361333
}
362-
} else {
363-
alertMessage = "Authentication not available"
364-
alertType = .error
365-
showAlert = true
366334
}
367335
}
368336

LoopFollow/Remote/TRC/BolusView.swift

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ struct BolusView: View {
7171
title: Text("Confirm Bolus"),
7272
message: Text("Are you sure you want to send \(bolusAmount.doubleValue(for: HKUnit.internationalUnit()), specifier: "%.2f") U?"),
7373
primaryButton: .default(Text("Confirm"), action: {
74-
authenticateUser { success in
75-
if success {
74+
AuthService.authenticate(reason: "Confirm your identity to send bolus.") { result in
75+
if case .success = result {
7676
sendBolus()
7777
}
7878
}
@@ -127,46 +127,6 @@ struct BolusView: View {
127127
}
128128
}
129129

130-
private func authenticateUser(completion: @escaping (Bool) -> Void) {
131-
let context = LAContext()
132-
var error: NSError?
133-
let reason = "Confirm your identity to send bolus."
134-
135-
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
136-
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, _ in
137-
DispatchQueue.main.async {
138-
if success {
139-
completion(true)
140-
} else {
141-
// Biometric failed, try passcode
142-
self.tryPasscode(completion: completion)
143-
}
144-
}
145-
}
146-
} else {
147-
// No biometrics available, try passcode directly
148-
tryPasscode(completion: completion)
149-
}
150-
}
151-
152-
private func tryPasscode(completion: @escaping (Bool) -> Void) {
153-
let context = LAContext()
154-
var error: NSError?
155-
let reason = "Confirm your identity to send bolus."
156-
157-
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
158-
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in
159-
DispatchQueue.main.async {
160-
completion(success)
161-
}
162-
}
163-
} else {
164-
DispatchQueue.main.async {
165-
completion(false)
166-
}
167-
}
168-
}
169-
170130
private func handleValidationError(_ message: String) {
171131
alertMessage = message
172132
alertType = .validation

LoopFollow/Remote/TRC/MealView.swift

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,8 @@ struct MealView: View {
195195
primaryButton: .default(Text("Confirm"), action: {
196196
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
197197
if bolusAmount > 0 {
198-
authenticateUser { success in
199-
if success {
198+
AuthService.authenticate(reason: "Confirm your identity to send bolus.") { result in
199+
if case .success = result {
200200
sendMealCommand()
201201
}
202202
}
@@ -300,44 +300,4 @@ struct MealView: View {
300300
alertType = .validationError
301301
showAlert = true
302302
}
303-
304-
private func authenticateUser(completion: @escaping (Bool) -> Void) {
305-
let context = LAContext()
306-
var error: NSError?
307-
let reason = "Confirm your identity to send bolus."
308-
309-
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
310-
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, _ in
311-
DispatchQueue.main.async {
312-
if success {
313-
completion(true)
314-
} else {
315-
// Biometric failed, try passcode
316-
self.tryPasscode(completion: completion)
317-
}
318-
}
319-
}
320-
} else {
321-
// No biometrics available, try passcode directly
322-
tryPasscode(completion: completion)
323-
}
324-
}
325-
326-
private func tryPasscode(completion: @escaping (Bool) -> Void) {
327-
let context = LAContext()
328-
var error: NSError?
329-
let reason = "Confirm your identity to send bolus."
330-
331-
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
332-
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in
333-
DispatchQueue.main.async {
334-
completion(success)
335-
}
336-
}
337-
} else {
338-
DispatchQueue.main.async {
339-
completion(false)
340-
}
341-
}
342-
}
343303
}

0 commit comments

Comments
 (0)