Forráskód Böngészése

Merge branch 'dev' into filtering-treatments

marv-out 6 hónapja
szülő
commit
f07087037f
41 módosított fájl, 33760 hozzáadás és 3738 törlés
  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 }})
 run-name: Build Trio (${{ github.ref_name }})
 on:
 on:
   workflow_dispatch:
   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:
   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:
 env:
   UPSTREAM_REPO: nightscout/Trio
   UPSTREAM_REPO: nightscout/Trio
@@ -19,6 +15,26 @@ env:
   ALIVE_BRANCH_DEV: alive-dev
   ALIVE_BRANCH_DEV: alive-dev
 
 
 jobs:
 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
   # Checks if Distribution certificate is present and valid, optionally nukes and
   # creates new certs if the repository variable ENABLE_NUKE_CERTS == 'true'
   # creates new certs if the repository variable ENABLE_NUKE_CERTS == 'true'
   check_certs:
   check_certs:
@@ -205,20 +221,20 @@ jobs:
   # Builds Trio
   # Builds Trio
   build:
   build:
     name: 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
     runs-on: macos-15
     permissions:
     permissions:
       contents: write
       contents: write
     if:
     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' ||
       github.event_name == 'workflow_dispatch' ||
       (needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
       (needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-        (vars.SCHEDULED_BUILD != 'false' && github.event.schedule == '43 6 1 * *') ||
+        (vars.SCHEDULED_BUILD != 'false' && needs.day_in_month.outputs.IS_SECOND_IN_MONTH == 'true') ||
         (vars.SCHEDULED_SYNC != 'false' && needs.check_latest_from_upstream.outputs.NEW_COMMITS == 'true' )
         (vars.SCHEDULED_SYNC != 'false' && needs.check_latest_from_upstream.outputs.NEW_COMMITS == 'true' )
       )
       )
     steps:
     steps:
       - name: Select Xcode version
       - 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
       - name: Checkout Repo for syncing
         if: |
         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
 TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 
 
 // The developers set the version numbers, please leave them alone
 // The developers set the version numbers, please leave them alone
-APP_VERSION = 0.5.1
-APP_DEV_VERSION = 0.5.1.22
+APP_VERSION = 0.6.0
+APP_DEV_VERSION = 0.6.0.5
 APP_BUILD_NUMBER = 1
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 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
 # This branch uses fastlane 2.228.0 plus pr 29596
 gem "fastlane",  git: "https://github.com/loopandlearn/fastlane.git", ref: "a670d4b092b274d58ebb5497126e47fc6a84f533"
 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)
     artifactory (3.0.17)
     atomos (0.1.3)
     atomos (0.1.3)
     aws-eventstream (1.4.0)
     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-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.992.0)
       aws-partitions (~> 1, >= 1.992.0)
       aws-sigv4 (~> 1.9)
       aws-sigv4 (~> 1.9)
       base64
       base64
+      bigdecimal
       jmespath (~> 1, >= 1.6.1)
       jmespath (~> 1, >= 1.6.1)
       logger
       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-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-sdk-kms (~> 1)
       aws-sigv4 (~> 1.5)
       aws-sigv4 (~> 1.5)
     aws-sigv4 (1.12.1)
     aws-sigv4 (1.12.1)
       aws-eventstream (~> 1, >= 1.0.2)
       aws-eventstream (~> 1, >= 1.0.2)
     babosa (1.0.4)
     babosa (1.0.4)
     base64 (0.3.0)
     base64 (0.3.0)
+    bigdecimal (3.2.3)
     claide (1.1.0)
     claide (1.1.0)
     colored (1.2)
     colored (1.2)
     colored2 (3.1.2)
     colored2 (3.1.2)
