Browse Source

Merge branch 'dev' into recommended-bolus-to-nightscout

Jonas Björkert 7 tháng trước cách đây
mục cha
commit
4b5d27c3b7

+ 4 - 4
.github/workflows/build_trio.yml

@@ -7,9 +7,9 @@ on:
   #push:
   #push:
 
 
   schedule:
   schedule:
-    # avoid starting an action at xx:00 when GitHub resources are more likely to be impacted
-    - cron: "43 8 * * 3" # Checks for updates at 08:43 UTC every Wednesday
-    - cron: "43 6 1 * *" # Builds the app on the 1st of every month at 06:43 UTC
+    # avoid starting an action at times when GitHub resources are more likely to be impacted
+    - cron: "43 8 * * 0" # Checks for updates at 08:43 UTC every Sunday
+    - cron: "43 6 8-14 * 6" # Builds the app on the second Saturday of each month at 06:43 UTC
 
 
 env:
 env:
   UPSTREAM_REPO: nightscout/Trio
   UPSTREAM_REPO: nightscout/Trio
@@ -213,7 +213,7 @@ jobs:
       | # runs if started manually, or if sync schedule is set and enabled and scheduled on the first Saturday each month, or if sync schedule is set and enabled and new commits were found
       | # runs if started manually, or if sync schedule is set and enabled and scheduled on the first Saturday each month, or if sync schedule is set and enabled and new commits were found
       github.event_name == 'workflow_dispatch' ||
       github.event_name == 'workflow_dispatch' ||
       (needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
       (needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-        (vars.SCHEDULED_BUILD != 'false' && github.event.schedule == '43 6 1 * *') ||
+        (vars.SCHEDULED_BUILD != 'false' && github.event.schedule == '43 6 8-14 * 6') ||
         (vars.SCHEDULED_SYNC != 'false' && needs.check_latest_from_upstream.outputs.NEW_COMMITS == 'true' )
         (vars.SCHEDULED_SYNC != 'false' && needs.check_latest_from_upstream.outputs.NEW_COMMITS == 'true' )
       )
       )
     steps:
     steps:

+ 2 - 2
Config.xcconfig

@@ -18,8 +18,8 @@ BUNDLE_IDENTIFIER = org.nightscout.$(DEVELOPMENT_TEAM).trio
 TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 
 
 // The developers set the version numbers, please leave them alone
 // The developers set the version numbers, please leave them alone
-APP_VERSION = 0.5.1
-APP_DEV_VERSION = 0.5.1.28
+APP_VERSION = 0.6.0
+APP_DEV_VERSION = 0.6.0.1
 APP_BUILD_NUMBER = 1
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 COPYRIGHT_NOTICE =
 
 

+ 33 - 4
Trio.xcodeproj/project.pbxproj

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

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

