@@ -11,36 +11,7 @@ import Combine
1111import LoopKit
1212import 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+
0 commit comments