BolusView.swift 6.1 KB

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