@@ -105,10 +107,10 @@ GEM
       faraday (>= 0.8.0)
       faraday (>= 0.8.0)
       http-cookie (~> 1.0.0)
       http-cookie (~> 1.0.0)
     faraday-em_http (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-excon (1.1.0)
     faraday-httpclient (1.0.1)
     faraday-httpclient (1.0.1)
-    faraday-multipart (1.1.0)
+    faraday-multipart (1.1.1)
       multipart-post (~> 2.0)
       multipart-post (~> 2.0)
     faraday-net_http (1.0.2)
     faraday-net_http (1.0.2)
     faraday-net_http_persistent (1.2.0)
     faraday-net_http_persistent (1.2.0)
@@ -163,13 +165,13 @@ GEM
     httpclient (2.9.0)
     httpclient (2.9.0)
       mutex_m
       mutex_m
     jmespath (1.6.2)
     jmespath (1.6.2)
-    json (2.12.2)
-    jwt (2.10.1)
+    json (2.15.0)
+    jwt (2.10.2)
       base64
       base64
     logger (1.7.0)
     logger (1.7.0)
     mini_magick (4.13.2)
     mini_magick (4.13.2)
     mini_mime (1.1.5)
     mini_mime (1.1.5)
-    multi_json (1.15.0)
+    multi_json (1.17.0)
     multipart-post (2.4.1)
     multipart-post (2.4.1)
     mutex_m (0.3.0)
     mutex_m (0.3.0)
     nanaimo (0.4.0)
     nanaimo (0.4.0)
@@ -185,15 +187,15 @@ GEM
       trailblazer-option (>= 0.1.1, < 0.2.0)
       trailblazer-option (>= 0.1.1, < 0.2.0)
       uber (< 0.2.0)
       uber (< 0.2.0)
     retriable (3.1.2)
     retriable (3.1.2)
-    rexml (3.4.1)
+    rexml (3.4.4)
     rouge (3.28.0)
     rouge (3.28.0)
     ruby2_keywords (0.0.5)
     ruby2_keywords (0.0.5)
     rubyzip (2.4.1)
     rubyzip (2.4.1)
     security (0.1.5)
     security (0.1.5)
-    signet (0.20.0)
+    signet (0.21.0)
       addressable (~> 2.8)
       addressable (~> 2.8)
       faraday (>= 0.17.5, < 3.a)
       faraday (>= 0.17.5, < 3.a)
-      jwt (>= 1.5, < 3.0)
+      jwt (>= 1.5, < 4.0)
       multi_json (~> 1.10)
       multi_json (~> 1.10)
     simctl (1.6.10)
     simctl (1.6.10)
       CFPropertyList
       CFPropertyList
@@ -233,6 +235,7 @@ PLATFORMS
 
 
 DEPENDENCIES
 DEPENDENCIES
   fastlane!
   fastlane!
+  rexml (>= 3.4.2)
 
 
 BUNDLED WITH
 BUNDLED WITH
    2.6.2
    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 */; };
 		BDFF7A8B2D25F97D0016C40C /* Unit Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF7A8A2D25F97D0016C40C /* Unit Tests.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		C21FE1E72DA59C6B007D550B /* GlucoseDailyDistributionChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21FE1E62DA59C6B007D550B /* GlucoseDailyDistributionChart.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 */; };
 		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 */; };
 		C29E268A2DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29E26892DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift */; };
 		C2A0A42F2CE03131003B98E8 /* ConstantValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */; };
 		C2A0A42F2CE03131003B98E8 /* ConstantValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */; };
 		C2A6D1E42DB1581D0036DB66 /* GlucoseStatsSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A6D1E32DB1581D0036DB66 /* GlucoseStatsSetup.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 */; };
 		DD1745502C55CA5500211FAC /* UnitsLimitsSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17454F2C55CA5500211FAC /* UnitsLimitsSettingsProvider.swift */; };
 		DD1745522C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745512C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift */; };
 		DD1745522C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745512C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift */; };
 		DD1745552C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */; };
 		DD1745552C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */; };
+		DD17A0292E3FE0BD008E1BF0 /* SwiftJWT in Frameworks */ = {isa = PBXBuildFile; productRef = DD17A0282E3FE0BD008E1BF0 /* SwiftJWT */; };
+		DD17A0322E3FEA1F008E1BF0 /* RemoteNotificationResponseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17A0312E3FEA1F008E1BF0 /* RemoteNotificationResponseManager.swift */; };
 		DD1DB7CC2BECCA1F0048B367 /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */; };
 		DD1DB7CC2BECCA1F0048B367 /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */; };
 		DD1E53592D273F26008F32A4 /* LoopStatusHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1E53582D273F20008F32A4 /* LoopStatusHelpView.swift */; };
 		DD1E53592D273F26008F32A4 /* LoopStatusHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1E53582D273F20008F32A4 /* LoopStatusHelpView.swift */; };
 		DD21FCB52C6952AD00AF2C25 /* DecimalPickerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */; };
 		DD21FCB52C6952AD00AF2C25 /* DecimalPickerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */; };
@@ -573,6 +577,7 @@
 		DD3F1F8B2D9E08B600DCE7B3 /* NightscoutLoginStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */; };
 		DD3F1F8B2D9E08B600DCE7B3 /* NightscoutLoginStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */; };
 		DD3F1F8D2D9E0E0600DCE7B3 /* NightscoutSetupStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutSetupStepView.swift */; };
 		DD3F1F8D2D9E0E0600DCE7B3 /* NightscoutSetupStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutSetupStepView.swift */; };
 		DD3F1F902D9E153F00DCE7B3 /* NightscoutImportStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */; };
 		DD3F1F902D9E153F00DCE7B3 /* NightscoutImportStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */; };
