Explorar o código

Merge pull request #71 from kskandis/APNsPumpNotifications

polscm32 hai 1 ano
pai
achega
04a9930af6

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -298,6 +298,7 @@
 		6EADD581738D64431902AC0A /* (null) in Sources */ = {isa = PBXBuildFile; };
 		6EADD581738D64431902AC0A /* (null) in Sources */ = {isa = PBXBuildFile; };
 		6FFAE524D1D9C262F2407CAE /* SnoozeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CAE81192B118804DCD23034 /* SnoozeProvider.swift */; };
 		6FFAE524D1D9C262F2407CAE /* SnoozeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CAE81192B118804DCD23034 /* SnoozeProvider.swift */; };
 		711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */; };
 		711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */; };
+		71D44AAB2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D44AAA2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift */; };
 		72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */; };
 		72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */; };
 		7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */; };
 		7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */; };
 		7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A401509D21F7F35D4E109EDA /* DataTableDataFlow.swift */; };
 		7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A401509D21F7F35D4E109EDA /* DataTableDataFlow.swift */; };
@@ -971,6 +972,7 @@
 		6B1A8D252B14D91700E76752 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		6B1A8D252B14D91700E76752 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityBridge.swift; sourceTree = "<group>"; };
 		6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityBridge.swift; sourceTree = "<group>"; };
 		6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActitiyAttributes.swift; sourceTree = "<group>"; };
 		6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActitiyAttributes.swift; sourceTree = "<group>"; };
