Jelajahi Sumber

Merge branch 'dev' into filtering-treatments

marv-out 6 bulan lalu
induk
melakukan
f07087037f
41 mengubah file dengan 33760 tambahan dan 3738 penghapusan
  1. 27 11
      .github/workflows/build_trio.yml
  2. 1 1
      CGMBLEKit
  3. 2 2
      Config.xcconfig
  4. 1 1
      DanaKit
  5. 1 1
      G7SensorKit
  6. 1 0
      Gemfile
  7. 17 14
      Gemfile.lock
  8. 1 1
      LibreTransmitter
  9. 1 1
      LoopKit
  10. 1 1
      MinimedKit
  11. 1 1
      OmniBLE
  12. 1 1
      OmniKit
  13. 1 1
      RileyLinkKit
  14. 1 1
      TidepoolService
  15. 41 4
      Trio.xcodeproj/project.pbxproj
  16. 64 1
      Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved
  17. 2 0
      Trio/Resources/Info.plist
  18. 3 3
      Trio/Sources/Application/AppDelegate.swift
  19. 32930 3310
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  20. 132 0
      Trio/Sources/Models/CommandPayload.swift
  21. 0 141
      Trio/Sources/Models/PushMessage.swift
  22. 7 0
      Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift
  23. 4 0
      Trio/Sources/Modules/Onboarding/View/OnboardingRootView.swift
  24. 31 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/Nightscout/NightscoutUploadGlucoseStepView.swift
  25. 45 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/Nightscout/NightscoutUploadStepView.swift
  26. 3 3
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/TherapySettings/CarbRatioStepView.swift
  27. 4 5
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/TherapySettings/InsulinSensitivityStepView.swift
  28. 1 1
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/WelcomeStepView.swift
  29. 2 0
      Trio/Sources/Modules/Onboarding/View/OnboardingView+Util.swift
  30. 15 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileChart.swift
  31. 79 0
      Trio/Sources/Services/RemoteControl/APNSJWTClaims.swift
  32. 110 0
      Trio/Sources/Services/RemoteControl/RemoteNotificationResponseManager.swift
  33. 38 0
      Trio/Sources/Services/RemoteControl/SecureMessenger.swift
  34. 37 24
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Bolus.swift
  35. 25 3
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Helpers.swift
  36. 26 40
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Meal.swift
  37. 28 57
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Override.swift
  38. 22 45
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+TempTarget.swift
  39. 38 62
      Trio/Sources/Services/RemoteControl/TrioRemoteControl.swift
  40. 1 1
      dexcom-share-client-swift
  41. 15 1
      scripts/swiftformat.sh

+ 27 - 11
.github/workflows/build_trio.yml

@@ -2,14 +2,10 @@ name: 4. Build Trio
 run-name: Build Trio (${{ github.ref_name }})
 on:
   workflow_dispatch:
-
-  ## Remove the "#" sign from the beginning of the line below to get automated builds on push (code changes in your repository)
-  #push:
-
   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
+    # Check for updates every Sunday
+    #   Later logic builds if there are updates or if it is the 2nd Sunday of the month
+    - cron: "43 6 * * 0" # Sunday at UTC 06:43
 
 env:
   UPSTREAM_REPO: nightscout/Trio
@@ -19,6 +15,26 @@ env:
   ALIVE_BRANCH_DEV: alive-dev
 
 jobs:
+
+  # Set a logic flag if this is the second instance of this day-of-week in this month
+  day_in_month:
+    runs-on: ubuntu-latest
+    name: Check day in month
+    outputs:
+      IS_SECOND_IN_MONTH: ${{ steps.date-check.outputs.is_second_instance }}
+
+    steps:
+      - id: date-check
+        name: Check if this is the second time this day-of-week happens this month
+        run: |
+          DAY_OF_MONTH=$(date +%-d)
+          WEEK_OF_MONTH=$(( ($(date +%-d) - 1) / 7 + 1 ))
+          if [[ $WEEK_OF_MONTH -eq 2 ]]; then
+            echo "is_second_instance=true" >> "$GITHUB_OUTPUT"
+          else
+            echo "is_second_instance=false" >> "$GITHUB_OUTPUT"
+          fi
+
   # Checks if Distribution certificate is present and valid, optionally nukes and
   # creates new certs if the repository variable ENABLE_NUKE_CERTS == 'true'
   check_certs:
@@ -205,20 +221,20 @@ jobs:
   # Builds Trio
   build:
     name: Build
-    needs: [check_certs, check_alive_and_permissions, check_latest_from_upstream]
+    needs: [check_certs, check_alive_and_permissions, check_latest_from_upstream, day_in_month]
     runs-on: macos-15
     permissions:
       contents: write
     if:
