Skip to content

Commit 885f1a3

Browse files
authored
Merge pull request #460 from loopandlearn/passcode-fallback
Add passcode fallback for Loop APNS insulin
2 parents b49614e + 3829a74 commit 885f1a3

File tree

5 files changed

+78
-86
lines changed

5 files changed

+78
-86
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 & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -315,39 +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-
alertMessage = "Authentication failed"
330-
alertType = .error
331-
showAlert = true
332-
}
333-
}
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
334333
}
335-
} else if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
336-
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in
337-
DispatchQueue.main.async {
338-
if success {
339-
sendInsulinConfirmed()
340-
} else {
341-
alertMessage = "Authentication failed"
342-
alertType = .error
343-
showAlert = true
344-
}
345-
}
346-
}
347-
} else {
348-
alertMessage = "Biometric authentication not available"
349-
alertType = .error
350-
showAlert = true
351334
}
352335
}
353336

LoopFollow/Remote/TRC/BolusView.swift

Lines changed: 2 additions & 27 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,31 +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-
134-
let reason = "Confirm your identity to send bolus."
135-
136-
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
137-
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, _ in
138-
DispatchQueue.main.async {
139-
completion(success)
140-
}
141-
}
142-
} else if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
143-
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in
144-
DispatchQueue.main.async {
145-
completion(success)
146-
}
147-
}
148-
} else {
149-
DispatchQueue.main.async {
150-
completion(false)
151-
}
152-
}
153-
}
154-
155130
private func handleValidationError(_ message: String) {
156131
alertMessage = message
157132
alertType = .validation

LoopFollow/Remote/TRC/MealView.swift

Lines changed: 2 additions & 27 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,29 +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-
308-
let reason = "Confirm your identity to send bolus."
309-
310-
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
311-
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, _ in
312-
DispatchQueue.main.async {
313-
completion(success)
314-
}
315-
}
316-
} else if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
317-
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in
318-
DispatchQueue.main.async {
319-
completion(success)
320-
}
321-
}
322-
} else {
323-
DispatchQueue.main.async {
324-
completion(false)
325-
}
326-
}
327-
}
328303
}

0 commit comments

Comments
 (0)