RemoteSettingsViewModel.swift 11 KB

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