+		71D44AAA2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertPermissionsChecker.swift; sourceTree = "<group>"; };
 		79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorDataFlow.swift; sourceTree = "<group>"; };
 		79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorDataFlow.swift; sourceTree = "<group>"; };
 		7E22146D3DF4853786C78132 /* CarbRatioEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorDataFlow.swift; sourceTree = "<group>"; };
 		7E22146D3DF4853786C78132 /* CarbRatioEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorDataFlow.swift; sourceTree = "<group>"; };
 		8782B44544F38F2B2D82C38E /* NightscoutConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigRootView.swift; sourceTree = "<group>"; };
 		8782B44544F38F2B2D82C38E /* NightscoutConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigRootView.swift; sourceTree = "<group>"; };
@@ -2078,6 +2080,7 @@
 		38B4F3C425E5016800E76A18 /* Notifications */ = {
 		38B4F3C425E5016800E76A18 /* Notifications */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				71D44AAA2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift */,
 				38B4F3CC25E5031100E76A18 /* Broadcaster.swift */,
 				38B4F3CC25E5031100E76A18 /* Broadcaster.swift */,
 				38B4F3C525E5017E00E76A18 /* NotificationCenter.swift */,
 				38B4F3C525E5017E00E76A18 /* NotificationCenter.swift */,
 				38B4F3C725E502C000E76A18 /* SwiftNotificationCenter */,
 				38B4F3C725E502C000E76A18 /* SwiftNotificationCenter */,
@@ -3682,6 +3685,7 @@
 				DDF847E42C5C288F0049BB3B /* LiveActivitySettingsRootView.swift in Sources */,
 				DDF847E42C5C288F0049BB3B /* LiveActivitySettingsRootView.swift in Sources */,
 				DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */,
 				DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */,
 				B7C465E9472624D8A2BE2A6A /* (null) in Sources */,
 				B7C465E9472624D8A2BE2A6A /* (null) in Sources */,
+				71D44AAB2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift in Sources */,
 				320D030F724170A637F06D50 /* (null) in Sources */,
 				320D030F724170A637F06D50 /* (null) in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
 				19E1F7E829D082D0005C8D20 /* IconConfigDataFlow.swift in Sources */,
 				19E1F7E829D082D0005C8D20 /* IconConfigDataFlow.swift in Sources */,

+ 38 - 0
FreeAPS/Resources/Assets.xcassets/Colors/ApnBackground.colorset/Contents.json

@@ -0,0 +1,38 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.929",
+          "green" : "0.926",
+          "red" : "0.918"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "0.950",
+          "blue" : "0.208",
+          "green" : "0.145",
+          "red" : "0.063"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 20 - 0
FreeAPS/Resources/Assets.xcassets/Colors/ApnBackgroundLightDark.colorset/Contents.json

@@ -0,0 +1,20 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "0.950",
+          "blue" : "0.639",
+          "green" : "0.572",
+          "red" : "0.478"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 0 - 10
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -648,15 +648,6 @@ extension BaseDeviceDataManager: AlertObserver {
     }
     }
 
 
     private func ackAlert(alert: AlertEntry) {
     private func ackAlert(alert: AlertEntry) {
-        let typeMessage: MessageType
-        let alertUp = alert.alertIdentifier.uppercased()
-        if alertUp.contains("FAULT") || alertUp.contains("ERROR") {
-            typeMessage = .errorPump
-        } else {
-            typeMessage = .warning
-        }
-
-        let messageCont = MessageContent(content: alert.contentBody ?? "Unknown", type: typeMessage)
         let alertIssueDate = alert.issuedDate
         let alertIssueDate = alert.issuedDate
 
 
         processQueue.async {
         processQueue.async {
@@ -676,7 +667,6 @@ extension BaseDeviceDataManager: AlertObserver {
             }
             }
 
 
             self.pumpManager?.acknowledgeAlert(alertIdentifier: alert.alertIdentifier) { error in
             self.pumpManager?.acknowledgeAlert(alertIdentifier: alert.alertIdentifier) { error in
-                self.router.alertMessage.send(messageCont)
                 if let error = error {
                 if let error = error {
                     self.alertHistoryStorage.ackAlert(alertIssueDate, error.localizedDescription)
                     self.alertHistoryStorage.ackAlert(alertIssueDate, error.localizedDescription)
                     debug(.deviceManager, "acknowledge not succeeded with error \(error.localizedDescription)")
                     debug(.deviceManager, "acknowledge not succeeded with error \(error.localizedDescription)")

+ 0 - 1
FreeAPS/Sources/Application/AppDelegate.swift

@@ -7,7 +7,6 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
         _ application: UIApplication,
         _ application: UIApplication,
         didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?
         didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?
     ) -> Bool {
     ) -> Bool {
-        UNUserNotificationCenter.current().delegate = self
         application.registerForRemoteNotifications()
         application.registerForRemoteNotifications()
         return true
         return true
     }
     }

+ 1 - 0
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -51,6 +51,7 @@ import Swinject
         _ = resolver.resolve(HealthKitManager.self)!
         _ = resolver.resolve(HealthKitManager.self)!
         _ = resolver.resolve(BluetoothStateManager.self)!
         _ = resolver.resolve(BluetoothStateManager.self)!
         _ = resolver.resolve(PluginManager.self)!
         _ = resolver.resolve(PluginManager.self)!
+        _ = resolver.resolve(AlertPermissionsChecker.self)!
         if #available(iOS 16.2, *) {
         if #available(iOS 16.2, *) {
             _ = resolver.resolve(LiveActivityBridge.self)!
             _ = resolver.resolve(LiveActivityBridge.self)!
         }
         }

+ 1 - 1
FreeAPS/Sources/Assemblies/ServiceAssembly.swift

@@ -20,7 +20,7 @@ final class ServiceAssembly: Assembly {
         container.register(UserNotificationsManager.self) { r in BaseUserNotificationsManager(resolver: r) }
         container.register(UserNotificationsManager.self) { r in BaseUserNotificationsManager(resolver: r) }
         container.register(WatchManager.self) { r in BaseWatchManager(resolver: r) }
         container.register(WatchManager.self) { r in BaseWatchManager(resolver: r) }
         container.register(GarminManager.self) { r in BaseGarminManager(resolver: r) }
         container.register(GarminManager.self) { r in BaseGarminManager(resolver: r) }
-
+        container.register(AlertPermissionsChecker.self) { r in AlertPermissionsChecker(resolver: r) }
         if #available(iOS 16.2, *) {
         if #available(iOS 16.2, *) {
             container.register(LiveActivityBridge.self) { r in
             container.register(LiveActivityBridge.self) { r in
                 LiveActivityBridge(resolver: r)
                 LiveActivityBridge(resolver: r)

+ 20 - 0
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -33,6 +33,10 @@ struct FreeAPSSettings: JSON, Equatable {
     var displayCalendarIOBandCOB: Bool = false
     var displayCalendarIOBandCOB: Bool = false
     var displayCalendarEmojis: Bool = false
     var displayCalendarEmojis: Bool = false
     var glucoseBadge: Bool = false
     var glucoseBadge: Bool = false
+    var notificationsPump: Bool = true
+    var notificationsCgm: Bool = true
+    var notificationsCarb: Bool = true
+    var notificationsAlgorithm: Bool = true
     var glucoseNotificationsAlways: Bool = false
     var glucoseNotificationsAlways: Bool = false
     var useAlarmSound: Bool = false
     var useAlarmSound: Bool = false
     var addSourceInfoToGlucoseNotifications: Bool = false
     var addSourceInfoToGlucoseNotifications: Bool = false
@@ -204,6 +208,22 @@ extension FreeAPSSettings: Decodable {
             settings.delay = delay
             settings.delay = delay
         }
         }
 
 
+        if let notificationsPump = try? container.decode(Bool.self, forKey: .notificationsPump) {
+            settings.notificationsPump = notificationsPump
+        }
+
+        if let notificationsCgm = try? container.decode(Bool.self, forKey: .notificationsCgm) {
+            settings.notificationsCgm = notificationsCgm
+        }
+
+        if let notificationsCarb = try? container.decode(Bool.self, forKey: .notificationsCarb) {
+            settings.notificationsCarb = notificationsCarb
+        }
+
+        if let notificationsAlgorithm = try? container.decode(Bool.self, forKey: .notificationsAlgorithm) {
+            settings.notificationsAlgorithm = notificationsAlgorithm
+        }
+
         if let glucoseNotificationsAlways = try? container.decode(Bool.self, forKey: .glucoseNotificationsAlways) {
         if let glucoseNotificationsAlways = try? container.decode(Bool.self, forKey: .glucoseNotificationsAlways) {
             settings.glucoseNotificationsAlways = glucoseNotificationsAlways
             settings.glucoseNotificationsAlways = glucoseNotificationsAlways
         }
         }

+ 10 - 0
FreeAPS/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift

@@ -9,12 +9,22 @@ extension GlucoseNotificationSettings {
         @Published var lowGlucose: Decimal = 0
         @Published var lowGlucose: Decimal = 0
         @Published var highGlucose: Decimal = 0
         @Published var highGlucose: Decimal = 0
 
 
+        @Published var notificationsPump = true
+        @Published var notificationsCgm = true
+        @Published var notificationsCarb = true
+        @Published var notificationsAlgorithm = true
+
         var units: GlucoseUnits = .mgdL
         var units: GlucoseUnits = .mgdL
 
 
         override func subscribe() {
         override func subscribe() {
             let units = settingsManager.settings.units
             let units = settingsManager.settings.units
             self.units = units
             self.units = units
 
 
+            subscribeSetting(\.notificationsPump, on: $notificationsPump) { notificationsPump = $0 }
+            subscribeSetting(\.notificationsCgm, on: $notificationsCgm) { notificationsCgm = $0 }
+            subscribeSetting(\.notificationsCarb, on: $notificationsCarb) { notificationsCarb = $0 }
+            subscribeSetting(\.notificationsAlgorithm, on: $notificationsAlgorithm) { notificationsAlgorithm = $0 }
+
             subscribeSetting(\.glucoseBadge, on: $glucoseBadge) { glucoseBadge = $0 }
             subscribeSetting(\.glucoseBadge, on: $glucoseBadge) { glucoseBadge = $0 }
             subscribeSetting(\.glucoseNotificationsAlways, on: $glucoseNotificationsAlways) { glucoseNotificationsAlways = $0 }
             subscribeSetting(\.glucoseNotificationsAlways, on: $glucoseNotificationsAlways) { glucoseNotificationsAlways = $0 }
             subscribeSetting(\.useAlarmSound, on: $useAlarmSound) { useAlarmSound = $0 }
             subscribeSetting(\.useAlarmSound, on: $useAlarmSound) { useAlarmSound = $0 }

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 72 - 2
FreeAPS/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift


+ 115 - 27
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -16,6 +16,8 @@ extension Home {
         @State var selectedTab: Int = 0
         @State var selectedTab: Int = 0
         @State private var statusTitle: String = ""
         @State private var statusTitle: String = ""
         @State var showPumpSelection: Bool = false
         @State var showPumpSelection: Bool = false
+        @State var notificationsDisabled = false
+        @State var alertSafetyNotificationsViewHeight = 0
 
 
         struct Buttons: Identifiable {
         struct Buttons: Identifiable {
             let label: String
             let label: String
@@ -54,6 +56,8 @@ extension Home {
             sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)]
             sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)]
         ) var enactedSliderTT: FetchedResults<TempTargetsSlider>
         ) var enactedSliderTT: FetchedResults<TempTargetsSlider>
 
 
+        // TODO: end todo
+
         var bolusProgressFormatter: NumberFormatter {
         var bolusProgressFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
             formatter.numberStyle = .decimal
@@ -684,41 +688,122 @@ extension Home {
             }
             }
         }
         }
 
 
-        @ViewBuilder func mainView() -> some View {
+        @ViewBuilder func alertSafetyNotificationsView(geo: GeometryProxy) -> some View {
+            ZStack {
+                /// rectangle as background
+                RoundedRectangle(cornerRadius: 15)
+                    .fill(
+                        Color(
+                            red: 0.9,
+                            green: 0.133333333,
+                            blue: 0.2156862745
+                        )
+                    )
+                    .clipShape(RoundedRectangle(cornerRadius: 15))
+                    .frame(height: geo.size.height * 0.08)
+                    .coordinateSpace(name: "alertSafetyNotificationsView")
+                    .shadow(
+                        color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) :
+                            Color.black.opacity(0.33),
+                        radius: 3
+                    )
+                HStack {
+                    Spacer()
+                    VStack {
+                        Text("⚠️ Safety Notifications are OFF")
+                            .font(.subheadline)
+                            .font(.system(size: 15, weight: .bold, design: .rounded))
+                            .foregroundStyle(.white.gradient)
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                        Text("Fix now by turning Notifications ON.")
+                            .font(.caption)
+                            .font(.system(size: 12, weight: .bold, design: .rounded))
+                            .foregroundStyle(.white.gradient)
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                    }.padding(.leading, 5)
+                    Spacer()
+                    Image(systemName: "chevron.right").foregroundColor(.white)
+                        .font(.system(size: 15, design: .rounded))
+                }.padding(.horizontal, 10)
+                    .padding(.trailing, 8)
+                    .onTapGesture {
+                        UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
+                    }
+            }.padding(.horizontal, 10)
+                .padding(.top, 0)
+        }
+
+        @ViewBuilder func mainViewWithScrollView() -> some View {
             GeometryReader { geo in
             GeometryReader { geo in
-                VStack(spacing: 0) {
-                    ZStack {
-                        /// glucose bobble
-                        glucoseView
+                ScrollView(.vertical, showsIndicators: false) {
+                    mainViewViews(geo)
+                }
+            }
+        }
 
 
-                        /// right panel with loop status and evBG
-                        HStack {
-                            Spacer()
-                            rightHeaderPanel(geo)
-                        }.padding(.trailing, 20)
+        @ViewBuilder func mainViewViews(_ geo: GeometryProxy) -> some View {
+            VStack(spacing: 0) {
+                if notificationsDisabled {
+                    alertSafetyNotificationsView(geo: geo)
+                        .padding(.top, UIDevice.adjustPadding(min: nil, max: 40))
+                }
+                ZStack {
+                    /// glucose bobble
+                    glucoseView
 
 
-                        /// left panel with pump related info
-                        HStack {
-                            pumpView
-                            Spacer()
-                        }.padding(.leading, 20)
-                    }.padding(.top, 10)
+                    /// right panel with loop status and evBG
+                    HStack {
+                        Spacer()
+                        rightHeaderPanel(geo)
+                    }.padding(.trailing, 20)
+
+                    /// left panel with pump related info
+                    HStack {
+                        pumpView
+                        Spacer()
+                    }.padding(.leading, 20)
+                }.padding(.top, 10)
 
 
-                    mealPanel(geo).padding(.top, UIDevice.adjustPadding(min: nil, max: 30))
-                        .padding(.bottom, UIDevice.adjustPadding(min: nil, max: 20))
+                mealPanel(geo).padding(.top, UIDevice.adjustPadding(min: nil, max: 30))
+                    .padding(.bottom, UIDevice.adjustPadding(min: nil, max: 20))
 
 
-                    mainChart(geo: geo)
+                mainChart(geo: geo)
 
 
-                    timeInterval.padding(.top, UIDevice.adjustPadding(min: 0, max: 12))
-                        .padding(.bottom, UIDevice.adjustPadding(min: 0, max: 12))
+                timeInterval.padding(.top, UIDevice.adjustPadding(min: 0, max: 12))
+                    .padding(.bottom, UIDevice.adjustPadding(min: 0, max: 12))
 
 
-                    if let progress = state.bolusProgress {
-                        bolusView(geo: geo, progress).padding(.bottom, UIDevice.adjustPadding(min: nil, max: 40))
-                    } else {
-                        profileView(geo: geo).padding(.bottom, UIDevice.adjustPadding(min: nil, max: 40))
+                if let progress = state.bolusProgress {
+                    bolusView(geo: geo, progress)
+                        .padding(.bottom, UIDevice.adjustPadding(min: nil, max: 40))
+                } else {
+                    profileView(geo: geo).padding(.bottom, UIDevice.adjustPadding(min: nil, max: 40))
+                }
+            }
+            .background(color)
+            .onReceive(
+                resolver.resolve(AlertPermissionsChecker.self)!.$notificationsDisabled,
+                perform: {
+                    if notificationsDisabled != $0 {
+                        notificationsDisabled = $0
+                        if notificationsDisabled {
+                            debug(.default, "notificationsDisabled")
+                        }
+                    }
+                }
+            )
+        }
+
+        @ViewBuilder func mainView() -> some View {
+            GeometryReader { geo in
+                if notificationsDisabled {
+                    ScrollView(.vertical, showsIndicators: false) {
+                        mainViewViews(geo)
+                    }
+                } else {
+                    GeometryReader { geo in
+                        mainViewViews(geo)
                     }
                     }
                 }
                 }
-                .background(color)
             }
             }
             .onChange(of: state.hours) {
             .onChange(of: state.hours) {
                 highlightButtons()
                 highlightButtons()
@@ -1006,7 +1091,10 @@ extension UIDevice {
         case largeDevice = 852 // Height for 6.1" iPhone 15 Pro
         case largeDevice = 852 // Height for 6.1" iPhone 15 Pro
     }
     }
 
 
-    @usableFromInline static func adjustPadding(min: CGFloat? = nil, max: CGFloat? = nil) -> CGFloat? {
+    @usableFromInline static func adjustPadding(
+        min: CGFloat? = nil,
+        max: CGFloat? = nil
+    ) -> CGFloat? {
         if UIScreen.screenHeight > UIDevice.DeviceSize.smallDevice.rawValue {
         if UIScreen.screenHeight > UIDevice.DeviceSize.smallDevice.rawValue {
             if UIScreen.screenHeight >= UIDevice.DeviceSize.largeDevice.rawValue {
             if UIScreen.screenHeight >= UIDevice.DeviceSize.largeDevice.rawValue {
                 return max
                 return max

+ 250 - 54
FreeAPS/Sources/Modules/Main/MainStateModel.swift

@@ -1,3 +1,4 @@
+import Combine
 import LoopKitUI
 import LoopKitUI
 import SwiftMessages
 import SwiftMessages
 import SwiftUI
 import SwiftUI
@@ -5,11 +6,201 @@ import Swinject
 
 
 extension Main {
 extension Main {
     final class StateModel: BaseStateModel<Provider> {
     final class StateModel: BaseStateModel<Provider> {
+        @Injected() var alertPermissionsChecker: AlertPermissionsChecker!
+        @Injected() var broadcaster: Broadcaster!
         private(set) var modal: Modal?
         private(set) var modal: Modal?
         @Published var isModalPresented = false
         @Published var isModalPresented = false
         @Published var isSecondaryModalPresented = false
         @Published var isSecondaryModalPresented = false
         @Published var secondaryModalView: AnyView? = nil
         @Published var secondaryModalView: AnyView? = nil
 
 
+        @Persisted(key: "UserNotificationsManager.snoozeUntilDate") private var snoozeUntilDate: Date = .distantPast
+        private var timers: [TimeInterval: Timer] = [:]
+
+        private func showTriggeredView(
+            message: MessageContent,
+            interval _: TimeInterval,
+            config: SwiftMessages.Config,
+            view: MessageView
+        ) {
+            view.customConfigureTheme(
+                colorSchemePreference: colorSchemePreference
+            )
+            setupAction(message: message, view: view)
+
+            SwiftMessages.show(config: config, view: view)
+        }
+
+        // Add or replace timer for a specific TimeInterval
+        private func addOrReplaceTriggerTimer(message: MessageContent, config: SwiftMessages.Config, view: MessageView) {
+            let trigger = message.trigger as! UNTimeIntervalNotificationTrigger
+            guard trigger.timeInterval > 0 else { return }
+            let interval = trigger.timeInterval
+
+            SwiftMessages.hide(id: view.id)
+
+            // If a timer already exists for this interval, invalidate it
+            if let existingTimer = timers[interval] {
+                existingTimer.invalidate()
+            }
+
+            // Create a new timer with the provided interval
+            let newTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in
+                self?.showTriggeredView(message: message, interval: interval, config: config, view: view)
+                self?.timers[interval] = nil
+            }
+
+            timers[interval] = newTimer
+        }
+
+        // Cancel all timers (optional cleanup method)
+        private func cancelAllTimers() {
+            timers.values.forEach { $0.invalidate() }
+            timers.removeAll()
+        }
+
+        private func setupPumpConfig() {
+            // display the pump configuration immediatly
+            if let pump = provider.deviceManager.pumpManager,
+               let bluetooth = provider.bluetoothProvider
+            {
+                let view = PumpConfig.PumpSettingsView(
+                    pumpManager: pump,
+                    bluetoothManager: bluetooth,
+                    completionDelegate: self
+                ).asAny()
+                router.mainSecondaryModalView.send(view)
+            }
+        }
+
+        private func setupButton(message _: MessageContent, view: MessageView) {
+            view.button?.setImage(UIImage(), for: .normal)
+            view.iconLabel = nil
+            let buttonImage = UIImage(systemName: "chevron.right")?.withTintColor(.white)
+            view.button?.setImage(buttonImage, for: .normal)
+            view.button?.backgroundColor = view.backgroundView.backgroundColor
+            view.button?.tintColor = view.iconImageView?.tintColor
+        }
+
+        private func setupAction(message: MessageContent, view: MessageView) {
+            switch message.action {
+            case .snooze:
+                setupButton(message: message, view: view)
+                view.buttonTapHandler = { _ in
+                    // Popup Snooze view when user taps on Glucose Notification
+                    SwiftMessages.hide()
+                    self.router.mainModalScreen.send(.snooze)
+                }
+            case .pumpConfig:
+                setupButton(message: message, view: view)
+                view.buttonTapHandler = { _ in
+                    SwiftMessages.hide()
+                    self.setupPumpConfig()
+                }
+            default: // break
+                view.button?.setImage(UIImage(), for: .normal)
+                view.buttonTapHandler = { _ in
+                    SwiftMessages.hide()
+                }
+            }
+        }
+
+        private func isApnPumpConfigAction(_ message: MessageContent) -> Bool {
+            if message.type != .error, message.action == .pumpConfig {
+                setupPumpConfig()
+                return true
+            }
+            return false
+        }
+
+        private func showAlertMessage(_ message: MessageContent) {
+            if message.useAPN, !alertPermissionsChecker.notificationsDisabled
+            {
+                showAPN(message)
+            } else {
+                showSwiftMessage(message)
+            }
+        }
+
+        private func showAPN(_ message: MessageContent) {
+            DispatchQueue.main.async {
+                self.broadcaster.notify(alertMessageNotificationObserver.self, on: .main) {
+                    $0.alertMessageNotification(message)
+                }
+            }
+        }
+
+        // Read the color scheme preference from UserDefaults; defaults to system default setting
+        @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
+
+        private func showSwiftMessage(_ message: MessageContent) {
+            if snoozeUntilDate > Date(), message.action == .snooze {
+                return
+            }
+
+            var config = SwiftMessages.defaultConfig
+            let view = MessageView.viewFromNib(layout: .cardView)
+
+            view.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
+            config.prefersStatusBarHidden = true
+
+            // Set id so that multiple notifications are not queued while waiting for user response; only the latest will be shown
+            if message.subtype == .glucose || message.subtype == .carb {
+                view.id = message.type.rawValue + message.subtype.rawValue
+            }
+
+            let titleContent: String
+
+            let iconName = UIApplication.shared.alternateIconName ?? "trioBlack"
+            let iconImage = UIImage(named: iconName) ?? UIImage()
+
+            view.configureContent(
+                title: "title",
+                body: NSLocalizedString(message.content, comment: "Info message"),
+                iconImage: nil,
+                iconText: nil,
+                buttonImage: nil,
+                buttonTitle: nil,
+                buttonTapHandler: nil
+            )
+
+            view.configureIcon(withSize: CGSize(width: 40, height: 40), contentMode: .scaleAspectFit)
+            view.iconImageView!.image = iconImage
+            view.iconImageView?.layer.cornerRadius = 10
+
+            view.customConfigureTheme(
+                colorSchemePreference: colorSchemePreference
+            )
+
+            view.iconImageView?.image = iconImage
+
+            switch message.type {
+            case .info,
+                 .other:
+                config.duration = .seconds(seconds: 5)
+                titleContent = message.title != "" ? message.title : NSLocalizedString("Info", comment: "Info title")
+            case .warning:
+                config.duration = .forever
+                titleContent = message.title != "" ? message
+                    .title : NSLocalizedString("Warning", comment: "Warning title")
+            case .error:
+                config.duration = .forever
+                titleContent = message.title != "" ? message
+                    .title : NSLocalizedString("Error", comment: "Error title")
+            }
+
+            view.titleLabel?.text = titleContent
+            config.dimMode = .gray(interactive: true)
+
+            setupAction(message: message, view: view)
+            if message.trigger != nil {
+                addOrReplaceTriggerTimer(message: message, config: config, view: view)
+            }
+
+            guard message.type == .error || message.action != .pumpConfig, message.trigger == nil, !view.isHidden else { return }
+
+            SwiftMessages.show(config: config, view: view)
+        }
+
         override func subscribe() {
         override func subscribe() {
             router.mainModalScreen
             router.mainModalScreen
                 .map { $0?.modal(resolver: self.resolver!) }
                 .map { $0?.modal(resolver: self.resolver!) }
@@ -31,60 +222,9 @@ extension Main {
             router.alertMessage
             router.alertMessage
                 .receive(on: DispatchQueue.main)
                 .receive(on: DispatchQueue.main)
                 .sink { message in
                 .sink { message in
-                    var config = SwiftMessages.defaultConfig
-                    let view = MessageView.viewFromNib(layout: .cardView)
-
-                    let titleContent: String
-
-                    view.configureContent(
-                        title: "title",
-                        body: NSLocalizedString(message.content, comment: "Info message"),
-                        iconImage: nil,
-                        iconText: nil,
-                        buttonImage: nil,
-                        buttonTitle: nil,
-                        buttonTapHandler: nil
-                    )
-
-                    switch message.type {
-                    case .info:
-                        view.backgroundColor = .secondarySystemGroupedBackground
-                        config.duration = .automatic
-
-                        titleContent = NSLocalizedString("Info", comment: "Info title")
-                    case .warning:
-                        view.configureTheme(.warning, iconStyle: .subtle)
-                        config.duration = .forever
-                        view.button?.setImage(Icon.warningSubtle.image, for: .normal)
-                        titleContent = NSLocalizedString("Warning", comment: "Warning title")
-                        view.buttonTapHandler = { _ in
-                            SwiftMessages.hide()
-                        }
-                    case .errorPump:
-                        view.configureTheme(.error, iconStyle: .subtle)
-                        config.duration = .forever
-                        view.button?.setImage(Icon.errorSubtle.image, for: .normal)
-                        titleContent = NSLocalizedString("Error", comment: "Error title")
-                        view.buttonTapHandler = { _ in
-                            SwiftMessages.hide()
-                            // display the pump configuration immediatly
-                            if let pump = self.provider.deviceManager.pumpManager,
-                               let bluetooth = self.provider.bluetoothProvider
-                            {
-                                let view = PumpConfig.PumpSettingsView(
-                                    pumpManager: pump,
-                                    bluetoothManager: bluetooth,
-                                    completionDelegate: self
-                                ).asAny()
-                                self.router.mainSecondaryModalView.send(view)
-                            }
-                        }
-                    }
-
-                    view.titleLabel?.text = titleContent
-                    config.dimMode = .gray(interactive: true)
-
-                    SwiftMessages.show(config: config, view: view)
+                    guard !self.isApnPumpConfigAction(message) else { return }
+                    guard self.router.allowNotify(message, self.settingsManager.settings) else { return }
+                    self.showAlertMessage(message)
                 }
                 }
                 .store(in: &lifetime)
                 .store(in: &lifetime)
 
 
@@ -107,6 +247,53 @@ extension Main {
     }
     }
 }
 }
 
 
+extension MessageView {
+    func currentColorScheme() -> ColorScheme {
+        let userInterfaceStyle = UITraitCollection.current.userInterfaceStyle
+        return userInterfaceStyle == .dark ? .dark : .light
+    }
+
+    func customConfigureTheme(colorSchemePreference: ColorSchemeOption) {
+        let defaultSystemColorScheme = currentColorScheme()
+        var backgroundColor = UIColor.systemBackground
+        var foregroundColor = UIColor.white
+        let ApnBackground = UIColor(named: "ApnBackground") ?? UIColor.lightGray
+        let iOSlightTrioDark = UIColor(named: "ApnBackgroundLightDark") ?? UIColor.lightGray
+
+        switch colorSchemePreference {
+        case .systemDefault:
+            backgroundColor = ApnBackground
+            foregroundColor = UIColor.label
+        case .dark:
+            backgroundColor = defaultSystemColorScheme == .light ? iOSlightTrioDark : ApnBackground
+            foregroundColor = defaultSystemColorScheme == .light ? UIColor.black : UIColor.white
+        case .light:
+            backgroundColor = defaultSystemColorScheme == .light ? ApnBackground : UIColor.gray
+            foregroundColor = defaultSystemColorScheme == .light ? UIColor.black : UIColor.white
+        }
+
+        iconImageView?.tintColor = foregroundColor
+        backgroundView.backgroundColor = backgroundColor
+        titleLabel?.textColor = foregroundColor
+        bodyLabel?.textColor = foregroundColor
+        iconImageView?.isHidden = iconImageView?.image == nil
+
+        backgroundView.layer.cornerRadius = 25
+
+        let adjustedFont = UIFont.systemFont(ofSize: 13.0, weight: .bold)
+        let preferredTitleFont = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: adjustedFont)
+        let preferredBodyFont = UIFont.preferredFontforStyle(forTextStyle: .footnote)
+        // Set the title and body font to the dynamic type sizes
+        titleLabel?.adjustsFontForContentSizeCategory = true
+        titleLabel?.font = preferredTitleFont
+        bodyLabel?.adjustsFontForContentSizeCategory = true
+        bodyLabel?.font = preferredBodyFont
+        // Set custom colors for title and body text
+        titleLabel?.textColor = foregroundColor
+        bodyLabel?.textColor = foregroundColor
+    }
+}
+
 @available(iOS 16.0, *)
 @available(iOS 16.0, *)
 extension Main.StateModel: CompletionDelegate {
 extension Main.StateModel: CompletionDelegate {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
@@ -114,3 +301,12 @@ extension Main.StateModel: CompletionDelegate {
         router.mainSecondaryModalView.send(nil)
         router.mainSecondaryModalView.send(nil)
     }
     }
 }
 }
+
+// Extension to convert SwiftUI TextStyle to UIFont
+extension UIFont {
+    static func preferredFontforStyle(forTextStyle: UIFont.TextStyle) -> UIFont {
+        let uiFontMetrics = UIFontMetrics.default
+        let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: forTextStyle)
+        return uiFontMetrics.scaledFont(for: UIFont(descriptor: descriptor, size: 0))
+    }
+}

+ 7 - 2
FreeAPS/Sources/Modules/Settings/SettingItems.swift

@@ -208,10 +208,15 @@ enum SettingItems {
     ]
     ]
 
 
     static let notificationItems = [
     static let notificationItems = [
+        SettingItem(title: "Manage iOS Preferences", view: .notificationSettings),
         SettingItem(
         SettingItem(
-            title: "Glucose Notifications",
+            title: "Trio Notifications",
             view: .glucoseNotificationSettings,
             view: .glucoseNotificationSettings,
             searchContents: [
             searchContents: [
+                "Always Notify Pump",
+                "Always Notify CGM",
+                "Always Notify Carb",
+                "Always Notify Algorithm",
                 "Show Glucose App Badge",
                 "Show Glucose App Badge",
                 "Always Notify Glucose",
                 "Always Notify Glucose",
                 "Play Alarm Sound",
                 "Play Alarm Sound",
@@ -219,7 +224,7 @@ enum SettingItems {
                 "Low Glucose Alarm Limit",
                 "Low Glucose Alarm Limit",
                 "High Glucose Alarm Limit"
                 "High Glucose Alarm Limit"
             ],
             ],
-            path: ["Notifications", "Glucose Notifications"]
+            path: ["Notifications", "Trio Notifications"] // Glucose
         ),
         ),
         SettingItem(
         SettingItem(
             title: "Live Activity",
             title: "Live Activity",

+ 109 - 1
FreeAPS/Sources/Modules/Settings/View/Subviews/NotificationsView.swift

@@ -5,6 +5,7 @@
 //  Created by Deniz Cengiz on 26.07.24.
 //  Created by Deniz Cengiz on 26.07.24.
 //
 //
 import Foundation
 import Foundation
+import LoopKitUI
 import SwiftUI
 import SwiftUI
 import Swinject
 import Swinject
 
 
@@ -12,6 +13,13 @@ struct NotificationsView: BaseView {
     let resolver: Resolver
     let resolver: Resolver
 
 
     @ObservedObject var state: Settings.StateModel
     @ObservedObject var state: Settings.StateModel
+    @State var notificationsDisabled = false
+    @State var showAlert = false
+    @State private var shouldDisplayHint: Bool = false
+    @State var hintDetent = PresentationDetent.large
+    @State var selectedVerboseHint: String? =
+        "Notifications give you important Trio information without requiring you to open the app.\n\nKeep these turned ON in your phone’s settings to ensure you receive Trio Notifications, Critical Alerts, and Time Sensitive Notifications."
+    @State var hintLabel: String? = "Manage iOS Preferences"
 
 
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.colorScheme) var colorScheme
     var color: LinearGradient {
     var color: LinearGradient {
@@ -34,9 +42,43 @@ struct NotificationsView: BaseView {
     var body: some View {
     var body: some View {
         Form {
         Form {
             Section(
             Section(
+                header: Text("Manage iOS Preferences"),
+                content: {
+                    manageNotifications
+                }
+            )
+            Section {
+                VStack {
+                    notificationsEnabledStatus
+                    HStack(alignment: .top) {
+                        Text(
+                            "Notifications give you important Trio information without requiring you to open the app."
+                        )
+                        .font(.footnote)
+                        .foregroundColor(.secondary)
+                        .lineLimit(nil)
+                        Spacer()
+                        Button(
+                            action: {
+                                hintLabel = "Manage iOS Preferences"
+                                selectedVerboseHint =
+                                    "Notifications give you important Trio information without requiring you to open the app.\n\nKeep these turned ON in your phone’s settings to ensure you receive Trio Notifications, Critical Alerts, and Time Sensitive Notifications."
+                                shouldDisplayHint.toggle()
+                            },
+                            label: {
+                                HStack {
+                                    Image(systemName: "questionmark.circle")
+                                }
+                            }
+                        ).buttonStyle(BorderlessButtonStyle())
+                    }.padding(.top)
+                }.padding(.bottom)
+            }.listRowBackground(Color.chart)
+            Section(
                 header: Text("Notification Center"),
                 header: Text("Notification Center"),
                 content: {
                 content: {
-                    Text("Glucose Notifications").navigationLink(to: .glucoseNotificationSettings, from: self)
+                    Text("Trio Notifications")
+                        .navigationLink(to: .glucoseNotificationSettings, from: self)
 
 
                     if #available(iOS 16.2, *) {
                     if #available(iOS 16.2, *) {
                         Text("Live Activity").navigationLink(to: .liveActivitySettings, from: self)
                         Text("Live Activity").navigationLink(to: .liveActivitySettings, from: self)
@@ -47,8 +89,74 @@ struct NotificationsView: BaseView {
             )
             )
             .listRowBackground(Color.chart)
             .listRowBackground(Color.chart)
         }
         }
+        .onReceive(
+            resolver.resolve(AlertPermissionsChecker.self)!.$notificationsDisabled,
+            perform: {
+                if notificationsDisabled != $0 {
+                    notificationsDisabled = $0
+                    if notificationsDisabled {
+                        showAlert = true
+                    }
+                }
+            }
+        )
+        .alert(
+            isPresented: self.$showAlert,
+            content: { self.notificationReminder() }
+        )
+        .sheet(isPresented: $shouldDisplayHint) {
+            SettingInputHintView(
+                hintDetent: $hintDetent,
+                shouldDisplayHint: $shouldDisplayHint,
+                hintLabel: hintLabel ?? "",
+                hintText: selectedVerboseHint ?? "",
+                sheetTitle: "Help"
+            )
+        }
         .scrollContentBackground(.hidden).background(color)
         .scrollContentBackground(.hidden).background(color)
         .navigationTitle("Notifications")
         .navigationTitle("Notifications")
         .navigationBarTitleDisplayMode(.automatic)
         .navigationBarTitleDisplayMode(.automatic)
     }
     }
 }
 }
+
+extension NotificationsView {
+    func notificationReminder() -> Alert {
+        Alert(
+            title: Text("\u{2757} Notifications are Required"),
+            message: Text(
+                "Please authorize notifications by tapping 'Open iOS Settings' > 'Notifications' and enable 'Allow Notifications' for 'Notification Center' and 'Banners' Alerts."
+            ),
+            dismissButton: .default(Text("Ok"))
+        )
+    }
+
+    @ViewBuilder private func onOff(_ val: Bool) -> some View {
+        if val {
+            Text(NSLocalizedString("On", comment: "Notification Setting Status is On"))
+        } else {
+            HStack {
+                Image(systemName: "exclamationmark.triangle.fill").foregroundColor(.critical)
+                Text(NSLocalizedString("Off", comment: "Notification Setting Status is Off"))
+            }
+        }
+    }
+
+    private var manageNotifications: some View {
+        Button(action: { UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) }) {
+            HStack {
+                Text(NSLocalizedString("Open iOS Settings", comment: "Manage Permissions in Settings button text"))
+                Spacer()
+                Image(systemName: "chevron.right").foregroundColor(.gray).font(.footnote)
+            }
+        }
+        .accentColor(.primary)
+    }
+
+    private var notificationsEnabledStatus: some View {
+        HStack {
+            Text(NSLocalizedString("Notifications", comment: "Notifications Status text"))
+            Spacer()
+            onOff(!notificationsDisabled)
+        }
+    }
+}

+ 37 - 3
FreeAPS/Sources/Router/Router.swift

@@ -2,15 +2,30 @@ import Combine
 import SwiftUI
 import SwiftUI
 import Swinject
 import Swinject
 
 
-enum MessageType {
+enum MessageType: String {
     case info
     case info
     case warning
     case warning
-    case errorPump
+    case error
+    case other
+}
+
+enum MessageSubtype: String {
+    case pump
+    case cgm
+    case carb
+    case glucose
+    case algorithm
+    case misc
 }
 }
 
 
 struct MessageContent {
 struct MessageContent {
     var content: String
     var content: String
     var type: MessageType = .info
     var type: MessageType = .info
+    var subtype: MessageSubtype = .misc
+    var title: String = ""
+    var useAPN: Bool = true
+    var trigger: UNNotificationTrigger? = nil
+    var action: NotificationAction = .none
 }
 }
 
 
 protocol Router {
 protocol Router {
@@ -18,13 +33,13 @@ protocol Router {
     var mainSecondaryModalView: CurrentValueSubject<AnyView?, Never> { get }
     var mainSecondaryModalView: CurrentValueSubject<AnyView?, Never> { get }
     var alertMessage: PassthroughSubject<MessageContent, Never> { get }
     var alertMessage: PassthroughSubject<MessageContent, Never> { get }
     func view(for screen: Screen) -> AnyView
     func view(for screen: Screen) -> AnyView
+    func allowNotify(_ message: MessageContent, _ settings: FreeAPSSettings) -> Bool
 }
 }
 
 
 final class BaseRouter: Router {
 final class BaseRouter: Router {
     let mainModalScreen = CurrentValueSubject<Screen?, Never>(nil)
     let mainModalScreen = CurrentValueSubject<Screen?, Never>(nil)
     let mainSecondaryModalView = CurrentValueSubject<AnyView?, Never>(nil)
     let mainSecondaryModalView = CurrentValueSubject<AnyView?, Never>(nil)
     let alertMessage = PassthroughSubject<MessageContent, Never>()
     let alertMessage = PassthroughSubject<MessageContent, Never>()
-
     private let resolver: Resolver
     private let resolver: Resolver
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
@@ -34,4 +49,23 @@ final class BaseRouter: Router {
     func view(for screen: Screen) -> AnyView {
     func view(for screen: Screen) -> AnyView {
         screen.view(resolver: resolver).asAny()
         screen.view(resolver: resolver).asAny()
     }
     }
+
+    func allowNotify(_ message: MessageContent, _ settings: FreeAPSSettings) -> Bool {
+        if message.type == .error { return true }
+        switch message.subtype {
+        case .pump:
+            guard settings.notificationsPump else { return false }
+        case .cgm:
+            guard settings.notificationsCgm else { return false }
+        case .carb:
+            guard settings.notificationsCarb else { return false }
+        case .glucose:
+            guard settings.glucoseNotificationsAlways else { return false }
+        case .algorithm:
+            guard settings.notificationsAlgorithm else { return false }
+        case .misc:
+            return true
+        }
+        return true
+    }
 }
 }

+ 63 - 0
FreeAPS/Sources/Services/Notifications/AlertPermissionsChecker.swift

@@ -0,0 +1,63 @@
+import Combine
+import Foundation
+import LoopKit
+import SwiftUI
+import Swinject
+
+public class AlertPermissionsChecker: ObservableObject, Injectable {
+    private lazy var cancellables = Set<AnyCancellable>()
+    private var listeningToNotificationCenter = false
+
+    @Published var notificationsDisabled: Bool = false
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+
+        Foundation.NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
+            .sink { [weak self] _ in
+                self?.check()
+            }
+            .store(in: &cancellables)
+
+        Foundation.NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
+            .sink { [weak self] _ in
+                self?.check()
+            }
+            .store(in: &cancellables)
+    }
+
+    func checkNow() {
+        check {
+            // Note: we do this, instead of calling notificationCenterSettingsChanged directly, so that we only
+            // get called when it _changes_.
+            self.listenToNotificationCenter()
+        }
+    }
+
+    private func check(then completion: (() -> Void)? = nil) {
+        UNUserNotificationCenter.current().getNotificationSettings { settings in
+            DispatchQueue.main.async {
+                self.notificationsDisabled = settings.alertSetting == .disabled
+                completion?()
+            }
+        }
+    }
+}
+
+extension AlertPermissionsChecker {
+    private func listenToNotificationCenter() {
+        if !listeningToNotificationCenter {
+            $notificationsDisabled
+                .receive(on: RunLoop.main)
+                .removeDuplicates()
+                .sink(receiveValue: notificationCenterSettingsChanged)
+                .store(in: &cancellables)
+            listeningToNotificationCenter = true
+        }
+    }
+
+    private func notificationCenterSettingsChanged(_: Bool) {
+        // TODO: Add processing for other actions in delegate AlertManager, InAppAlertScheduler, etc., from Loop
+        debug(.default, "notificationCenterSettingsChanged")
+    }
+}

+ 254 - 143
FreeAPS/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -20,12 +20,18 @@ enum NotificationAction: String {
     static let key = "action"
     static let key = "action"
 
 
     case snooze
     case snooze
+    case pumpConfig
+    case none
 }
 }
 
 
 protocol BolusFailureObserver {
 protocol BolusFailureObserver {
     func bolusDidFail()
     func bolusDidFail()
 }
 }
 
 
+protocol alertMessageNotificationObserver {
+    func alertMessageNotification(_ message: MessageContent)
+}
+
 protocol pumpNotificationObserver {
 protocol pumpNotificationObserver {
     func pumpNotification(alert: AlertEntry)
     func pumpNotification(alert: AlertEntry)
     func pumpRemoveNotification()
     func pumpRemoveNotification()
@@ -33,14 +39,16 @@ protocol pumpNotificationObserver {
 
 
 final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, Injectable {
 final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, Injectable {
     private enum Identifier: String {
     private enum Identifier: String {
-        case glucocoseNotification = "FreeAPS.glucoseNotification"
+        case glucoseNotification = "FreeAPS.glucoseNotification"
         case carbsRequiredNotification = "FreeAPS.carbsRequiredNotification"
         case carbsRequiredNotification = "FreeAPS.carbsRequiredNotification"
         case noLoopFirstNotification = "FreeAPS.noLoopFirstNotification"
         case noLoopFirstNotification = "FreeAPS.noLoopFirstNotification"
         case noLoopSecondNotification = "FreeAPS.noLoopSecondNotification"
         case noLoopSecondNotification = "FreeAPS.noLoopSecondNotification"
         case bolusFailedNotification = "FreeAPS.bolusFailedNotification"
         case bolusFailedNotification = "FreeAPS.bolusFailedNotification"
         case pumpNotification = "FreeAPS.pumpNotification"
         case pumpNotification = "FreeAPS.pumpNotification"
+        case alertMessageNotification = "FreeAPS.alertMessageNotification"
     }
     }
 
 
+    @Injected() var alertPermissionsChecker: AlertPermissionsChecker!
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var glucoseStorage: GlucoseStorage!
@@ -60,6 +68,9 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
     private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
     private var subscriptions = Set<AnyCancellable>()
     private var subscriptions = Set<AnyCancellable>()
 
 
+    let firstInterval = 20 // min
+    let secondInterval = 40 // min
+
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         super.init()
         super.init()
         center.delegate = self
         center.delegate = self
@@ -74,6 +85,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
         broadcaster.register(DeterminationObserver.self, observer: self)
         broadcaster.register(DeterminationObserver.self, observer: self)
         broadcaster.register(BolusFailureObserver.self, observer: self)
         broadcaster.register(BolusFailureObserver.self, observer: self)
         broadcaster.register(pumpNotificationObserver.self, observer: self)
         broadcaster.register(pumpNotificationObserver.self, observer: self)
+        broadcaster.register(alertMessageNotificationObserver.self, observer: self)
         requestNotificationPermissionsIfNeeded()
         requestNotificationPermissionsIfNeeded()
         Task {
         Task {
             await sendGlucoseNotification()
             await sendGlucoseNotification()
@@ -116,7 +128,12 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     private func addAppBadge(glucose: Int?) {
     private func addAppBadge(glucose: Int?) {
         guard let glucose = glucose, settingsManager.settings.glucoseBadge else {
         guard let glucose = glucose, settingsManager.settings.glucoseBadge else {
             DispatchQueue.main.async {
             DispatchQueue.main.async {
-                UIApplication.shared.applicationIconBadgeNumber = 0
+                self.center.setBadgeCount(-1) { error in
+                    guard let error else {
+                        return
+                    }
+                    print(error)
+                }
             }
             }
             return
             return
         }
         }
@@ -129,97 +146,97 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
         }
         }
 
 
         DispatchQueue.main.async {
         DispatchQueue.main.async {
-            UIApplication.shared.applicationIconBadgeNumber = badge
+            self.center.setBadgeCount(badge) { error in
+                guard let error else {
+                    return
+                }
+                print(error)
+            }
         }
         }
     }
     }
 
 
     private func notifyCarbsRequired(_ carbs: Int) {
     private func notifyCarbsRequired(_ carbs: Int) {
         guard Decimal(carbs) >= settingsManager.settings.carbsRequiredThreshold,
         guard Decimal(carbs) >= settingsManager.settings.carbsRequiredThreshold,
-              settingsManager.settings.showCarbsRequiredBadge else { return }
-
-        ensureCanSendNotification {
-            var titles: [String] = []
+              settingsManager.settings.showCarbsRequiredBadge, settingsManager.settings.notificationsCarb else { return }
 
 
-            let content = UNMutableNotificationContent()
+        var titles: [String] = []
 
 
-            if self.snoozeUntilDate > Date() {
-                titles.append(NSLocalizedString("(Snoozed)", comment: "(Snoozed)"))
-            } else {
-                content.sound = .default
-                self.playSoundIfNeeded()
-            }
+        let content = UNMutableNotificationContent()
 
 
-            titles.append(String(format: NSLocalizedString("Carbs required: %d g", comment: "Carbs required"), carbs))
+        if snoozeUntilDate > Date() {
+            titles.append(NSLocalizedString("(Snoozed)", comment: "(Snoozed)"))
+        } else {
+            content.sound = .default
+            playSoundIfNeeded()
+        }
 
 
-            content.title = titles.joined(separator: " ")
-            content.body = String(
-                format: NSLocalizedString(
-                    "To prevent LOW required %d g of carbs",
-                    comment: "To prevent LOW required %d g of carbs"
-                ),
-                carbs
-            )
+        titles.append(String(format: NSLocalizedString("Carbs required: %d g", comment: "Carbs required"), carbs))
 
 
-            self.addRequest(identifier: .carbsRequiredNotification, content: content, deleteOld: true)
-        }
+        content.title = titles.joined(separator: " ")
+        content.body = String(
+            format: NSLocalizedString(
+                "To prevent LOW required %d g of carbs",
+                comment: "To prevent LOW required %d g of carbs"
+            ),
+            carbs
+        )
+        addRequest(identifier: .carbsRequiredNotification, content: content, deleteOld: true, messageSubtype: .carb)
     }
     }
 
 
     private func scheduleMissingLoopNotifiactions(date _: Date) {
     private func scheduleMissingLoopNotifiactions(date _: Date) {
-        ensureCanSendNotification {
-            let title = NSLocalizedString("Trio Not Active", comment: "Trio Not Active")
-            let body = NSLocalizedString("Last loop was more than %d min ago", comment: "Last loop was more than %d min ago")
-
-            let firstInterval = 20 // min
-            let secondInterval = 40 // min
-
-            let firstContent = UNMutableNotificationContent()
-            firstContent.title = title
-            firstContent.body = String(format: body, firstInterval)
-            firstContent.sound = .default
-
-            let secondContent = UNMutableNotificationContent()
-            secondContent.title = title
-            secondContent.body = String(format: body, secondInterval)
-            secondContent.sound = .default
-
-            let firstTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 60 * TimeInterval(firstInterval), repeats: false)
-            let secondTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 60 * TimeInterval(secondInterval), repeats: false)
-
-            self.addRequest(
-                identifier: .noLoopFirstNotification,
-                content: firstContent,
-                deleteOld: true,
-                trigger: firstTrigger
-            )
-            self.addRequest(
-                identifier: .noLoopSecondNotification,
-                content: secondContent,
-                deleteOld: true,
-                trigger: secondTrigger
-            )
-        }
+        let title = NSLocalizedString("Trio Not Active", comment: "Trio Not Active")
+        let body = NSLocalizedString("Last loop was more than %d min ago", comment: "Last loop was more than %d min ago")
+
+        let firstContent = UNMutableNotificationContent()
+        firstContent.title = title
+        firstContent.body = String(format: body, firstInterval)
+        firstContent.sound = .default
+
+        let secondContent = UNMutableNotificationContent()
+        secondContent.title = title
+        secondContent.body = String(format: body, secondInterval)
+        secondContent.sound = .default
+
+        let firstTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 60 * TimeInterval(firstInterval), repeats: false)
+        let secondTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 60 * TimeInterval(secondInterval), repeats: false)
+
+        addRequest(
+            identifier: .noLoopFirstNotification,
+            content: firstContent,
+            deleteOld: true,
+            trigger: firstTrigger,
+            messageType: .error,
+            messageSubtype: .algorithm
+        )
+        addRequest(
+            identifier: .noLoopSecondNotification,
+            content: secondContent,
+            deleteOld: true,
+            trigger: secondTrigger,
+            messageType: .error,
+            messageSubtype: .algorithm
+        )
     }
     }
 
 
     private func notifyBolusFailure() {
     private func notifyBolusFailure() {
-        ensureCanSendNotification {
-            let title = NSLocalizedString("Bolus failed", comment: "Bolus failed")
-            let body = NSLocalizedString(
-                "Bolus failed or inaccurate. Check pump history before repeating.",
-                comment: "Bolus failed or inaccurate. Check pump history before repeating."
-            )
-
-            let content = UNMutableNotificationContent()
-            content.title = title
-            content.body = body
-            content.sound = .default
-
-            self.addRequest(
-                identifier: .noLoopFirstNotification,
-                content: content,
-                deleteOld: true,
-                trigger: nil
-            )
-        }
+        let title = NSLocalizedString("Bolus failed", comment: "Bolus failed")
+        let body = NSLocalizedString(
+            "Bolus failed or inaccurate. Check pump history before repeating.",
+            comment: "Bolus failed or inaccurate. Check pump history before repeating."
+        )
+        let content = UNMutableNotificationContent()
+        content.title = title
+        content.body = body
+        content.sound = .default
+
+        addRequest(
+            identifier: .noLoopFirstNotification,
+            content: content,
+            deleteOld: true,
+            trigger: nil,
+            messageType: .error,
+            messageSubtype: .pump
+        )
     }
     }
 
 
     private func fetchGlucoseIDs() async -> [NSManagedObjectID] {
     private func fetchGlucoseIDs() async -> [NSManagedObjectID] {
@@ -255,45 +272,53 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
 
 
             guard glucoseStorage.alarm != nil || settingsManager.settings.glucoseNotificationsAlways else { return }
             guard glucoseStorage.alarm != nil || settingsManager.settings.glucoseNotificationsAlways else { return }
 
 
-            ensureCanSendNotification {
-                var titles: [String] = []
-                var notificationAlarm = false
-
-                switch self.glucoseStorage.alarm {
-                case .none:
-                    titles.append(NSLocalizedString("Glucose", comment: "Glucose"))
-                case .low:
-                    titles.append(NSLocalizedString("LOWALERT!", comment: "LOWALERT!"))
-                    notificationAlarm = true
-                case .high:
-                    titles.append(NSLocalizedString("HIGHALERT!", comment: "HIGHALERT!"))
-                    notificationAlarm = true
-                }
+            var titles: [String] = []
+            var notificationAlarm = false
+            var messageType = MessageType.info
+
+            switch glucoseStorage.alarm {
+            case .none:
+                titles.append(NSLocalizedString("Glucose", comment: "Glucose"))
+            case .low:
+                titles.append(NSLocalizedString("LOWALERT!", comment: "LOWALERT!"))
+                messageType = MessageType.warning
+                notificationAlarm = true
+            case .high:
+                titles.append(NSLocalizedString("HIGHALERT!", comment: "HIGHALERT!"))
+                messageType = MessageType.warning
+                notificationAlarm = true
+            }
 
 
-                let delta = glucoseObjects.count >= 2 ? lastReading - secondLastReading : nil
-                let body = self.glucoseText(
-                    glucoseValue: Int(lastReading),
-                    delta: Int(delta ?? 0),
-                    direction: lastDirection
-                ) + self.infoBody()
-
-                if self.snoozeUntilDate > Date() {
-                    titles.append(NSLocalizedString("(Snoozed)", comment: "(Snoozed)"))
-                    notificationAlarm = false
-                } else {
-                    titles.append(body)
-                    let content = UNMutableNotificationContent()
-                    content.title = titles.joined(separator: " ")
-                    content.body = body
-
-                    if notificationAlarm {
-                        self.playSoundIfNeeded()
-                        content.sound = .default
-                        content.userInfo[NotificationAction.key] = NotificationAction.snooze.rawValue
-                    }
+            let delta = glucoseObjects.count >= 2 ? lastReading - secondLastReading : nil
+            let body = glucoseText(
+                glucoseValue: Int(lastReading),
+                delta: Int(delta ?? 0),
+                direction: lastDirection
+            ) + infoBody()
 
 
-                    self.addRequest(identifier: .glucocoseNotification, content: content, deleteOld: true)
+            if snoozeUntilDate > Date() {
+                titles.append(NSLocalizedString("(Snoozed)", comment: "(Snoozed)"))
+                notificationAlarm = false
+            } else {
+                titles.append(body)
+                let content = UNMutableNotificationContent()
+                content.title = titles.joined(separator: " ")
+                content.body = body
+
+                if notificationAlarm {
+                    playSoundIfNeeded()
+                    content.sound = .default
+                    content.userInfo[NotificationAction.key] = NotificationAction.snooze.rawValue
                 }
                 }
+
+                addRequest(
+                    identifier: .glucoseNotification,
+                    content: content,
+                    deleteOld: true,
+                    messageType: messageType,
+                    messageSubtype: .glucose,
+                    action: NotificationAction.snooze
+                )
             }
             }
         } catch {
         } catch {
             debugPrint(
             debugPrint(
@@ -378,31 +403,39 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
         }
         }
     }
     }
 
 
-    private func ensureCanSendNotification(_ completion: @escaping () -> Void) {
-        center.getNotificationSettings { settings in
-            guard settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional else {
-                warning(.service, "ensureCanSendNotification failed, authorization denied")
-                return
-            }
-
-            debug(.service, "Sending notification was allowed")
-
-            completion()
-        }
-    }
-
     private func addRequest(
     private func addRequest(
         identifier: Identifier,
         identifier: Identifier,
         content: UNMutableNotificationContent,
         content: UNMutableNotificationContent,
         deleteOld: Bool = false,
         deleteOld: Bool = false,
-        trigger: UNNotificationTrigger? = nil
+        trigger: UNNotificationTrigger? = nil,
+        messageType: MessageType = MessageType.other,
+        messageSubtype: MessageSubtype = MessageSubtype.misc,
+        action: NotificationAction = NotificationAction.none
     ) {
     ) {
-        let request = UNNotificationRequest(identifier: identifier.rawValue, content: content, trigger: trigger)
+        let messageCont = MessageContent(
+            content: content.body,
+            type: messageType,
+            subtype: messageSubtype,
+            title: content.title,
+            useAPN: false,
+            trigger: trigger,
+            action: action
+        )
+        if alertPermissionsChecker.notificationsDisabled {
+            router.alertMessage.send(messageCont)
+            return
+        }
+        guard router.allowNotify(messageCont, settingsManager.settings) else { return }
+
+        var alertIdentifier = identifier.rawValue
+        alertIdentifier = identifier == .pumpNotification ? alertIdentifier + content
+            .title : (identifier == .alertMessageNotification ? alertIdentifier + content.body : alertIdentifier)
+        let request = UNNotificationRequest(identifier: alertIdentifier, content: content, trigger: trigger)
 
 
         if deleteOld {
         if deleteOld {
             DispatchQueue.main.async {
             DispatchQueue.main.async {
-                self.center.removeDeliveredNotifications(withIdentifiers: [identifier.rawValue])
-                self.center.removePendingNotificationRequests(withIdentifiers: [identifier.rawValue])
+                self.center.removeDeliveredNotifications(withIdentifiers: [alertIdentifier])
+                self.center.removePendingNotificationRequests(withIdentifiers: [alertIdentifier])
             }
             }
         }
         }
 
 
@@ -413,7 +446,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
                     return
                     return
                 }
                 }
 
 
-                debug(.service, "Sending \(identifier) notification")
+                debug(.service, "Sending \(identifier) notification for \(request.content.title)")
             }
             }
         }
         }
     }
     }
@@ -463,20 +496,88 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     }
     }
 }
 }
 
 
+extension BaseUserNotificationsManager: alertMessageNotificationObserver {
+    func alertMessageNotification(_ message: MessageContent) {
+        let content = UNMutableNotificationContent()
+        var identifier: Identifier = .alertMessageNotification
+
+        if message.title == "" {
+            switch message.type {
+            case .info:
+                content.title = NSLocalizedString("Info", comment: "Info title")
+            case .warning:
+                content.title = NSLocalizedString("Warning", comment: "Warning title")
+            case .error:
+                content.title = NSLocalizedString("Error", comment: "Error title")
+            default:
+                content.title = message.title
+            }
+        } else {
+            content.title = message.title
+        }
+        switch message.subtype {
+        case .pump:
+            identifier = .pumpNotification
+        case .carb:
+            identifier = .carbsRequiredNotification
+        case .glucose:
+            identifier = .glucoseNotification
+        case .algorithm:
+            if message.trigger != nil {
+                identifier = message.content.contains(String(firstInterval)) ? Identifier.noLoopFirstNotification : Identifier
+                    .noLoopSecondNotification
+            } else {
+                identifier = Identifier.alertMessageNotification
+            }
+        default:
+            identifier = .alertMessageNotification
+        }
+        switch message.action {
+        case .snooze:
+            content.userInfo[NotificationAction.key] = NotificationAction.snooze.rawValue
+        case .pumpConfig:
+            content.userInfo[NotificationAction.key] = NotificationAction.pumpConfig.rawValue
+        default: break
+        }
+
+        content.body = NSLocalizedString(message.content, comment: "Info message")
+        content.sound = .default
+        addRequest(
+            identifier: identifier,
+            content: content,
+            deleteOld: true,
+            trigger: message.trigger,
+            messageType: message.type,
+            messageSubtype: message.subtype,
+            action: message.action
+        )
+    }
+}
+
 extension BaseUserNotificationsManager: pumpNotificationObserver {
 extension BaseUserNotificationsManager: pumpNotificationObserver {
     func pumpNotification(alert: AlertEntry) {
     func pumpNotification(alert: AlertEntry) {
-        ensureCanSendNotification {
-            let content = UNMutableNotificationContent()
-            content.title = alert.contentTitle ?? "Unknown"
-            content.body = alert.contentBody ?? "Unknown"
-            content.sound = .default
-            self.addRequest(
-                identifier: .pumpNotification,
-                content: content,
-                deleteOld: true,
-                trigger: nil
-            )
+        let content = UNMutableNotificationContent()
+        let alertUp = alert.alertIdentifier.uppercased()
+        let typeMessage: MessageType
+        if alertUp.contains("FAULT") || alertUp.contains("ERROR") {
+            content.userInfo[NotificationAction.key] = NotificationAction.pumpConfig.rawValue
+            typeMessage = .error
+        } else {
+            typeMessage = .warning
+            guard settingsManager.settings.notificationsPump else { return }
         }
         }
+        content.title = alert.contentTitle ?? "Unknown"
+        content.body = alert.contentBody ?? "Unknown"
+        content.sound = .default
+        addRequest(
+            identifier: .pumpNotification,
+            content: content,
+            deleteOld: true,
+            trigger: nil,
+            messageType: typeMessage,
+            messageSubtype: .pump,
+            action: .pumpConfig
+        )
     }
     }
 
 
     func pumpRemoveNotification() {
     func pumpRemoveNotification() {
@@ -507,7 +608,7 @@ extension BaseUserNotificationsManager: UNUserNotificationCenterDelegate {
         willPresent _: UNNotification,
         willPresent _: UNNotification,
         withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
         withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
     ) {
     ) {
-        completionHandler([.banner, .badge, .sound])
+        completionHandler([.banner, .badge, .sound, .list])
     }
     }
 
 
     func userNotificationCenter(
     func userNotificationCenter(
@@ -523,6 +624,16 @@ extension BaseUserNotificationsManager: UNUserNotificationCenterDelegate {
         switch action {
         switch action {
         case .snooze:
         case .snooze:
             router.mainModalScreen.send(.snooze)
             router.mainModalScreen.send(.snooze)
+        case .pumpConfig:
+            let messageCont = MessageContent(
+                content: response.notification.request.content.body,
+                type: MessageType.other,
+                subtype: .pump,
+                useAPN: false,
+                action: .pumpConfig
+            )
+            router.alertMessage.send(messageCont)
+        default: break
         }
         }
     }
     }
 }
 }