+		DD485F182E466F1800CE8CBF /* SecureMessenger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD485F172E466F1800CE8CBF /* SecureMessenger.swift */; };
 		DD498F2B2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2B2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2C2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2C2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
@@ -600,12 +605,13 @@
 		DD73FA0F2D74F58E00D19D1E /* BackgroundTask+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */; };
 		DD73FA0F2D74F58E00D19D1E /* BackgroundTask+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */; };
 		DD8262CB2D289297009F6F62 /* BolusConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */; };
 		DD8262CB2D289297009F6F62 /* BolusConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */; };
 		DD82D4B82DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */; };
 		DD82D4B82DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */; };
+		DD868FD82E381A54005D3308 /* APNSJWTClaims.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */; };
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
 		DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */; };
 		DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */; };
 		DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */; };
 		DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */; };
 		DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98ACBF2D71013200C0778F /* StatChartUtils.swift */; };
 		DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98ACBF2D71013200C0778F /* StatChartUtils.swift */; };
 		DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */; };
 		DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */; };
-		DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */; };
+		DD9ECB6A2CA99F6C00AA7C45 /* CommandPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB692CA99F6C00AA7C45 /* CommandPayload.swift */; };
 		DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */; };
 		DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */; };
 		DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */; };
 		DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */; };
 		DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */; };
 		DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */; };
@@ -1267,7 +1273,9 @@
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		DD17454F2C55CA5500211FAC /* UnitsLimitsSettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsProvider.swift; sourceTree = "<group>"; };
 		DD1745512C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsStateModel.swift; sourceTree = "<group>"; };
 		DD1745512C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsStateModel.swift; sourceTree = "<group>"; };
 		DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsRootView.swift; sourceTree = "<group>"; };
 		DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsRootView.swift; sourceTree = "<group>"; };
+		DD17A0312E3FEA1F008E1BF0 /* RemoteNotificationResponseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteNotificationResponseManager.swift; sourceTree = "<group>"; };
 		DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = "<group>"; };
 		DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = "<group>"; };
 		DD1E53582D273F20008F32A4 /* LoopStatusHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatusHelpView.swift; sourceTree = "<group>"; };
 		DD1E53582D273F20008F32A4 /* LoopStatusHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatusHelpView.swift; sourceTree = "<group>"; };
 		DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalPickerSettings.swift; sourceTree = "<group>"; };
 		DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalPickerSettings.swift; sourceTree = "<group>"; };
@@ -1398,6 +1407,7 @@
 		DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutLoginStepView.swift; sourceTree = "<group>"; };
 		DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutLoginStepView.swift; sourceTree = "<group>"; };
 		DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutSetupStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSetupStepView.swift; sourceTree = "<group>"; };
 		DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutSetupStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSetupStepView.swift; sourceTree = "<group>"; };
 		DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutImportStepView.swift; sourceTree = "<group>"; };
 		DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutImportStepView.swift; sourceTree = "<group>"; };
+		DD485F172E466F1800CE8CBF /* SecureMessenger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureMessenger.swift; sourceTree = "<group>"; };
 		DD4A00202DAEEEC400AB7387 /* OnboardingView+AlgorithmUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AlgorithmUtil.swift"; sourceTree = "<group>"; };
 		DD4A00202DAEEEC400AB7387 /* OnboardingView+AlgorithmUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AlgorithmUtil.swift"; sourceTree = "<group>"; };
 		DD4A00232DAEF5DC00AB7387 /* AlgorithmSettingsSubstepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmSettingsSubstepView.swift; sourceTree = "<group>"; };
 		DD4A00232DAEF5DC00AB7387 /* AlgorithmSettingsSubstepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmSettingsSubstepView.swift; sourceTree = "<group>"; };
 		DD4AFFF02DADB59100AB7387 /* AlgorithmSettingsContentsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmSettingsContentsStepView.swift; sourceTree = "<group>"; };
 		DD4AFFF02DADB59100AB7387 /* AlgorithmSettingsContentsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmSettingsContentsStepView.swift; sourceTree = "<group>"; };
@@ -1422,12 +1432,13 @@
 		DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackgroundTask+Helper.swift"; sourceTree = "<group>"; };
 		DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackgroundTask+Helper.swift"; sourceTree = "<group>"; };
 		DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusConfirmationView.swift; sourceTree = "<group>"; };
 		DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusConfirmationView.swift; sourceTree = "<group>"; };
 		DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyPersistentFlags.swift; sourceTree = "<group>"; };
 		DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyPersistentFlags.swift; sourceTree = "<group>"; };
+		DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSJWTClaims.swift; sourceTree = "<group>"; };
 		DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
 		DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
 		DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseColorScheme.swift; sourceTree = "<group>"; };
 		DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseColorScheme.swift; sourceTree = "<group>"; };
 		DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicGlucoseColor.swift; sourceTree = "<group>"; };
 		DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicGlucoseColor.swift; sourceTree = "<group>"; };
 		DD98ACBF2D71013200C0778F /* StatChartUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatChartUtils.swift; sourceTree = "<group>"; };
 		DD98ACBF2D71013200C0778F /* StatChartUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatChartUtils.swift; sourceTree = "<group>"; };
 		DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioRemoteControl.swift; sourceTree = "<group>"; };
 		DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioRemoteControl.swift; sourceTree = "<group>"; };
-		DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessage.swift; sourceTree = "<group>"; };
+		DD9ECB692CA99F6C00AA7C45 /* CommandPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPayload.swift; sourceTree = "<group>"; };
 		DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigStateModel.swift; sourceTree = "<group>"; };
 		DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigStateModel.swift; sourceTree = "<group>"; };
 		DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigProvider.swift; sourceTree = "<group>"; };
 		DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigProvider.swift; sourceTree = "<group>"; };
 		DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigDataFlow.swift; sourceTree = "<group>"; };
 		DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigDataFlow.swift; sourceTree = "<group>"; };
@@ -1583,6 +1594,7 @@
 				3B4BA7862D8DBD690069D5B8 /* RileyLinkKitUI.framework in Frameworks */,
 				3B4BA7862D8DBD690069D5B8 /* RileyLinkKitUI.framework in Frameworks */,
 				CEB434FD28B90B7C00B70274 /* SwiftCharts in Frameworks */,
 				CEB434FD28B90B7C00B70274 /* SwiftCharts in Frameworks */,
 				CE95BF5F2BA7715800DC3DE3 /* MockKit.framework in Frameworks */,
 				CE95BF5F2BA7715800DC3DE3 /* MockKit.framework in Frameworks */,
