@@ -11,26 +11,28 @@ import Combine
1111import LoopKit
1212import SwiftUI
1313
14+ protocol AlertPermissionsCheckerDelegate : AnyObject {
15+ func notificationsPermissions( requiresRiskMitigation: Bool , scheduledDeliveryEnabled: Bool )
16+ }
17+
1418public class AlertPermissionsChecker : ObservableObject {
1519
16- private weak var alertManager : AlertManager ?
17-
1820 private var isAppInBackground : Bool {
1921 return UIApplication . shared. applicationState == UIApplication . State. background
2022 }
21-
23+
2224 private lazy var cancellables = Set < AnyCancellable > ( )
2325 private var listeningToNotificationCenter = false
2426
2527 @Published var notificationCenterSettings : NotificationCenterSettingsFlags = . none
26-
28+
2729 var showWarning : Bool {
2830 notificationCenterSettings. requiresRiskMitigation
2931 }
30-
31- init ( alertManager : AlertManager ? = nil ) {
32- self . alertManager = alertManager
33-
32+
33+ weak var delegate : AlertPermissionsCheckerDelegate ?
34+
35+ init ( ) {
3436 // Check on loop complete, but only while in the background.
3537 NotificationCenter . default. publisher ( for: . LoopCompleted)
3638 . receive ( on: RunLoop . main)
@@ -41,7 +43,7 @@ public class AlertPermissionsChecker: ObservableObject {
4143 }
4244 }
4345 . store ( in: & cancellables)
44-
46+
4547 NotificationCenter . default. publisher ( for: UIApplication . willEnterForegroundNotification)
4648 . sink { [ weak self] _ in
4749 self ? . check ( )
@@ -54,15 +56,15 @@ public class AlertPermissionsChecker: ObservableObject {
5456 }
5557 . store ( in: & cancellables)
5658 }
57-
59+
5860 func checkNow( ) {
5961 check {
6062 // Note: we do this, instead of calling notificationCenterSettingsChanged directly, so that we only
6163 // get called when it _changes_.
6264 self . listenToNotificationCenter ( )
6365 }
6466 }
65-
67+
6668 private func check( then completion: ( ( ) -> Void ) ? = nil ) {
6769 UNUserNotificationCenter . current ( ) . getNotificationSettings { settings in
6870 DispatchQueue . main. async {
@@ -81,13 +83,17 @@ public class AlertPermissionsChecker: ObservableObject {
8183 }
8284 }
8385
84- func gotoSettings( ) {
85- UIApplication . shared. open ( URL ( string: UIApplication . openSettingsURLString) !)
86+ static func gotoSettings( ) {
87+ // TODO with iOS 16 this API changes to UIApplication.openNotificationSettingsURLString
88+ if #available( iOS 15 . 4 , * ) {
89+ UIApplication . shared. open ( URL ( string: UIApplicationOpenNotificationSettingsURLString) !)
90+ } else {
91+ UIApplication . shared. open ( URL ( string: UIApplication . openSettingsURLString) !)
92+ }
8693 }
8794}
8895
89- fileprivate extension AlertPermissionsChecker {
90-
96+ extension AlertPermissionsChecker {
9197 private func listenToNotificationCenter( ) {
9298 if !listeningToNotificationCenter {
9399 $notificationCenterSettings
@@ -98,97 +104,81 @@ fileprivate extension AlertPermissionsChecker {
98104 listeningToNotificationCenter = true
99105 }
100106 }
101-
102- private func notificationCenterSettingsChanged( _ newValue: NotificationCenterSettingsFlags ) {
103- if !issueOrRetract( alert: Self . riskMitigatingAlert,
104- condition: newValue. requiresRiskMitigation,
105- alreadyIssued: UserDefaults . standard. hasIssuedRiskMitigatingAlert,
106- setAlreadyIssued: { UserDefaults . standard. hasIssuedRiskMitigatingAlert = $0} ) {
107- _ = issueOrRetract ( alert: Self . scheduledDeliveryEnabledAlert,
108- condition: newValue. scheduledDeliveryEnabled,
109- alreadyIssued: UserDefaults . standard. hasIssuedScheduledDeliveryEnabledAlert,
110- setAlreadyIssued: { UserDefaults . standard. hasIssuedScheduledDeliveryEnabledAlert = $0} )
111- }
112- }
113-
114- private func issueOrRetract( alert: LoopKit . Alert , condition: Bool , alreadyIssued: Bool , setAlreadyIssued: ( Bool ) -> Void ) -> Bool {
115- if condition {
116- if !alreadyIssued {
117- alertManager? . issueAlert ( alert)
118- setAlreadyIssued ( true )
119- }
120- return true
121- } else {
122- if alreadyIssued {
123- setAlreadyIssued ( false )
124- alertManager? . retractAlert ( identifier: alert. identifier)
125- }
126- return false
127- }
128- }
129- }
130107
131- fileprivate extension AlertPermissionsChecker {
132-
133108 // MARK: Risk Mitigating Alert
134- private static let riskMitigatingAlertIdentifier = Alert . Identifier ( managerIdentifier: " LoopAppManager " , alertIdentifier: " riskMitigatingAlert " )
135- private static let riskMitigatingAlertContent = Alert . Content (
136- title: NSLocalizedString ( " Alert Permissions Need Attention " ,
109+ static let unsafeNotificationPermissionsAlertIdentifier = Alert . Identifier ( managerIdentifier: " LoopAppManager " , alertIdentifier: " unsafeNotificationPermissionsAlert " )
110+
111+ private static let unsafeNotificationPermissionsAlertContent = Alert . Content (
112+ title: NSLocalizedString ( " Warning! Safety notifications are turned OFF " ,
137113 comment: " Alert Permissions Need Attention alert title " ) ,
138- 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 ." ,
114+ body: String ( format: NSLocalizedString ( " You may not get sound, visual or vibration alerts regarding critical safety information. \n \n To fix the issue, tap ‘Settings’ and make sure Notifications, Critical Alerts and Time Sensitive Notifications are turned ON ." ,
139115 comment: " Format for Notifications permissions disabled alert body. (1: app name) " ) ,
140116 Bundle . main. bundleDisplayName) ,
141117 acknowledgeActionButtonLabel: NSLocalizedString ( " OK " , comment: " Notifications permissions disabled alert button " )
142118 )
143- private static let riskMitigatingAlert = Alert ( identifier: riskMitigatingAlertIdentifier,
144- foregroundContent: riskMitigatingAlertContent,
145- backgroundContent: riskMitigatingAlertContent,
146- trigger: . immediate)
147-
119+
120+ static let unsafeNotificationPermissionsAlert = Alert ( identifier: unsafeNotificationPermissionsAlertIdentifier,
121+ foregroundContent: nil ,
122+ backgroundContent: unsafeNotificationPermissionsAlertContent,
123+ trigger: . immediate)
124+
125+ static func constructUnsafeNotificationPermissionsInAppAlert( acknowledgementCompletion: @escaping ( ) -> Void ) -> UIAlertController {
126+ dispatchPrecondition ( condition: . onQueue( . main) )
127+ let alertController = UIAlertController ( title: Self . unsafeNotificationPermissionsAlertContent. title,
128+ message: Self . unsafeNotificationPermissionsAlertContent. body,
129+ preferredStyle: . alert)
130+ let titleImageAttachment = NSTextAttachment ( )
131+ titleImageAttachment. image = UIImage ( systemName: " exclamationmark.triangle.fill " ) ? . withTintColor ( . critical)
132+ titleImageAttachment. bounds = CGRect ( x: titleImageAttachment. bounds. origin. x, y: - 10 , width: 40 , height: 35 )
133+ let titleWithImage = NSMutableAttributedString ( attachment: titleImageAttachment)
134+ titleWithImage. append ( NSMutableAttributedString ( string: " \n \n " , attributes: [ . font: UIFont . systemFont ( ofSize: 8 ) ] ) )
135+ titleWithImage. append ( NSMutableAttributedString ( string: Self . unsafeNotificationPermissionsAlertContent. title, attributes: [ . font: UIFont . preferredFont ( forTextStyle: . headline) ] ) )
136+ alertController. setValue ( titleWithImage, forKey: " attributedTitle " )
137+
138+ let messageImageAttachment = NSTextAttachment ( )
139+ messageImageAttachment. image = UIImage ( named: " notification-permissions-on " )
140+ messageImageAttachment. bounds = CGRect ( x: 0 , y: - 12 , width: 228 , height: 126 )
141+ let messageWithImageAttributed = NSMutableAttributedString ( string: " \n " , attributes: [ . font: UIFont . systemFont ( ofSize: 8 ) ] )
142+ messageWithImageAttributed. append ( NSMutableAttributedString ( string: Self . unsafeNotificationPermissionsAlertContent. body, attributes: [ . font: UIFont . preferredFont ( forTextStyle: . footnote) ] ) )
143+ messageWithImageAttributed. append ( NSMutableAttributedString ( string: " \n \n " , attributes: [ . font: UIFont . systemFont ( ofSize: 12 ) ] ) )
144+ messageWithImageAttributed. append ( NSMutableAttributedString ( attachment: messageImageAttachment) )
145+ alertController. setValue ( messageWithImageAttributed, forKey: " attributedMessage " )
146+
147+ alertController. addAction ( UIAlertAction ( title: NSLocalizedString ( " Settings " , comment: " Label of button that navigation user to iOS Settings " ) ,
148+ style: . default,
149+ handler: { _ in
150+ AlertPermissionsChecker . gotoSettings ( )
151+ acknowledgementCompletion ( )
152+ } ) )
153+ alertController. addAction ( UIAlertAction ( title: NSLocalizedString ( " Close " , comment: " The button label of the action used to dismiss the risk mitigation alert " ) ,
154+ style: . cancel,
155+ handler: { _ in acknowledgementCompletion ( )
156+ } ) )
157+ return alertController
158+ }
159+
148160 // MARK: Scheduled Delivery Enabled Alert
149161 private static let scheduledDeliveryEnabledAlertIdentifier = Alert . Identifier ( managerIdentifier: " LoopAppManager " ,
150162 alertIdentifier: " scheduledDeliveryEnabledAlert " )
151163 private static let scheduledDeliveryEnabledAlertContent = Alert . Content (
152164 title: NSLocalizedString ( " Notifications Delayed " ,
153165 comment: " Scheduled Delivery Enabled alert title " ) ,
154166 body: String ( format: NSLocalizedString ( """
155- Notification delivery is set to Scheduled Summary in your phone’s settings.
156-
157- To avoid delay in receiving notifications from %1$@, we recommend notification delivery be set to Immediate Delivery.
158- """ ,
167+ Notification delivery is set to Scheduled Summary in your phone’s settings.
168+
169+ To avoid delay in receiving notifications from %1$@, we recommend notification delivery be set to Immediate Delivery.
170+ """ ,
159171 comment: " Format for Critical Alerts permissions disabled alert body. (1: app name) " ) ,
160172 Bundle . main. bundleDisplayName) ,
161173 acknowledgeActionButtonLabel: NSLocalizedString ( " OK " , comment: " Critical Alert permissions disabled alert button " )
162174 )
163- private static let scheduledDeliveryEnabledAlert = Alert ( identifier: scheduledDeliveryEnabledAlertIdentifier,
164- foregroundContent: scheduledDeliveryEnabledAlertContent,
165- backgroundContent: scheduledDeliveryEnabledAlertContent,
166- trigger: . immediate)
167- }
168-
169- fileprivate extension UserDefaults {
170-
171- private enum Key : String {
172- case hasIssuedRiskMitigatingAlert = " com.loopkit.Loop.HasIssuedRiskMitigatingAlert "
173- case hasIssuedScheduledDeliveryEnabledAlert = " com.loopkit.Loop.HasIssuedScheduledDeliveryEnabledAlert "
174- }
175-
176- var hasIssuedRiskMitigatingAlert : Bool {
177- get {
178- return object ( forKey: Key . hasIssuedRiskMitigatingAlert. rawValue) as? Bool ?? false
179- }
180- set {
181- set ( newValue, forKey: Key . hasIssuedRiskMitigatingAlert. rawValue)
182- }
183- }
175+ static let scheduledDeliveryEnabledAlert = Alert ( identifier: scheduledDeliveryEnabledAlertIdentifier,
176+ foregroundContent: scheduledDeliveryEnabledAlertContent,
177+ backgroundContent: scheduledDeliveryEnabledAlertContent,
178+ trigger: . immediate)
184179
185- var hasIssuedScheduledDeliveryEnabledAlert : Bool {
186- get {
187- return object ( forKey: Key . hasIssuedScheduledDeliveryEnabledAlert. rawValue) as? Bool ?? false
188- }
189- set {
190- set ( newValue, forKey: Key . hasIssuedScheduledDeliveryEnabledAlert. rawValue)
191- }
180+ private func notificationCenterSettingsChanged( _ newValue: NotificationCenterSettingsFlags ) {
181+ delegate? . notificationsPermissions ( requiresRiskMitigation: newValue. requiresRiskMitigation, scheduledDeliveryEnabled: newValue. scheduledDeliveryEnabled)
192182 }
193183}
194184
@@ -251,4 +241,3 @@ fileprivate extension OptionSet {
251241 }
252242 }
253243}
254-
0 commit comments