Skip to content

Commit e0f08ef

Browse files
authored
[LOOP-4289-4348] Critical alert modal and banner update (#525)
* initial pass at adding go to settings button in the critical alert modal * remove foreground content from the riskMitigationAlert, since it is handle by the AlertManager * added placeholder text for the alert premissions disabled banner * add dismiss button to alert permissions disabled warning * acknowledge alert with close option * response to PR comments * clean up * more cleanup * updated notification permissions banner * updated style of alert permission disabled alert * copy update
1 parent e0cf42e commit e0f08ef

File tree

9 files changed

+197
-108
lines changed

9 files changed

+197
-108
lines changed
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"info" : {
3-
"version" : 1,
4-
"author" : "xcode"
3+
"author" : "xcode",
4+
"version" : 1
55
}
6-
}
6+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "notification-permissions-on.png",
5+
"idiom" : "universal"
6+
}
7+
],
8+
"info" : {
9+
"author" : "xcode",
10+
"version" : 1
11+
}
12+
}
88.3 KB
Loading

Loop/Managers/AlertPermissionsChecker.swift

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

14+
protocol AlertPermissionsCheckerDelegate: AnyObject {
15+
func notificationsPermissions(requiresRiskMitigation: Bool, scheduledDeliveryEnabled: Bool)
16+
}
17+
1418
public 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\nTo 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-

Loop/Managers/Alerts/AlertManager.swift

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ public final class AlertManager {
8282
self?.loopDidComplete()
8383
}
8484
.store(in: &cancellables)
85-
8685
}
8786

8887
public func addAlertResponder(managerIdentifier: String, alertResponder: AlertResponder) {
@@ -131,10 +130,10 @@ public final class AlertManager {
131130
body: fgBody,
132131
acknowledgeActionButtonLabel: NSLocalizedString("Dismiss", comment: "Default alert dismissal"))
133132
issueAlert(Alert(identifier: bluetoothPoweredOffIdentifier,
134-
foregroundContent: fgcontent,
135-
backgroundContent: bgcontent,
136-
trigger: .immediate,
137-
interruptionLevel: .critical))
133+
foregroundContent: fgcontent,
134+
backgroundContent: bgcontent,
135+
trigger: .immediate,
136+
interruptionLevel: .critical))
138137
}
139138

140139
// MARK: - Loop Not Running alerts
@@ -584,3 +583,77 @@ extension AlertManager: PresetActivationObserver {
584583
}
585584
}
586585
}
586+
587+
// MARK: - Issue/Retract Alert Permissions Warning
588+
extension AlertManager: AlertPermissionsCheckerDelegate {
589+
func notificationsPermissions(requiresRiskMitigation: Bool, scheduledDeliveryEnabled: Bool) {
590+
if !issueOrRetract(alert: AlertPermissionsChecker.unsafeNotificationPermissionsAlert,
591+
condition: requiresRiskMitigation,
592+
alreadyIssued: UserDefaults.standard.hasIssuedNotificationPermissionsAlert,
593+
setAlreadyIssued: { UserDefaults.standard.hasIssuedNotificationPermissionsAlert = $0 },
594+
issueHandler: { alert in
595+
// the risk mitigation in-app alert is presented with a button to navigate to settings
596+
self.recordIssued(alert: alert)
597+
let alertController = AlertPermissionsChecker.constructUnsafeNotificationPermissionsInAppAlert() { [weak self] in
598+
self?.acknowledgeAlert(identifier: AlertPermissionsChecker.unsafeNotificationPermissionsAlertIdentifier)
599+
}
600+
self.alertPresenter.present(alertController, animated: true)
601+
}) {
602+
_ = issueOrRetract(alert: AlertPermissionsChecker.scheduledDeliveryEnabledAlert,
603+
condition: scheduledDeliveryEnabled,
604+
alreadyIssued: UserDefaults.standard.hasIssuedScheduledDeliveryEnabledAlert,
605+
setAlreadyIssued: { UserDefaults.standard.hasIssuedScheduledDeliveryEnabledAlert = $0 }, issueHandler: { alert in self.issueAlert(alert) })
606+
}
607+
}
608+
609+
private func issueOrRetract(alert: LoopKit.Alert,
610+
condition: Bool,
611+
alreadyIssued: Bool,
612+
setAlreadyIssued: (Bool) -> Void,
613+
issueHandler: @escaping (LoopKit.Alert) -> Void) -> Bool {
614+
if condition {
615+
if !alreadyIssued {
616+
issueHandler(alert)
617+
setAlreadyIssued(true)
618+
}
619+
return true
620+
} else {
621+
if alreadyIssued {
622+
setAlreadyIssued(false)
623+
retractAlert(identifier: alert.identifier)
624+
}
625+
return false
626+
}
627+
}
628+
}
629+
630+
fileprivate extension AlertManager {
631+
private var isAppInBackground: Bool {
632+
return UIApplication.shared.applicationState == UIApplication.State.background
633+
}
634+
}
635+
636+
fileprivate extension UserDefaults {
637+
private enum Key: String {
638+
case hasIssuedNotificationPermissionsAlert = "com.loopkit.Loop.HasIssuedNotificationPermissionsAlert"
639+
case hasIssuedScheduledDeliveryEnabledAlert = "com.loopkit.Loop.HasIssuedScheduledDeliveryEnabledAlert"
640+
}
641+
642+
var hasIssuedNotificationPermissionsAlert: Bool {
643+
get {
644+
return object(forKey: Key.hasIssuedNotificationPermissionsAlert.rawValue) as? Bool ?? false
645+
}
646+
set {
647+
set(newValue, forKey: Key.hasIssuedNotificationPermissionsAlert.rawValue)
648+
}
649+
}
650+
651+
var hasIssuedScheduledDeliveryEnabledAlert: Bool {
652+
get {
653+
return object(forKey: Key.hasIssuedScheduledDeliveryEnabledAlert.rawValue) as? Bool ?? false
654+
}
655+
set {
656+
set(newValue, forKey: Key.hasIssuedScheduledDeliveryEnabledAlert.rawValue)
657+
}
658+
}
659+
}

Loop/Managers/LoopAppManager.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,9 @@ class LoopAppManager: NSObject {
160160
expireAfter: Bundle.main.localCacheDuration,
161161
bluetoothProvider: bluetoothStateManager)
162162

163-
self.alertPermissionsChecker = AlertPermissionsChecker(alertManager: alertManager)
163+
self.alertPermissionsChecker = AlertPermissionsChecker()
164+
self.alertPermissionsChecker.delegate = alertManager
165+
164166
self.trustedTimeChecker = TrustedTimeChecker(alertManager: alertManager)
165167

166168
self.settingsManager = SettingsManager(cacheStore: cacheStore,

0 commit comments

Comments
 (0)