Преглед изворни кода

Inroduce acknowledge logic for treatments

Deniz Cengiz пре 1 година
родитељ
комит
a1d81b716a

+ 20 - 0
Trio Watch App Extension/Helper/Helper+Enums.swift

@@ -0,0 +1,20 @@
+import SwiftUI
+
+enum NavigationDestinations: String {
+    case acknowledgmentPending
+    case carbInput
+    case bolusInput
+    case bolusConfirm
+}
+
+enum MealBolusStep: String {
+    case savingCarbs = "Saving Carbs..."
+    case enactingBolus = "Enacting Bolus..."
+    case completed = ""
+}
+
+enum AcknowledgementStatus: String, CaseIterable {
+    case success
+    case failure
+    case pending
+}

+ 0 - 7
Trio Watch App Extension/Helper/Helper+Navigation.swift

@@ -1,7 +0,0 @@
-import SwiftUI
-
-enum NavigationDestinations: String {
-    case carbInput
-    case bolusInput
-    case bolusConfirm
-}

+ 51 - 0
Trio Watch App Extension/Views/AcknowledgementPendingView.swift

@@ -0,0 +1,51 @@
+import SwiftUI
+
+struct AcknowledgementPendingView: View {
+    @Binding var navigationPath: [NavigationDestinations]
+    let state: WatchState
+
+    var trioBackgroundColor = LinearGradient(
+        gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
+        startPoint: .top,
+        endPoint: .bottom
+    )
+
+    var statusIcon: some View {
+        switch state.acknowledgementStatus {
+        case .pending:
+            return Image(systemName: "progress.indicator").foregroundStyle(Color.secondary)
+        case .success:
+            return Image(systemName: "checkmark.circle").foregroundStyle(Color.green)
+        case .failure:
+            return Image(systemName: "progress.indicator").foregroundStyle(Color.red)
+        }
+    }
+
+    var body: some View {
+        GeometryReader { geometry in
+            VStack {
+                if state.isMealBolusCombo {
+                    ProgressView()
+                    Text(state.mealBolusStep.rawValue)
+                } else if state.showCommsAnimation {
+                    ProgressView()
+                    Text("Processing…")
+                } else if state.showAcknowledgmentBanner {
+                    statusIcon.padding()
+                    Text(state.acknowledgmentMessage)
+                }
+            }
+            .padding()
+            .frame(width: geometry.size.width, height: geometry.size.height)
+        }
+        .navigationBarBackButtonHidden(true)
+        .toolbar(.hidden)
+        .background(trioBackgroundColor)
+        .onChange(of: state.showAcknowledgmentBanner) { _, newValue in
+            if !newValue {
+                // Navigate back to the root when acknowledgment banner disappears
+                navigationPath.removeAll()
+            }
+        }
+    }
+}

+ 3 - 5
Trio Watch App Extension/Views/BolusConfirmationView.swift

@@ -52,7 +52,7 @@ struct BolusConfirmationView: View {
                 }
                 bolusAmount = 0 // reset bolus in state
                 confirmationProgress = 0 // reset auth progress
-                navigationPath.removeLast(navigationPath.count)
+                navigationPath.removeAll()
             }
             .buttonStyle(.bordered)
         }
@@ -83,9 +83,7 @@ struct BolusConfirmationView: View {
                     state.sendBolusRequest(Decimal(bolusAmount))
                     bolusAmount = 0 // reset bolus in state
                     confirmationProgress = 0 // reset auth progress
-                    navigationPath.removeLast(navigationPath.count)
-
-                    // TODO: add a fancy success animation
+                    navigationPath.append(NavigationDestinations.acknowledgmentPending)
                 }
             } else if newValue > 0 {
                 WKInterfaceDevice.current().play(.click)
@@ -102,7 +100,7 @@ struct BolusConfirmationView: View {
         }
         .blur(radius: state.bolusProgress > 0 && state.bolusProgress < 1.0 && !state.isBolusCanceled ? 3 : 0)
         .overlay {
-            if state.bolusProgress > 0 && state.bolusProgress < 1.0 && !state.isBolusCanceled {
+            if state.bolusProgress > 0 && state.bolusProgress < 1.0 && !state.isBolusCanceled && !state.showAcknowledgmentBanner {
                 BolusProgressOverlay(state: state)
                     .transition(.opacity)
             }

+ 3 - 0
Trio Watch App Extension/Views/BolusProgressOverlay.swift

@@ -54,5 +54,8 @@ struct BolusProgressOverlay: View {
                 state.activeBolusAmount = 0 // Reset only when bolus is complete
             }
         }
+        .onDisappear {
+            state.activeBolusAmount = 0 // Double-check to reset when view disappears
+        }
     }
 }

