BolusView.swift 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. // LoopFollow
  2. // BolusView.swift
  3. import HealthKit
  4. import LocalAuthentication
  5. import SwiftUI
  6. struct BolusView: View {
  7. @Environment(\.presentationMode) private var presentationMode
  8. @State private var bolusAmount = HKQuantity(unit: .internationalUnit(), doubleValue: 0.0)
  9. private let pushNotificationManager = PushNotificationManager()
  10. @ObservedObject private var maxBolus = Storage.shared.maxBolus
  11. @FocusState private var bolusFieldIsFocused: Bool
  12. @State private var showAlert = false
  13. @State private var alertType: AlertType? = nil
  14. @State private var alertMessage: String? = nil
  15. @State private var isLoading = false
  16. @State private var statusMessage: String? = nil
  17. enum AlertType {
  18. case confirmBolus
  19. case statusSuccess
  20. case statusFailure
  21. case validation
  22. }
  23. var body: some View {
  24. NavigationView {
  25. VStack {
  26. Form {
  27. Section {
  28. HKQuantityInputView(
  29. label: "Bolus Amount",
  30. quantity: $bolusAmount,
  31. unit: .internationalUnit(),
  32. maxLength: 4,
  33. minValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0.05),
  34. maxValue: maxBolus.value,
  35. isFocused: $bolusFieldIsFocused,
  36. onValidationError: { message in
  37. handleValidationError(message)
  38. }
  39. )
  40. }
  41. LoadingButtonView(
  42. buttonText: "Send Bolus",
  43. progressText: "Sending Bolus...",
  44. isLoading: isLoading,
  45. action: {
  46. bolusFieldIsFocused = false
  47. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
  48. if bolusAmount.doubleValue(for: HKUnit.internationalUnit()) > 0.0 {
  49. alertType = .confirmBolus
  50. showAlert = true
  51. }
  52. }
  53. },
  54. isDisabled: isLoading
  55. )
  56. }
  57. .navigationTitle("Bolus")
  58. .navigationBarTitleDisplayMode(.inline)
  59. }
  60. .alert(isPresented: $showAlert) {
  61. switch alertType {
  62. case .confirmBolus:
  63. return Alert(
  64. title: Text("Confirm Bolus"),
  65. message: Text("Are you sure you want to send \(bolusAmount.doubleValue(for: HKUnit.internationalUnit()), specifier: "%.2f") U?"),
  66. primaryButton: .default(Text("Confirm"), action: {
  67. authenticateUser { success in
  68. if success {
  69. sendBolus()
  70. }
  71. }
  72. }),
  73. secondaryButton: .cancel()
  74. )
  75. case .statusSuccess:
  76. return Alert(
  77. title: Text("Status"),
  78. message: Text(statusMessage ?? ""),
  79. dismissButton: .default(Text("OK"), action: {
  80. presentationMode.wrappedValue.dismiss()
  81. })
  82. )
  83. case .statusFailure:
  84. return Alert(
  85. title: Text("Status"),
  86. message: Text(statusMessage ?? ""),
  87. dismissButton: .default(Text("OK"))
  88. )
  89. case .validation:
  90. return Alert(
  91. title: Text("Validation Error"),
  92. message: Text(alertMessage ?? "Invalid input."),
  93. dismissButton: .default(Text("OK"))
  94. )
  95. case .none:
  96. return Alert(title: Text("Unknown Alert"))
  97. }
  98. }
  99. }
  100. }
  101. private func sendBolus() {
  102. isLoading = true
  103. pushNotificationManager.sendBolusPushNotification(bolusAmount: bolusAmount) { success, errorMessage in
  104. DispatchQueue.main.async {
  105. isLoading = false
  106. if success {
  107. statusMessage = "Bolus command sent successfully."
  108. LogManager.shared.log(category: .apns, message: "sendBolusPushNotification succeeded - Bolus: \(bolusAmount.doubleValue(for: .internationalUnit())) U")
  109. bolusAmount = HKQuantity(unit: .internationalUnit(), doubleValue: 0.0)
  110. alertType = .statusSuccess
  111. } else {
  112. statusMessage = errorMessage ?? "Failed to send bolus command."
  113. LogManager.shared.log(category: .apns, message: "sendBolusPushNotification failed with error: \(errorMessage ?? "unknown error")")
  114. alertType = .statusFailure
  115. }
  116. showAlert = true
  117. }
  118. }
  119. }
  120. private func authenticateUser(completion: @escaping (Bool) -> Void) {
  121. let context = LAContext()
  122. var error: NSError?
  123. let reason = "Confirm your identity to send bolus."
  124. if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
  125. context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, _ in
  126. DispatchQueue.main.async {
  127. if success {
  128. completion(true)
  129. } else {
  130. // Biometric failed, try passcode
  131. self.tryPasscode(completion: completion)
  132. }
  133. }
  134. }
  135. } else {
  136. // No biometrics available, try passcode directly
  137. tryPasscode(completion: completion)
  138. }
  139. }
  140. private func tryPasscode(completion: @escaping (Bool) -> Void) {
  141. let context = LAContext()
  142. var error: NSError?
  143. let reason = "Confirm your identity to send bolus."
  144. if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
  145. context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in
  146. DispatchQueue.main.async {
  147. completion(success)
  148. }
  149. }
  150. } else {
  151. DispatchQueue.main.async {
  152. completion(false)
  153. }
  154. }
  155. }
  156. private func handleValidationError(_ message: String) {
  157. alertMessage = message
  158. alertType = .validation
  159. showAlert = true
  160. }
  161. }