+				DD17A0292E3FE0BD008E1BF0 /* SwiftJWT in Frameworks */,
 				3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */,
 				3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */,
 				38DF1789276FC8C400B3528F /* SwiftMessages in Frameworks */,
 				38DF1789276FC8C400B3528F /* SwiftMessages in Frameworks */,
 				3B4BA7802D8DBD690069D5B8 /* OmniKitUI.framework in Frameworks */,
 				3B4BA7802D8DBD690069D5B8 /* OmniKitUI.framework in Frameworks */,
@@ -2395,7 +2407,7 @@
 				BDC2EA462C3045AD00E5BBD0 /* Override.swift */,
 				BDC2EA462C3045AD00E5BBD0 /* Override.swift */,
 				DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */,
 				DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */,
 				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
 				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
-				DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */,
+				DD9ECB692CA99F6C00AA7C45 /* CommandPayload.swift */,
 				DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */,
 				DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */,
 			);
 			);
 			path = Models;
 			path = Models;
@@ -3347,7 +3359,9 @@
 		DD3F1F8E2D9E151200DCE7B3 /* Nightscout */ = {
 		DD3F1F8E2D9E151200DCE7B3 /* Nightscout */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				C263D59E2E4267F400CBF08C /* NightscoutUploadGlucoseStepView.swift */,
 				DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */,
 				DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */,
+				C29835AF2E2AA3F30068C5BB /* NightscoutUploadStepView.swift */,
 				DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutSetupStepView.swift */,
 				DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutSetupStepView.swift */,
 				DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */,
 				DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */,
 			);
 			);
