Skip to content

Commit e0705f4

Browse files
authored
Customizing bolus errors with new certainty distinction (#465)
* Customizing bolus errors with new certainty distinction * Clarify crosstalk, and display message associated with rfCommsFailure * Just return error reason for rfCommsFailure errors, without wordy prefix * Pump and bolus error localizations are in RLKit framework now
1 parent 5521476 commit e0705f4

File tree

11 files changed

+113
-69
lines changed

11 files changed

+113
-69
lines changed

Loop.xcodeproj/project.pbxproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1089,7 +1089,7 @@
10891089
4F70C1E71DE8DCA7006380B7 /* PBXTargetDependency */,
10901090
);
10911091
name = Loop;
1092-
productName = Naterade;
1092+
productName = Loop;
10931093
productReference = 43776F8C1B8022E90074EA36 /* Loop.app */;
10941094
productType = "com.apple.product-type.application";
10951095
};

Loop/AppDelegate.swift

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
7070
extension AppDelegate: UNUserNotificationCenterDelegate {
7171
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
7272
switch response.actionIdentifier {
73-
case NotificationManager.Action.RetryBolus.rawValue:
74-
if let units = response.notification.request.content.userInfo[NotificationManager.UserInfoKey.BolusAmount.rawValue] as? Double,
75-
let startDate = response.notification.request.content.userInfo[NotificationManager.UserInfoKey.BolusStartDate.rawValue] as? Date,
73+
case NotificationManager.Action.retryBolus.rawValue:
74+
if let units = response.notification.request.content.userInfo[NotificationManager.UserInfoKey.bolusAmount.rawValue] as? Double,
75+
let startDate = response.notification.request.content.userInfo[NotificationManager.UserInfoKey.bolusStartDate.rawValue] as? Date,
7676
startDate.timeIntervalSinceNow >= TimeInterval(minutes: -5)
7777
{
7878
AnalyticsManager.sharedManager.didRetryBolus()
7979

80-
deviceManager.enactBolus(units: units) { (error) in
81-
if error != nil {
82-
NotificationManager.sendBolusFailureNotificationForAmount(units, atStartDate: startDate)
83-
}
84-
80+
deviceManager.enactBolus(units: units, at: startDate) { (_) in
8581
completionHandler()
8682
}
8783
return

Loop/Managers/AnalyticsManager.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,15 @@ final class AnalyticsManager {
118118
// MARK: - Loop Events
119119

120120
func didAddCarbsFromWatch(_ carbs: Double) {
121-
logEvent("Carb entry created", withProperties: ["source" : "Watch", "value": carbs], outOfSession: true)
121+
logEvent("Carb entry created", withProperties: ["source" : "Watch"], outOfSession: true)
122122
}
123123

124124
func didRetryBolus() {
125125
logEvent("Bolus Retry", outOfSession: true)
126126
}
127127

128128
func didSetBolusFromWatch(_ units: Double) {
129-
logEvent("Bolus set", withProperties: ["source" : "Watch", "value": units], outOfSession: true)
129+
logEvent("Bolus set", withProperties: ["source" : "Watch"], outOfSession: true)
130130
}
131131

132132
func loopDidSucceed() {

Loop/Managers/DeviceDataManager.swift

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -361,30 +361,38 @@ final class DeviceDataManager {
361361
/// - parameter units: The number of units to deliver
362362
/// - parameter completion: A clsure called after the command is complete. This closure takes a single argument:
363363
/// - error: An error describing why the command failed
364-
func enactBolus(units: Double, completion: @escaping (_ error: Error?) -> Void) {
364+
func enactBolus(units: Double, at startDate: Date = Date(), completion: @escaping (_ error: Error?) -> Void) {
365+
let notify = { (error: Error?) -> Void in
366+
if let error = error {
367+
NotificationManager.sendBolusFailureNotification(for: error, units: units, at: startDate)
368+
}
369+
370+
completion(error)
371+
}
372+
365373
guard units > 0 else {
366-
completion(nil)
374+
notify(nil)
367375
return
368376
}
369377

370378
guard let device = rileyLinkManager.firstConnectedDevice else {
371-
completion(LoopError.connectionError)
379+
notify(LoopError.connectionError)
372380
return
373381
}
374382

375383
guard let ops = device.ops else {
376-
completion(LoopError.configurationError("PumpOps"))
384+
notify(LoopError.configurationError("PumpOps"))
377385
return
378386
}
379387

380388
let setBolus = {
381389
ops.setNormalBolus(units: units) { (error) in
382390
if let error = error {
383391
self.logger.addError(error, fromSource: "Bolus")
384-
completion(LoopError.communicationError)
392+
notify(error)
385393
} else {
386394
self.loopManager.addExpectedBolus(units, at: Date())
387-
completion(nil)
395+
notify(nil)
388396
}
389397
}
390398
}
@@ -401,13 +409,18 @@ final class DeviceDataManager {
401409
switch result {
402410
case .failure(let error):
403411
self.logger.addError(error, fromSource: "Bolus")
404-
completion(error)
412+
notify(error)
405413
case .success:
406414
setBolus()
407415
}
408416
}
409417
case .failure(let error):
410-
completion(error)
418+
switch error {
419+
case let error as PumpCommsError:
420+
notify(SetBolusError.certain(error))
421+
default:
422+
notify(error)
423+
}
411424
}
412425
}
413426
} else {

Loop/Managers/NotificationManager.swift

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,38 @@
99
import UIKit
1010
import UserNotifications
1111

12+
import RileyLinkKit
13+
1214

1315
struct NotificationManager {
1416
enum Category: String {
15-
case BolusFailure
16-
case LoopNotRunning
17-
case PumpBatteryLow
18-
case PumpReservoirEmpty
19-
case PumpReservoirLow
17+
case bolusFailure
18+
case loopNotRunning
19+
case pumpBatteryLow
20+
case pumpReservoirEmpty
21+
case pumpReservoirLow
2022
}
2123

2224
enum Action: String {
23-
case RetryBolus
25+
case retryBolus
2426
}
2527

2628
enum UserInfoKey: String {
27-
case BolusAmount
28-
case BolusStartDate
29+
case bolusAmount
30+
case bolusStartDate
2931
}
3032

3133
private static var notificationCategories: Set<UNNotificationCategory> {
3234
var categories = [UNNotificationCategory]()
3335

3436
let retryBolusAction = UNNotificationAction(
35-
identifier: Action.RetryBolus.rawValue,
37+
identifier: Action.retryBolus.rawValue,
3638
title: NSLocalizedString("Retry", comment: "The title of the notification action to retry a bolus command"),
3739
options: []
3840
)
3941

4042
categories.append(UNNotificationCategory(
41-
identifier: Category.BolusFailure.rawValue,
43+
identifier: Category.bolusFailure.rawValue,
4244
actions: [retryBolusAction],
4345
intentIdentifiers: [],
4446
options: []
@@ -57,25 +59,35 @@ struct NotificationManager {
5759

5860
// MARK: - Notifications
5961

60-
static func sendBolusFailureNotificationForAmount(_ units: Double, atStartDate startDate: Date) {
62+
static func sendBolusFailureNotification(for error: Error, units: Double, at startDate: Date) {
6163
let notification = UNMutableNotificationContent()
6264

6365
notification.title = NSLocalizedString("Bolus", comment: "The notification title for a bolus failure")
64-
notification.body = String(format: NSLocalizedString("%@ U bolus may have failed.", comment: "The notification alert describing a possible bolus failure. The substitution parameter is the size of the bolus in units."), NumberFormatter.localizedString(from: NSNumber(value: units), number: .decimal))
66+
67+
switch error {
68+
case let error as RileyLinkKit.SetBolusError:
69+
notification.subtitle = error.errorDescriptionWithUnits(units)
70+
notification.body = String(format: "%@ %@", error.failureReason!, error.recoverySuggestion!)
71+
case let error as LocalizedError:
72+
notification.body = error.errorDescription ?? error.localizedDescription
73+
default:
74+
notification.body = error.localizedDescription
75+
}
76+
6577
notification.sound = UNNotificationSound.default()
6678

6779
if startDate.timeIntervalSinceNow >= TimeInterval(minutes: -5) {
68-
notification.categoryIdentifier = Category.BolusFailure.rawValue
80+
notification.categoryIdentifier = Category.bolusFailure.rawValue
6981
}
7082

7183
notification.userInfo = [
72-
UserInfoKey.BolusAmount.rawValue: units,
73-
UserInfoKey.BolusStartDate.rawValue: startDate
84+
UserInfoKey.bolusAmount.rawValue: units,
85+
UserInfoKey.bolusStartDate.rawValue: startDate
7486
]
7587

7688
let request = UNNotificationRequest(
7789
// Only support 1 bolus notification at once
78-
identifier: Category.BolusFailure.rawValue,
90+
identifier: Category.bolusFailure.rawValue,
7991
content: notification,
8092
trigger: nil
8193
)
@@ -107,11 +119,11 @@ struct NotificationManager {
107119

108120
notification.title = NSLocalizedString("Loop Failure", comment: "The notification title for a loop failure")
109121
notification.sound = UNNotificationSound.default()
110-
notification.categoryIdentifier = Category.LoopNotRunning.rawValue
111-
notification.threadIdentifier = Category.LoopNotRunning.rawValue
122+
notification.categoryIdentifier = Category.loopNotRunning.rawValue
123+
notification.threadIdentifier = Category.loopNotRunning.rawValue
112124

113125
let request = UNNotificationRequest(
114-
identifier: "\(Category.LoopNotRunning.rawValue)\(failureInterval)",
126+
identifier: "\(Category.loopNotRunning.rawValue)\(failureInterval)",
115127
content: notification,
116128
trigger: UNTimeIntervalNotificationTrigger(
117129
timeInterval: failureInterval + gracePeriod,
@@ -129,10 +141,10 @@ struct NotificationManager {
129141
notification.title = NSLocalizedString("Pump Battery Low", comment: "The notification title for a low pump battery")
130142
notification.body = NSLocalizedString("Change the pump battery immediately", comment: "The notification alert describing a low pump battery")
131143
notification.sound = UNNotificationSound.default()
132-
notification.categoryIdentifier = Category.PumpBatteryLow.rawValue
144+
notification.categoryIdentifier = Category.pumpBatteryLow.rawValue
133145

134146
let request = UNNotificationRequest(
135-
identifier: Category.PumpBatteryLow.rawValue,
147+
identifier: Category.pumpBatteryLow.rawValue,
136148
content: notification,
137149
trigger: nil
138150
)
@@ -146,11 +158,11 @@ struct NotificationManager {
146158
notification.title = NSLocalizedString("Pump Reservoir Empty", comment: "The notification title for an empty pump reservoir")
147159
notification.body = NSLocalizedString("Change the pump reservoir now", comment: "The notification alert describing an empty pump reservoir")
148160
notification.sound = UNNotificationSound.default()
149-
notification.categoryIdentifier = Category.PumpReservoirEmpty.rawValue
161+
notification.categoryIdentifier = Category.pumpReservoirEmpty.rawValue
150162

151163
let request = UNNotificationRequest(
152164
// Not a typo: this should replace any pump reservoir low notifications
153-
identifier: Category.PumpReservoirLow.rawValue,
165+
identifier: Category.pumpReservoirLow.rawValue,
154166
content: notification,
155167
trigger: nil
156168
)
@@ -179,10 +191,10 @@ struct NotificationManager {
179191
}
180192

181193
notification.sound = UNNotificationSound.default()
182-
notification.categoryIdentifier = Category.PumpReservoirLow.rawValue
194+
notification.categoryIdentifier = Category.pumpReservoirLow.rawValue
183195

184196
let request = UNNotificationRequest(
185-
identifier: Category.PumpReservoirLow.rawValue,
197+
identifier: Category.pumpReservoirLow.rawValue,
186198
content: notification,
187199
trigger: nil
188200
)

Loop/Managers/WatchDataManager.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,8 @@ final class WatchDataManager: NSObject, WCSessionDelegate {
154154
}
155155
case SetBolusUserInfo.name?:
156156
if let bolus = SetBolusUserInfo(rawValue: message as SetBolusUserInfo.RawValue) {
157-
self.deviceDataManager.enactBolus(units: bolus.value) { (error) in
158-
if error != nil {
159-
NotificationManager.sendBolusFailureNotificationForAmount(bolus.value, atStartDate: bolus.startDate)
160-
} else {
157+
self.deviceDataManager.enactBolus(units: bolus.value, at: bolus.startDate) { (error) in
158+
if error == nil {
161159
AnalyticsManager.sharedManager.didSetBolusFromWatch(bolus.value)
162160
}
163161

Loop/Models/LoopError.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
//
88

99
import Foundation
10+
import RileyLinkKit
11+
1012

1113
enum LoopError: Error {
12-
// Failure during device communication
13-
case communicationError
14+
// A bolus failed to start
15+
case bolusCommand(SetBolusError)
1416

1517
// Missing or unexpected configuration values
1618
case configurationError(String)
@@ -34,6 +36,7 @@ enum LoopError: Error {
3436
case invalidData(details: String)
3537
}
3638

39+
3740
extension LoopError: LocalizedError {
3841

3942
public var recoverySuggestion: String? {
@@ -46,14 +49,13 @@ extension LoopError: LocalizedError {
4649
}
4750

4851
public var errorDescription: String? {
49-
5052
let formatter = DateComponentsFormatter()
5153
formatter.allowedUnits = [.minute]
5254
formatter.unitsStyle = .full
5355

5456
switch self {
55-
case .communicationError:
56-
return NSLocalizedString("Communication Error", comment: "The error message displayed after a communication error.")
57+
case .bolusCommand(let error):
58+
return error.errorDescription
5759
case .configurationError(let details):
5860
return String(format: NSLocalizedString("Configuration Error: %1$@", comment: "The error message displayed for configuration errors. (1: configuration error details)"), details)
5961
case .connectionError:
@@ -76,3 +78,4 @@ extension LoopError: LocalizedError {
7678
}
7779
}
7880

81+

Loop/View Controllers/StatusTableViewController.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -798,12 +798,8 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
798798
if let bolusViewController = segue.source as? BolusViewController {
799799
if let bolus = bolusViewController.bolus, bolus > 0 {
800800
self.bolusState = .enacting
801-
let startDate = Date()
802-
dataManager.enactBolus(units: bolus) { (error) in
801+
dataManager.enactBolus(units: bolus) { (_) in
803802
self.bolusState = nil
804-
if error != nil {
805-
NotificationManager.sendBolusFailureNotificationForAmount(bolus, atStartDate: startDate)
806-
}
807803
}
808804
} else {
809805
self.bolusState = nil

WatchApp Extension/ExtensionDelegate.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import WatchConnectivity
1010
import WatchKit
1111
import os
12+
import UserNotifications
1213

1314

1415
final class ExtensionDelegate: NSObject, WKExtensionDelegate {
@@ -40,6 +41,7 @@ final class ExtensionDelegate: NSObject, WKExtensionDelegate {
4041

4142
func applicationDidFinishLaunching() {
4243
// Perform any final initialization of your application.
44+
UNUserNotificationCenter.current().delegate = self
4345
}
4446

4547
func applicationDidBecomeActive() {
@@ -170,6 +172,13 @@ extension ExtensionDelegate: WCSessionDelegate {
170172
}
171173

172174

175+
extension ExtensionDelegate: UNUserNotificationCenterDelegate {
176+
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
177+
completionHandler([.badge, .sound, .alert])
178+
}
179+
}
180+
181+
173182
extension ExtensionDelegate {
174183

175184
/// Global shortcut to present an alert for a specific error out-of-context with a specific interface controller.

WatchApp Extension/PushNotificationPayload.apns

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
{
22
"aps": {
33
"alert": {
4-
"body": "Test message",
5-
"title": "Optional title"
4+
"body": "RileyLink timed out. Check your pump before retrying.",
5+
"title": "Bolus",
6+
"subtitle": "3.5 U bolus failed"
67
},
7-
"category": "myCategory"
8+
"category": "bolusFailure"
89
},
910

1011
"WatchKit Simulator Actions": [
1112
{
12-
"title": "First Button",
13-
"identifier": "firstButtonAction"
13+
"title": "Retry",
14+
"identifier": "retryBolus"
1415
}
1516
],
1617

0 commit comments

Comments
 (0)