+ 1 - 2
Trio Watch App Extension/Views/CarbsInputView.swift

@@ -100,9 +100,8 @@ struct CarbsInputView: View {
                     state.carbsAmount = Int(min(carbsAmount, effectiveCarbsLimit))
                     navigationPath.append(NavigationDestinations.bolusInput)
                 } else {
-                    // TODO: add a fancy success animation
                     state.sendCarbsRequest(Int(min(carbsAmount, effectiveCarbsLimit)))
-                    navigationPath.removeLast(navigationPath.count)
+                    navigationPath.append(NavigationDestinations.acknowledgmentPending)
                 }
             }
             .buttonStyle(.bordered)

+ 5 - 0
Trio Watch App Extension/Views/TrioMainWatchView.swift

@@ -115,6 +115,11 @@ struct TrioMainWatchView: View {
             }
             .navigationDestination(for: NavigationDestinations.self) { destination in
                 switch destination {
+                case .acknowledgmentPending:
+                    AcknowledgementPendingView(
+                        navigationPath: $navigationPath,
+                        state: state
+                    )
                 case .carbInput:
                     CarbsInputView(
                         navigationPath: $navigationPath,

+ 65 - 0
Trio Watch App Extension/WatchState.swift

@@ -41,6 +41,15 @@ import WatchConnectivity
     var maxIOB: Decimal = 0
     var maxCOB: Decimal = 120
 
+    // acknowlegement handling
+    var showCommsAnimation: Bool = false
+    var showAcknowledgmentBanner: Bool = false
+    var acknowledgementStatus: AcknowledgementStatus = .pending
+    var acknowledgmentMessage: String = ""
+    // Meal bolus-specific properties
+    var mealBolusStep: MealBolusStep = .savingCarbs
+    var isMealBolusCombo: Bool = false
+
     override init() {
         super.init()
         setupSession()
@@ -75,6 +84,9 @@ import WatchConnectivity
         session.sendMessage(message, replyHandler: nil) { error in
             print("Error sending bolus request: \(error.localizedDescription)")
         }
+
+        // Display pending communication animation
+        showCommsAnimation = true
     }
 
     /// Sends a carbohydrate entry request to the paired iPhone
@@ -92,6 +104,9 @@ import WatchConnectivity
         session.sendMessage(message, replyHandler: nil) { error in
             print("Error sending carbs request: \(error.localizedDescription)")
         }
+
+        // Display pending communication animation
+        showCommsAnimation = true
     }
 
     /// Sends a meal and bolus insulin combo request to the paired iPhone
@@ -110,6 +125,10 @@ import WatchConnectivity
         session.sendMessage(message, replyHandler: nil) { error in
             print("Error sending meal bolus combo request: \(error.localizedDescription)")
         }
+
+        // Display pending communication animation
+        showCommsAnimation = true
+        isMealBolusCombo = true
     }
 
     func sendCancelOverrideRequest() {
@@ -173,6 +192,29 @@ import WatchConnectivity
         }
     }
 
+    // MARK: – Handle Acknowledgement Messages FROM Phone
+
+    private func handleAcknowledgment(success: Bool, message: String, isFinal: Bool = true) {
+        if success {
+            print("⌚️ Acknowledgment received: \(message)")
+            showCommsAnimation = false // Hide progress animation
+            acknowledgementStatus = .success
+            acknowledgmentMessage = "\(message)"
+        } else {
+            print("⌚️ Acknowledgment failed: \(message)")
+            showCommsAnimation = false // Hide progress animation
+            acknowledgementStatus = .failure
+            acknowledgmentMessage = "\(message)"
+        }
+
+        if isFinal {
+            showAcknowledgmentBanner = true
+            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+                self.showAcknowledgmentBanner = false
+            }
+        }
+    }
+
     // MARK: - WCSessionDelegate
 
     /// Called when the session has completed activation