@@ -3405,6 +3419,9 @@
 		DD9ECB662CA99EFE00AA7C45 /* RemoteControl */ = {
 		DD9ECB662CA99EFE00AA7C45 /* RemoteControl */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				DD485F172E466F1800CE8CBF /* SecureMessenger.swift */,
+				DD17A0312E3FEA1F008E1BF0 /* RemoteNotificationResponseManager.swift */,
+				DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */,
 				DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */,
 				DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */,
 				DD32CF9F2CC824D3003686D6 /* TrioRemoteControl+APNS.swift */,
 				DD32CF9F2CC824D3003686D6 /* TrioRemoteControl+APNS.swift */,
 				DD32CF9D2CC824C2003686D6 /* TrioRemoteControl+Override.swift */,
 				DD32CF9D2CC824C2003686D6 /* TrioRemoteControl+Override.swift */,
@@ -3761,6 +3778,7 @@
 				3BD9687B2D8DDD4600899469 /* SlideButton */,
 				3BD9687B2D8DDD4600899469 /* SlideButton */,
 				3BD9687E2D8DDD8800899469 /* CryptoSwift */,
 				3BD9687E2D8DDD8800899469 /* CryptoSwift */,
 				3B47C60F2DA0A28F00B0E5EF /* FirebaseCrashlytics */,
 				3B47C60F2DA0A28F00B0E5EF /* FirebaseCrashlytics */,
+				DD17A0282E3FE0BD008E1BF0 /* SwiftJWT */,
 			);
 			);
 			productName = Trio;
 			productName = Trio;
 			productReference = 388E595825AD948C0019842D /* Trio.app */;
 			productReference = 388E595825AD948C0019842D /* Trio.app */;
@@ -3937,6 +3955,7 @@
 				3BD9687A2D8DDD4600899469 /* XCRemoteSwiftPackageReference "SlideButton" */,
 				3BD9687A2D8DDD4600899469 /* XCRemoteSwiftPackageReference "SlideButton" */,
 				3BD9687D2D8DDD8800899469 /* XCRemoteSwiftPackageReference "CryptoSwift" */,
 				3BD9687D2D8DDD8800899469 /* XCRemoteSwiftPackageReference "CryptoSwift" */,
 				3B47C60E2DA0A28F00B0E5EF /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
 				3B47C60E2DA0A28F00B0E5EF /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
+				DD868FD92E381E1C005D3308 /* XCRemoteSwiftPackageReference "Swift-JWT" */,
 			);
 			);
 			productRefGroup = 388E595925AD948C0019842D /* Products */;
 			productRefGroup = 388E595925AD948C0019842D /* Products */;
 			projectDirPath = "";
 			projectDirPath = "";
@@ -4095,7 +4114,7 @@
 				3811DEEB25CA063400A708ED /* PersistedProperty.swift in Sources */,
 				3811DEEB25CA063400A708ED /* PersistedProperty.swift in Sources */,
 				38E44537274E411700EC9A94 /* Disk+Helpers.swift in Sources */,
 				38E44537274E411700EC9A94 /* Disk+Helpers.swift in Sources */,
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
-				DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */,
+				DD9ECB6A2CA99F6C00AA7C45 /* CommandPayload.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
 				5825A1BE2C97335C0046467E /* EditTempTargetForm.swift in Sources */,
 				5825A1BE2C97335C0046467E /* EditTempTargetForm.swift in Sources */,
 				BD47FDDD2D8B65B10043966B /* GlucoseTargetStepView.swift in Sources */,
 				BD47FDDD2D8B65B10043966B /* GlucoseTargetStepView.swift in Sources */,
@@ -4197,11 +4216,13 @@
 				C2A6D1E42DB1581D0036DB66 /* GlucoseStatsSetup.swift in Sources */,
 				C2A6D1E42DB1581D0036DB66 /* GlucoseStatsSetup.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
+				DD868FD82E381A54005D3308 /* APNSJWTClaims.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
 				BD249D862D42FBEC00412DEB /* GlucoseMetricsView.swift in Sources */,
 				BD249D862D42FBEC00412DEB /* GlucoseMetricsView.swift in Sources */,
 				58645BA32CA2D325008AFCE7 /* BatterySetup.swift in Sources */,
 				58645BA32CA2D325008AFCE7 /* BatterySetup.swift in Sources */,
 				DD82D4B82DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift in Sources */,
 				DD82D4B82DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift in Sources */,
 				388E5A5C25B6F0770019842D /* JSON.swift in Sources */,
 				388E5A5C25B6F0770019842D /* JSON.swift in Sources */,
+				C263D59F2E4267F400CBF08C /* NightscoutUploadGlucoseStepView.swift in Sources */,
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */,
 				5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
@@ -4253,6 +4274,7 @@
 				DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */,
 				DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */,
 				CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */,
 				CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */,
 				38569347270B5DFB0002C50D /* CGMType.swift in Sources */,
 				38569347270B5DFB0002C50D /* CGMType.swift in Sources */,
+				DD485F182E466F1800CE8CBF /* SecureMessenger.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
 				384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */,
 				384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */,
 				CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */,
 				CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */,