-      | # 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
+      | # builds with manual start; if automatic: once a month or when new commits are found
       github.event_name == 'workflow_dispatch' ||
       (needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-        (vars.SCHEDULED_BUILD != 'false' && github.event.schedule == '43 6 1 * *') ||
+        (vars.SCHEDULED_BUILD != 'false' && needs.day_in_month.outputs.IS_SECOND_IN_MONTH == 'true') ||
         (vars.SCHEDULED_SYNC != 'false' && needs.check_latest_from_upstream.outputs.NEW_COMMITS == 'true' )
       )
     steps:
       - name: Select Xcode version
-        run: "sudo xcode-select --switch /Applications/Xcode_16.3.app/Contents/Developer"
+        run: "sudo xcode-select --switch /Applications/Xcode_16.4.app/Contents/Developer"
       
       - name: Checkout Repo for syncing
         if: |

+ 1 - 1
CGMBLEKit

@@ -1 +1 @@
-Subproject commit cd8f6faec67b30231987b79daf0117dfcbb54741
+Subproject commit 1cc9e9e7627cf8fb76ccdb015dd6991196038e31

+ 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
 
 // The developers set the version numbers, please leave them alone
-APP_VERSION = 0.5.1
-APP_DEV_VERSION = 0.5.1.22
+APP_VERSION = 0.6.0
+APP_DEV_VERSION = 0.6.0.5
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 

+ 1 - 1
DanaKit

@@ -1 +1 @@
-Subproject commit 33a8d4705fc82b371daf4bd5977ed2cfaf420204
+Subproject commit 084de69f69b1b17c92b595b4d5afeaed5b5d1e55

+ 1 - 1
G7SensorKit

@@ -1 +1 @@
-Subproject commit 7a9341c77c89b3493254da25912e3dc558e1ae26
+Subproject commit 9fa27889f0b216cbe0a23844e888de6698793b63

+ 1 - 0
Gemfile

@@ -4,3 +4,4 @@ source "https://rubygems.org"
 
 # This branch uses fastlane 2.228.0 plus pr 29596
 gem "fastlane",  git: "https://github.com/loopandlearn/fastlane.git", ref: "a670d4b092b274d58ebb5497126e47fc6a84f533"
+gem "rexml", ">=3.4.2"

+ 17 - 14
Gemfile.lock

@@ -58,25 +58,27 @@ GEM
     artifactory (3.0.17)
     atomos (0.1.3)
     aws-eventstream (1.4.0)
-    aws-partitions (1.1116.0)
-    aws-sdk-core (3.225.2)
+    aws-partitions (1.1163.0)
+    aws-sdk-core (3.232.0)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.992.0)
       aws-sigv4 (~> 1.9)
       base64
+      bigdecimal
       jmespath (~> 1, >= 1.6.1)
       logger
-    aws-sdk-kms (1.105.0)
-      aws-sdk-core (~> 3, >= 3.225.0)
+    aws-sdk-kms (1.112.0)
+      aws-sdk-core (~> 3, >= 3.231.0)
       aws-sigv4 (~> 1.5)
-    aws-sdk-s3 (1.189.1)
-      aws-sdk-core (~> 3, >= 3.225.0)
+    aws-sdk-s3 (1.199.0)
+      aws-sdk-core (~> 3, >= 3.231.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.5)
     aws-sigv4 (1.12.1)
       aws-eventstream (~> 1, >= 1.0.2)
     babosa (1.0.4)
     base64 (0.3.0)
+    bigdecimal (3.2.3)
     claide (1.1.0)
     colored (1.2)
     colored2 (3.1.2)
@@ -105,10 +107,10 @@ GEM
       faraday (>= 0.8.0)
       http-cookie (~> 1.0.0)
     faraday-em_http (1.0.0)
-    faraday-em_synchrony (1.0.0)
+    faraday-em_synchrony (1.0.1)
     faraday-excon (1.1.0)
     faraday-httpclient (1.0.1)
-    faraday-multipart (1.1.0)
+    faraday-multipart (1.1.1)
       multipart-post (~> 2.0)
     faraday-net_http (1.0.2)
     faraday-net_http_persistent (1.2.0)
@@ -163,13 +165,13 @@ GEM
     httpclient (2.9.0)
       mutex_m
     jmespath (1.6.2)
-    json (2.12.2)
-    jwt (2.10.1)
+    json (2.15.0)
+    jwt (2.10.2)
       base64
     logger (1.7.0)
     mini_magick (4.13.2)
     mini_mime (1.1.5)
