RemoteSettingsViewModel.swift 8.9 KB

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