@@ -198,6 +240,29 @@ import WatchConnectivity
         DispatchQueue.main.async { [weak self] in
             guard let self = self else { return }
 
+            if let acknowledged = message["acknowledged"] as? Bool,
+               let ackMessage = message["message"] as? String
+            {
+                switch ackMessage {
+                case "Saving carbs...":
+                    self.isMealBolusCombo = true
+                    self.mealBolusStep = .savingCarbs
+                    self.showCommsAnimation = true
+                    self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: false)
+                case "Enacting bolus...":
+                    self.isMealBolusCombo = true
+                    self.mealBolusStep = .enactingBolus
+                    self.showCommsAnimation = true
+                    self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: false)
+                case "Carbs and bolus logged successfully":
+                    self.isMealBolusCombo = false
+                    self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: true)
+                default:
+                    self.isMealBolusCombo = false
+                    self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: true)
+                }
+            }
+
             if let currentGlucose = message["currentGlucose"] as? String {
                 self.currentGlucose = currentGlucose
             }

+ 8 - 4
Trio.xcodeproj/project.pbxproj

@@ -413,7 +413,8 @@
 		DD09D47D2C5986DA003FEA5D /* CalendarEventSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09D47C2C5986DA003FEA5D /* CalendarEventSettingsProvider.swift */; };
 		DD09D47F2C5986E5003FEA5D /* CalendarEventSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09D47E2C5986E5003FEA5D /* CalendarEventSettingsStateModel.swift */; };
 		DD09D4822C5986F6003FEA5D /* CalendarEventSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09D4812C5986F6003FEA5D /* CalendarEventSettingsRootView.swift */; };
-		DD09D5C72D29EB2F000D82C9 /* Helper+Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09D5C62D29EB26000D82C9 /* Helper+Navigation.swift */; };
+		DD09D5C72D29EB2F000D82C9 /* Helper+Enums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09D5C62D29EB26000D82C9 /* Helper+Enums.swift */; };
+		DD09D5C92D29F3D0000D82C9 /* AcknowledgementPendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09D5C82D29F3D0000D82C9 /* AcknowledgementPendingView.swift */; };
 		DD1745132C54169400211FAC /* DevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745122C54169400211FAC /* DevicesView.swift */; };
 		DD1745152C54388A00211FAC /* TherapySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745142C54388A00211FAC /* TherapySettingsView.swift */; };
 		DD1745172C54389F00211FAC /* FeatureSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745162C54389F00211FAC /* FeatureSettingsView.swift */; };