-    multi_json (1.15.0)
+    multi_json (1.17.0)
     multipart-post (2.4.1)
     mutex_m (0.3.0)
     nanaimo (0.4.0)
@@ -185,15 +187,15 @@ GEM
       trailblazer-option (>= 0.1.1, < 0.2.0)
       uber (< 0.2.0)
     retriable (3.1.2)
-    rexml (3.4.1)
+    rexml (3.4.4)
     rouge (3.28.0)
     ruby2_keywords (0.0.5)
     rubyzip (2.4.1)
     security (0.1.5)
-    signet (0.20.0)
+    signet (0.21.0)
       addressable (~> 2.8)
       faraday (>= 0.17.5, < 3.a)
-      jwt (>= 1.5, < 3.0)
+      jwt (>= 1.5, < 4.0)
       multi_json (~> 1.10)
     simctl (1.6.10)
       CFPropertyList
@@ -233,6 +235,7 @@ PLATFORMS
 
 DEPENDENCIES
   fastlane!
+  rexml (>= 3.4.2)
 
 BUNDLED WITH
    2.6.2

+ 1 - 1
LibreTransmitter

@@ -1 +1 @@
-Subproject commit a80ffb4bbc1cc72778cbf4eb69e90b4ff63dd5bf
+Subproject commit 1950f1fec2a0e9f256c1be6e5bafd06ff79d3144

+ 1 - 1
LoopKit

@@ -1 +1 @@
-Subproject commit edb69560cb921a8848ea0a450c89bd26cbe54046
+Subproject commit ddee20aca806f7635b8421617a675ddbd9c6d924

+ 1 - 1
MinimedKit

@@ -1 +1 @@
-Subproject commit ecd3588bdab3844617e17601ba2da1c28e786e77
+Subproject commit a1888623f398994e07ad970a0164be1117e9bec1

+ 1 - 1
OmniBLE

@@ -1 +1 @@
-Subproject commit 97fe52f1a43edad69a80fccce5fddb10cc813b3d
+Subproject commit e4378ba744a46c5f06f9507eabceb4072c058992

+ 1 - 1
OmniKit

@@ -1 +1 @@
-Subproject commit 12058d3d0394cd4269468513d838e570faf5853b
+Subproject commit 1be14fcc27f22258cf8daa0355ac70c89737c0cc

+ 1 - 1
RileyLinkKit

@@ -1 +1 @@
-Subproject commit eddfd4f00bbf0d78035dc31e6f7715e72252c566
+Subproject commit b280e8b9b7e75674b763f3ebf96d8b21dddcf80a

+ 1 - 1
TidepoolService

@@ -1 +1 @@
-Subproject commit b28625628e181b96f0db7ec3739d920a3c92465b
+Subproject commit 59b0cd9384d180c7cccaf2cd2416fa2592a0ce45

+ 41 - 4
Trio.xcodeproj/project.pbxproj

@@ -446,7 +446,9 @@
 		BDFF7A8B2D25F97D0016C40C /* Unit Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF7A8A2D25F97D0016C40C /* Unit Tests.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		C21FE1E72DA59C6B007D550B /* GlucoseDailyDistributionChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21FE1E62DA59C6B007D550B /* GlucoseDailyDistributionChart.swift */; };
+		C263D59F2E4267F400CBF08C /* NightscoutUploadGlucoseStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C263D59E2E4267F400CBF08C /* NightscoutUploadGlucoseStepView.swift */; };
 		C28DD7262DBA9A9E00EC02DD /* GlucosePercentileDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28DD7252DBA9A9E00EC02DD /* GlucosePercentileDetailView.swift */; };
+		C29835B02E2AA3F30068C5BB /* NightscoutUploadStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29835AF2E2AA3F30068C5BB /* NightscoutUploadStepView.swift */; };
 		C29E268A2DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29E26892DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift */; };
 		C2A0A42F2CE03131003B98E8 /* ConstantValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */; };
 		C2A6D1E42DB1581D0036DB66 /* GlucoseStatsSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A6D1E32DB1581D0036DB66 /* GlucoseStatsSetup.swift */; };
@@ -550,6 +552,8 @@
 		DD1745502C55CA5500211FAC /* UnitsLimitsSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17454F2C55CA5500211FAC /* UnitsLimitsSettingsProvider.swift */; };
 		DD1745522C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745512C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift */; };
 		DD1745552C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */; };
