RemoteSettingsViewModel.swift 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. // LoopFollow
  2. // RemoteSettingsViewModel.swift
  3. // Created by Jonas Björkert.
  4. import Combine
  5. import Foundation
  6. import HealthKit
  7. class RemoteSettingsViewModel: ObservableObject {
  8. @Published var remoteType: RemoteType
  9. @Published var user: String
  10. @Published var sharedSecret: String
  11. @Published var apnsKey: String
  12. @Published var keyId: String
  13. @Published var maxBolus: HKQuantity
  14. @Published var maxCarbs: HKQuantity
  15. @Published var maxProtein: HKQuantity
  16. @Published var maxFat: HKQuantity
  17. @Published var mealWithBolus: Bool
  18. @Published var mealWithFatProtein: Bool
  19. @Published var isTrioDevice: Bool = (Storage.shared.device.value == "Trio")
  20. @Published var isLoopDevice: Bool = (Storage.shared.device.value == "Loop")
  21. // MARK: - Return Notification Properties
  22. @Published var returnApnsKey: String
  23. @Published var returnKeyId: String
  24. // MARK: - Loop APNS Setup Properties
  25. @Published var loopDeveloperTeamId: String
  26. @Published var loopAPNSQrCodeURL: String
  27. @Published var productionEnvironment: Bool
  28. @Published var isShowingLoopAPNSScanner: Bool = false
  29. @Published var loopAPNSErrorMessage: String?
  30. let loopFollowTeamId: String = BuildDetails.default.teamID ?? "Unknown"
  31. /// Determines if the target app's Team ID is different from this app's build Team ID.
  32. var areTeamIdsDifferent: Bool {
  33. // Get LoopFollow's own Team ID from the build details.
  34. guard let loopFollowTeamID = BuildDetails.default.teamID, !loopFollowTeamID.isEmpty, loopFollowTeamID != "Unknown" else {
  35. return false
  36. }
  37. // The property `loopDeveloperTeamId` holds the value from `Storage.shared.teamId`
  38. let targetTeamId = loopDeveloperTeamId
  39. // Determine if a comparison is needed and perform it.
  40. switch remoteType {
  41. case .loopAPNS, .trc:
  42. // For both Loop and TRC, the target Team ID is in the same storage location.
  43. // If the target ID is empty, there's nothing to compare.
  44. guard !targetTeamId.isEmpty else {
  45. return false
  46. }
  47. // Return true if the IDs are different.
  48. return loopFollowTeamID != targetTeamId
  49. case .none, .nightscout:
  50. // For other remote types, this check is not applicable.
  51. return false
  52. }
  53. }
  54. // MARK: - Computed property for Loop APNS Setup validation
  55. var loopAPNSSetup: Bool {
  56. !keyId.isEmpty &&
  57. !apnsKey.isEmpty &&
  58. !loopDeveloperTeamId.isEmpty &&
  59. !loopAPNSQrCodeURL.isEmpty &&
  60. !Storage.shared.deviceToken.value.isEmpty &&
  61. !Storage.shared.bundleId.value.isEmpty
  62. }
  63. private var storage = Storage.shared
  64. private var cancellables = Set<AnyCancellable>()
  65. init() {
  66. // Initialize published properties from storage
  67. remoteType = storage.remoteType.value
  68. user = storage.user.value
  69. sharedSecret = storage.sharedSecret.value
  70. apnsKey = storage.apnsKey.value
  71. keyId = storage.keyId.value
  72. maxBolus = storage.maxBolus.value
  73. maxCarbs = storage.maxCarbs.value
  74. maxProtein = storage.maxProtein.value
  75. maxFat = storage.maxFat.value
  76. mealWithBolus = storage.mealWithBolus.value
  77. mealWithFatProtein = storage.mealWithFatProtein.value
  78. loopDeveloperTeamId = storage.teamId.value ?? ""
  79. loopAPNSQrCodeURL = storage.loopAPNSQrCodeURL.value
  80. productionEnvironment = storage.productionEnvironment.value
  81. returnApnsKey = storage.returnApnsKey.value
  82. returnKeyId = storage.returnKeyId.value
  83. setupBindings()
  84. }
  85. private func setupBindings() {
  86. // Basic property bindings
  87. $remoteType
  88. .dropFirst()
  89. .sink { [weak self] in self?.storage.remoteType.value = $0 }
  90. .store(in: &cancellables)
  91. $user
  92. .dropFirst()
  93. .sink { [weak self] in self?.storage.user.value = $0 }
  94. .store(in: &cancellables)
  95. $sharedSecret
  96. .dropFirst()
  97. .sink { [weak self] in self?.storage.sharedSecret.value = $0 }
  98. .store(in: &cancellables)
  99. $apnsKey
  100. .dropFirst()
  101. .sink { [weak self] newValue in
  102. // Validate and fix the APNS key format using the service
  103. let apnsService = LoopAPNSService()
  104. let fixedKey = apnsService.validateAndFixAPNSKey(newValue)
  105. self?.storage.apnsKey.value = fixedKey
  106. }
  107. .store(in: &cancellables)
  108. $keyId
  109. .dropFirst()
  110. .sink { [weak self] in self?.storage.keyId.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. // Return notification bindings
  158. $returnApnsKey
  159. .dropFirst()
  160. .sink { [weak self] in self?.storage.returnApnsKey.value = $0 }
  161. .store(in: &cancellables)
  162. $returnKeyId
  163. .dropFirst()
  164. .sink { [weak self] in self?.storage.returnKeyId.value = $0 }
  165. .store(in: &cancellables)
  166. }
  167. func handleLoopAPNSQRCodeScanResult(_ result: Result<String, Error>) {
  168. DispatchQueue.main.async {
  169. switch result {
  170. case let .success(code):
  171. self.loopAPNSQrCodeURL = code
  172. LogManager.shared.log(category: .apns, message: "Loop APNS QR code scanned: \(code)")
  173. case let .failure(error):
  174. self.loopAPNSErrorMessage = "Scanning failed: \(error.localizedDescription)"
  175. }
  176. self.isShowingLoopAPNSScanner = false
  177. }
  178. }
  179. }