Jelajahi Sumber

Merge pull request #740 from bjorkert/remote-apns-feedback

Implement APNS feedback for remote commands
Mike Plante 7 bulan lalu
induk
melakukan
6603f3091e

+ 33 - 4
Trio.xcodeproj/project.pbxproj

@@ -552,6 +552,8 @@
 		DD1745502C55CA5500211FAC /* UnitsLimitsSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17454F2C55CA5500211FAC /* UnitsLimitsSettingsProvider.swift */; };
 		DD1745522C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745512C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift */; };
 		DD1745552C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */; };
+		DD17A0292E3FE0BD008E1BF0 /* SwiftJWT in Frameworks */ = {isa = PBXBuildFile; productRef = DD17A0282E3FE0BD008E1BF0 /* SwiftJWT */; };
+		DD17A0322E3FEA1F008E1BF0 /* RemoteNotificationResponseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17A0312E3FEA1F008E1BF0 /* RemoteNotificationResponseManager.swift */; };
 		DD1DB7CC2BECCA1F0048B367 /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */; };
 		DD1E53592D273F26008F32A4 /* LoopStatusHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1E53582D273F20008F32A4 /* LoopStatusHelpView.swift */; };
 		DD21FCB52C6952AD00AF2C25 /* DecimalPickerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */; };
@@ -575,6 +577,7 @@
 		DD3F1F8B2D9E08B600DCE7B3 /* NightscoutLoginStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */; };
 		DD3F1F8D2D9E0E0600DCE7B3 /* NightscoutSetupStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutSetupStepView.swift */; };
 		DD3F1F902D9E153F00DCE7B3 /* NightscoutImportStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */; };
+		DD485F182E466F1800CE8CBF /* SecureMessenger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD485F172E466F1800CE8CBF /* SecureMessenger.swift */; };
 		DD498F2B2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2C2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
@@ -602,12 +605,13 @@
 		DD73FA0F2D74F58E00D19D1E /* BackgroundTask+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */; };
 		DD8262CB2D289297009F6F62 /* BolusConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */; };
 		DD82D4B82DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */; };
+		DD868FD82E381A54005D3308 /* APNSJWTClaims.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */; };
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
 		DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */; };
 		DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */; };
 		DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98ACBF2D71013200C0778F /* StatChartUtils.swift */; };
 		DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */; };
-		DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */; };
+		DD9ECB6A2CA99F6C00AA7C45 /* CommandPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB692CA99F6C00AA7C45 /* CommandPayload.swift */; };
 		DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */; };
 		DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */; };
 		DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */; };
@@ -1379,6 +1383,7 @@
 		DD17454F2C55CA5500211FAC /* UnitsLimitsSettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsProvider.swift; sourceTree = "<group>"; };
 		DD1745512C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsStateModel.swift; sourceTree = "<group>"; };
 		DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsRootView.swift; sourceTree = "<group>"; };
+		DD17A0312E3FEA1F008E1BF0 /* RemoteNotificationResponseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteNotificationResponseManager.swift; sourceTree = "<group>"; };
 		DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = "<group>"; };
 		DD1E53582D273F20008F32A4 /* LoopStatusHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatusHelpView.swift; sourceTree = "<group>"; };
 		DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalPickerSettings.swift; sourceTree = "<group>"; };
@@ -1402,6 +1407,7 @@
 		DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutLoginStepView.swift; sourceTree = "<group>"; };
 		DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutSetupStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSetupStepView.swift; sourceTree = "<group>"; };
 		DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutImportStepView.swift; sourceTree = "<group>"; };
+		DD485F172E466F1800CE8CBF /* SecureMessenger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureMessenger.swift; sourceTree = "<group>"; };
 		DD4A00202DAEEEC400AB7387 /* OnboardingView+AlgorithmUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AlgorithmUtil.swift"; sourceTree = "<group>"; };
 		DD4A00232DAEF5DC00AB7387 /* AlgorithmSettingsSubstepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmSettingsSubstepView.swift; sourceTree = "<group>"; };
 		DD4AFFF02DADB59100AB7387 /* AlgorithmSettingsContentsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmSettingsContentsStepView.swift; sourceTree = "<group>"; };
@@ -1426,12 +1432,13 @@
 		DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackgroundTask+Helper.swift"; sourceTree = "<group>"; };
 		DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusConfirmationView.swift; sourceTree = "<group>"; };
 		DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyPersistentFlags.swift; sourceTree = "<group>"; };
+		DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSJWTClaims.swift; sourceTree = "<group>"; };
 		DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
 		DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseColorScheme.swift; sourceTree = "<group>"; };
 		DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicGlucoseColor.swift; sourceTree = "<group>"; };
 		DD98ACBF2D71013200C0778F /* StatChartUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatChartUtils.swift; sourceTree = "<group>"; };
 		DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioRemoteControl.swift; sourceTree = "<group>"; };
-		DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessage.swift; sourceTree = "<group>"; };
+		DD9ECB692CA99F6C00AA7C45 /* CommandPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPayload.swift; sourceTree = "<group>"; };
 		DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigStateModel.swift; sourceTree = "<group>"; };
 		DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigProvider.swift; sourceTree = "<group>"; };
 		DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigDataFlow.swift; sourceTree = "<group>"; };
@@ -1587,6 +1594,7 @@
 				3B4BA7862D8DBD690069D5B8 /* RileyLinkKitUI.framework in Frameworks */,
 				CEB434FD28B90B7C00B70274 /* SwiftCharts in Frameworks */,
 				CE95BF5F2BA7715800DC3DE3 /* MockKit.framework in Frameworks */,
+				DD17A0292E3FE0BD008E1BF0 /* SwiftJWT in Frameworks */,
 				3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */,
 				38DF1789276FC8C400B3528F /* SwiftMessages in Frameworks */,
 				3B4BA7802D8DBD690069D5B8 /* OmniKitUI.framework in Frameworks */,
@@ -2399,7 +2407,7 @@
 				BDC2EA462C3045AD00E5BBD0 /* Override.swift */,
 				DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */,
 				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
-				DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */,
+				DD9ECB692CA99F6C00AA7C45 /* CommandPayload.swift */,
 				DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */,
 			);
 			path = Models;
@@ -3411,6 +3419,9 @@
 		DD9ECB662CA99EFE00AA7C45 /* RemoteControl */ = {
 			isa = PBXGroup;
 			children = (
+				DD485F172E466F1800CE8CBF /* SecureMessenger.swift */,
+				DD17A0312E3FEA1F008E1BF0 /* RemoteNotificationResponseManager.swift */,
+				DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */,
 				DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */,
 				DD32CF9F2CC824D3003686D6 /* TrioRemoteControl+APNS.swift */,
 				DD32CF9D2CC824C2003686D6 /* TrioRemoteControl+Override.swift */,
@@ -3767,6 +3778,7 @@
 				3BD9687B2D8DDD4600899469 /* SlideButton */,
 				3BD9687E2D8DDD8800899469 /* CryptoSwift */,
 				3B47C60F2DA0A28F00B0E5EF /* FirebaseCrashlytics */,
+				DD17A0282E3FE0BD008E1BF0 /* SwiftJWT */,
 			);
 			productName = Trio;
 			productReference = 388E595825AD948C0019842D /* Trio.app */;
@@ -3943,6 +3955,7 @@
 				3BD9687A2D8DDD4600899469 /* XCRemoteSwiftPackageReference "SlideButton" */,
 				3BD9687D2D8DDD8800899469 /* XCRemoteSwiftPackageReference "CryptoSwift" */,
 				3B47C60E2DA0A28F00B0E5EF /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
+				DD868FD92E381E1C005D3308 /* XCRemoteSwiftPackageReference "Swift-JWT" */,
 			);
 			productRefGroup = 388E595925AD948C0019842D /* Products */;
 			projectDirPath = "";