+		DD17A0292E3FE0BD008E1BF0 /* SwiftJWT in Frameworks */ = {isa = PBXBuildFile; productRef = DD17A0282E3FE0BD008E1BF0 /* SwiftJWT */; };
+		DD17A0322E3FEA1F008E1BF0 /* RemoteNotificationResponseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17A0312E3FEA1F008E1BF0 /* RemoteNotificationResponseManager.swift */; };
 		DD1DB7CC2BECCA1F0048B367 /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */; };
 		DD1E53592D273F26008F32A4 /* LoopStatusHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1E53582D273F20008F32A4 /* LoopStatusHelpView.swift */; };
 		DD21FCB52C6952AD00AF2C25 /* DecimalPickerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */; };
@@ -573,6 +577,7 @@
 		DD3F1F8B2D9E08B600DCE7B3 /* NightscoutLoginStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */; };
 		DD3F1F8D2D9E0E0600DCE7B3 /* NightscoutSetupStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutSetupStepView.swift */; };
 		DD3F1F902D9E153F00DCE7B3 /* NightscoutImportStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */; };
+		DD485F182E466F1800CE8CBF /* SecureMessenger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD485F172E466F1800CE8CBF /* SecureMessenger.swift */; };
 		DD498F2B2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2C2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
@@ -600,12 +605,13 @@
 		DD73FA0F2D74F58E00D19D1E /* BackgroundTask+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */; };
 		DD8262CB2D289297009F6F62 /* BolusConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */; };
 		DD82D4B82DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */; };
+		DD868FD82E381A54005D3308 /* APNSJWTClaims.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */; };
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
 		DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */; };
 		DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */; };
 		DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98ACBF2D71013200C0778F /* StatChartUtils.swift */; };
 		DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */; };
-		DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */; };
+		DD9ECB6A2CA99F6C00AA7C45 /* CommandPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB692CA99F6C00AA7C45 /* CommandPayload.swift */; };
 		DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */; };
 		DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */; };
 		DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */; };
@@ -1267,7 +1273,9 @@
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* TreatmentsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsProvider.swift; sourceTree = "<group>"; };
 		C21FE1E62DA59C6B007D550B /* GlucoseDailyDistributionChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDailyDistributionChart.swift; sourceTree = "<group>"; };
+		C263D59E2E4267F400CBF08C /* NightscoutUploadGlucoseStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUploadGlucoseStepView.swift; sourceTree = "<group>"; };
 		C28DD7252DBA9A9E00EC02DD /* GlucosePercentileDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucosePercentileDetailView.swift; sourceTree = "<group>"; };
+		C29835AF2E2AA3F30068C5BB /* NightscoutUploadStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUploadStepView.swift; sourceTree = "<group>"; };
 		C29E26892DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDailyPercentileChart.swift; sourceTree = "<group>"; };
 		C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantValues.swift; sourceTree = "<group>"; };
 		C2A6D1E32DB1581D0036DB66 /* GlucoseStatsSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStatsSetup.swift; sourceTree = "<group>"; };
@@ -1375,6 +1383,7 @@
 		DD17454F2C55CA5500211FAC /* UnitsLimitsSettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsProvider.swift; sourceTree = "<group>"; };
 		DD1745512C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsStateModel.swift; sourceTree = "<group>"; };
 		DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsRootView.swift; sourceTree = "<group>"; };
+		DD17A0312E3FEA1F008E1BF0 /* RemoteNotificationResponseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteNotificationResponseManager.swift; sourceTree = "<group>"; };
 		DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = "<group>"; };
 		DD1E53582D273F20008F32A4 /* LoopStatusHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatusHelpView.swift; sourceTree = "<group>"; };
 		DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalPickerSettings.swift; sourceTree = "<group>"; };
@@ -1398,6 +1407,7 @@
 		DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutLoginStepView.swift; sourceTree = "<group>"; };
 		DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutSetupStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSetupStepView.swift; sourceTree = "<group>"; };
 		DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutImportStepView.swift; sourceTree = "<group>"; };
+		DD485F172E466F1800CE8CBF /* SecureMessenger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureMessenger.swift; sourceTree = "<group>"; };
 		DD4A00202DAEEEC400AB7387 /* OnboardingView+AlgorithmUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AlgorithmUtil.swift"; sourceTree = "<group>"; };
 		DD4A00232DAEF5DC00AB7387 /* AlgorithmSettingsSubstepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmSettingsSubstepView.swift; sourceTree = "<group>"; };
 		DD4AFFF02DADB59100AB7387 /* AlgorithmSettingsContentsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmSettingsContentsStepView.swift; sourceTree = "<group>"; };
@@ -1422,12 +1432,13 @@
 		DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackgroundTask+Helper.swift"; sourceTree = "<group>"; };
 		DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusConfirmationView.swift; sourceTree = "<group>"; };
 		DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyPersistentFlags.swift; sourceTree = "<group>"; };
+		DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSJWTClaims.swift; sourceTree = "<group>"; };
 		DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
 		DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseColorScheme.swift; sourceTree = "<group>"; };
 		DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicGlucoseColor.swift; sourceTree = "<group>"; };
 		DD98ACBF2D71013200C0778F /* StatChartUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatChartUtils.swift; sourceTree = "<group>"; };
 		DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioRemoteControl.swift; sourceTree = "<group>"; };
-		DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessage.swift; sourceTree = "<group>"; };
+		DD9ECB692CA99F6C00AA7C45 /* CommandPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPayload.swift; sourceTree = "<group>"; };
 		DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigStateModel.swift; sourceTree = "<group>"; };
 		DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigProvider.swift; sourceTree = "<group>"; };
 		DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigDataFlow.swift; sourceTree = "<group>"; };
@@ -1583,6 +1594,7 @@
 				3B4BA7862D8DBD690069D5B8 /* RileyLinkKitUI.framework in Frameworks */,
 				CEB434FD28B90B7C00B70274 /* SwiftCharts in Frameworks */,
 				CE95BF5F2BA7715800DC3DE3 /* MockKit.framework in Frameworks */,
