Skip to content

Commit 16fd68d

Browse files
author
Rick Pasetto
authored
LOOP-3646: Alert when CA or Notification permissions disabled (#419)
* LOOP-3646: Alert when CA or Notification permissions disabled * Add persistence so we only notify once * PR Feedback * Split Notifications and Critical Alerts Permissions Alerts * More splitting, added Feature Flags
1 parent c3334fd commit 16fd68d

File tree

3 files changed

+161
-0
lines changed

3 files changed

+161
-0
lines changed

Loop.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
1D4990E824A25931005CC357 /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89E267FB2292456700A3F2AF /* FeatureFlags.swift */; };
3131
1D4A3E2D2478628500FD601B /* StoredAlert+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4A3E2B2478628500FD601B /* StoredAlert+CoreDataClass.swift */; };
3232
1D4A3E2E2478628500FD601B /* StoredAlert+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4A3E2C2478628500FD601B /* StoredAlert+CoreDataProperties.swift */; };
33+
1D6B1B6726866D89009AC446 /* AlertPermissionsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6B1B6626866D89009AC446 /* AlertPermissionsChecker.swift */; };
3334
1D80313D24746274002810DF /* AlertStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D80313C24746274002810DF /* AlertStoreTests.swift */; };
3435
1D82E6A025377C6B009131FB /* TrustedTimeChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D82E69F25377C6B009131FB /* TrustedTimeChecker.swift */; };
3536
1D8D55BC252274650044DBB6 /* BolusEntryViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8D55BB252274650044DBB6 /* BolusEntryViewModelTests.swift */; };
@@ -749,6 +750,7 @@
749750
1D49795724E7289700948F05 /* ServicesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesViewModel.swift; sourceTree = "<group>"; };
750751
1D4A3E2B2478628500FD601B /* StoredAlert+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoredAlert+CoreDataClass.swift"; sourceTree = "<group>"; };
751752
1D4A3E2C2478628500FD601B /* StoredAlert+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoredAlert+CoreDataProperties.swift"; sourceTree = "<group>"; };
753+
1D6B1B6626866D89009AC446 /* AlertPermissionsChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPermissionsChecker.swift; sourceTree = "<group>"; };
752754
1D80313C24746274002810DF /* AlertStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertStoreTests.swift; sourceTree = "<group>"; };
753755
1D82E69F25377C6B009131FB /* TrustedTimeChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedTimeChecker.swift; sourceTree = "<group>"; };
754756
1D8D55BB252274650044DBB6 /* BolusEntryViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusEntryViewModelTests.swift; sourceTree = "<group>"; };
@@ -2014,6 +2016,7 @@
20142016
43F5C2E41B93C5D4003EB13D /* Managers */ = {
20152017
isa = PBXGroup;
20162018
children = (
2019+
1D6B1B6626866D89009AC446 /* AlertPermissionsChecker.swift */,
20172020
439897361CD2F80600223065 /* AnalyticsServicesManager.swift */,
20182021
B4F3D25024AF890C0095CE44 /* BluetoothStateManager.swift */,
20192022
439BED291E76093C00B0AED5 /* CGMManager.swift */,
@@ -3437,6 +3440,7 @@
34373440
4FC8C8011DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift in Sources */,
34383441
43E93FB61E469A4000EAB8DB /* NumberFormatter.swift in Sources */,
34393442
C1FB428C217806A400FAB378 /* StateColorPalette.swift in Sources */,
3443+
1D6B1B6726866D89009AC446 /* AlertPermissionsChecker.swift in Sources */,
34403444
4F08DE8F1E7BB871006741EA /* CollectionType+Loop.swift in Sources */,
34413445
A9F703772489D8AA00C98AD8 /* PersistentDeviceLog+SimulatedCoreData.swift in Sources */,
34423446
C148CEE724FD91BD00711B3B /* DeliveryUncertaintyAlertManager.swift in Sources */,
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
//
2+
// AlertPermissionsChecker.swift
3+
// Loop
4+
//
5+
// Created by Rick Pasetto on 6/25/21.
6+
// Copyright © 2021 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import Combine
11+
import LoopKit
12+
import SwiftUI
13+
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)
44+
45+
private weak var alertManager: AlertManager?
46+
47+
private var isAppInBackground: Bool {
48+
return UIApplication.shared.applicationState == UIApplication.State.background
49+
}
50+
51+
private lazy var cancellables = Set<AnyCancellable>()
52+
53+
init(alertManager: AlertManager) {
54+
self.alertManager = alertManager
55+
56+
// Check on loop complete, but only while in the background.
57+
NotificationCenter.default.publisher(for: .LoopCompleted)
58+
.receive(on: RunLoop.main)
59+
.sink { [weak self] _ in
60+
guard let self = self else { return }
61+
if self.isAppInBackground {
62+
self.check()
63+
}
64+
}
65+
.store(in: &cancellables)
66+
67+
// Check on app resume
68+
NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
69+
.receive(on: RunLoop.main)
70+
.sink { [weak self] _ in
71+
self?.check()
72+
}
73+
.store(in: &cancellables)
74+
75+
NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
76+
.receive(on: RunLoop.main)
77+
.sink { [weak self] _ in
78+
self?.check()
79+
}
80+
.store(in: &cancellables)
81+
}
82+
83+
func check() {
84+
UNUserNotificationCenter.current().getNotificationSettings { settings in
85+
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+
}
94+
if FeatureFlags.criticalAlertsEnabled {
95+
if criticalAlertsPermissions == .disabled {
96+
self.maybeNotifyCriticalAlertPermissionsDisabled()
97+
} else {
98+
self.criticalAlertPermissionsEnabled()
99+
}
100+
}
101+
}
102+
}
103+
}
104+
105+
private func maybeNotifyNotificationPermissionsDisabled() {
106+
if !UserDefaults.standard.hasIssuedNotificationsPermissionsAlert {
107+
alertManager?.issueAlert(AlertPermissionsChecker.notificationsPermissionsAlert)
108+
UserDefaults.standard.hasIssuedNotificationsPermissionsAlert = true
109+
}
110+
}
111+
112+
private func notificationsPermissionsEnabled() {
113+
alertManager?.retractAlert(identifier: AlertPermissionsChecker.notificationsPermissionsAlertIdentifier)
114+
UserDefaults.standard.hasIssuedNotificationsPermissionsAlert = false
115+
}
116+
117+
private func maybeNotifyCriticalAlertPermissionsDisabled() {
118+
if !UserDefaults.standard.hasIssuedCriticalAlertPermissionsAlert {
119+
alertManager?.issueAlert(AlertPermissionsChecker.criticalAlertPermissionsAlert)
120+
UserDefaults.standard.hasIssuedCriticalAlertPermissionsAlert = true
121+
}
122+
}
123+
124+
private func criticalAlertPermissionsEnabled() {
125+
alertManager?.retractAlert(identifier: AlertPermissionsChecker.criticalAlertPermissionsAlertIdentifier)
126+
UserDefaults.standard.hasIssuedCriticalAlertPermissionsAlert = false
127+
}
128+
129+
}
130+
131+
extension UserDefaults {
132+
133+
private enum Key: String {
134+
case hasIssuedNotificationsPermissionsAlert = "com.loopkit.Loop.HasIssuedNotificationsPermissionsAlert"
135+
case hasIssuedCriticalAlertPermissionsAlert = "com.loopkit.Loop.HasIssuedCriticalAlertPermissionsAlert"
136+
}
137+
138+
var hasIssuedNotificationsPermissionsAlert: Bool {
139+
get {
140+
return object(forKey: Key.hasIssuedNotificationsPermissionsAlert.rawValue) as? Bool ?? false
141+
}
142+
set {
143+
set(newValue, forKey: Key.hasIssuedNotificationsPermissionsAlert.rawValue)
144+
}
145+
}
146+
147+
var hasIssuedCriticalAlertPermissionsAlert: Bool {
148+
get {
149+
return object(forKey: Key.hasIssuedCriticalAlertPermissionsAlert.rawValue) as? Bool ?? false
150+
}
151+
set {
152+
set(newValue, forKey: Key.hasIssuedCriticalAlertPermissionsAlert.rawValue)
153+
}
154+
}
155+
}

Loop/Managers/LoopAppManager.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class LoopAppManager: NSObject {
5858
private var trustedTimeChecker: TrustedTimeChecker!
5959
private var deviceDataManager: DeviceDataManager!
6060
private var onboardingManager: OnboardingManager!
61+
private var alertPermissionsChecker: AlertPermissionsChecker!
6162

6263
private var state: State = .initialize
6364

@@ -121,6 +122,7 @@ class LoopAppManager: NSObject {
121122
self.bluetoothStateManager = BluetoothStateManager()
122123
self.alertManager = AlertManager(alertPresenter: self,
123124
expireAfter: Bundle.main.localCacheDuration)
125+
self.alertPermissionsChecker = AlertPermissionsChecker(alertManager: alertManager)
124126
self.loopAlertsManager = LoopAlertsManager(alertManager: alertManager,
125127
bluetoothProvider: bluetoothStateManager)
126128
self.trustedTimeChecker = TrustedTimeChecker(alertManager: alertManager)

0 commit comments

Comments
 (0)