@@ -1129,7 +1130,8 @@
 		DD09D47C2C5986DA003FEA5D /* CalendarEventSettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarEventSettingsProvider.swift; sourceTree = "<group>"; };
 		DD09D47E2C5986E5003FEA5D /* CalendarEventSettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarEventSettingsStateModel.swift; sourceTree = "<group>"; };
 		DD09D4812C5986F6003FEA5D /* CalendarEventSettingsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarEventSettingsRootView.swift; sourceTree = "<group>"; };
-		DD09D5C62D29EB26000D82C9 /* Helper+Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+Navigation.swift"; sourceTree = "<group>"; };
+		DD09D5C62D29EB26000D82C9 /* Helper+Enums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+Enums.swift"; sourceTree = "<group>"; };
+		DD09D5C82D29F3D0000D82C9 /* AcknowledgementPendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgementPendingView.swift; sourceTree = "<group>"; };
 		DD1745122C54169400211FAC /* DevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesView.swift; sourceTree = "<group>"; };
 		DD1745142C54388A00211FAC /* TherapySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TherapySettingsView.swift; sourceTree = "<group>"; };
 		DD1745162C54389F00211FAC /* FeatureSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureSettingsView.swift; sourceTree = "<group>"; };
@@ -2482,6 +2484,7 @@
 		BDA25F1A2D26BCE800035F34 /* Views */ = {
 			isa = PBXGroup;
 			children = (
+				DD09D5C82D29F3D0000D82C9 /* AcknowledgementPendingView.swift */,
 				BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */,
 				DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */,
 				BDA25F212D26D62200035F34 /* BolusInputView.swift */,
@@ -2832,7 +2835,7 @@
 		DD3A3CEC2D29CFBA00AE478E /* Helper */ = {
 			isa = PBXGroup;
 			children = (
-				DD09D5C62D29EB26000D82C9 /* Helper+Navigation.swift */,
+				DD09D5C62D29EB26000D82C9 /* Helper+Enums.swift */,
 				DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */,
 				DD3A3CE62D29C93F00AE478E /* Helper+Extensions.swift */,
 			);
@@ -3996,7 +3999,7 @@
 				BDA25F202D26D5FE00035F34 /* CarbsInputView.swift in Sources */,
 				BDA25F1C2D26BD0700035F34 /* TrendShape.swift in Sources */,
 				BDFF7A892D25F97D0016C40C /* TrioWatchApp.swift in Sources */,
-				DD09D5C72D29EB2F000D82C9 /* Helper+Navigation.swift in Sources */,
+				DD09D5C72D29EB2F000D82C9 /* Helper+Enums.swift in Sources */,
 				BD54A9742D281AEF00F9C1EE /* TempTargetPresetWatch.swift in Sources */,
 				BD54A95C2D2808A300F9C1EE /* OverridePresetWatch.swift in Sources */,
 				BD54A9592D27FB7800F9C1EE /* OverridePresetsView.swift in Sources */,
@@ -4009,6 +4012,7 @@
 				DD3A3CE92D29C97800AE478E /* Helper+ButtonStyles.swift in Sources */,
 				BDA25EE62D260D5E00035F34 /* WatchState.swift in Sources */,
 				BD04ECCE2D29952A008C5FEB /* BolusProgressOverlay.swift in Sources */,
+				DD09D5C92D29F3D0000D82C9 /* AcknowledgementPendingView.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 37 - 3
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -245,7 +245,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         }
     }
 
-    // MARK: - Send Data to Watch
+    // MARK: - Send to Watch
 
     /// Sends the state of type WatchState to the connected Watch
     /// - Parameter state: Current WatchState containing glucose data to be sent
@@ -298,6 +298,22 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         }
     }
 
+    func sendAcknowledgment(toWatch success: Bool, message: String = "") {
+        guard let session = session, session.isReachable else {
+            debug(.watchManager, "⌚️ Watch not reachable for acknowledgment")
+            return
+        }
+
+        let ackMessage: [String: Any] = [
+            "acknowledged": success,
+            "message": message
+        ]
+
+        session.sendMessage(ackMessage, replyHandler: nil) { error in
+            debug(.watchManager, "❌ Error sending acknowledgment: \(error.localizedDescription)")
+        }
+    }
+
     // MARK: - WCSessionDelegate
 
     func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
@@ -405,6 +421,8 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         Task {
             await apsManager.enactBolus(amount: Double(amount), isSMB: false)
             debug(.watchManager, "📱 Enacted bolus via APS Manager: \(amount)U")
+            // Acknowledge success
+            self.sendAcknowledgment(toWatch: true, message: "Enacted bolus successfully")
         }
     }
 
@@ -428,8 +446,14 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     guard context.hasChanges else { return }
                     try context.save()
                     debug(.watchManager, "📱 Saved carbs from watch: \(amount)g at \(date)")
+
+                    // Acknowledge success
+                    self.sendAcknowledgment(toWatch: true, message: "Carbs logged successfully")
                 } catch {
                     debug(.watchManager, "❌ Error saving carbs: \(error.localizedDescription)")
+
+                    // Acknowledge failure
+                    self.sendAcknowledgment(toWatch: false, message: "Error logging carbs")
                 }
             }
         }
@@ -445,6 +469,9 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             let context = CoreDataStack.shared.newTaskContext()
 
             do {
+                // Notify Watch: "Saving carbs..."
+                self.sendAcknowledgment(toWatch: true, message: "Saving Carbs...")
+
                 // Save carbs entry in Core Data
                 try await context.perform {
                     let carbEntry = CarbEntryStored(context: context)
@@ -455,15 +482,22 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
                     guard context.hasChanges else { return }
                     try context.save()
-                    debug(.watchManager, "📱 Saved carbs from watch: \(carbsAmount)g at \(date)")
+                    debug(.watchManager, "📱 Saved carbs from watch: \(carbsAmount) g at \(date)")
                 }
 
+                // Notify Watch: "Enacting bolus..."
+                sendAcknowledgment(toWatch: true, message: "Enacting bolus...")
+
                 // Enact bolus via APS Manager
                 let bolusDouble = NSDecimalNumber(decimal: bolusAmount).doubleValue
                 await apsManager.enactBolus(amount: bolusDouble, isSMB: false)
-                debug(.watchManager, "📱 Enacted bolus from watch via APS Manager: \(bolusDouble)U")
+                debug(.watchManager, "📱 Enacted bolus from watch via APS Manager: \(bolusDouble) U")
+                // Notify Watch: "Carbs and bolus logged successfully"
+                sendAcknowledgment(toWatch: true, message: "Carbs and Bolus logged successfully")
+
             } catch {
                 debug(.watchManager, "❌ Error processing combined request: \(error.localizedDescription)")
+                sendAcknowledgment(toWatch: false, message: "Failed to log carbs and bolus")
             }
         }
     }