RemoteSettingsViewModel.swift 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. // LoopFollow
  2. // RemoteSettingsViewModel.swift
  3. import Combine
  4. import Foundation
  5. import HealthKit
  6. class RemoteSettingsViewModel: ObservableObject {
  7. @Published var remoteType: RemoteType
  8. @Published var user: String
  9. @Published var sharedSecret: String
  10. @Published var apnsKey: String
  11. @Published var keyId: String
  12. @Published var maxBolus: HKQuantity
  13. @Published var maxCarbs: HKQuantity
  14. @Published var maxProtein: HKQuantity
  15. @Published var maxFat: HKQuantity
  16. @Published var mealWithBolus: Bool
  17. @Published var mealWithFatProtein: Bool
  18. @Published var isTrioDevice: Bool = (Storage.shared.device.value == "Trio")
  19. @Published var isLoopDevice: Bool = (Storage.shared.device.value == "Loop")
  20. // MARK: - Loop APNS Setup Properties
  21. @Published var loopDeveloperTeamId: String
  22. @Published var loopAPNSQrCodeURL: String
  23. @Published var productionEnvironment: Bool
  24. @Published var isShowingLoopAPNSScanner: Bool = false
  25. @Published var loopAPNSErrorMessage: String?
  26. // MARK: - QR Code Sharing Properties
  27. @Published var isShowingQRCodeScanner: Bool = false
  28. @Published var isShowingQRCodeDisplay: Bool = false
  29. @Published var qrCodeErrorMessage: String?
  30. // MARK: - URL/Token Validation Properties
  31. @Published var pendingSettings: RemoteCommandSettings?
  32. @Published var showURLTokenValidation: Bool = false
  33. @Published var validationMessage: String = ""
  34. @Published var shouldPromptForURL: Bool = false
  35. @Published var shouldPromptForToken: Bool = false
  36. // MARK: - Computed property for Loop APNS Setup validation
  37. var loopAPNSSetup: Bool {
  38. !keyId.isEmpty &&
  39. !apnsKey.isEmpty &&
  40. !loopDeveloperTeamId.isEmpty &&
  41. !loopAPNSQrCodeURL.isEmpty &&
  42. !Storage.shared.deviceToken.value.isEmpty &&
  43. !Storage.shared.bundleId.value.isEmpty
  44. }
  45. private var storage = Storage.shared
  46. private var cancellables = Set<AnyCancellable>()
  47. init() {
  48. // Initialize published properties from storage
  49. remoteType = storage.remoteType.value
  50. user = storage.user.value
  51. sharedSecret = storage.sharedSecret.value
  52. apnsKey = storage.apnsKey.value
  53. keyId = storage.keyId.value
  54. maxBolus = storage.maxBolus.value
  55. maxCarbs = storage.maxCarbs.value
  56. maxProtein = storage.maxProtein.value
  57. maxFat = storage.maxFat.value
  58. mealWithBolus = storage.mealWithBolus.value
  59. mealWithFatProtein = storage.mealWithFatProtein.value
  60. loopDeveloperTeamId = storage.teamId.value ?? ""
  61. loopAPNSQrCodeURL = storage.loopAPNSQrCodeURL.value
  62. productionEnvironment = storage.productionEnvironment.value
  63. setupBindings()
  64. }
  65. private func setupBindings() {
  66. // Basic property bindings
  67. $remoteType
  68. .dropFirst()
  69. .sink { [weak self] in self?.storage.remoteType.value = $0 }
  70. .store(in: &cancellables)
  71. $user
  72. .dropFirst()
  73. .sink { [weak self] in self?.storage.user.value = $0 }
  74. .store(in: &cancellables)
  75. $sharedSecret
  76. .dropFirst()
  77. .sink { [weak self] in self?.storage.sharedSecret.value = $0 }
  78. .store(in: &cancellables)
  79. $apnsKey
  80. .dropFirst()
  81. .sink { [weak self] newValue in
  82. // Validate and fix the APNS key format using the service
  83. let apnsService = LoopAPNSService()
  84. let fixedKey = apnsService.validateAndFixAPNSKey(newValue)
  85. self?.storage.apnsKey.value = fixedKey
  86. }
  87. .store(in: &cancellables)
  88. $keyId
  89. .dropFirst()
  90. .sink { [weak self] in self?.storage.keyId.value = $0 }
  91. .store(in: &cancellables)
  92. $maxBolus
  93. .dropFirst()
  94. .sink { [weak self] in self?.storage.maxBolus.value = $0 }
  95. .store(in: &cancellables)
  96. $maxCarbs
  97. .dropFirst()
  98. .sink { [weak self] in self?.storage.maxCarbs.value = $0 }
  99. .store(in: &cancellables)
  100. $maxProtein
  101. .dropFirst()
  102. .sink { [weak self] in self?.storage.maxProtein.value = $0 }
  103. .store(in: &cancellables)
  104. $maxFat
  105. .dropFirst()
  106. .sink { [weak self] in self?.storage.maxFat.value = $0 }
  107. .store(in: &cancellables)
  108. $mealWithBolus
  109. .dropFirst()
  110. .sink { [weak self] in self?.storage.mealWithBolus.value = $0 }
  111. .store(in: &cancellables)
  112. $mealWithFatProtein
  113. .dropFirst()
  114. .sink { [weak self] in self?.storage.mealWithFatProtein.value = $0 }
  115. .store(in: &cancellables)
  116. // Device type monitoring
  117. Storage.shared.device.$value
  118. .receive(on: DispatchQueue.main)
  119. .sink { [weak self] newValue in
  120. self?.isTrioDevice = (newValue == "Trio")
  121. self?.isLoopDevice = (newValue == "Loop")
  122. }
  123. .store(in: &cancellables)
  124. // Loop APNS bindings
  125. $loopDeveloperTeamId
  126. .dropFirst()
  127. .sink { [weak self] in self?.storage.teamId.value = $0 }
  128. .store(in: &cancellables)
  129. $loopAPNSQrCodeURL
  130. .dropFirst()
  131. .sink { [weak self] in self?.storage.loopAPNSQrCodeURL.value = $0 }
  132. .store(in: &cancellables)
  133. $productionEnvironment
  134. .dropFirst()
  135. .sink { [weak self] in self?.storage.productionEnvironment.value = $0 }
  136. .store(in: &cancellables)
  137. }
  138. func handleLoopAPNSQRCodeScanResult(_ result: Result<String, Error>) {
  139. DispatchQueue.main.async {
  140. switch result {
  141. case let .success(code):
  142. self.loopAPNSQrCodeURL = code
  143. // Set device type and remote type for Loop APNS
  144. Storage.shared.device.value = "Loop"
  145. Storage.shared.remoteType.value = .loopAPNS
  146. // Update view model properties
  147. self.remoteType = .loopAPNS
  148. self.isLoopDevice = true
  149. self.isTrioDevice = false
  150. LogManager.shared.log(category: .apns, message: "Loop APNS QR code scanned: \(code)")
  151. case let .failure(error):
  152. self.loopAPNSErrorMessage = "Scanning failed: \(error.localizedDescription)"
  153. }
  154. self.isShowingLoopAPNSScanner = false
  155. }
  156. }
  157. // MARK: - QR Code Sharing Methods
  158. func handleRemoteCommandQRCodeScanResult(_ result: Result<String, Error>) {
  159. DispatchQueue.main.async {
  160. switch result {
  161. case let .success(jsonString):
  162. if let settings = RemoteCommandSettings.decodeFromJSON(jsonString) {
  163. if settings.isValid() {
  164. // Check URL and token compatibility
  165. let validation = settings.validateCompatibilityWithCurrentStorage()
  166. if validation.isCompatible {
  167. // No conflicts, apply settings directly
  168. settings.applyToStorage()
  169. self.updateViewModelFromStorage()
  170. LogManager.shared.log(category: .remote, message: "Remote command settings imported from QR code")
  171. } else {
  172. // Conflicts detected, show validation view
  173. self.pendingSettings = settings
  174. self.validationMessage = validation.message
  175. self.shouldPromptForURL = validation.shouldPromptForURL
  176. self.shouldPromptForToken = validation.shouldPromptForToken
  177. self.showURLTokenValidation = true
  178. }
  179. } else {
  180. self.qrCodeErrorMessage = "Invalid remote command settings in QR code"
  181. }
  182. } else {
  183. self.qrCodeErrorMessage = "Failed to decode remote command settings from QR code"
  184. }
  185. case let .failure(error):
  186. self.qrCodeErrorMessage = "Scanning failed: \(error.localizedDescription)"
  187. }
  188. self.isShowingQRCodeScanner = false
  189. }
  190. }
  191. func generateQRCodeForCurrentSettings() -> String? {
  192. let settings = RemoteCommandSettings.fromCurrentStorage()
  193. return settings.encodeToJSON()
  194. }
  195. // MARK: - Public Methods for View Access
  196. /// Updates the view model properties from storage (accessible from view)
  197. func updateViewModelFromStorage() {
  198. let storage = Storage.shared
  199. remoteType = storage.remoteType.value
  200. user = storage.user.value
  201. sharedSecret = storage.sharedSecret.value
  202. apnsKey = storage.apnsKey.value
  203. keyId = storage.keyId.value
  204. maxBolus = storage.maxBolus.value
  205. maxCarbs = storage.maxCarbs.value
  206. maxProtein = storage.maxProtein.value
  207. maxFat = storage.maxFat.value
  208. mealWithBolus = storage.mealWithBolus.value
  209. mealWithFatProtein = storage.mealWithFatProtein.value
  210. loopDeveloperTeamId = storage.teamId.value ?? ""
  211. loopAPNSQrCodeURL = storage.loopAPNSQrCodeURL.value
  212. productionEnvironment = storage.productionEnvironment.value
  213. // Update device-related properties
  214. isTrioDevice = (storage.device.value == "Trio")
  215. isLoopDevice = (storage.device.value == "Loop")
  216. }
  217. }