@@ -4101,7 +4114,7 @@
 				3811DEEB25CA063400A708ED /* PersistedProperty.swift in Sources */,
 				38E44537274E411700EC9A94 /* Disk+Helpers.swift in Sources */,
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
-				DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */,
+				DD9ECB6A2CA99F6C00AA7C45 /* CommandPayload.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
 				5825A1BE2C97335C0046467E /* EditTempTargetForm.swift in Sources */,
 				BD47FDDD2D8B65B10043966B /* GlucoseTargetStepView.swift in Sources */,
@@ -4203,6 +4216,7 @@
 				C2A6D1E42DB1581D0036DB66 /* GlucoseStatsSetup.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
+				DD868FD82E381A54005D3308 /* APNSJWTClaims.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
 				BD249D862D42FBEC00412DEB /* GlucoseMetricsView.swift in Sources */,
 				58645BA32CA2D325008AFCE7 /* BatterySetup.swift in Sources */,
@@ -4260,6 +4274,7 @@
 				DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */,
 				CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */,
 				38569347270B5DFB0002C50D /* CGMType.swift in Sources */,
+				DD485F182E466F1800CE8CBF /* SecureMessenger.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
 				384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */,
 				CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */,
@@ -4587,6 +4602,7 @@
 				38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */,
 				DDD163122C4C689900CD525A /* AdjustmentsStateModel.swift in Sources */,
 				BD47FDD72D8B64D20043966B /* CarbRatioStepView.swift in Sources */,
+				DD17A0322E3FEA1F008E1BF0 /* RemoteNotificationResponseManager.swift in Sources */,
 				3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */,
 				3BA8D1B32DDB87150006191F /* DecimalExtensions.swift in Sources */,
 				DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */,
@@ -5454,6 +5470,14 @@
 				kind = branch;
 			};
 		};
+		DD868FD92E381E1C005D3308 /* XCRemoteSwiftPackageReference "Swift-JWT" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "http://github.com/Kitura/Swift-JWT.git";
+			requirement = {
+				kind = exactVersion;
+				version = 4.0.1;
+			};
+		};
 /* End XCRemoteSwiftPackageReference section */
 
 /* Begin XCSwiftPackageProductDependency section */
@@ -5502,6 +5526,11 @@
 			package = CEB434FB28B90B7C00B70274 /* XCRemoteSwiftPackageReference "SwiftCharts" */;
 			productName = SwiftCharts;
 		};
+		DD17A0282E3FE0BD008E1BF0 /* SwiftJWT */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = DD868FD92E381E1C005D3308 /* XCRemoteSwiftPackageReference "Swift-JWT" */;
+			productName = SwiftJWT;
+		};
 /* End XCSwiftPackageProductDependency section */
 
 /* Begin XCVersionGroup section */

