Storage+Migrate.swift 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. // LoopFollow
  2. // Storage+Migrate.swift
  3. import Foundation
  4. import UserNotifications
  5. extension Storage {
  6. func migrateStep5() {
  7. // Users upgrading from main had snoozer hardcoded at position 3, but that
  8. // key never existed in UserDefaults. On dev the default for snoozerPosition
  9. // is .menu, so an unstored value silently becomes .menu and snoozer
  10. // disappears from the tab bar.
  11. //
  12. // Detect this by checking .exists: if snoozerPosition was never stored,
  13. // the user expects snoozer at position 3 (where it always was on main).
  14. guard !snoozerPosition.exists else { return }
  15. LogManager.shared.log(category: .general, message: "migrateStep5: snoozerPosition not stored, restoring snoozer to position 3")
  16. // If position 3 is occupied by another item whose position also wasn't
  17. // stored (e.g. nightscout landing there from a changed default), move
  18. // that item out first.
  19. for item in TabItem.allCases where item != .snoozer {
  20. if position(for: item).normalized == .position3,
  21. !migratePositionExists(for: item)
  22. {
  23. setPosition(.menu, for: item)
  24. }
  25. }
  26. if tabItem(at: .position3) == nil {
  27. snoozerPosition.value = .position3
  28. }
  29. }
  30. func migrateStep8() {
  31. // Migrate showTITR bool + custom lowLine/highLine into timeInRangeModeRaw.
  32. LogManager.shared.log(category: .general, message: "Running migrateStep8 — timeInRangeMode migration")
  33. if showTITR.value {
  34. timeInRangeModeRaw.value = TimeInRangeDisplayMode.titr.rawValue
  35. } else if lowLine.value != 70.0 || highLine.value != 180.0 {
  36. timeInRangeModeRaw.value = TimeInRangeDisplayMode.custom.rawValue
  37. }
  38. // else: default "TIR" is already set
  39. }
  40. func migrateStep7() {
  41. // Cancel notifications scheduled with old hardcoded identifiers.
  42. // Replaced with bundle-ID-scoped identifiers for multi-instance support.
  43. LogManager.shared.log(category: .general, message: "Running migrateStep7 — cancel legacy notification identifiers")
  44. let legacyNotificationIDs = [
  45. "loopfollow.background.alert.6min",
  46. "loopfollow.background.alert.12min",
  47. "loopfollow.background.alert.18min",
  48. "loopfollow.la.renewal.failed",
  49. ]
  50. UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: legacyNotificationIDs)
  51. UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: legacyNotificationIDs)
  52. }
  53. func migrateStep9() {
  54. // Default for debugLogLevel changed from false to true so users ship useful
  55. // logs when they report a problem. Force-enable for existing users.
  56. LogManager.shared.log(category: .general, message: "Running migrateStep9 — enabling debug log level")
  57. debugLogLevel.value = true
  58. }
  59. func migrateStep6() {
  60. // APNs credential separation
  61. LogManager.shared.log(category: .general, message: "Running migrateStep6 — APNs credential separation")
  62. let legacyReturnApnsKey = StorageValue<String>(key: "returnApnsKey", defaultValue: "")
  63. let legacyReturnKeyId = StorageValue<String>(key: "returnKeyId", defaultValue: "")
  64. let legacyApnsKey = StorageValue<String>(key: "apnsKey", defaultValue: "")
  65. let legacyKeyId = StorageValue<String>(key: "keyId", defaultValue: "")
  66. // 1. If returnApnsKey had a value, that was LoopFollow's own key (different team scenario)
  67. if legacyReturnApnsKey.exists, !legacyReturnApnsKey.value.isEmpty {
  68. lfApnsKey.value = legacyReturnApnsKey.value
  69. lfKeyId.value = legacyReturnKeyId.value
  70. }
  71. // 2. If lfApnsKey is still empty and the old primary key exists,
  72. // check if same team — if so, the primary key was used for everything
  73. if lfApnsKey.value.isEmpty, legacyApnsKey.exists, !legacyApnsKey.value.isEmpty {
  74. let lfTeamId = BuildDetails.default.teamID ?? ""
  75. let remoteTeamId = teamId.value ?? ""
  76. let sameTeam = !lfTeamId.isEmpty && (remoteTeamId.isEmpty || lfTeamId == remoteTeamId)
  77. if sameTeam {
  78. lfApnsKey.value = legacyApnsKey.value
  79. lfKeyId.value = legacyKeyId.value
  80. }
  81. }
  82. // 3. Move old primary credentials to remoteApnsKey/remoteKeyId
  83. if legacyApnsKey.exists, !legacyApnsKey.value.isEmpty {
  84. remoteApnsKey.value = legacyApnsKey.value
  85. remoteKeyId.value = legacyKeyId.value
  86. }
  87. // 4. Clean up old keys
  88. legacyReturnApnsKey.remove()
  89. legacyReturnKeyId.remove()
  90. legacyApnsKey.remove()
  91. legacyKeyId.remove()
  92. }
  93. func migrateStep3() {
  94. LogManager.shared.log(category: .general, message: "Running migrateStep3 - this should only happen once!")
  95. let legacyForceDarkMode = StorageValue<Bool>(key: "forceDarkMode", defaultValue: true)
  96. if legacyForceDarkMode.exists {
  97. Storage.shared.appearanceMode.value = legacyForceDarkMode.value ? .dark : .system
  98. legacyForceDarkMode.remove()
  99. }
  100. if !TabPosition.customizablePositions.contains(homePosition.value.normalized) {
  101. LogManager.shared.log(category: .general, message: "migrateStep3: Setting home to position1 (was \(homePosition.value.rawValue))")
  102. homePosition.value = .position1
  103. }
  104. if !TabPosition.customizablePositions.contains(snoozerPosition.value.normalized) {
  105. // Move any unstored occupant at position 3 to menu before placing snoozer,
  106. // to avoid a collision when dev defaults differ from main.
  107. for item in TabItem.allCases where item != .snoozer {
  108. if position(for: item).normalized == .position3,
  109. !migratePositionExists(for: item)
  110. {
  111. setPosition(.menu, for: item)
  112. }
  113. }
  114. if tabItem(at: .position3) == nil {
  115. snoozerPosition.value = .position3
  116. }
  117. }
  118. if alarmsPosition.value == .more {
  119. alarmsPosition.value = .menu
  120. }
  121. if remotePosition.value == .more {
  122. remotePosition.value = .menu
  123. }
  124. if nightscoutPosition.value == .more {
  125. nightscoutPosition.value = .menu
  126. }
  127. }
  128. func migrateStep2() {
  129. // Migrate from old system to new position-based system
  130. if remoteType.value != .none {
  131. remotePosition.value = .position2
  132. alarmsPosition.value = .menu
  133. } else {
  134. alarmsPosition.value = .position2
  135. remotePosition.value = .menu
  136. }
  137. nightscoutPosition.value = .position4
  138. }
  139. // MARK: - Migration helpers (can be removed when step 5 is removed)
  140. /// Check whether a tab item's position was explicitly stored in UserDefaults
  141. /// (as opposed to using the StorageValue default).
  142. private func migratePositionExists(for item: TabItem) -> Bool {
  143. switch item {
  144. case .home: return homePosition.exists
  145. case .alarms: return alarmsPosition.exists
  146. case .remote: return remotePosition.exists
  147. case .nightscout: return nightscoutPosition.exists
  148. case .snoozer: return snoozerPosition.exists
  149. case .stats: return statisticsPosition.exists
  150. case .treatments: return treatmentsPosition.exists
  151. }
  152. }
  153. }