BolusView.swift 5.6 KB

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