@@ -1,5 +1,5 @@
 {
 {
-  "originHash" : "89074a88ed67a58ecd7534519854c5a0928a4046d7c8a6123a7d70f27bf8b44d",
+  "originHash" : "94bad7ee77953ff12d8447c80f68d417ecb6f69ad08c1fdb1a8f59473b79c3b7",
   "pins" : [
   "pins" : [
     {
     {
       "identity" : "abseil-cpp-binary",
       "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",
       "identity" : "cryptoswift",
       "kind" : "remoteSourceControl",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/krzyzanowskim/CryptoSwift",
       "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",
       "identity" : "leveldb",
       "kind" : "remoteSourceControl",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/firebase/leveldb.git",
       "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",
       "identity" : "mkringprogressview",
       "kind" : "remoteSourceControl",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/maxkonovalov/MKRingProgressView.git",
       "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",
       "identity" : "swift-numerics",
       "kind" : "remoteSourceControl",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/apple/swift-numerics",
       "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 {
         do {
             let jsonData = try JSONSerialization.data(withJSONObject: userInfo)
             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 {
             Task {
                 do {
                 do {
-                    try await TrioRemoteControl.shared.handleRemoteNotification(pushMessage: pushMessage)
+                    try await TrioRemoteControl.shared.handleRemoteNotification(encryptedData: encryptedMessage.encryptedData)
                     completionHandler(.newData)
                     completionHandler(.newData)
                 } catch {
                 } catch {
                     debug(
                     debug(
@@ -47,7 +47,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
                 }
                 }
             }
             }
         } catch {
         } catch {
-            debug(.remoteControl, "Error decoding push message: \(error)")
+            debug(.remoteControl, "Error decoding push message shell: \(error)")
             completionHandler(.failed)
             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 Foundation
+import HealthKit
 
 
 extension TrioRemoteControl {
 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
             return
         }
         }
 
 
@@ -12,7 +13,7 @@ extension TrioRemoteControl {
         if bolusAmount > maxBolus {
         if bolusAmount > maxBolus {
             await logError(
             await logError(
                 "Command rejected: bolus amount (\(bolusAmount) units) exceeds the maximum allowed (\(maxBolus) units).",
                 "Command rejected: bolus amount (\(bolusAmount) units) exceeds the maximum allowed (\(maxBolus) units).",
-                pushMessage: pushMessage
+                payload: payload
             )
             )
             return
             return
         }
         }
@@ -24,18 +25,18 @@ extension TrioRemoteControl {
         if (currentIOB + bolusAmount) > maxIOB {
         if (currentIOB + bolusAmount) > maxIOB {
             await logError(
             await logError(
                 "Command rejected: bolus amount (\(bolusAmount) units) would exceed max IOB (\(maxIOB) units). Current IOB: \(currentIOB) units.",
                 "Command rejected: bolus amount (\(bolusAmount) units) would exceed max IOB (\(maxIOB) units). Current IOB: \(currentIOB) units.",
-                pushMessage: pushMessage
+                payload: payload
             )
             )
             return
             return
         }
         }
 
 
         let totalRecentBolusAmount =
         let totalRecentBolusAmount =
-            try await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: pushMessage.timestamp))
+            try await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: payload.timestamp))
 
 
         if totalRecentBolusAmount >= bolusAmount * 0.2 {
         if totalRecentBolusAmount >= bolusAmount * 0.2 {
             await logError(
             await logError(
                 "Command rejected: boluses totaling more than 20% of the requested amount have been delivered since the command was sent.",
                 "Command rejected: boluses totaling more than 20% of the requested amount have been delivered since the command was sent.",
-                pushMessage: pushMessage
+                payload: payload
             )
             )
             return
             return
         }
         }
@@ -45,17 +46,38 @@ extension TrioRemoteControl {
         guard let apsManager = await TrioApp.resolver.resolve(APSManager.self) else {
         guard let apsManager = await TrioApp.resolver.resolve(APSManager.self) else {
             await logError(
             await logError(
                 "Error: unable to process bolus command because the APS Manager is not available.",
                 "Error: unable to process bolus command because the APS Manager is not available.",
-                pushMessage: pushMessage
+                payload: payload
             )
             )
             return
             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 {
     private func fetchTotalRecentBolusAmount(since date: Date) async throws -> Decimal {
@@ -64,24 +86,15 @@ extension TrioRemoteControl {
             PumpEventStored.EventType.bolus.rawValue,
             PumpEventStored.EventType.bolus.rawValue,
             date as NSDate
             date as NSDate
         )
         )
-
         let results: Any = try await CoreDataStack.shared.fetchEntitiesAsync(
         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 {
         guard let bolusDictionaries = results as? [[String: Any]] else {
             await logError("Failed to cast fetched bolus events. Fetched entities type: \(type(of: results))")
             await logError("Failed to cast fetched bolus events. Fetched entities type: \(type(of: results))")
             throw CoreDataError.fetchError(function: #function, file: #file)
             throw CoreDataError.fetchError(function: #function, file: #file)
         }
         }
-
         let totalAmount = bolusDictionaries.compactMap { ($0["bolus.amount"] as? NSNumber)?.decimalValue }.reduce(0, +)
         let totalAmount = bolusDictionaries.compactMap { ($0["bolus.amount"] as? NSNumber)?.decimalValue }.reduce(0, +)
-
         return totalAmount
         return totalAmount
     }
     }
 }
 }

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

@@ -1,12 +1,34 @@
 import Foundation
 import Foundation
 
 
 extension TrioRemoteControl {
 extension TrioRemoteControl {
-    func logError(_ errorMessage: String, pushMessage: PushMessage? = nil) async {
+    func logError(_ errorMessage: String, payload: CommandPayload? = nil) async {
         var note = errorMessage
         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)
         debug(.remoteControl, note)
         await nightscoutManager.uploadNoteTreatment(note: 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
 import Foundation
 
 
 extension TrioRemoteControl {
 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
             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 settings = await TrioApp.resolver.resolve(SettingsManager.self)?.settings
         let maxCarbs = settings?.maxCarbs ?? Decimal(0)
         let maxCarbs = settings?.maxCarbs ?? Decimal(0)
@@ -19,35 +19,29 @@ extension TrioRemoteControl {
         if let carbs = carbsDecimal, carbs > maxCarbs {
         if let carbs = carbsDecimal, carbs > maxCarbs {
             await logError(
             await logError(
                 "Command rejected: carbs amount (\(carbs)g) exceeds the maximum allowed (\(maxCarbs)g).",
                 "Command rejected: carbs amount (\(carbs)g) exceeds the maximum allowed (\(maxCarbs)g).",
-                pushMessage: pushMessage
+                payload: payload
             )
             )
             return
             return
         }
         }
-
         if let fat = fatDecimal, fat > maxFat {
         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
             return
         }
         }
-
         if let protein = proteinDecimal, protein > maxProtein {
         if let protein = proteinDecimal, protein > maxProtein {
             await logError(
             await logError(
                 "Command rejected: protein amount (\(protein)g) exceeds the maximum allowed (\(maxProtein)g).",
                 "Command rejected: protein amount (\(protein)g) exceeds the maximum allowed (\(maxProtein)g).",
-                pushMessage: pushMessage
+                payload: payload
             )
             )
             return
             return
         }
         }
 
 
-        let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
+        let payloadDate = Date(timeIntervalSince1970: payload.timestamp)
         let taskContext = CoreDataStack.shared.newTaskContext()
         let taskContext = CoreDataStack.shared.newTaskContext()
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
         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 {
         await taskContext.perform {
@@ -56,38 +50,30 @@ extension TrioRemoteControl {
                 Task {
                 Task {
                     await self.logError(
                     await self.logError(
                         "Command rejected: newer carb entries have been logged since the command was sent.",
                         "Command rejected: newer carb entries have been logged since the command was sent.",
-                        pushMessage: pushMessage
+                        payload: payload
                     )
                     )
                     return
                     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(
         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
             fpuID: fatDecimal ?? 0 > 0 || proteinDecimal ?? 0 > 0 ? UUID().uuidString : nil
         )
         )
 
 
         try await carbsStorage.storeCarbs([mealEntry], areFetchedFromRemote: false)
         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 CoreData
 import Foundation
 import Foundation
+import UIKit
 
 
 extension TrioRemoteControl {
 extension TrioRemoteControl {
-    @MainActor internal func handleCancelOverrideCommand(_ pushMessage: PushMessage) async {
+    @MainActor internal func handleCancelOverrideCommand(_ payload: CommandPayload) async {
         await disableAllActiveOverrides()
         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 {
         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
                 return
             }
             }
-
             let presetIDs = try await overrideStorage.fetchForOverridePresets()
             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 }) {
             if let preset = presets.first(where: { $0.name == overrideName }) {
-                await enactOverridePreset(preset: preset, pushMessage: pushMessage)
+                await enactOverridePreset(preset: preset, payload: payload)
             } else {
             } else {
-                await logError(
-                    "Command rejected: override preset '\(overrideName)' not found.",
-                    pushMessage: pushMessage
-                )
+                await logError("Command rejected: override preset '\(overrideName)' not found.", payload: payload)
             }
             }
         } catch {
         } 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.enabled = true
         preset.date = Date()
         preset.date = Date()
         preset.isUploadedToNS = false
         preset.isUploadedToNS = false
-
         await disableAllActiveOverrides(except: preset.objectID)
         await disableAllActiveOverrides(except: preset.objectID)
-
         do {
         do {
             if viewContext.hasChanges {
             if viewContext.hasChanges {
                 try viewContext.save()
                 try viewContext.save()
-
                 Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
                 Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
                 await awaitNotification(.didUpdateOverrideConfiguration)
                 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 {
         } catch {
             debug(.remoteControl, "Failed to enact override preset: \(error)")
             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 {
     @MainActor private func disableAllActiveOverrides(except overrideID: NSManagedObjectID? = nil) async {
         do {
         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 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 }
                 guard !results.isEmpty else { return false }
-
                 for canceledOverride in results where canceledOverride.enabled {
                 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)
                     let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
                     newOverrideRunStored.id = UUID()
                     newOverrideRunStored.id = UUID()
                     newOverrideRunStored.name = canceledOverride.name
                     newOverrideRunStored.name = canceledOverride.name
                     newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
                     newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
                     newOverrideRunStored.endDate = Date()
                     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.override = canceledOverride
                     newOverrideRunStored.isUploadedToNS = false
                     newOverrideRunStored.isUploadedToNS = false
-
                     canceledOverride.enabled = false
                     canceledOverride.enabled = false
                     canceledOverride.isUploadedToNS = false
                     canceledOverride.isUploadedToNS = false
                 }
                 }
-
                 if self.viewContext.hasChanges {
                 if self.viewContext.hasChanges {
                     try self.viewContext.save()
                     try self.viewContext.save()
                     Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
                     Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
@@ -104,15 +81,9 @@ extension TrioRemoteControl {
                     return false
                     return false
                 }
                 }
             }
             }
-
-            if didPostNotification {
-                await awaitNotification(.didUpdateOverrideConfiguration)
-            }
+            if didPostNotification { await awaitNotification(.didUpdateOverrideConfiguration) }
         } catch {
         } 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 CoreData
 import Foundation
 import Foundation
+import UIKit
 
 
 extension TrioRemoteControl {
 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
             return
         }
         }
 
 
         let durationInMinutes = Int(durationValue)
         let durationInMinutes = Int(durationValue)
-        let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
+        let payloadDate = Date(timeIntervalSince1970: payload.timestamp)
 
 
         let tempTarget = TempTarget(
         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
             halfBasalTarget: settings.preferences.halfBasalExerciseTarget
         )
         )
 
 
         try await tempTargetsStorage.storeTempTarget(tempTarget: tempTarget)
         try await tempTargetsStorage.storeTempTarget(tempTarget: tempTarget)
         tempTargetsStorage.saveTempTargetsToStorage([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.")
         debug(.remoteControl, "Cancelling temp target.")
-
         await disableAllActiveTempTargets()
         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 {
     @MainActor func disableAllActiveTempTargets() async {
         do {
         do {
             let ids = try await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
             let ids = try await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
-
             let didPostNotification = try await viewContext.perform { () -> Bool in
             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 {
                 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
                     return false
                 }
                 }
-
                 for canceledTempTarget in results where canceledTempTarget.enabled {
                 for canceledTempTarget in results where canceledTempTarget.enabled {
                     let newTempTargetRunStored = TempTargetRunStored(context: self.viewContext)
                     let newTempTargetRunStored = TempTargetRunStored(context: self.viewContext)
                     newTempTargetRunStored.id = UUID()
                     newTempTargetRunStored.id = UUID()
@@ -71,31 +58,21 @@ extension TrioRemoteControl {
                     newTempTargetRunStored.target = canceledTempTarget.target ?? 0
                     newTempTargetRunStored.target = canceledTempTarget.target ?? 0
                     newTempTargetRunStored.tempTarget = canceledTempTarget
                     newTempTargetRunStored.tempTarget = canceledTempTarget
                     newTempTargetRunStored.isUploadedToNS = false
                     newTempTargetRunStored.isUploadedToNS = false
-
                     canceledTempTarget.enabled = false
                     canceledTempTarget.enabled = false
                     canceledTempTarget.isUploadedToNS = false
                     canceledTempTarget.isUploadedToNS = false
                 }
                 }
-
                 if self.viewContext.hasChanges {
                 if self.viewContext.hasChanges {
                     try self.viewContext.save()
                     try self.viewContext.save()
                     Foundation.NotificationCenter.default.post(name: .willUpdateTempTargetConfiguration, object: nil)
                     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))])
                     self.tempTargetsStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date().addingTimeInterval(-1))])
                     return true
                     return true
                 } else {
                 } else {
                     return false
                     return false
                 }
                 }
             }
             }
-
-            if didPostNotification {
-                await awaitNotification(.didUpdateTempTargetConfiguration)
-            }
+            if didPostNotification { await awaitNotification(.didUpdateTempTargetConfiguration) }
         } catch {
         } 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)")
             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 settings: SettingsManager!
     @Injected() internal var iobService: IOBService!
     @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 pumpHistoryFetchContext: NSManagedObjectContext
     internal let viewContext: NSManagedObjectContext
     internal let viewContext: NSManagedObjectContext
@@ -23,96 +23,72 @@ class TrioRemoteControl: Injectable {
         injectServices(TrioApp.resolver)
         injectServices(TrioApp.resolver)
     }
     }
 
 
-    func handleRemoteNotification(pushMessage: PushMessage) async throws {
+    func handleRemoteNotification(encryptedData: String) async throws {
         let isTrioRemoteControlEnabled = UserDefaults.standard.bool(forKey: "isTrioRemoteControlEnabled")
         let isTrioRemoteControlEnabled = UserDefaults.standard.bool(forKey: "isTrioRemoteControlEnabled")
         guard isTrioRemoteControlEnabled else {
         guard isTrioRemoteControlEnabled else {
             await logError("Remote command received, but remote control is disabled in settings. Ignoring the command.")
             await logError("Remote command received, but remote control is disabled in settings. Ignoring the command.")
             return
             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
             return
-        } else if timeDifference < -timeWindow {
+        }
+
+        let commandPayload: CommandPayload
+        do {
+            commandPayload = try messenger.decrypt(base64EncodedString: encryptedData)
+        } catch {
             await logError(
             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
             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(
             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
             return
-        }
-
-        guard pushMessage.sharedSecret == storedSecret else {
+        } else if timeDifference < -timeWindow {
             await logError(
             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
             return
         }
         }
 
 
-        switch pushMessage.commandType {
+        debug(
+            .remoteControl,
+            "Command successfully decrypted and authenticated. Time difference: \(Int(timeDifference)) seconds."
+        )
+
+        switch commandPayload.commandType {
         case .bolus:
         case .bolus:
-            try await handleBolusCommand(pushMessage)
+            try await handleBolusCommand(commandPayload)
         case .tempTarget:
         case .tempTarget:
-            try await handleTempTargetCommand(pushMessage)
+            try await handleTempTargetCommand(commandPayload)
         case .cancelTempTarget:
         case .cancelTempTarget:
-            await cancelTempTarget(pushMessage)
+            await cancelTempTarget(commandPayload)
         case .meal:
         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:
         case .startOverride:
-            await handleStartOverrideCommand(pushMessage)
+            await handleStartOverrideCommand(commandPayload)
         case .cancelOverride:
         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)
         }
         }
     }
     }
 }
 }