RemoteSettingsViewModel.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  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: - Return Notification Properties
  21. @Published var returnApnsKey: String
  22. @Published var returnKeyId: String
  23. // MARK: - Loop APNS Setup Properties
  24. @Published var loopDeveloperTeamId: String
  25. @Published var loopAPNSQrCodeURL: String
  26. @Published var productionEnvironment: Bool
  27. @Published var isShowingLoopAPNSScanner: Bool = false
  28. @Published var loopAPNSErrorMessage: String?
  29. // MARK: - QR Code Sharing Properties
  30. @Published var isShowingQRCodeScanner: Bool = false
  31. @Published var isShowingQRCodeDisplay: Bool = false
  32. @Published var qrCodeErrorMessage: String?
  33. // MARK: - URL/Token Validation Properties
  34. @Published var pendingSettings: RemoteCommandSettings?
  35. @Published var showURLTokenValidation: Bool = false
  36. @Published var validationMessage: String = ""
  37. @Published var shouldPromptForURL: Bool = false
  38. @Published var shouldPromptForToken: Bool = false
  39. let loopFollowTeamId: String = BuildDetails.default.teamID ?? "Unknown"
  40. /// Determines if the target app's Team ID is different from this app's build Team ID.
  41. var areTeamIdsDifferent: Bool {
  42. // Get LoopFollow's own Team ID from the build details.
  43. guard let loopFollowTeamID = BuildDetails.default.teamID, !loopFollowTeamID.isEmpty, loopFollowTeamID != "Unknown" else {
  44. return false
  45. }
  46. // The property `loopDeveloperTeamId` holds the value from `Storage.shared.teamId`
  47. let targetTeamId = loopDeveloperTeamId
  48. // Determine if a comparison is needed and perform it.
  49. switch remoteType {
  50. case .loopAPNS, .trc:
  51. // For both Loop and TRC, the target Team ID is in the same storage location.
  52. // If the target ID is empty, there's nothing to compare.
  53. guard !targetTeamId.isEmpty else {
  54. return false
  55. }
  56. // Return true if the IDs are different.
  57. return loopFollowTeamID != targetTeamId
  58. case .none, .nightscout:
  59. // For other remote types, this check is not applicable.
  60. return false
  61. }
  62. }
  63. // MARK: - Computed property for Loop APNS Setup validation
  64. var loopAPNSSetup: Bool {
  65. !keyId.isEmpty &&
  66. !apnsKey.isEmpty &&
  67. !loopDeveloperTeamId.isEmpty &&
  68. !loopAPNSQrCodeURL.isEmpty &&
  69. !Storage.shared.deviceToken.value.isEmpty &&
  70. !Storage.shared.bundleId.value.isEmpty
  71. }
  72. private var storage = Storage.shared
  73. private var cancellables = Set<AnyCancellable>()
  74. init() {
  75. // Initialize published properties from storage
  76. remoteType = storage.remoteType.value
  77. user = storage.user.value
  78. sharedSecret = storage.sharedSecret.value
  79. apnsKey = storage.apnsKey.value
  80. keyId = storage.keyId.value
  81. maxBolus = storage.maxBolus.value
  82. maxCarbs = storage.maxCarbs.value
  83. maxProtein = storage.maxProtein.value
  84. maxFat = storage.maxFat.value
  85. mealWithBolus = storage.mealWithBolus.value
  86. mealWithFatProtein = storage.mealWithFatProtein.value
  87. loopDeveloperTeamId = storage.teamId.value ?? ""
  88. loopAPNSQrCodeURL = storage.loopAPNSQrCodeURL.value
  89. productionEnvironment = storage.productionEnvironment.value
  90. returnApnsKey = storage.returnApnsKey.value
  91. returnKeyId = storage.returnKeyId.value
  92. setupBindings()
  93. }
  94. private func setupBindings() {
  95. // Basic property bindings
  96. $remoteType
  97. .dropFirst()
  98. .sink { [weak self] in self?.storage.remoteType.value = $0 }
  99. .store(in: &cancellables)
  100. $user
  101. .dropFirst()
  102. .sink { [weak self] in self?.storage.user.value = $0 }
  103. .store(in: &cancellables)
  104. $sharedSecret
  105. .dropFirst()
  106. .sink { [weak self] in self?.storage.sharedSecret.value = $0 }
  107. .store(in: &cancellables)
  108. $apnsKey
  109. .dropFirst()
  110. .sink { [weak self] newValue in
  111. // Validate and fix the APNS key format using the service
  112. let apnsService = LoopAPNSService()
  113. let fixedKey = apnsService.validateAndFixAPNSKey(newValue)
  114. self?.storage.apnsKey.value = fixedKey
  115. }
  116. .store(in: &cancellables)
  117. $keyId
  118. .dropFirst()
  119. .sink { [weak self] in self?.storage.keyId.value = $0 }
  120. .store(in: &cancellables)
  121. $maxBolus
  122. .dropFirst()
  123. .sink { [weak self] in self?.storage.maxBolus.value = $0 }
  124. .store(in: &cancellables)
  125. $maxCarbs
  126. .dropFirst()
  127. .sink { [weak self] in self?.storage.maxCarbs.value = $0 }
  128. .store(in: &cancellables)
  129. $maxProtein
  130. .dropFirst()
  131. .sink { [weak self] in self?.storage.maxProtein.value = $0 }
  132. .store(in: &cancellables)
  133. $maxFat
  134. .dropFirst()
  135. .sink { [weak self] in self?.storage.maxFat.value = $0 }
  136. .store(in: &cancellables)
  137. $mealWithBolus
  138. .dropFirst()
  139. .sink { [weak self] in self?.storage.mealWithBolus.value = $0 }
  140. .store(in: &cancellables)
  141. $mealWithFatProtein
  142. .dropFirst()
  143. .sink { [weak self] in self?.storage.mealWithFatProtein.value = $0 }
  144. .store(in: &cancellables)
  145. // Device type monitoring
  146. Storage.shared.device.$value
  147. .receive(on: DispatchQueue.main)
  148. .sink { [weak self] newValue in
  149. self?.isTrioDevice = (newValue == "Trio")
  150. self?.isLoopDevice = (newValue == "Loop")
  151. }
  152. .store(in: &cancellables)
  153. // Loop APNS bindings
  154. $loopDeveloperTeamId
  155. .dropFirst()
  156. .sink { [weak self] in self?.storage.teamId.value = $0 }
  157. .store(in: &cancellables)
  158. $loopAPNSQrCodeURL
  159. .dropFirst()
  160. .sink { [weak self] in self?.storage.loopAPNSQrCodeURL.value = $0 }
  161. .store(in: &cancellables)
  162. $productionEnvironment
  163. .dropFirst()
  164. .sink { [weak self] in self?.storage.productionEnvironment.value = $0 }
  165. .store(in: &cancellables)
  166. // Return notification bindings
  167. $returnApnsKey
  168. .dropFirst()
  169. .sink { [weak self] in self?.storage.returnApnsKey.value = $0 }
  170. .store(in: &cancellables)
  171. $returnKeyId
  172. .dropFirst()
  173. .sink { [weak self] in self?.storage.returnKeyId.value = $0 }
  174. .store(in: &cancellables)
  175. }
  176. func handleLoopAPNSQRCodeScanResult(_ result: Result<String, Error>) {
  177. DispatchQueue.main.async {
  178. switch result {
  179. case let .success(code):
  180. self.loopAPNSQrCodeURL = code
  181. // Set device type and remote type for Loop APNS
  182. Storage.shared.device.value = "Loop"
  183. Storage.shared.remoteType.value = .loopAPNS
  184. // Update view model properties
  185. self.remoteType = .loopAPNS
  186. self.isLoopDevice = true
  187. self.isTrioDevice = false
  188. LogManager.shared.log(category: .apns, message: "Loop APNS QR code scanned: \(code)")
  189. case let .failure(error):
  190. self.loopAPNSErrorMessage = "Scanning failed: \(error.localizedDescription)"
  191. }
  192. self.isShowingLoopAPNSScanner = false
  193. }
  194. }
  195. // MARK: - QR Code Sharing Methods
  196. func handleRemoteCommandQRCodeScanResult(_ result: Result<String, Error>) {
  197. DispatchQueue.main.async {
  198. switch result {
  199. case let .success(jsonString):
  200. if let settings = RemoteCommandSettings.decodeFromJSON(jsonString) {
  201. if settings.isValid() {
  202. // Check URL and token compatibility
  203. let validation = settings.validateCompatibilityWithCurrentStorage()
  204. if validation.isCompatible {
  205. // No conflicts, apply settings directly
  206. settings.applyToStorage()
  207. self.updateViewModelFromStorage()
  208. LogManager.shared.log(category: .remote, message: "Remote command settings imported from QR code")
  209. } else {
  210. // Conflicts detected, show validation view
  211. self.pendingSettings = settings
  212. self.validationMessage = validation.message
  213. self.shouldPromptForURL = validation.shouldPromptForURL
  214. self.shouldPromptForToken = validation.shouldPromptForToken
  215. self.showURLTokenValidation = true
  216. }
  217. } else {
  218. self.qrCodeErrorMessage = "Invalid remote command settings in QR code"
  219. }
  220. } else {
  221. self.qrCodeErrorMessage = "Failed to decode remote command settings from QR code"
  222. }
  223. case let .failure(error):
  224. self.qrCodeErrorMessage = "Scanning failed: \(error.localizedDescription)"
  225. }
  226. self.isShowingQRCodeScanner = false
  227. }
  228. }
  229. func generateQRCodeForCurrentSettings() -> String? {
  230. let settings = RemoteCommandSettings.fromCurrentStorage()
  231. return settings.encodeToJSON()
  232. }
  233. // MARK: - Public Methods for View Access
  234. /// Updates the view model properties from storage (accessible from view)
  235. func updateViewModelFromStorage() {
  236. let storage = Storage.shared
  237. remoteType = storage.remoteType.value
  238. user = storage.user.value
  239. sharedSecret = storage.sharedSecret.value
  240. apnsKey = storage.apnsKey.value
  241. keyId = storage.keyId.value
  242. maxBolus = storage.maxBolus.value
  243. maxCarbs = storage.maxCarbs.value
  244. maxProtein = storage.maxProtein.value
  245. maxFat = storage.maxFat.value
  246. mealWithBolus = storage.mealWithBolus.value
  247. mealWithFatProtein = storage.mealWithFatProtein.value
  248. loopDeveloperTeamId = storage.teamId.value ?? ""
  249. loopAPNSQrCodeURL = storage.loopAPNSQrCodeURL.value
  250. productionEnvironment = storage.productionEnvironment.value
  251. // Update device-related properties
  252. isTrioDevice = (storage.device.value == "Trio")
  253. isLoopDevice = (storage.device.value == "Loop")
  254. }
  255. }