@@ -4498,6 +4520,7 @@
 				CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */,
 				CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */,
 				110AEDED2C51A0AE00615CC9 /* ShortcutsConfigProvider.swift in Sources */,
 				110AEDED2C51A0AE00615CC9 /* ShortcutsConfigProvider.swift in Sources */,
 				38E4453D274E411700EC9A94 /* Disk+Errors.swift in Sources */,
 				38E4453D274E411700EC9A94 /* Disk+Errors.swift in Sources */,
+				C29835B02E2AA3F30068C5BB /* NightscoutUploadStepView.swift in Sources */,
 				58D08B3A2C8DFECD00AA37D3 /* TempTargets.swift in Sources */,
 				58D08B3A2C8DFECD00AA37D3 /* TempTargets.swift in Sources */,
 				38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */,
 				38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */,
 				CE7CA3542A064973004BE681 /* TempPresetsIntentRequest.swift in Sources */,
 				CE7CA3542A064973004BE681 /* TempPresetsIntentRequest.swift in Sources */,
@@ -4579,6 +4602,7 @@
 				38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */,
 				38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */,
 				DDD163122C4C689900CD525A /* AdjustmentsStateModel.swift in Sources */,
 				DDD163122C4C689900CD525A /* AdjustmentsStateModel.swift in Sources */,
 				BD47FDD72D8B64D20043966B /* CarbRatioStepView.swift in Sources */,
 				BD47FDD72D8B64D20043966B /* CarbRatioStepView.swift in Sources */,
+				DD17A0322E3FEA1F008E1BF0 /* RemoteNotificationResponseManager.swift in Sources */,
 				3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */,
 				3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */,
 				3BA8D1B32DDB87150006191F /* DecimalExtensions.swift in Sources */,
 				3BA8D1B32DDB87150006191F /* DecimalExtensions.swift in Sources */,
 				DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */,
 				DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */,
@@ -5446,6 +5470,14 @@
 				kind = branch;
 				kind = branch;
 			};
 			};
 		};
 		};
+		DD868FD92E381E1C005D3308 /* XCRemoteSwiftPackageReference "Swift-JWT" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "http://github.com/Kitura/Swift-JWT.git";
+			requirement = {
+				kind = exactVersion;
+				version = 4.0.1;
+			};
+		};
 /* End XCRemoteSwiftPackageReference section */
 /* End XCRemoteSwiftPackageReference section */
 
 
 /* Begin XCSwiftPackageProductDependency section */
 /* Begin XCSwiftPackageProductDependency section */
@@ -5494,6 +5526,11 @@
 			package = CEB434FB28B90B7C00B70274 /* XCRemoteSwiftPackageReference "SwiftCharts" */;
 			package = CEB434FB28B90B7C00B70274 /* XCRemoteSwiftPackageReference "SwiftCharts" */;
 			productName = SwiftCharts;
 			productName = SwiftCharts;
 		};
 		};
+		DD17A0282E3FE0BD008E1BF0 /* SwiftJWT */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = DD868FD92E381E1C005D3308 /* XCRemoteSwiftPackageReference "Swift-JWT" */;
+			productName = SwiftJWT;
+		};
 /* End XCSwiftPackageProductDependency section */
 /* End XCSwiftPackageProductDependency section */
 
 
 /* Begin XCVersionGroup section */
 /* Begin XCVersionGroup section */

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

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

+ 2 - 0
Trio/Resources/Info.plist

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

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

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

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 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 isConnectedToNS: Bool = false
         var nightscoutImportError: NightscoutImportError?
         var nightscoutImportError: NightscoutImportError?
         var nightscoutImportStatus: ImportStatus = .none
         var nightscoutImportStatus: ImportStatus = .none
+        var isUploadEnabled: Bool = true
+        var uploadGlucose: Bool = true
 
 
         // MARK: - Units and Pump Omboarding Option
         // MARK: - Units and Pump Omboarding Option
 
 
