Skip to content

Commit 222c19d

Browse files
authored
Merge branch 'dev' into share-setting-with-qr
2 parents d5c5f8f + bebd8c1 commit 222c19d

File tree

7 files changed

+114
-114
lines changed

7 files changed

+114
-114
lines changed

Config.xcconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
unique_id = ${DEVELOPMENT_TEAM}
77

88
//Version (DEFAULT)
9-
LOOP_FOLLOW_MARKETING_VERSION = 3.2.0
9+
LOOP_FOLLOW_MARKETING_VERSION = 3.2.2

LoopFollow.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE42ACF2383009A6922 /* Treatments.swift */; };
8282
DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */; };
8383
DD493AE92ACF2445009A6922 /* BGData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE82ACF2445009A6922 /* BGData.swift */; };
84+
DD4A407E2E6AFEE6007B318B /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A407D2E6AFEE6007B318B /* AuthService.swift */; };
8485
DD4AFB3B2DB55CB600BB593F /* TimeOfDay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */; };
8586
DD4AFB3D2DB55D2900BB593F /* AlarmConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */; };
8687
DD4AFB492DB576C200BB593F /* AlarmSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */; };
@@ -469,6 +470,7 @@
469470
DD493AE42ACF2383009A6922 /* Treatments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Treatments.swift; sourceTree = "<group>"; };
470471
DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatus.swift; sourceTree = "<group>"; };
471472
DD493AE82ACF2445009A6922 /* BGData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGData.swift; sourceTree = "<group>"; };
473+
DD4A407D2E6AFEE6007B318B /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = "<group>"; };
472474
DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeOfDay.swift; sourceTree = "<group>"; };
473475
DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmConfiguration.swift; sourceTree = "<group>"; };
474476
DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingsView.swift; sourceTree = "<group>"; };
@@ -1496,6 +1498,7 @@
14961498
isa = PBXGroup;
14971499
children = (
14981500
656F8C112E49F3780008DC1D /* QRCodeGenerator.swift */,
1501+
DD4A407D2E6AFEE6007B318B /* AuthService.swift */,
14991502
DD1D52B82E1EB5DC00432050 /* TabPosition.swift */,
15001503
DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */,
15011504
DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */,
@@ -2028,6 +2031,7 @@
20282031
DD493AD72ACF2139009A6922 /* SuspendPump.swift in Sources */,
20292032
DDB9FC7F2DDB584500EFAA76 /* BolusEntry.swift in Sources */,
20302033
FC9788182485969B00A7906C /* AppDelegate.swift in Sources */,
2034+
DD4A407E2E6AFEE6007B318B /* AuthService.swift in Sources */,
20312035
654134182E1DC09700BDBE08 /* OverridePresetsView.swift in Sources */,
20322036
DDDC01DD2E244B3100D9975C /* JWTManager.swift in Sources */,
20332037
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/Helpers/Views/TogglableSecureInput.swift

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,22 @@ struct TogglableSecureInput: View {
2020
Group {
2121
switch style {
2222
case .singleLine:
23-
if isVisible {
24-
TextField(placeholder, text: $text)
25-
.multilineTextAlignment(.trailing)
26-
.textContentType(textContentType)
27-
.submitLabel(.done)
28-
.focused($isFocused)
29-
} else {
30-
SecureField(placeholder, text: $text)
31-
.multilineTextAlignment(.trailing)
32-
.textContentType(textContentType)
33-
.submitLabel(.done)
34-
.focused($isFocused)
23+
ZStack(alignment: .trailing) {
24+
if isVisible {
25+
TextField(placeholder, text: $text)
26+
.multilineTextAlignment(.trailing)
27+
.textContentType(textContentType)
28+
.submitLabel(.done)
29+
.focused($isFocused)
30+
} else {
31+
HStack {
32+
Spacer()
33+
Text(maskString)
34+
.font(.body.monospaced())
35+
.foregroundColor(.primary)
36+
.allowsHitTesting(false)
37+
}
38+
}
3539
}
3640

3741
case .multiLine:
@@ -53,15 +57,13 @@ struct TogglableSecureInput: View {
5357
}
5458

5559
if !isVisible {
56-
Text(maskString)
57-
.font(.body.monospaced())
58-
.foregroundColor(.primary)
59-
.frame(maxWidth: .infinity,
60-
maxHeight: .infinity,
61-
alignment: .topLeading)
62-
.padding(.top, 8)
63-
.padding(.leading, 5)
64-
.allowsHitTesting(false)
60+
HStack {
61+
Spacer()
62+
Text(maskString)
63+
.font(.body.monospaced())
64+
.foregroundColor(.primary)
65+
.allowsHitTesting(false)
66+
}
6567
}
6668
}
6769
.frame(minHeight: 100)
@@ -80,13 +82,19 @@ struct TogglableSecureInput: View {
8082
}
8183
.contentShape(Rectangle())
8284
.onTapGesture {
83-
if style == .multiLine && !isVisible {
85+
if !isVisible {
8486
isVisible = true
85-
isMultilineFocused = true
86-
} else if style == .singleLine {
87-
isFocused = true
88-
} else if style == .multiLine && isVisible {
89-
isMultilineFocused = true
87+
if style == .singleLine {
88+
isFocused = true
89+
} else if style == .multiLine {
90+
isMultilineFocused = true
91+
}
92+
} else {
93+
if style == .singleLine {
94+
isFocused = true
95+
} else if style == .multiLine {
96+
isMultilineFocused = true
97+
}
9098
}
9199
}
92100
}

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)