+				DD17A0292E3FE0BD008E1BF0 /* SwiftJWT in Frameworks */,
 				3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */,
 				38DF1789276FC8C400B3528F /* SwiftMessages in Frameworks */,
 				3B4BA7802D8DBD690069D5B8 /* OmniKitUI.framework in Frameworks */,
@@ -2395,7 +2407,7 @@
 				BDC2EA462C3045AD00E5BBD0 /* Override.swift */,
 				DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */,
 				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
-				DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */,
+				DD9ECB692CA99F6C00AA7C45 /* CommandPayload.swift */,
 				DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */,
 			);
 			path = Models;
@@ -3347,7 +3359,9 @@
 		DD3F1F8E2D9E151200DCE7B3 /* Nightscout */ = {
 			isa = PBXGroup;
 			children = (
+				C263D59E2E4267F400CBF08C /* NightscoutUploadGlucoseStepView.swift */,
 				DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */,
+				C29835AF2E2AA3F30068C5BB /* NightscoutUploadStepView.swift */,
 				DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutSetupStepView.swift */,
 				DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */,
 			);
@@ -3405,6 +3419,9 @@
 		DD9ECB662CA99EFE00AA7C45 /* RemoteControl */ = {
 			isa = PBXGroup;
 			children = (
+				DD485F172E466F1800CE8CBF /* SecureMessenger.swift */,
+				DD17A0312E3FEA1F008E1BF0 /* RemoteNotificationResponseManager.swift */,
+				DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */,
 				DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */,
 				DD32CF9F2CC824D3003686D6 /* TrioRemoteControl+APNS.swift */,
 				DD32CF9D2CC824C2003686D6 /* TrioRemoteControl+Override.swift */,
@@ -3761,6 +3778,7 @@
 				3BD9687B2D8DDD4600899469 /* SlideButton */,
 				3BD9687E2D8DDD8800899469 /* CryptoSwift */,
 				3B47C60F2DA0A28F00B0E5EF /* FirebaseCrashlytics */,
+				DD17A0282E3FE0BD008E1BF0 /* SwiftJWT */,
 			);
 			productName = Trio;
 			productReference = 388E595825AD948C0019842D /* Trio.app */;
@@ -3937,6 +3955,7 @@
 				3BD9687A2D8DDD4600899469 /* XCRemoteSwiftPackageReference "SlideButton" */,
 				3BD9687D2D8DDD8800899469 /* XCRemoteSwiftPackageReference "CryptoSwift" */,
 				3B47C60E2DA0A28F00B0E5EF /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
+				DD868FD92E381E1C005D3308 /* XCRemoteSwiftPackageReference "Swift-JWT" */,
 			);
 			productRefGroup = 388E595925AD948C0019842D /* Products */;
 			projectDirPath = "";
@@ -4095,7 +4114,7 @@
 				3811DEEB25CA063400A708ED /* PersistedProperty.swift in Sources */,
 				38E44537274E411700EC9A94 /* Disk+Helpers.swift in Sources */,
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
-				DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */,
+				DD9ECB6A2CA99F6C00AA7C45 /* CommandPayload.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
 				5825A1BE2C97335C0046467E /* EditTempTargetForm.swift in Sources */,
 				BD47FDDD2D8B65B10043966B /* GlucoseTargetStepView.swift in Sources */,
@@ -4197,11 +4216,13 @@
 				C2A6D1E42DB1581D0036DB66 /* GlucoseStatsSetup.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
+				DD868FD82E381A54005D3308 /* APNSJWTClaims.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
 				BD249D862D42FBEC00412DEB /* GlucoseMetricsView.swift in Sources */,
 				58645BA32CA2D325008AFCE7 /* BatterySetup.swift in Sources */,
 				DD82D4B82DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift in Sources */,
 				388E5A5C25B6F0770019842D /* JSON.swift in Sources */,
+				C263D59F2E4267F400CBF08C /* NightscoutUploadGlucoseStepView.swift in Sources */,
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
@@ -4253,6 +4274,7 @@
 				DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */,
 				CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */,
 				38569347270B5DFB0002C50D /* CGMType.swift in Sources */,
+				DD485F182E466F1800CE8CBF /* SecureMessenger.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
 				384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */,
 				CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */,
@@ -4498,6 +4520,7 @@
 				CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */,
 				110AEDED2C51A0AE00615CC9 /* ShortcutsConfigProvider.swift in Sources */,
 				38E4453D274E411700EC9A94 /* Disk+Errors.swift in Sources */,
+				C29835B02E2AA3F30068C5BB /* NightscoutUploadStepView.swift in Sources */,
 				58D08B3A2C8DFECD00AA37D3 /* TempTargets.swift in Sources */,
 				38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */,
 				CE7CA3542A064973004BE681 /* TempPresetsIntentRequest.swift in Sources */,
@@ -4579,6 +4602,7 @@
 				38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */,
 				DDD163122C4C689900CD525A /* AdjustmentsStateModel.swift in Sources */,
 				BD47FDD72D8B64D20043966B /* CarbRatioStepView.swift in Sources */,
+				DD17A0322E3FEA1F008E1BF0 /* RemoteNotificationResponseManager.swift in Sources */,
 				3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */,
 				3BA8D1B32DDB87150006191F /* DecimalExtensions.swift in Sources */,
 				DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */,
@@ -5446,6 +5470,14 @@
 				kind = branch;
 			};
 		};