+ 64 - 1
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,5 +1,5 @@
 {
-  "originHash" : "89074a88ed67a58ecd7534519854c5a0928a4046d7c8a6123a7d70f27bf8b44d",
+  "originHash" : "94bad7ee77953ff12d8447c80f68d417ecb6f69ad08c1fdb1a8f59473b79c3b7",
   "pins" : [
     {
       "identity" : "abseil-cpp-binary",
@@ -20,6 +20,33 @@
       }
     },
     {
+      "identity" : "bluecryptor",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Kitura/BlueCryptor.git",
+      "state" : {
+        "revision" : "cec97c24b111351e70e448972a7d3fe68a756d6d",
+        "version" : "2.0.2"
+      }
+    },
+    {
+      "identity" : "blueecc",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Kitura/BlueECC.git",
+      "state" : {
+        "revision" : "1485268a54f8135435a825a855e733f026fa6cc8",
+        "version" : "1.2.201"
+      }
+    },
+    {
+      "identity" : "bluersa",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Kitura/BlueRSA.git",
+      "state" : {
+        "revision" : "f40325520344a966523b214394aa350132a6af68",
+        "version" : "1.0.203"
+      }
+    },
+    {
       "identity" : "cryptoswift",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/krzyzanowskim/CryptoSwift",
@@ -92,6 +119,15 @@
       }
     },
     {
+      "identity" : "kituracontracts",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Kitura/KituraContracts.git",
+      "state" : {
+        "revision" : "6edf7ac3dd2b3a2c61284778d430bbad7d8a6f23",
+        "version" : "2.0.1"
+      }
+    },
+    {
       "identity" : "leveldb",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/firebase/leveldb.git",
@@ -101,6 +137,15 @@
       }
     },
     {
+      "identity" : "loggerapi",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Kitura/LoggerAPI.git",
+      "state" : {
+        "revision" : "4e6b45e850ffa275e8e26a24c6454fd709d5b6ac",
+        "version" : "2.0.0"
+      }
+    },
+    {
       "identity" : "mkringprogressview",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/maxkonovalov/MKRingProgressView.git",
@@ -146,6 +191,24 @@
       }
     },
     {
+      "identity" : "swift-jwt",
+      "kind" : "remoteSourceControl",
+      "location" : "http://github.com/Kitura/Swift-JWT.git",
+      "state" : {
+        "revision" : "f68ec28fbd90a651597e9e825ea7f315f8d52a1f",
+        "version" : "4.0.1"
+      }
+    },
+    {
+      "identity" : "swift-log",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/apple/swift-log.git",
+      "state" : {
+        "revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2",
+        "version" : "1.6.4"
+      }
+    },
+    {
       "identity" : "swift-numerics",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/apple/swift-numerics",

+ 3 - 3
Trio/Sources/Application/AppDelegate.swift

@@ -32,11 +32,11 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
 
         do {
             let jsonData = try JSONSerialization.data(withJSONObject: userInfo)
-            let pushMessage = try JSONDecoder().decode(PushMessage.self, from: jsonData)
+            let encryptedMessage = try JSONDecoder().decode(EncryptedPushMessage.self, from: jsonData)
 
             Task {
                 do {
-                    try await TrioRemoteControl.shared.handleRemoteNotification(pushMessage: pushMessage)
+                    try await TrioRemoteControl.shared.handleRemoteNotification(encryptedData: encryptedMessage.encryptedData)
                     completionHandler(.newData)
                 } catch {
                     debug(
@@ -47,7 +47,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
                 }
             }
         } catch {
-            debug(.remoteControl, "Error decoding push message: \(error)")
+            debug(.remoteControl, "Error decoding push message shell: \(error)")
             completionHandler(.failed)
         }
     }

+ 132 - 0
Trio/Sources/Models/CommandPayload.swift

@@ -0,0 +1,132 @@
+import Foundation
+
+struct EncryptedPushMessage: Decodable {
+    let encryptedData: String
+
+    enum CodingKeys: String, CodingKey {
+        case encryptedData = "encrypted_data"
+    }
+}
+
+struct CommandPayload: Decodable, Sendable {
+    var user: String
+    var commandType: TrioRemoteControl.CommandType
+    var timestamp: TimeInterval
+    var bolusAmount: Decimal?
+    var target: Int?
+    var duration: Int?
+    var carbs: Int?
+    var protein: Int?
+    var fat: Int?
+    var overrideName: String?
+    var scheduledTime: TimeInterval?
+    var returnNotification: ReturnNotificationInfo?
+
+    struct ReturnNotificationInfo: Decodable, Sendable {
+        let productionEnvironment: Bool
+        let deviceToken: String
+        let bundleId: String
+        let teamId: String
+        let keyId: String
+        let apnsKey: String
+
+        enum CodingKeys: String, CodingKey {
+            case productionEnvironment = "production_environment"
+            case deviceToken = "device_token"
+            case bundleId = "bundle_id"
+            case teamId = "team_id"
+            case keyId = "key_id"
+            case apnsKey = "apns_key"
+        }
+    }
+
+    enum CodingKeys: String, CodingKey {
+        case user
+        case timestamp
+        case target
+        case duration
+        case carbs
+        case protein
+        case fat
+        case overrideName
+        case commandType = "command_type"
+        case bolusAmount = "bolus_amount"
+        case scheduledTime = "scheduled_time"
+        case returnNotification = "return_notification"
+    }
+
+    func humanReadableDescription() -> String {
+        var description = "User: \(user). Command Type: \(commandType.description). "
+
+        if let override = overrideName {
+            description += "Override Name: \(override). "
+        }
+
+        switch commandType {
+        case .bolus:
+            if let amount = bolusAmount {
+                description += "Bolus Amount: \(amount) units."
+            } else {
+                description += "Bolus Amount: unknown."
+            }
+        case .tempTarget:
+            let targetDesc = target != nil ? "\(target!) mg/dL" : "unknown target"
+            let durationDesc = duration != nil ? "\(duration!) minutes" : "unknown duration"
+            description += "Temp Target: \(targetDesc), Duration: \(durationDesc)."
+        case .cancelTempTarget:
+            description += "Cancel Temp Target command."
+        case .meal:
+            let carbsDesc = carbs != nil ? "\(carbs!)g carbs" : "unknown carbs"
+            let fatDesc = fat != nil ? "\(fat!)g fat" : "unknown fat"
+            let proteinDesc = protein != nil ? "\(protein!)g protein" : "unknown protein"
+            description += "Meal with \(carbsDesc), \(fatDesc), \(proteinDesc)."
+        case .startOverride:
+            if let override = overrideName {
+                description += "Start Override: \(override)."
+            } else {
+                description += "Start Override: unknown override name."
+            }
+        case .cancelOverride:
+            description += "Cancel Override command."
+        }
+
+        if let scheduledTime = scheduledTime {
+            let date = Date(timeIntervalSince1970: scheduledTime)
+            let formatter = DateFormatter()
+            formatter.dateStyle = .short
+            formatter.timeStyle = .short
+            let dateString = formatter.string(from: date)
+            description += " Scheduled for: \(dateString)."
+        }
+
+        return description
+    }
+}
+
+extension TrioRemoteControl {
+    enum CommandType: String, Codable {
+        case bolus
+        case tempTarget = "temp_target"
+        case cancelTempTarget = "cancel_temp_target"
+        case meal
+        case startOverride = "start_override"
+        case cancelOverride = "cancel_override"
+
+        var description: String {
+            switch self {
+            case .bolus:
+                return "Bolus"
+            case .tempTarget:
+                return "Temporary Target"
+            case .cancelTempTarget:
+                return "Cancel Temporary Target"
+            case .meal:
+                return "Meal"
+            case .startOverride:
+                return "Start Override"
+            case .cancelOverride:
+                return "Cancel Override"
+            }
+        }
+    }
+}

+ 0 - 141
Trio/Sources/Models/PushMessage.swift

@@ -1,141 +0,0 @@
-import Foundation
-
-struct PushMessage: Codable, Sendable {
-    var user: String
-    var commandType: TrioRemoteControl.CommandType
-    var bolusAmount: Decimal?
-    var target: Int?
-    var duration: Int?
-    var carbs: Int?
-    var protein: Int?
-    var fat: Int?
-    var sharedSecret: String
-    var timestamp: TimeInterval
-    var overrideName: String?
-    var scheduledTime: TimeInterval?
-
-    enum CodingKeys: String, CodingKey {
-        case aps
-        case user
-        case commandType = "command_type"
-        case bolusAmount = "bolus_amount"
-        case target
-        case duration
-        case carbs
-        case protein
-        case fat
-        case sharedSecret = "shared_secret"
-        case timestamp
-        case overrideName
-        case scheduledTime = "scheduled_time"
-    }
-
-    func encode(to encoder: Encoder) throws {
-        var container = encoder.container(keyedBy: CodingKeys.self)
-        try container.encode(user, forKey: .user)
-        try container.encode(commandType, forKey: .commandType)
-        try container.encodeIfPresent(bolusAmount, forKey: .bolusAmount)
-        try container.encodeIfPresent(target, forKey: .target)
-        try container.encodeIfPresent(duration, forKey: .duration)
-        try container.encodeIfPresent(carbs, forKey: .carbs)
-        try container.encodeIfPresent(protein, forKey: .protein)
-        try container.encodeIfPresent(fat, forKey: .fat)
-        try container.encode(sharedSecret, forKey: .sharedSecret)
-        try container.encode(timestamp, forKey: .timestamp)
-        try container.encodeIfPresent(overrideName, forKey: .overrideName)
-        if let scheduledTime = scheduledTime {
-            try container.encode(scheduledTime, forKey: .scheduledTime)
-        }
-    }
-
-    init(from decoder: Decoder) throws {
-        let container = try decoder.container(keyedBy: CodingKeys.self)
-        user = try container.decode(String.self, forKey: .user)
-        commandType = try container.decode(TrioRemoteControl.CommandType.self, forKey: .commandType)
-        bolusAmount = try container.decodeIfPresent(Decimal.self, forKey: .bolusAmount)
-        target = try container.decodeIfPresent(Int.self, forKey: .target)
-        duration = try container.decodeIfPresent(Int.self, forKey: .duration)
-        carbs = try container.decodeIfPresent(Int.self, forKey: .carbs)
-        protein = try container.decodeIfPresent(Int.self, forKey: .protein)
-        fat = try container.decodeIfPresent(Int.self, forKey: .fat)
-        sharedSecret = try container.decode(String.self, forKey: .sharedSecret)
-        timestamp = try container.decode(TimeInterval.self, forKey: .timestamp)
-        overrideName = try container.decodeIfPresent(String.self, forKey: .overrideName)
-        scheduledTime = try container.decodeIfPresent(TimeInterval.self, forKey: .scheduledTime)
-    }
-
-    init(
-        user: String,
-        commandType: TrioRemoteControl.CommandType,
-        bolusAmount: Decimal? = nil,
-        target: Int? = nil,
-        duration: Int? = nil,
-        carbs: Int? = nil,
-        protein: Int? = nil,
-        fat: Int? = nil,
-        sharedSecret: String,
-        timestamp: TimeInterval,
-        overrideName: String? = nil,
-        scheduledTime: TimeInterval? = nil
-    ) {
-        self.user = user
-        self.commandType = commandType
-        self.bolusAmount = bolusAmount
-        self.target = target
-        self.duration = duration
-        self.carbs = carbs
-        self.protein = protein
-        self.fat = fat
-        self.sharedSecret = sharedSecret
-        self.timestamp = timestamp
-        self.overrideName = overrideName
-        self.scheduledTime = scheduledTime
-    }
-
-    func humanReadableDescription() -> String {
-        var description = "User: \(user). Command Type: \(commandType.description). "
-
-        if let override = overrideName {
-            description += "Override Name: \(override). "
-        }
-
-        switch commandType {
-        case .bolus:
-            if let amount = bolusAmount {
-                description += "Bolus Amount: \(amount) units."
-            } else {
-                description += "Bolus Amount: unknown."
-            }
-        case .tempTarget:
-            let targetDesc = target != nil ? "\(target!) mg/dL" : "unknown target"
-            let durationDesc = duration != nil ? "\(duration!) minutes" : "unknown duration"
-            description += "Temp Target: \(targetDesc), Duration: \(durationDesc)."
-        case .cancelTempTarget:
-            description += "Cancel Temp Target command."
-        case .meal:
-            let carbsDesc = carbs != nil ? "\(carbs!)g carbs" : "unknown carbs"
-            let fatDesc = fat != nil ? "\(fat!)g fat" : "unknown fat"
-            let proteinDesc = protein != nil ? "\(protein!)g protein" : "unknown protein"
-            description += "Meal with \(carbsDesc), \(fatDesc), \(proteinDesc)."
-        case .startOverride:
-            if let override = overrideName {
-                description += "Start Override: \(override)."
-            } else {
-                description += "Start Override: unknown override name."
-            }
-        case .cancelOverride:
-            description += "Cancel Override command."
-        }
-
-        if let scheduledTime = scheduledTime {
-            let date = Date(timeIntervalSince1970: scheduledTime)
-            let formatter = DateFormatter()
-            formatter.dateStyle = .short
-            formatter.timeStyle = .short
-            let dateString = formatter.string(from: date)
-            description += " Scheduled for: \(dateString)."
-        }
-
-        return description
-    }
-}

+ 79 - 0
Trio/Sources/Services/RemoteControl/APNSJWTClaims.swift

@@ -0,0 +1,79 @@
+import Foundation
+import SwiftJWT
+
+struct APNSJWTClaims: Claims {
+    let iss: String
+    let iat: Date
+}
+
+class APNSJWTManager {
+    static let shared = APNSJWTManager()
+
+    private init() {}
+
+    private struct JWTCacheKey: Hashable {
+        let keyId: String
+        let teamId: String
+    }
+
+    private struct CachedJWT {
+        let token: String
+        let expirationDate: Date
+    }
+
+    // Cache multiple JWTs for different LoopFollow instances
+    private var jwtCache: [JWTCacheKey: CachedJWT] = [:]
+    private let cacheQueue = DispatchQueue(label: "com.trio.apnsjwtmanager.cache", attributes: .concurrent)
+
+    func getOrGenerateJWT(keyId: String, teamId: String, apnsKey: String) -> String? {
+        let cacheKey = JWTCacheKey(keyId: keyId, teamId: teamId)
+
+        // Check cache first
+        if let cachedJWT = getCachedJWT(for: cacheKey) {
+            return cachedJWT
+        }
+
+        // Generate new JWT
+        let header = Header(kid: keyId)
+        let claims = APNSJWTClaims(iss: teamId, iat: Date())
+        var jwt = JWT(header: header, claims: claims)
+
+        do {
+            let privateKey = Data(apnsKey.utf8)
+            let jwtSigner = JWTSigner.es256(privateKey: privateKey)
+            let signedJWT = try jwt.sign(using: jwtSigner)
+
+            // Cache the JWT with 55 minute expiration (5 minute buffer before 1 hour)
+            let expirationDate = Date().addingTimeInterval(3300)
+            cacheJWT(signedJWT, for: cacheKey, expirationDate: expirationDate)
+
+            return signedJWT
+        } catch {
+            debug(.remoteControl, "Failed to sign JWT: \(error.localizedDescription)")
+            return nil
+        }
+    }
+
+    private func getCachedJWT(for key: JWTCacheKey) -> String? {
+        cacheQueue.sync {
+            guard let cached = jwtCache[key],
+                  Date() < cached.expirationDate
+            else {
+                return nil
+            }
+            return cached.token
+        }
+    }
+
+    private func cacheJWT(_ token: String, for key: JWTCacheKey, expirationDate: Date) {
+        cacheQueue.async(flags: .barrier) {
+            self.jwtCache[key] = CachedJWT(token: token, expirationDate: expirationDate)
+        }
+    }
+
+    func invalidateCache() {
+        cacheQueue.async(flags: .barrier) {
+            self.jwtCache.removeAll()
+        }
+    }
+}

+ 110 - 0
Trio/Sources/Services/RemoteControl/RemoteNotificationResponseManager.swift

@@ -0,0 +1,110 @@
+import Foundation
+
+class RemoteNotificationResponseManager {
+    static let shared = RemoteNotificationResponseManager()
+
+    private init() {}
+
+    struct NotificationPayload: Encodable {
+        let aps: APSPayload
+        let commandStatus: String
+        let commandType: String
+        let timestamp: TimeInterval
+
+        enum CodingKeys: String, CodingKey {
+            case aps
+            case commandStatus = "command_status"
+            case commandType = "command_type"
+            case timestamp
+        }
+    }
+
+    struct APSPayload: Encodable {
+        let alert: Alert
+        let sound: String = "default"
+    }
+
+    struct Alert: Encodable {
+        let title: String
+        let body: String
+    }
+
+    func sendResponseNotification(
+        to returnInfo: CommandPayload.ReturnNotificationInfo?,
+        commandType: TrioRemoteControl.CommandType,
+        success: Bool,
+        message: String
+    ) async {
+        guard let returnInfo = returnInfo,
+              !returnInfo.deviceToken.isEmpty
+        else {
+            debug(.remoteControl, "No return notification info provided, skipping response")
+            return
+        }
+
+        let payload = NotificationPayload(
+            aps: APSPayload(
+                alert: Alert(
+                    title: success ? "Command Successful" : "Command Failed",
+                    body: message
+                )
+            ),
+            commandStatus: success ? "success" : "failed",
+            commandType: commandType.rawValue,
+            timestamp: Date().timeIntervalSince1970
+        )
+
+        await sendPushNotification(
+            payload: payload,
+            to: returnInfo.deviceToken,
+            using: returnInfo
+        )
+    }
+
+    private func sendPushNotification(
+        payload: NotificationPayload,
+        to deviceToken: String,
+        using returnInfo: CommandPayload.ReturnNotificationInfo
+    ) async {
+        guard let jwt = APNSJWTManager.shared.getOrGenerateJWT(
+            keyId: returnInfo.keyId,
+            teamId: returnInfo.teamId,
+            apnsKey: returnInfo.apnsKey
+        ) else {
+            debug(.remoteControl, "Failed to generate JWT for response notification")
+            return
+        }
+
+        let host = returnInfo.productionEnvironment ? "api.push.apple.com" : "api.sandbox.push.apple.com"
+        guard let url = URL(string: "https://\(host)/3/device/\(deviceToken)") else {
+            debug(.remoteControl, "Failed to construct APNs URL")
+            return
+        }
+
+        var request = URLRequest(url: url)
+        request.httpMethod = "POST"
+        request.setValue("bearer \(jwt)", forHTTPHeaderField: "authorization")
+        request.setValue("application/json", forHTTPHeaderField: "content-type")
+        request.setValue("10", forHTTPHeaderField: "apns-priority")
+        request.setValue("0", forHTTPHeaderField: "apns-expiration")
+        request.setValue(returnInfo.bundleId, forHTTPHeaderField: "apns-topic")
+        request.setValue("alert", forHTTPHeaderField: "apns-push-type")
+
+        do {
+            let jsonData = try JSONEncoder().encode(payload)
+            request.httpBody = jsonData
+
+            let (_, response) = try await URLSession.shared.data(for: request)
+
+            if let httpResponse = response as? HTTPURLResponse {
+                if httpResponse.statusCode == 200 {
+                    debug(.remoteControl, "Response notification sent successfully")
+                } else {
+                    debug(.remoteControl, "Failed to send response notification: \(httpResponse.statusCode)")
+                }
+            }
+        } catch {
+            debug(.remoteControl, "Error sending response notification: \(error.localizedDescription)")
+        }
+    }
+}

+ 38 - 0
Trio/Sources/Services/RemoteControl/SecureMessenger.swift

@@ -0,0 +1,38 @@
+import CryptoSwift
+import Foundation
+import Security
+
+struct SecureMessenger {
+    private let sharedKey: [UInt8]
+
+    init?(sharedSecret: String) {
+        guard let secretData = sharedSecret.data(using: .utf8) else {
+            return nil
+        }
+        sharedKey = Array(secretData.sha256())
+    }
+
+    func decrypt(base64EncodedString: String) throws -> CommandPayload {
+        guard let combinedData = Data(base64Encoded: base64EncodedString) else {
+            throw NSError(domain: "SecureMessenger", code: 100, userInfo: [NSLocalizedDescriptionKey: "Invalid Base64 string"])
+        }
+
+        let nonceSize = 12
+        guard combinedData.count > nonceSize else {
+            throw NSError(
+                domain: "SecureMessenger",
+                code: 101,
+                userInfo: [NSLocalizedDescriptionKey: "Encrypted data is too short to contain a nonce"]
+            )
+        }
+        let nonce = Array(combinedData.prefix(nonceSize))
+        let ciphertextAndTag = Array(combinedData.suffix(from: nonceSize))
+        let gcm = GCM(iv: nonce, mode: .combined)
+        let aes = try AES(key: sharedKey, blockMode: gcm, padding: .noPadding)
+        let decryptedBytes = try aes.decrypt(ciphertextAndTag)
+        let decryptedData = Data(decryptedBytes)
+        let commandPayload = try JSONDecoder().decode(CommandPayload.self, from: decryptedData)
+
+        return commandPayload
+    }
+}

+ 37 - 24
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Bolus.swift

@@ -1,9 +1,10 @@
 import Foundation
+import HealthKit
 
 extension TrioRemoteControl {
-    internal func handleBolusCommand(_ pushMessage: PushMessage) async throws {
-        guard let bolusAmount = pushMessage.bolusAmount else {
-            await logError("Command rejected: bolus amount is missing or invalid.", pushMessage: pushMessage)
+    internal func handleBolusCommand(_ payload: CommandPayload) async throws {
+        guard let bolusAmount = payload.bolusAmount else {
+            await logError("Command rejected: bolus amount is missing or invalid.", payload: payload)
             return
         }
 
@@ -12,7 +13,7 @@ extension TrioRemoteControl {
         if bolusAmount > maxBolus {
             await logError(
                 "Command rejected: bolus amount (\(bolusAmount) units) exceeds the maximum allowed (\(maxBolus) units).",
-                pushMessage: pushMessage
+                payload: payload
             )
             return
         }
@@ -24,18 +25,18 @@ extension TrioRemoteControl {
         if (currentIOB + bolusAmount) > maxIOB {
             await logError(
                 "Command rejected: bolus amount (\(bolusAmount) units) would exceed max IOB (\(maxIOB) units). Current IOB: \(currentIOB) units.",
-                pushMessage: pushMessage
+                payload: payload
             )
             return
         }
 
         let totalRecentBolusAmount =
-            try await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: pushMessage.timestamp))
+            try await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: payload.timestamp))
 
         if totalRecentBolusAmount >= bolusAmount * 0.2 {
             await logError(
                 "Command rejected: boluses totaling more than 20% of the requested amount have been delivered since the command was sent.",
-                pushMessage: pushMessage
+                payload: payload
             )
             return
         }
@@ -45,17 +46,38 @@ extension TrioRemoteControl {
         guard let apsManager = await TrioApp.resolver.resolve(APSManager.self) else {
             await logError(
                 "Error: unable to process bolus command because the APS Manager is not available.",
-                pushMessage: pushMessage
+                payload: payload
             )
             return
         }
 
-        await apsManager.enactBolus(amount: Double(truncating: bolusAmount as NSNumber), isSMB: false, callback: nil)
+        if let returnInfo = payload.returnNotification {
+            await RemoteNotificationResponseManager.shared.sendResponseNotification(
+                to: returnInfo,
+                commandType: payload.commandType,
+                success: true,
+                message: "Initiating bolus..."
+            )
+        }
 
-        debug(
-            .remoteControl,
-            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
-        )
+        await apsManager
+            .enactBolus(amount: Double(truncating: bolusAmount as NSNumber), isSMB: false) { [weak self] success, message in
+                guard let self = self else { return }
+                Task {
+                    if success {
+                        await self.logSuccess(
+                            "Remote command processed successfully. \(payload.humanReadableDescription())",
+                            payload: payload,
+                            customNotificationMessage: "Bolus started"
+                        )
+                    } else {
+                        await self.logError(
+                            message,
+                            payload: payload
+                        )
+                    }
+                }
+            }
     }
 
     private func fetchTotalRecentBolusAmount(since date: Date) async throws -> Decimal {
@@ -64,24 +86,15 @@ extension TrioRemoteControl {
             PumpEventStored.EventType.bolus.rawValue,
             date as NSDate
         )
-
         let results: Any = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self,
-            onContext: pumpHistoryFetchContext,
-            predicate: predicate,
-            key: "timestamp",
-            ascending: true,
-            fetchLimit: nil,
-            propertiesToFetch: ["bolus.amount"]
+            ofType: PumpEventStored.self, onContext: pumpHistoryFetchContext, predicate: predicate, key: "timestamp",
+            ascending: true, fetchLimit: nil, propertiesToFetch: ["bolus.amount"]
         )
-
         guard let bolusDictionaries = results as? [[String: Any]] else {
             await logError("Failed to cast fetched bolus events. Fetched entities type: \(type(of: results))")
             throw CoreDataError.fetchError(function: #function, file: #file)
         }
-
         let totalAmount = bolusDictionaries.compactMap { ($0["bolus.amount"] as? NSNumber)?.decimalValue }.reduce(0, +)
-
         return totalAmount
     }
 }

+ 25 - 3
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Helpers.swift

@@ -1,12 +1,34 @@
 import Foundation
 
 extension TrioRemoteControl {
-    func logError(_ errorMessage: String, pushMessage: PushMessage? = nil) async {
+    func logError(_ errorMessage: String, payload: CommandPayload? = nil) async {
         var note = errorMessage
-        if let pushMessage = pushMessage {
-            note += " Details: \(pushMessage.humanReadableDescription())"
+        if let payload = payload {
+            note += " Details: \(payload.humanReadableDescription())"
+
+            if let returnInfo = payload.returnNotification {
+                await RemoteNotificationResponseManager.shared.sendResponseNotification(
+                    to: returnInfo,
+                    commandType: payload.commandType,
+                    success: false,
+                    message: errorMessage
+                )
+            }
         }
         debug(.remoteControl, note)
         await nightscoutManager.uploadNoteTreatment(note: note)
     }
+
+    func logSuccess(_ message: String, payload: CommandPayload, customNotificationMessage: String? = nil) async {
+        debug(.remoteControl, message)
+
+        if let returnInfo = payload.returnNotification {
+            await RemoteNotificationResponseManager.shared.sendResponseNotification(
+                to: returnInfo,
+                commandType: payload.commandType,
+                success: true,
+                message: customNotificationMessage ?? "Command successful"
+            )
+        }
+    }
 }

+ 26 - 40
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Meal.swift

@@ -1,15 +1,15 @@
 import Foundation
 
 extension TrioRemoteControl {
-    func handleMealCommand(_ pushMessage: PushMessage) async throws {
-        guard pushMessage.carbs != nil || pushMessage.fat != nil || pushMessage.protein != nil else {
-            await logError("Command rejected: meal data is incomplete or invalid.", pushMessage: pushMessage)
+    func handleMealCommand(_ payload: CommandPayload) async throws {
+        guard payload.carbs != nil || payload.fat != nil || payload.protein != nil else {
+            await logError("Command rejected: meal data is incomplete or invalid.", payload: payload)
             return
         }
 
-        let carbsDecimal = pushMessage.carbs != nil ? Decimal(pushMessage.carbs!) : nil
-        let fatDecimal = pushMessage.fat != nil ? Decimal(pushMessage.fat!) : nil
-        let proteinDecimal = pushMessage.protein != nil ? Decimal(pushMessage.protein!) : nil
+        let carbsDecimal = payload.carbs != nil ? Decimal(payload.carbs!) : nil
+        let fatDecimal = payload.fat != nil ? Decimal(payload.fat!) : nil
+        let proteinDecimal = payload.protein != nil ? Decimal(payload.protein!) : nil
 
         let settings = await TrioApp.resolver.resolve(SettingsManager.self)?.settings
         let maxCarbs = settings?.maxCarbs ?? Decimal(0)
@@ -19,35 +19,29 @@ extension TrioRemoteControl {
         if let carbs = carbsDecimal, carbs > maxCarbs {
             await logError(
                 "Command rejected: carbs amount (\(carbs)g) exceeds the maximum allowed (\(maxCarbs)g).",
-                pushMessage: pushMessage
+                payload: payload
             )
             return
         }
-
         if let fat = fatDecimal, fat > maxFat {
-            await logError(
-                "Command rejected: fat amount (\(fat)g) exceeds the maximum allowed (\(maxFat)g).",
-                pushMessage: pushMessage
-            )
+            await logError("Command rejected: fat amount (\(fat)g) exceeds the maximum allowed (\(maxFat)g).", payload: payload)
             return
         }
-
         if let protein = proteinDecimal, protein > maxProtein {
             await logError(
                 "Command rejected: protein amount (\(protein)g) exceeds the maximum allowed (\(maxProtein)g).",
-                pushMessage: pushMessage
+                payload: payload
             )
             return
         }
 
-        let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
+        let payloadDate = Date(timeIntervalSince1970: payload.timestamp)
         let taskContext = CoreDataStack.shared.newTaskContext()
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: CarbEntryStored.self,
-            onContext: taskContext,
-            predicate: NSPredicate(format: "date > %@", pushMessageDate as NSDate),
-            key: "date",
-            ascending: false
+            ofType: CarbEntryStored.self, onContext: taskContext, predicate: NSPredicate(
+                format: "date > %@",
+                payloadDate as NSDate
+            ), key: "date", ascending: false
         )
 
         await taskContext.perform {
@@ -56,38 +50,30 @@ extension TrioRemoteControl {
                 Task {
                     await self.logError(
                         "Command rejected: newer carb entries have been logged since the command was sent.",
-                        pushMessage: pushMessage
+                        payload: payload
                     )
                     return
                 }
             }
         }
 
-        let actualDate: Date?
-        if let scheduledTime = pushMessage.scheduledTime {
-            actualDate = Date(timeIntervalSince1970: scheduledTime)
-        } else {
-            actualDate = nil
-        }
+        let actualDate = payload.scheduledTime.map { Date(timeIntervalSince1970: $0) }
 
         let mealEntry = CarbsEntry(
-            id: UUID().uuidString,
-            createdAt: Date(),
-            actualDate: actualDate,
-            carbs: carbsDecimal ?? 0,
-            fat: fatDecimal,
-            protein: proteinDecimal,
-            note: "Remote meal command",
-            enteredBy: CarbsEntry.local,
-            isFPU: false,
+            id: UUID().uuidString, createdAt: Date(), actualDate: actualDate,
+            carbs: carbsDecimal ?? 0, fat: fatDecimal, protein: proteinDecimal,
+            note: "Remote meal command", enteredBy: CarbsEntry.local, isFPU: false,
             fpuID: fatDecimal ?? 0 > 0 || proteinDecimal ?? 0 > 0 ? UUID().uuidString : nil
         )
 
         try await carbsStorage.storeCarbs([mealEntry], areFetchedFromRemote: false)
 
-        debug(
-            .remoteControl,
-            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
-        )
+        if payload.bolusAmount == nil {
+            await logSuccess(
+                "Remote command processed successfully. \(payload.humanReadableDescription())",
+                payload: payload,
+                customNotificationMessage: "Meal logged"
+            )
+        }
     }
 }

+ 28 - 57
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Override.swift

@@ -1,101 +1,78 @@
 import CoreData
 import Foundation
+import UIKit
 
 extension TrioRemoteControl {
-    @MainActor internal func handleCancelOverrideCommand(_ pushMessage: PushMessage) async {
+    @MainActor internal func handleCancelOverrideCommand(_ payload: CommandPayload) async {
         await disableAllActiveOverrides()
-
-        debug(
-            .remoteControl,
-            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        await logSuccess(
+            "Remote command processed successfully. \(payload.humanReadableDescription())",
+            payload: payload,
+            customNotificationMessage: "Override canceled"
         )
     }
 
-    @MainActor internal func handleStartOverrideCommand(_ pushMessage: PushMessage) async {
+    @MainActor internal func handleStartOverrideCommand(_ payload: CommandPayload) async {
         do {
-            guard let overrideName = pushMessage.overrideName, !overrideName.isEmpty else {
-                await logError("Command rejected: override name is missing.", pushMessage: pushMessage)
+            guard let overrideName = payload.overrideName, !overrideName.isEmpty else {
+                await logError("Command rejected: override name is missing.", payload: payload)
                 return
             }
-
             let presetIDs = try await overrideStorage.fetchForOverridePresets()
-
-            let presets = try presetIDs.compactMap { id in
-                try viewContext.existingObject(with: id) as? OverrideStored
-            }
-
+            let presets = try presetIDs.compactMap { try viewContext.existingObject(with: $0) as? OverrideStored }
             if let preset = presets.first(where: { $0.name == overrideName }) {
-                await enactOverridePreset(preset: preset, pushMessage: pushMessage)
+                await enactOverridePreset(preset: preset, payload: payload)
             } else {
-                await logError(
-                    "Command rejected: override preset '\(overrideName)' not found.",
-                    pushMessage: pushMessage
-                )
+                await logError("Command rejected: override preset '\(overrideName)' not found.", payload: payload)
             }
         } catch {
-            debug(
-                .remoteControl,
-                "\(DebuggingIdentifiers.failed) Failed to handle start override command: \(error)"
-            )
-            await logError(
-                "Command failed: \(error.localizedDescription)",
-                pushMessage: pushMessage
-            )
+            debug(.remoteControl, "\(DebuggingIdentifiers.failed) Failed to handle start override command: \(error)")
+            await logError("Command failed: \(error.localizedDescription)", payload: payload)
         }
     }
 
-    @MainActor private func enactOverridePreset(preset: OverrideStored, pushMessage: PushMessage) async {
+    @MainActor private func enactOverridePreset(preset: OverrideStored, payload: CommandPayload) async {
         preset.enabled = true
         preset.date = Date()
         preset.isUploadedToNS = false
-
         await disableAllActiveOverrides(except: preset.objectID)
-
         do {
             if viewContext.hasChanges {
                 try viewContext.save()
-
                 Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
                 await awaitNotification(.didUpdateOverrideConfiguration)
-
-                debug(.remoteControl, "Remote command processed successfully. \(pushMessage.humanReadableDescription())")
+                await logSuccess(
+                    "Remote command processed successfully. \(payload.humanReadableDescription())",
+                    payload: payload,
+                    customNotificationMessage: "Override started"
+                )
             }
         } catch {
             debug(.remoteControl, "Failed to enact override preset: \(error)")
+            await logError("Failed to enact override preset: \(error.localizedDescription)", payload: payload)
         }
     }
 
     @MainActor private func disableAllActiveOverrides(except overrideID: NSManagedObjectID? = nil) async {
         do {
-            let ids = try await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
-
+            let ids = try await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0)
             let didPostNotification = try await viewContext.perform { () -> Bool in
-                let results = try ids.compactMap { id in
-                    try self.viewContext.existingObject(with: id) as? OverrideStored
-                }
-
+                let results = try ids.compactMap { try self.viewContext.existingObject(with: $0) as? OverrideStored }
                 guard !results.isEmpty else { return false }
-
                 for canceledOverride in results where canceledOverride.enabled {
-                    if let overrideID = overrideID, canceledOverride.objectID == overrideID {
-                        continue
-                    }
-
+                    if let overrideID = overrideID, canceledOverride.objectID == overrideID { continue }
                     let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
                     newOverrideRunStored.id = UUID()
                     newOverrideRunStored.name = canceledOverride.name
                     newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
                     newOverrideRunStored.endDate = Date()
-                    newOverrideRunStored.target = NSDecimalNumber(
-                        decimal: self.overrideStorage.calculateTarget(override: canceledOverride)
-                    )
+                    newOverrideRunStored
+                        .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
                     newOverrideRunStored.override = canceledOverride
                     newOverrideRunStored.isUploadedToNS = false
-
                     canceledOverride.enabled = false
                     canceledOverride.isUploadedToNS = false
                 }
-
                 if self.viewContext.hasChanges {
                     try self.viewContext.save()
                     Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
@@ -104,15 +81,9 @@ extension TrioRemoteControl {
                     return false
                 }
             }
-
-            if didPostNotification {
-                await awaitNotification(.didUpdateOverrideConfiguration)
-            }
+            if didPostNotification { await awaitNotification(.didUpdateOverrideConfiguration) }
         } catch {
-            debug(
-                .remoteControl,
-                "\(DebuggingIdentifiers.failed) Failed to disable active overrides: \(error)"
-            )
+            debug(.remoteControl, "\(DebuggingIdentifiers.failed) Failed to disable active overrides: \(error)")
         }
     }
 }

+ 22 - 45
Trio/Sources/Services/RemoteControl/TrioRemoteControl+TempTarget.swift

@@ -1,67 +1,54 @@
 import CoreData
 import Foundation
+import UIKit
 
 extension TrioRemoteControl {
-    @MainActor func handleTempTargetCommand(_ pushMessage: PushMessage) async throws {
-        guard let targetValue = pushMessage.target,
-              let durationValue = pushMessage.duration
-        else {
-            await logError("Command rejected: temp target data is incomplete or invalid.", pushMessage: pushMessage)
+    @MainActor func handleTempTargetCommand(_ payload: CommandPayload) async throws {
+        guard let targetValue = payload.target, let durationValue = payload.duration else {
+            await logError("Command rejected: temp target data is incomplete or invalid.", payload: payload)
             return
         }
 
         let durationInMinutes = Int(durationValue)
-        let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
+        let payloadDate = Date(timeIntervalSince1970: payload.timestamp)
 
         let tempTarget = TempTarget(
-            name: TempTarget.custom,
-            createdAt: pushMessageDate,
-            targetTop: Decimal(targetValue),
-            targetBottom: Decimal(targetValue),
-            duration: Decimal(durationInMinutes),
-            enteredBy: TempTarget.local,
-            reason: TempTarget.custom,
-            isPreset: false,
-            enabled: true,
+            name: TempTarget.custom, createdAt: payloadDate,
+            targetTop: Decimal(targetValue), targetBottom: Decimal(targetValue),
+            duration: Decimal(durationInMinutes), enteredBy: TempTarget.local,
+            reason: TempTarget.custom, isPreset: false, enabled: true,
             halfBasalTarget: settings.preferences.halfBasalExerciseTarget
         )
 
         try await tempTargetsStorage.storeTempTarget(tempTarget: tempTarget)
         tempTargetsStorage.saveTempTargetsToStorage([tempTarget])
 
-        debug(
-            .remoteControl,
-            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        await logSuccess(
+            "Remote command processed successfully. \(payload.humanReadableDescription())",
+            payload: payload,
+            customNotificationMessage: "Temp target set"
         )
     }
 
-    @MainActor func cancelTempTarget(_ pushMessage: PushMessage) async {
+    @MainActor func cancelTempTarget(_ payload: CommandPayload) async {
         debug(.remoteControl, "Cancelling temp target.")
-
         await disableAllActiveTempTargets()
-
-        debug(
-            .remoteControl,
-            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        await logSuccess(
+            "Remote command processed successfully. \(payload.humanReadableDescription())",
+            payload: payload,
+            customNotificationMessage: "Temp target canceled"
         )
     }
 
     @MainActor func disableAllActiveTempTargets() async {
         do {
             let ids = try await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
-
             let didPostNotification = try await viewContext.perform { () -> Bool in
-                let results = try ids.compactMap { id in
-                    try self.viewContext.existingObject(with: id) as? TempTargetStored
-                }
-
+                let results = try ids.compactMap { try self.viewContext.existingObject(with: $0) as? TempTargetStored }
                 guard !results.isEmpty else {
-                    Task {
-                        await self.logError("Command rejected: no active temp target to cancel.")
-                    }
+                    Task { await self.logError("Command rejected: no active temp target to cancel.") }
                     return false
                 }
-
                 for canceledTempTarget in results where canceledTempTarget.enabled {
                     let newTempTargetRunStored = TempTargetRunStored(context: self.viewContext)
                     newTempTargetRunStored.id = UUID()
@@ -71,31 +58,21 @@ extension TrioRemoteControl {
                     newTempTargetRunStored.target = canceledTempTarget.target ?? 0
                     newTempTargetRunStored.tempTarget = canceledTempTarget
                     newTempTargetRunStored.isUploadedToNS = false
-
                     canceledTempTarget.enabled = false
                     canceledTempTarget.isUploadedToNS = false
                 }
-
                 if self.viewContext.hasChanges {
                     try self.viewContext.save()
                     Foundation.NotificationCenter.default.post(name: .willUpdateTempTargetConfiguration, object: nil)
-
-                    // Update the storage so oref can pick up cancellation
                     self.tempTargetsStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date().addingTimeInterval(-1))])
                     return true
                 } else {
                     return false
                 }
             }
-
-            if didPostNotification {
-                await awaitNotification(.didUpdateTempTargetConfiguration)
-            }
+            if didPostNotification { await awaitNotification(.didUpdateTempTargetConfiguration) }
         } catch {
-            debug(
-                .remoteControl,
-                "\(DebuggingIdentifiers.failed) Failed to disable active temp targets: \(error)"
-            )
+            debug(.remoteControl, "\(DebuggingIdentifiers.failed) Failed to disable active temp targets: \(error)")
             await logError("Failed to disable temp targets: \(error.localizedDescription)")
         }
     }

+ 38 - 62
Trio/Sources/Services/RemoteControl/TrioRemoteControl.swift

@@ -12,7 +12,7 @@ class TrioRemoteControl: Injectable {
     @Injected() internal var settings: SettingsManager!
     @Injected() internal var iobService: IOBService!
 
-    private let timeWindow: TimeInterval = 600 // Defines how old messages that are accepted, 10 minutes
+    private let timeWindow: TimeInterval = 600
 
     internal let pumpHistoryFetchContext: NSManagedObjectContext
     internal let viewContext: NSManagedObjectContext
@@ -23,96 +23,72 @@ class TrioRemoteControl: Injectable {
         injectServices(TrioApp.resolver)
     }
 
-    func handleRemoteNotification(pushMessage: PushMessage) async throws {
+    func handleRemoteNotification(encryptedData: String) async throws {
         let isTrioRemoteControlEnabled = UserDefaults.standard.bool(forKey: "isTrioRemoteControlEnabled")
         guard isTrioRemoteControlEnabled else {
             await logError("Remote command received, but remote control is disabled in settings. Ignoring the command.")
             return
         }
 
-        let currentTime = Date().timeIntervalSince1970
-        let timeDifference = currentTime - pushMessage.timestamp
+        let storedSecret = UserDefaults.standard.string(forKey: "trioRemoteControlSharedSecret") ?? ""
+        guard !storedSecret.isEmpty else {
+            await logError("Command rejected: shared secret is missing in settings. Cannot authenticate the command.")
+            return
+        }
 
-        if timeDifference > timeWindow {
-            await logError(
-                "Command rejected: the message is too old (sent \(Int(timeDifference)) seconds ago, which exceeds the allowed limit).",
-                pushMessage: pushMessage
-            )
+        guard let messenger = SecureMessenger(sharedSecret: storedSecret) else {
+            await logError("Command rejected: Failed to initialize security module. The shared secret might be invalid.")
             return
-        } else if timeDifference < -timeWindow {
+        }
+
+        let commandPayload: CommandPayload
+        do {
+            commandPayload = try messenger.decrypt(base64EncodedString: encryptedData)
+        } catch {
             await logError(
-                "Command rejected: the message has an invalid future timestamp (timestamp is \(Int(-timeDifference)) seconds ahead of the current time).",
-                pushMessage: pushMessage
+                "Command rejected: Decryption failed. Mismatched shared secret or corrupted message. Error: \(error.localizedDescription)"
             )
             return
         }
 
-        debug(.remoteControl, "Command received with acceptable time difference: \(Int(timeDifference)) seconds.")
+        let currentTime = Date().timeIntervalSince1970
+        let timeDifference = currentTime - commandPayload.timestamp
 
-        let storedSecret = UserDefaults.standard.string(forKey: "trioRemoteControlSharedSecret") ?? ""
-        guard !storedSecret.isEmpty else {
+        if timeDifference > timeWindow {
             await logError(
-                "Command rejected: shared secret is missing in settings. Cannot authenticate the command.",
-                pushMessage: pushMessage
+                "Command rejected: the message is too old (sent \(Int(timeDifference)) seconds ago).",
+                payload: commandPayload
             )
             return
-        }
-
-        guard pushMessage.sharedSecret == storedSecret else {
+        } else if timeDifference < -timeWindow {
             await logError(
-                "Command rejected: shared secret does not match. Cannot authenticate the command.",
-                pushMessage: pushMessage
+                "Command rejected: the message has an invalid future timestamp.",
+                payload: commandPayload
             )
             return
         }
 
-        switch pushMessage.commandType {
+        debug(
+            .remoteControl,
+            "Command successfully decrypted and authenticated. Time difference: \(Int(timeDifference)) seconds."
+        )
+
+        switch commandPayload.commandType {
         case .bolus:
-            try await handleBolusCommand(pushMessage)
+            try await handleBolusCommand(commandPayload)
         case .tempTarget:
-            try await handleTempTargetCommand(pushMessage)
+            try await handleTempTargetCommand(commandPayload)
         case .cancelTempTarget:
-            await cancelTempTarget(pushMessage)
+            await cancelTempTarget(commandPayload)
         case .meal:
-            try await handleMealCommand(pushMessage)
-
-            if pushMessage.bolusAmount != nil {
-                try await handleBolusCommand(pushMessage)
+            try await handleMealCommand(commandPayload)
+            if commandPayload.bolusAmount != nil {
+                try await handleBolusCommand(commandPayload)
             }
         case .startOverride:
-            await handleStartOverrideCommand(pushMessage)
+            await handleStartOverrideCommand(commandPayload)
         case .cancelOverride:
-            await handleCancelOverrideCommand(pushMessage)
-        }
-    }
-}
-
-// MARK: - CommandType Enum
-
-extension TrioRemoteControl {
-    enum CommandType: String, Codable {
-        case bolus
-        case tempTarget = "temp_target"
-        case cancelTempTarget = "cancel_temp_target"
-        case meal
-        case startOverride = "start_override"
-        case cancelOverride = "cancel_override"
-
-        var description: String {
-            switch self {
-            case .bolus:
-                return "Bolus"
-            case .tempTarget:
-                return "Temporary Target"
-            case .cancelTempTarget:
-                return "Cancel Temporary Target"
-            case .meal:
-                return "Meal"
-            case .startOverride:
-                return "Start Override"
-            case .cancelOverride:
-                return "Cancel Override"
-            }
+            await handleCancelOverrideCommand(commandPayload)
         }
     }
 }