@@ -700,6 +702,11 @@ extension Onboarding {
             var settingsCopy = settingsManager.settings
             var settingsCopy = settingsManager.settings
             settingsCopy.units = units
             settingsCopy.units = units
 
 
+            if nightscoutSetupOption == .setupNightscout {
+                settingsCopy.isUploadEnabled = isUploadEnabled
+                settingsCopy.uploadGlucose = uploadGlucose
+            }
+
             // ensure existing values cannot exceed new guardrails
             // ensure existing values cannot exceed new guardrails
             if !isFreshTrioInstall {
             if !isFreshTrioInstall {
                 let providedSettings = settingsProvider.settings
                 let providedSettings = settingsProvider.settings

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

@@ -333,6 +333,10 @@ struct OnboardingStepContent: View {
                                     NightscoutSetupStepView(state: state)
                                     NightscoutSetupStepView(state: state)
                                 case .connectToNightscout:
                                 case .connectToNightscout:
                                     NightscoutLoginStepView(state: state)
                                     NightscoutLoginStepView(state: state)
+                                case .uploadToNightscout:
+                                    NightscoutUploadStepView(state: state)
+                                case .uploadGlucoseToNightscout:
+                                    NightscoutUploadGlucoseStepView(state: state)
                                 case .importFromNightscout:
                                 case .importFromNightscout:
                                     NightscoutImportStepView(state: state)
                                     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)
                             .padding(.horizontal)
 
 
                         VStack(alignment: .leading, spacing: 8) {
                         VStack(alignment: .leading, spacing: 8) {
-                            Text("For 45g of carbs, you would need:")
+                            Text("For 45 g of carbs, you would need:")
                                 .font(.subheadline)
                                 .font(.subheadline)
                                 .padding(.horizontal)
                                 .padding(.horizontal)
 
 
@@ -79,7 +79,7 @@ struct CarbRatioStepView: View {
                                         .carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber
                                         .carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber
                                 )
                                 )
                             Text(
                             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")
                                     " " + String(localized: "U", comment: "Insulin unit abbreviation")
                             )
                             )
                             .font(.system(.body, design: .monospaced))
                             .font(.system(.body, design: .monospaced))
@@ -100,7 +100,7 @@ struct CarbRatioStepView: View {
                             .padding(.horizontal)
                             .padding(.horizontal)
 
 
                         VStack(alignment: .leading, spacing: 4) {
                         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 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("• A higher number means you need less insulin for the same amount of carbs")
                             Text("• Different times of day may require different ratios")
                             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)
                             .padding(.horizontal)
 
 
                         VStack(alignment: .leading, spacing: 4) {
                         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(
                             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)
                         .font(.caption)
                         .foregroundColor(.secondary)
                         .foregroundColor(.secondary)

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

@@ -15,7 +15,7 @@ struct WelcomeStepView: View {
                     .multilineTextAlignment(.center)
                     .multilineTextAlignment(.center)
 
 
                 Text(
                 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)
                 .multilineTextAlignment(.leading)
                 .foregroundColor(.secondary)
                 .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 {
 enum NightscoutSubstep: Int, CaseIterable, Identifiable {
     case setupSelection
     case setupSelection
     case connectToNightscout
     case connectToNightscout
+    case uploadToNightscout
+    case uploadGlucoseToNightscout
     case importFromNightscout
     case importFromNightscout
 
 
     var id: Int { rawValue }
     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 }
         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 {
     var body: some View {
         VStack(alignment: .leading, spacing: 8) {
         VStack(alignment: .leading, spacing: 8) {
             Text("Ambulatory Glucose Profile (AGP)")
             Text("Ambulatory Glucose Profile (AGP)")
@@ -131,6 +145,7 @@ struct GlucosePercentileChart: View {
                     }
                     }
                 }
                 }
             }
             }
+            .chartYScale(domain: minYValue ... maxYValue)
             .chartYAxis {
             .chartYAxis {
                 AxisMarks(position: .trailing) { value in
                 AxisMarks(position: .trailing) { value in
                     if let glucose = value.as(Double.self) {
                     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 Foundation
+import HealthKit
 
 
 extension TrioRemoteControl {
 extension TrioRemoteControl {
-    internal func handleBolusCommand(_ pushMessage: PushMessage) async throws {
-        guard let bolusAmount = pushMessage.bolusAmount else {
-            await logError("Command rejected: bolus amount is missing or invalid.", pushMessage: pushMessage)
+    internal func handleBolusCommand(_ payload: CommandPayload) async throws {
+        guard let bolusAmount = payload.bolusAmount else {
+            await logError("Command rejected: bolus amount is missing or invalid.", payload: payload)
             return
             return
         }
         }
 
 
@@ -12,7 +13,7 @@ extension TrioRemoteControl {
         if bolusAmount > maxBolus {
         if bolusAmount > maxBolus {
             await logError(
             await logError(
                 "Command rejected: bolus amount (\(bolusAmount) units) exceeds the maximum allowed (\(maxBolus) units).",
                 "Command rejected: bolus amount (\(bolusAmount) units) exceeds the maximum allowed (\(maxBolus) units).",
-                pushMessage: pushMessage
+                payload: payload
             )
             )
             return
             return
         }
         }
@@ -24,18 +25,18 @@ extension TrioRemoteControl {
         if (currentIOB + bolusAmount) > maxIOB {
         if (currentIOB + bolusAmount) > maxIOB {
             await logError(
             await logError(
                 "Command rejected: bolus amount (\(bolusAmount) units) would exceed max IOB (\(maxIOB) units). Current IOB: \(currentIOB) units.",
                 "Command rejected: bolus amount (\(bolusAmount) units) would exceed max IOB (\(maxIOB) units). Current IOB: \(currentIOB) units.",
-                pushMessage: pushMessage
+                payload: payload
             )
             )
             return
             return
         }
         }
 
 
         let totalRecentBolusAmount =
         let totalRecentBolusAmount =
-            try await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: pushMessage.timestamp))
+            try await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: payload.timestamp))
 
 
         if totalRecentBolusAmount >= bolusAmount * 0.2 {
         if totalRecentBolusAmount >= bolusAmount * 0.2 {
             await logError(
             await logError(
                 "Command rejected: boluses totaling more than 20% of the requested amount have been delivered since the command was sent.",
                 "Command rejected: boluses totaling more than 20% of the requested amount have been delivered since the command was sent.",
-                pushMessage: pushMessage
+                payload: payload
             )
             )
             return
             return
         }
         }
@@ -45,17 +46,38 @@ extension TrioRemoteControl {
         guard let apsManager = await TrioApp.resolver.resolve(APSManager.self) else {
         guard let apsManager = await TrioApp.resolver.resolve(APSManager.self) else {
             await logError(
             await logError(
                 "Error: unable to process bolus command because the APS Manager is not available.",
                 "Error: unable to process bolus command because the APS Manager is not available.",
-                pushMessage: pushMessage
+                payload: payload
             )
             )
             return
             return
         }
         }
 
 
-        await apsManager.enactBolus(amount: Double(truncating: bolusAmount as NSNumber), isSMB: false, callback: nil)
+        if let returnInfo = payload.returnNotification {
+            await RemoteNotificationResponseManager.shared.sendResponseNotification(
+                to: returnInfo,
+                commandType: payload.commandType,
+                success: true,
+                message: "Initiating bolus..."
+            )
+        }
 
 
-        debug(
-            .remoteControl,
-            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
-        )
+        await apsManager
+            .enactBolus(amount: Double(truncating: bolusAmount as NSNumber), isSMB: false) { [weak self] success, message in
+                guard let self = self else { return }
+                Task {
+                    if success {
+                        await self.logSuccess(
+                            "Remote command processed successfully. \(payload.humanReadableDescription())",
+                            payload: payload,
+                            customNotificationMessage: "Bolus started"
+                        )
+                    } else {
+                        await self.logError(
+                            message,
+                            payload: payload
+                        )
+                    }
+                }
+            }
     }
     }
 
 
     private func fetchTotalRecentBolusAmount(since date: Date) async throws -> Decimal {
     private func fetchTotalRecentBolusAmount(since date: Date) async throws -> Decimal {
@@ -64,24 +86,15 @@ extension TrioRemoteControl {
             PumpEventStored.EventType.bolus.rawValue,
             PumpEventStored.EventType.bolus.rawValue,
             date as NSDate
             date as NSDate
         )
         )
-
         let results: Any = try await CoreDataStack.shared.fetchEntitiesAsync(
         let results: Any = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self,
-            onContext: pumpHistoryFetchContext,
-            predicate: predicate,
-            key: "timestamp",
-            ascending: true,
-            fetchLimit: nil,
-            propertiesToFetch: ["bolus.amount"]
+            ofType: PumpEventStored.self, onContext: pumpHistoryFetchContext, predicate: predicate, key: "timestamp",
+            ascending: true, fetchLimit: nil, propertiesToFetch: ["bolus.amount"]
         )
         )
-
         guard let bolusDictionaries = results as? [[String: Any]] else {
         guard let bolusDictionaries = results as? [[String: Any]] else {
             await logError("Failed to cast fetched bolus events. Fetched entities type: \(type(of: results))")
             await logError("Failed to cast fetched bolus events. Fetched entities type: \(type(of: results))")
             throw CoreDataError.fetchError(function: #function, file: #file)
             throw CoreDataError.fetchError(function: #function, file: #file)
         }
         }
-
         let totalAmount = bolusDictionaries.compactMap { ($0["bolus.amount"] as? NSNumber)?.decimalValue }.reduce(0, +)
         let totalAmount = bolusDictionaries.compactMap { ($0["bolus.amount"] as? NSNumber)?.decimalValue }.reduce(0, +)
-
         return totalAmount
         return totalAmount
     }
     }
 }
 }

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

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

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

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

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

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

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

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

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

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

+ 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 \
 --typeattributes same-line \
 --varattributes same-line \
 --varattributes same-line \
 --wrapcollections before-first \
 --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