+		DD868FD92E381E1C005D3308 /* XCRemoteSwiftPackageReference "Swift-JWT" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "http://github.com/Kitura/Swift-JWT.git";
+			requirement = {
+				kind = exactVersion;
+				version = 4.0.1;
+			};
+		};
 /* End XCRemoteSwiftPackageReference section */
 
 /* Begin XCSwiftPackageProductDependency section */
@@ -5494,6 +5526,11 @@
 			package = CEB434FB28B90B7C00B70274 /* XCRemoteSwiftPackageReference "SwiftCharts" */;
 			productName = SwiftCharts;
 		};
+		DD17A0282E3FE0BD008E1BF0 /* SwiftJWT */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = DD868FD92E381E1C005D3308 /* XCRemoteSwiftPackageReference "Swift-JWT" */;
+			productName = SwiftJWT;
+		};
 /* End XCSwiftPackageProductDependency section */
 
 /* Begin XCVersionGroup section */

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

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

+ 2 - 0
Trio/Resources/Info.plist

@@ -107,6 +107,8 @@
 		<string>remote-notification</string>
 		<string>audio</string>
 	</array>
+    <key>UIDesignRequiresCompatibility</key>
+    <true/>
 	<key>UIFileSharingEnabled</key>
 	<true/>
 	<key>UILaunchScreen</key>

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

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

File diff ditekan karena terlalu besar
+ 32930 - 3310
Trio/Sources/Localizations/Main/Localizable.xcstrings


+ 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
-    }
-}

+ 7 - 0
Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift

@@ -100,6 +100,8 @@ extension Onboarding {
         var isConnectedToNS: Bool = false
         var nightscoutImportError: NightscoutImportError?
         var nightscoutImportStatus: ImportStatus = .none
+        var isUploadEnabled: Bool = true
+        var uploadGlucose: Bool = true
 
         // MARK: - Units and Pump Omboarding Option
 
@@ -700,6 +702,11 @@ extension Onboarding {
             var settingsCopy = settingsManager.settings
             settingsCopy.units = units
 
+            if nightscoutSetupOption == .setupNightscout {
+                settingsCopy.isUploadEnabled = isUploadEnabled
+                settingsCopy.uploadGlucose = uploadGlucose
+            }
+
             // ensure existing values cannot exceed new guardrails
             if !isFreshTrioInstall {
                 let providedSettings = settingsProvider.settings

+ 4 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingRootView.swift

@@ -333,6 +333,10 @@ struct OnboardingStepContent: View {
                                     NightscoutSetupStepView(state: state)
                                 case .connectToNightscout:
                                     NightscoutLoginStepView(state: state)
+                                case .uploadToNightscout:
+                                    NightscoutUploadStepView(state: state)
+                                case .uploadGlucoseToNightscout:
+                                    NightscoutUploadGlucoseStepView(state: state)
                                 case .importFromNightscout:
                                     NightscoutImportStepView(state: state)
                                 }

+ 31 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/Nightscout/NightscoutUploadGlucoseStepView.swift

@@ -0,0 +1,31 @@
+import SwiftUI
+
+struct NightscoutUploadGlucoseStepView: View {
+    @Bindable var state: Onboarding.StateModel
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 20) {
+            Text(
+                "Please choose if you want to upload CGM readings from Trio to Nightscout."
+            )
+            .font(.headline)
+            .padding(.horizontal)
+            .multilineTextAlignment(.leading)
+
+            HStack {
+                Toggle(isOn: $state.uploadGlucose) {
+                    Text("Upload Glucose")
+                }.tint(Color.accentColor)
+            }
+            .padding()
+            .background(Color.chart.opacity(0.65))
+            .cornerRadius(10)
+
+            Text("Enabling this setting allows CGM readings from Trio to be used in Nightscout.")
+                .padding(.horizontal)
+                .font(.footnote)
+                .foregroundStyle(Color.secondary)
+                .multilineTextAlignment(.leading)
+        }
+    }
+}

+ 45 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/Nightscout/NightscoutUploadStepView.swift

@@ -0,0 +1,45 @@
+import SwiftUI
+
+struct NightscoutUploadStepView: View {
+    @Bindable var state: Onboarding.StateModel
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 20) {
+            Text(
+                "Please choose if you want to upload treatment data to Nightscout."
+            )
+            .font(.headline)
+            .padding(.horizontal)
+            .multilineTextAlignment(.leading)
+
+            HStack {
+                Toggle(isOn: $state.isUploadEnabled) {
+                    Text("Allow Uploading to Nightscout")
+                }.tint(Color.accentColor)
+            }
+            .padding()
+            .background(Color.chart.opacity(0.65))
+            .cornerRadius(10)
+
+            Text(
+                "The Upload Treatments toggle enables uploading of the following data sets to your connected Nightscout URL:"
+            )
+            .padding(.horizontal)
+            .font(.footnote)
+            .foregroundStyle(Color.secondary)
+            .multilineTextAlignment(.leading)
+
+            VStack(alignment: .leading, spacing: 5) {
+                Text("• Carbs")
+                Text("• Temp Targets")
+                Text("• Device Status")
+                Text("• Preferences")
+                Text("• Settings")
+            }
+            .padding(.horizontal)
+            .font(.footnote)
+            .foregroundStyle(Color.secondary)
+            .multilineTextAlignment(.leading)
+        }
+    }
+}

+ 3 - 3
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/TherapySettings/CarbRatioStepView.swift

