Skip to content

Commit 4b51da3

Browse files
author
Rick Pasetto
authored
LOOP-3938, LOOP-3946, LOOP-3997, LOOP-3998: Updates to warning the user about Time Sensitive and Scheduled Delivery notification settings (#482)
* checkpoint * LOOP-3871: Make all alerts/notifications "Time Sensitive" Also fixes what appears to be a crash introduced into `StatusTableViewController` where there appears to be an extra call to `redrawCharts`. I'm hoping it fixes the crash. * ckpt * ckpt * Checkpoint: Moved ViewModel to the checker, for one source of truth * Get rid of NotificaitonsCriticalAlertPermissionsViewModel and unify it with AlertPermissionsChecker Change to agree with design a bit more * Some tweaks * whitespace and renaming * Some small tweaks as I re-reviewed * One more last-minute design change * Updated copy from Figma. * Do not start checking alert permissions status until after onboarding. * PR Feedback
1 parent d323b66 commit 4b51da3

File tree

8 files changed

+263
-212
lines changed

8 files changed

+263
-212
lines changed

Loop.xcodeproj/project.pbxproj

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
1DD0B76724EC77AC008A2DC3 /* SupportScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD0B76624EC77AC008A2DC3 /* SupportScreenView.swift */; };
5252
1DDE273D24AEA4B000796622 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB1CA4E24A56D7600B3B94C /* SettingsViewModel.swift */; };
5353
1DDE273E24AEA4B000796622 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE09BA824A3E23F009EE9F9 /* SettingsView.swift */; };
54-
1DDE273F24AEA4F200796622 /* NotificationsCriticalAlertPermissionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B42C951624A3CAF200857C73 /* NotificationsCriticalAlertPermissionsViewModel.swift */; };
5554
1DDE274024AEA4F200796622 /* NotificationsCriticalAlertPermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA46B5F2492E2E300D71A63 /* NotificationsCriticalAlertPermissionsView.swift */; };
5655
1DFE9E172447B6270082C280 /* UserNotificationAlertIssuerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE9E162447B6270082C280 /* UserNotificationAlertIssuerTests.swift */; };
5756
43027F0F1DFE0EC900C51989 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F526D5E1DF2459000A04910 /* HKUnit.swift */; };
@@ -1340,7 +1339,6 @@
13401339
A9FB75F0252BE320004C7D3F /* BolusDosingDecision.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusDosingDecision.swift; sourceTree = "<group>"; };
13411340
B40D07C6251A89D500C1C6D7 /* GlucoseDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDisplay.swift; sourceTree = "<group>"; };
13421341
B42C951324A3C76000857C73 /* CGMStatusHUDViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMStatusHUDViewModel.swift; sourceTree = "<group>"; };
1343-
B42C951624A3CAF200857C73 /* NotificationsCriticalAlertPermissionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsCriticalAlertPermissionsViewModel.swift; sourceTree = "<group>"; };
13441342
B43DA44024D9C12100CAFF4E /* DismissibleHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissibleHostingController.swift; sourceTree = "<group>"; };
13451343
B44251B2252350CE00605937 /* ChartAxisValuesStaticGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartAxisValuesStaticGeneratorTests.swift; sourceTree = "<group>"; };
13461344
B44251B52523578300605937 /* PredictedGlucoseChartTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PredictedGlucoseChartTests.swift; sourceTree = "<group>"; };
@@ -2378,7 +2376,6 @@
23782376
897A5A9824C22DE800C4E71D /* BolusEntryViewModel.swift */,
23792377
A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */,
23802378
C11BD0542523CFED00236B08 /* SimpleBolusViewModel.swift */,
2381-
B42C951624A3CAF200857C73 /* NotificationsCriticalAlertPermissionsViewModel.swift */,
23822379
1DB1CA4E24A56D7600B3B94C /* SettingsViewModel.swift */,
23832380
1D49795724E7289700948F05 /* ServicesViewModel.swift */,
23842381
C174233B259BEB0F00399C9D /* ManualEntryDoseViewModel.swift */,
@@ -3453,7 +3450,6 @@
34533450
89D6953E23B6DF8A002B3066 /* PotentialCarbEntryTableViewCell.swift in Sources */,
34543451
89CA2B30226C0161004D9350 /* DirectoryObserver.swift in Sources */,
34553452
1DA649A7244126CD00F61E75 /* UserNotificationAlertIssuer.swift in Sources */,
3456-
1DDE273F24AEA4F200796622 /* NotificationsCriticalAlertPermissionsViewModel.swift in Sources */,
34573453
439A7942211F631C0041B75F /* RootNavigationController.swift in Sources */,
34583454
4F11D3C020DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift in Sources */,
34593455
89E267FC2292456700A3F2AF /* FeatureFlags.swift in Sources */,

