DexcomSettingsViewModel.swift 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. // LoopFollow
  2. // DexcomSettingsViewModel.swift
  3. import Combine
  4. import Foundation
  5. import ShareClient
  6. class DexcomSettingsViewModel: ObservableObject {
  7. enum ConnectionStatusKind {
  8. case idle
  9. case checking
  10. case connected
  11. case error
  12. }
  13. /// Whether this is a fresh setup (credentials were empty when view appeared)
  14. private(set) var isFreshSetup: Bool = false
  15. @Published var userName: String = Storage.shared.shareUserName.value {
  16. willSet {
  17. if newValue != userName {
  18. Storage.shared.shareUserName.value = newValue
  19. scheduleVerification()
  20. }
  21. }
  22. }
  23. @Published var password: String = Storage.shared.sharePassword.value {
  24. willSet {
  25. if newValue != password {
  26. Storage.shared.sharePassword.value = newValue
  27. scheduleVerification()
  28. }
  29. }
  30. }
  31. @Published var server: String = Storage.shared.shareServer.value {
  32. willSet {
  33. if newValue != server {
  34. Storage.shared.shareServer.value = newValue
  35. scheduleVerification()
  36. }
  37. }
  38. }
  39. /// Whether credentials are filled in
  40. var hasCredentials: Bool {
  41. !userName.isEmpty && !password.isEmpty
  42. }
  43. // MARK: - Verification
  44. @Published var statusKind: ConnectionStatusKind = .idle
  45. @Published var statusMessage: String = "Enter your username and password"
  46. /// True when a real Dexcom Share login succeeded.
  47. @Published private(set) var isVerified: Bool = false
  48. /// The credentials were explicitly rejected by Dexcom (as opposed to a network
  49. /// failure we can't draw a conclusion from).
  50. private(set) var loginRejected: Bool = false
  51. /// Can move on: verified, or the only problem is that we couldn't reach Dexcom
  52. /// (so we don't trap a user on a flaky network). A rejected login always blocks.
  53. var canVerifyProceed: Bool {
  54. hasCredentials && statusKind != .checking && !loginRejected
  55. }
  56. private var verifyGeneration = 0
  57. private var cancellables = Set<AnyCancellable>()
  58. private let verifySubject = PassthroughSubject<Void, Never>()
  59. init() {
  60. isFreshSetup = Storage.shared.shareUserName.value.isEmpty
  61. verifySubject
  62. .debounce(for: .seconds(1.5), scheduler: DispatchQueue.main)
  63. .sink { [weak self] in self?.verify() }
  64. .store(in: &cancellables)
  65. scheduleVerification()
  66. }
  67. /// Resets status to "checking" and queues a debounced verification.
  68. private func scheduleVerification() {
  69. verifyGeneration += 1
  70. loginRejected = false
  71. isVerified = false
  72. if hasCredentials {
  73. statusKind = .checking
  74. statusMessage = "Checking your account…"
  75. verifySubject.send()
  76. } else {
  77. statusKind = .idle
  78. statusMessage = "Enter your username and password"
  79. }
  80. }
  81. private func verify() {
  82. guard hasCredentials else { return }
  83. let generation = verifyGeneration
  84. let serverURL = server == "US"
  85. ? KnownShareServers.US.rawValue
  86. : KnownShareServers.NON_US.rawValue
  87. let client = ShareClient(username: userName, password: password, shareServer: serverURL)
  88. client.fetchData(1) { [weak self] error, _ in
  89. DispatchQueue.main.async {
  90. guard let self, generation == self.verifyGeneration else { return }
  91. if let error = error {
  92. switch error {
  93. case .loginError:
  94. self.statusKind = .error
  95. self.statusMessage = "Username or password not accepted"
  96. self.isVerified = false
  97. self.loginRejected = true
  98. case .httpError:
  99. self.statusKind = .error
  100. self.statusMessage = "Network error — check your connection"
  101. self.isVerified = false
  102. self.loginRejected = false
  103. default:
  104. // Login succeeded but there's no recent reading yet; the
  105. // credentials are valid, which is all we're confirming.
  106. self.statusKind = .connected
  107. self.statusMessage = "Connected"
  108. self.isVerified = true
  109. self.loginRejected = false
  110. }
  111. } else {
  112. self.statusKind = .connected
  113. self.statusMessage = "Connected"
  114. self.isVerified = true
  115. self.loginRejected = false
  116. }
  117. }
  118. }
  119. }
  120. }