@@ -69,7 +69,7 @@ struct CarbRatioStepView: View {
                             .padding(.horizontal)
 
                         VStack(alignment: .leading, spacing: 8) {
-                            Text("For 45g of carbs, you would need:")
+                            Text("For 45 g of carbs, you would need:")
                                 .font(.subheadline)
                                 .padding(.horizontal)
 
@@ -79,7 +79,7 @@ struct CarbRatioStepView: View {
                                         .carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber
                                 )
                             Text(
-                                "45 \(String(localized: "g", comment: "Gram abbreviation")) / \(formatter.string(from: state.carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber) ?? "--")  = \(String(format: "%.1f", insulinNeeded))" +
+                                "45 \(String(localized: "g", comment: "Gram abbreviation")) / \(formatter.string(from: state.carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber) ?? "--") \(String(localized: "g/U")) = \(String(format: "%.1f", insulinNeeded))" +
                                     " " + String(localized: "U", comment: "Insulin unit abbreviation")
                             )
                             .font(.system(.body, design: .monospaced))
@@ -100,7 +100,7 @@ struct CarbRatioStepView: View {
                             .padding(.horizontal)
 
                         VStack(alignment: .leading, spacing: 4) {
-                            Text("• A ratio of 10 g/U means 1 unit of insulin covers 10g of carbs")
+                            Text("• A ratio of 10 g/U means 1 unit of insulin covers 10 g of carbs")
                             Text("• A lower number means you need more insulin for the same amount of carbs")
                             Text("• A higher number means you need less insulin for the same amount of carbs")
                             Text("• Different times of day may require different ratios")

+ 4 - 5
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/TherapySettings/InsulinSensitivityStepView.swift

@@ -106,13 +106,12 @@ struct InsulinSensitivityStepView: View {
                             .padding(.horizontal)
 
                         VStack(alignment: .leading, spacing: 4) {
-                            let isfValue = "\(state.units == .mgdL ? Decimal(50) : 50.asMmolL)" +
-                                "\(state.units.rawValue)"
+                            let isfValue = "\(state.units == .mgdL ? Decimal(50) : 50.asMmolL)"
                             Text(
-                                "• An ISF of \(isfValue) means 1 U lowers your glucose by \(isfValue)"
+                                "• An ISF of \(isfValue) \(state.units.rawValue)/U means 1 U lowers your glucose by \(isfValue) \(state.units.rawValue)"
                             )
-                            Text("• A lower number means you're more sensitive to insulin")
-                            Text("• A higher number means you're less sensitive to insulin")
+                            Text("• A lower number means you're less sensitive (more resistant) to insulin")
+                            Text("• A higher number means you're more sensitive (less resistant) to insulin")
                         }
                         .font(.caption)
                         .foregroundColor(.secondary)

+ 1 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/WelcomeStepView.swift

@@ -15,7 +15,7 @@ struct WelcomeStepView: View {
                     .multilineTextAlignment(.center)
 
                 Text(
-                    "Welcome to Trio - an automated insulin delivery system for iOS based on the OpenAPS algorithm with adaptations."
+                    "Welcome to Trio  an automated insulin delivery system for iOS based on the OpenAPS algorithm with adaptations."
                 )
                 .multilineTextAlignment(.leading)
                 .foregroundColor(.secondary)

+ 2 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingView+Util.swift

@@ -562,6 +562,8 @@ enum NightscoutImportOption: String, Equatable, CaseIterable, Identifiable {
 enum NightscoutSubstep: Int, CaseIterable, Identifiable {
     case setupSelection
     case connectToNightscout
+    case uploadToNightscout
+    case uploadGlucoseToNightscout
     case importFromNightscout
 
     var id: Int { rawValue }

+ 15 - 0
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileChart.swift

@@ -36,6 +36,20 @@ struct GlucosePercentileChart: View {
         return hourlyStats.first { Int($0.hour) == hour }
     }
 
+    /// The minimum Y-axis value based on the lowest possible cgm reading
+    private var minYValue: Double {
+        40.0.asUnit(units)
+    }
+
+    /// The maximum Y-axis value based on the highest 90th percentile
+    private var maxYValue: Double {
+        let topLimit = 400.0.asUnit(units)
+        let validStats = hourlyStats.filter { $0.median > 0 }
+        guard !validStats.isEmpty else { return topLimit }
+        let maxPercentile90 = validStats.map(\.percentile90).max() ?? topLimit
+        return maxPercentile90.asUnit(units)
+    }
+
     var body: some View {
         VStack(alignment: .leading, spacing: 8) {
             Text("Ambulatory Glucose Profile (AGP)")
@@ -131,6 +145,7 @@ struct GlucosePercentileChart: View {
                     }
                 }
             }
+            .chartYScale(domain: minYValue ... maxYValue)
             .chartYAxis {
                 AxisMarks(position: .trailing) { value in
                     if let glucose = value.as(Double.self) {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 1 - 1
dexcom-share-client-swift

@@ -1 +1 @@
-Subproject commit 21d8657d727f26df76342188e580022bf4884714
+Subproject commit 41cf95dab00f125f7a7602c433aac79fea8fc549

+ 15 - 1
scripts/swiftformat.sh

@@ -97,4 +97,18 @@ trailingClosures \
 --typeattributes same-line \
 --varattributes same-line \
 --wrapcollections before-first \
---exclude Pods,Generated,R.generated.swift,fastlane/swift,Dependencies, LoopKit, LibreTransmitter,G7SensorKit,OmniKit, dexcom-share-client-swift,CGMBLEKit,RileyLinkKit,OmniBLE,MinimedKit,TidepoolService
+--exclude Pods, Generated, \
+  R.generated.swift, \
+  fastlane/swift, \
+  Dependencies,  \
+  LoopKit,  \
+  LibreTransmitter, \
+  G7SensorKit, \
+  OmniKit,  \
+  dexcom-share-client-swift, \
+  CGMBLEKit, \
+  RileyLinkKit, \
+  OmniBLE, \
+  MinimedKit, \
+  TidepoolService \
+  DanaKit