RemoteSettingsViewModel.swift 8.4 KB

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