Loop/Managers/AlertPermissionsChecker.swift

Lines changed: 164 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,7 @@ import Combine
1111
import LoopKit
1212
import SwiftUI
1313

14-
class AlertPermissionsChecker {
15-
private static let notificationsPermissionsAlertIdentifier = Alert.Identifier(managerIdentifier: "LoopAppManager",
16-
alertIdentifier: "notificationsPermissionsAlert")
17-
private static let notificationsPermissionsAlertContent = Alert.Content(
18-
title: NSLocalizedString("Notifications Disabled",
19-
comment: "Notifications permissions disabled alert title"),
20-
body: String(format: NSLocalizedString("Keep Notifications turned ON in your phone’s settings to ensure that you can receive %1$@ notifications.",
21-
comment: "Format for Notifications permissions disabled alert body. (1: app name)"),
22-
Bundle.main.bundleDisplayName),
23-
acknowledgeActionButtonLabel: NSLocalizedString("OK", comment: "Notifications permissions disabled alert button")
24-
)
25-
private static let notificationsPermissionsAlert = Alert(identifier: notificationsPermissionsAlertIdentifier,
26-
foregroundContent: notificationsPermissionsAlertContent,
27-
backgroundContent: notificationsPermissionsAlertContent,
28-
trigger: .immediate)
29-
30-
private static let criticalAlertPermissionsAlertIdentifier = Alert.Identifier(managerIdentifier: "LoopAppManager",
31-
alertIdentifier: "criticalAlertPermissionsAlert")
32-
private static let criticalAlertPermissionsAlertContent = Alert.Content(
33-
title: NSLocalizedString("Critical Alerts Disabled",
34-
comment: "Critical Alert permissions disabled alert title"),
35-
body: String(format: NSLocalizedString("Keep Critical Alerts turned ON in your phone’s settings to ensure that you can receive %1$@ critical alerts.",
36-
comment: "Format for Critical Alerts permissions disabled alert body. (1: app name)"),
37-
Bundle.main.bundleDisplayName),
38-
acknowledgeActionButtonLabel: NSLocalizedString("OK", comment: "Critical Alert permissions disabled alert button")
39-
)
40-
private static let criticalAlertPermissionsAlert = Alert(identifier: criticalAlertPermissionsAlertIdentifier,
41-
foregroundContent: criticalAlertPermissionsAlertContent,
42-
backgroundContent: criticalAlertPermissionsAlertContent,
43-
trigger: .immediate)
14+
public class AlertPermissionsChecker: ObservableObject {
4415

4516
private weak var alertManager: AlertManager?
4617

@@ -49,8 +20,15 @@ class AlertPermissionsChecker {
4920
}
5021

5122
private lazy var cancellables = Set<AnyCancellable>()
23+
private var listeningToNotificationCenter = false
5224

53-
init(alertManager: AlertManager) {
25+
@Published var notificationCenterSettings: NotificationCenterSettingsFlags = .none
26+
27+
var showWarning: Bool {
28+
notificationCenterSettings.requiresRiskMitigation
29+
}
30+
31+
init(alertManager: AlertManager? = nil) {
5432
self.alertManager = alertManager
5533

5634
// Check on loop complete, but only while in the background.
@@ -64,92 +42,201 @@ class AlertPermissionsChecker {
6442
}
6543
.store(in: &cancellables)
6644

67-
// Check on app resume
6845
NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
69-
.receive(on: RunLoop.main)
7046
.sink { [weak self] _ in
7147
self?.check()
7248
}
7349
.store(in: &cancellables)
7450

7551
NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
76-
.receive(on: RunLoop.main)
7752
.sink { [weak self] _ in
7853
self?.check()
7954
}
8055
.store(in: &cancellables)
8156
}
82-
83-
func check() {
57+
58+
func checkNow() {
59+
check {
60+
// Note: we do this, instead of calling notificationCenterSettingsChanged directly, so that we only
61+
// get called when it _changes_.
62+
self.listenToNotificationCenter()
63+
}
64+
}
65+
66+
private func check(then completion: (() -> Void)? = nil) {
8467
UNUserNotificationCenter.current().getNotificationSettings { settings in
8568
DispatchQueue.main.async {
86-
let notificationsPermissions = settings.alertSetting
87-
let criticalAlertsPermissions = settings.criticalAlertSetting
88-
89-
if notificationsPermissions == .disabled {
90-
self.maybeNotifyNotificationPermissionsDisabled()
91-
} else {
92-
self.notificationsPermissionsEnabled()
93-
}
69+
self.notificationCenterSettings.notificationsDisabled = settings.alertSetting == .disabled
9470
if FeatureFlags.criticalAlertsEnabled {
95-
if criticalAlertsPermissions == .disabled {
96-
self.maybeNotifyCriticalAlertPermissionsDisabled()
97-
} else {
98-
self.criticalAlertPermissionsEnabled()
99-
}
71+
self.notificationCenterSettings.criticalAlertsDisabled = settings.criticalAlertSetting == .disabled
72+
}
73+
if #available(iOS 15.0, *) {
74+
self.notificationCenterSettings.scheduledDeliveryEnabled = settings.scheduledDeliverySetting == .enabled
75+
self.notificationCenterSettings.timeSensitiveNotificationsDisabled = settings.alertSetting != .disabled && settings.timeSensitiveSetting == .disabled
10076
}
77+
completion?()
10178
}
10279
}
10380
}
104-
105-
private func maybeNotifyNotificationPermissionsDisabled() {
106-
if !UserDefaults.standard.hasIssuedNotificationsPermissionsAlert {
107-
alertManager?.issueAlert(AlertPermissionsChecker.notificationsPermissionsAlert)
108-
UserDefaults.standard.hasIssuedNotificationsPermissionsAlert = true
109-
}
81+
82+
func gotoSettings() {
83+
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
11084
}
111-
112-
private func notificationsPermissionsEnabled() {
113-
alertManager?.retractAlert(identifier: AlertPermissionsChecker.notificationsPermissionsAlertIdentifier)
114-
UserDefaults.standard.hasIssuedNotificationsPermissionsAlert = false
85+
}
86+
87+
fileprivate extension AlertPermissionsChecker {
88+
89+
private func listenToNotificationCenter() {
90+
if !listeningToNotificationCenter {
91+
$notificationCenterSettings
92+
.receive(on: RunLoop.main)
93+
.removeDuplicates()
94+
.sink(receiveValue: notificationCenterSettingsChanged)
95+
.store(in: &cancellables)
96+
listeningToNotificationCenter = true
97+
}
11598
}
11699

117-
private func maybeNotifyCriticalAlertPermissionsDisabled() {
118-
if !UserDefaults.standard.hasIssuedCriticalAlertPermissionsAlert {
119-
alertManager?.issueAlert(AlertPermissionsChecker.criticalAlertPermissionsAlert)
120-
UserDefaults.standard.hasIssuedCriticalAlertPermissionsAlert = true
100+
private func notificationCenterSettingsChanged(_ newValue: NotificationCenterSettingsFlags) {
101+
if newValue.requiresRiskMitigation && !UserDefaults.standard.hasIssuedRiskMitigatingAlert {
102+
alertManager?.issueAlert(AlertPermissionsChecker.riskMitigatingAlert)
103+
UserDefaults.standard.hasIssuedRiskMitigatingAlert = true
104+
} else if newValue.scheduledDeliveryEnabled && !UserDefaults.standard.hasIssuedScheduledDeliveryEnabledAlert {
105+
alertManager?.issueAlert(AlertPermissionsChecker.scheduledDeliveryEnabledAlert)
106+
UserDefaults.standard.hasIssuedScheduledDeliveryEnabledAlert = true
107+
}
108+
if !newValue.requiresRiskMitigation {
109+
UserDefaults.standard.hasIssuedRiskMitigatingAlert = false
110+
alertManager?.retractAlert(identifier: AlertPermissionsChecker.riskMitigatingAlertIdentifier)
111+
}
112+
if !newValue.scheduledDeliveryEnabled {
113+
UserDefaults.standard.hasIssuedScheduledDeliveryEnabledAlert = false
114+
alertManager?.retractAlert(identifier: AlertPermissionsChecker.scheduledDeliveryEnabledAlertIdentifier)
121115
}
122116
}
117+
}
118+
119+
fileprivate extension AlertPermissionsChecker {
123120

124-
private func criticalAlertPermissionsEnabled() {
125-
alertManager?.retractAlert(identifier: AlertPermissionsChecker.criticalAlertPermissionsAlertIdentifier)
126-
UserDefaults.standard.hasIssuedCriticalAlertPermissionsAlert = false
127-
}
121+
// MARK: Risk Mitigating Alert
122+
private static let riskMitigatingAlertIdentifier = Alert.Identifier(managerIdentifier: "LoopAppManager", alertIdentifier: "riskMitigatingAlert")
123+
private static let riskMitigatingAlertContent = Alert.Content(
124+
title: NSLocalizedString("Alert Permissions Need Attention",
125+
comment: "Alert Permissions Need Attention alert title"),
126+
body: String(format: NSLocalizedString("It is important that you always keep %1$@ Notifications, Critical Alerts, and Time Sensitive Notifications turned ON in your phone’s settings to ensure that you get notified by the app.",
127+
comment: "Format for Notifications permissions disabled alert body. (1: app name)"),
128+
Bundle.main.bundleDisplayName),
129+
acknowledgeActionButtonLabel: NSLocalizedString("OK", comment: "Notifications permissions disabled alert button")
130+
)
131+
private static let riskMitigatingAlert = Alert(identifier: riskMitigatingAlertIdentifier,
132+
foregroundContent: riskMitigatingAlertContent,
133+
backgroundContent: riskMitigatingAlertContent,
134+
trigger: .immediate)
128135

136+
// MARK: Scheduled Delivery Enabled Alert
137+
private static let scheduledDeliveryEnabledAlertIdentifier = Alert.Identifier(managerIdentifier: "LoopAppManager",
138+
alertIdentifier: "scheduledDeliveryEnabledAlert")
139+
private static let scheduledDeliveryEnabledAlertContent = Alert.Content(
140+
title: NSLocalizedString("Alert Permissions Might Need Attention",
141+
comment: "Scheduled Delivery Enabled alert title"),
142+
body: String(format: NSLocalizedString("""
143+
Notification delivery is set to Scheduled Summary in your phone’s settings.
144+
145+
To avoid delay in receiving notifications from %1$@, we recommend notification delivery be set to Immediate Delivery.
146+
""",
147+
comment: "Format for Critical Alerts permissions disabled alert body. (1: app name)"),
148+
Bundle.main.bundleDisplayName),
149+
acknowledgeActionButtonLabel: NSLocalizedString("OK", comment: "Critical Alert permissions disabled alert button")
150+
)
151+
private static let scheduledDeliveryEnabledAlert = Alert(identifier: scheduledDeliveryEnabledAlertIdentifier,
152+
foregroundContent: scheduledDeliveryEnabledAlertContent,
153+
backgroundContent: scheduledDeliveryEnabledAlertContent,
154+
trigger: .immediate)
129155
}
130156

131-
extension UserDefaults {
157+
fileprivate extension UserDefaults {
132158

133159
private enum Key: String {
134-
case hasIssuedNotificationsPermissionsAlert = "com.loopkit.Loop.HasIssuedNotificationsPermissionsAlert"
135-
case hasIssuedCriticalAlertPermissionsAlert = "com.loopkit.Loop.HasIssuedCriticalAlertPermissionsAlert"
160+
case hasIssuedRiskMitigatingAlert = "com.loopkit.Loop.HasIssuedRiskMitigatingAlert"
161+
case hasIssuedScheduledDeliveryEnabledAlert = "com.loopkit.Loop.HasIssuedScheduledDeliveryEnabledAlert"
136162
}
137163

138-
var hasIssuedNotificationsPermissionsAlert: Bool {
164+
var hasIssuedRiskMitigatingAlert: Bool {
139165
get {
140-
return object(forKey: Key.hasIssuedNotificationsPermissionsAlert.rawValue) as? Bool ?? false
166+
return object(forKey: Key.hasIssuedRiskMitigatingAlert.rawValue) as? Bool ?? false
141167
}
142168
set {
143-
set(newValue, forKey: Key.hasIssuedNotificationsPermissionsAlert.rawValue)
169+
set(newValue, forKey: Key.hasIssuedRiskMitigatingAlert.rawValue)
144170
}
145171
}
146-
147-
var hasIssuedCriticalAlertPermissionsAlert: Bool {
172+
173+
var hasIssuedScheduledDeliveryEnabledAlert: Bool {
148174
get {
149-
return object(forKey: Key.hasIssuedCriticalAlertPermissionsAlert.rawValue) as? Bool ?? false
175+
return object(forKey: Key.hasIssuedScheduledDeliveryEnabledAlert.rawValue) as? Bool ?? false
150176
}
151177
set {
152-
set(newValue, forKey: Key.hasIssuedCriticalAlertPermissionsAlert.rawValue)
178+
set(newValue, forKey: Key.hasIssuedScheduledDeliveryEnabledAlert.rawValue)
153179
}
154180
}
155181
}
182+
183+
struct NotificationCenterSettingsFlags: OptionSet {
184+
let rawValue: Int
185+
186+
static let none = NotificationCenterSettingsFlags([])
187+
static let notificationsDisabled = NotificationCenterSettingsFlags(rawValue: 1 << 0)
188+
static let criticalAlertsDisabled = NotificationCenterSettingsFlags(rawValue: 1 << 1)
189+
static let timeSensitiveNotificationsDisabled = NotificationCenterSettingsFlags(rawValue: 1 << 2)
190+
static let scheduledDeliveryEnabled = NotificationCenterSettingsFlags(rawValue: 1 << 3)
191+
192+
static let requiresRiskMitigation: NotificationCenterSettingsFlags = [ .notificationsDisabled, .criticalAlertsDisabled, .timeSensitiveNotificationsDisabled ]
193+
}
194+
195+
extension NotificationCenterSettingsFlags {
196+
var notificationsDisabled: Bool {
197+
get {
198+
contains(.notificationsDisabled)
199+
}
200+
set {
201+
update(.notificationsDisabled, newValue)
202+
}
203+
}
204+
var criticalAlertsDisabled: Bool {
205+
get {
206+
contains(.criticalAlertsDisabled)
207+
}
208+
set {
209+
update(.criticalAlertsDisabled, newValue)
210+
}
211+
}
212+
var timeSensitiveNotificationsDisabled: Bool {
213+
get {
214+
contains(.timeSensitiveNotificationsDisabled)
215+
}
216+
set {
217+
update(.timeSensitiveNotificationsDisabled, newValue)
218+
}
219+
}
220+
var scheduledDeliveryEnabled: Bool {
221+
get {
222+
contains(.scheduledDeliveryEnabled)
223+
}
224+
set {
225+
update(.scheduledDeliveryEnabled, newValue)
226+
}
227+
}
228+
var requiresRiskMitigation: Bool {
229+
!self.intersection(.requiresRiskMitigation).isEmpty
230+
}
231+
}
232+
233+
fileprivate extension OptionSet {
234+
mutating func update(_ element: Self.Element, _ value: Bool) {
235+
if value {
236+
insert(element)
237+
} else {
238+
remove(element)
239+
}
240+
}
241+
}
242+

Loop/Managers/LoopAppManager.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ class LoopAppManager: NSObject {
205205

206206
let storyboard = UIStoryboard(name: "Main", bundle: Bundle(for: Self.self))
207207
let statusTableViewController = storyboard.instantiateViewController(withIdentifier: "MainStatusViewController") as! StatusTableViewController
208+
statusTableViewController.alertPermissionsChecker = alertPermissionsChecker
208209
statusTableViewController.closedLoopStatus = closedLoopStatus
209210
statusTableViewController.deviceManager = deviceDataManager
210211
statusTableViewController.onboardingManager = onboardingManager

0 commit comments

Comments
 (0)