Storage+Migrate.swift 80 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844
  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 migrateStep6() {
  54. // APNs credential separation
  55. LogManager.shared.log(category: .general, message: "Running migrateStep6 — APNs credential separation")
  56. let legacyReturnApnsKey = StorageValue<String>(key: "returnApnsKey", defaultValue: "")
  57. let legacyReturnKeyId = StorageValue<String>(key: "returnKeyId", defaultValue: "")
  58. let legacyApnsKey = StorageValue<String>(key: "apnsKey", defaultValue: "")
  59. let legacyKeyId = StorageValue<String>(key: "keyId", defaultValue: "")
  60. // 1. If returnApnsKey had a value, that was LoopFollow's own key (different team scenario)
  61. if legacyReturnApnsKey.exists, !legacyReturnApnsKey.value.isEmpty {
  62. lfApnsKey.value = legacyReturnApnsKey.value
  63. lfKeyId.value = legacyReturnKeyId.value
  64. }
  65. // 2. If lfApnsKey is still empty and the old primary key exists,
  66. // check if same team — if so, the primary key was used for everything
  67. if lfApnsKey.value.isEmpty, legacyApnsKey.exists, !legacyApnsKey.value.isEmpty {
  68. let lfTeamId = BuildDetails.default.teamID ?? ""
  69. let remoteTeamId = teamId.value ?? ""
  70. let sameTeam = !lfTeamId.isEmpty && (remoteTeamId.isEmpty || lfTeamId == remoteTeamId)
  71. if sameTeam {
  72. lfApnsKey.value = legacyApnsKey.value
  73. lfKeyId.value = legacyKeyId.value
  74. }
  75. }
  76. // 3. Move old primary credentials to remoteApnsKey/remoteKeyId
  77. if legacyApnsKey.exists, !legacyApnsKey.value.isEmpty {
  78. remoteApnsKey.value = legacyApnsKey.value
  79. remoteKeyId.value = legacyKeyId.value
  80. }
  81. // 4. Clean up old keys
  82. legacyReturnApnsKey.remove()
  83. legacyReturnKeyId.remove()
  84. legacyApnsKey.remove()
  85. legacyKeyId.remove()
  86. }
  87. func migrateStep3() {
  88. LogManager.shared.log(category: .general, message: "Running migrateStep3 - this should only happen once!")
  89. let legacyForceDarkMode = StorageValue<Bool>(key: "forceDarkMode", defaultValue: true)
  90. if legacyForceDarkMode.exists {
  91. Storage.shared.appearanceMode.value = legacyForceDarkMode.value ? .dark : .system
  92. legacyForceDarkMode.remove()
  93. }
  94. if !TabPosition.customizablePositions.contains(homePosition.value.normalized) {
  95. LogManager.shared.log(category: .general, message: "migrateStep3: Setting home to position1 (was \(homePosition.value.rawValue))")
  96. homePosition.value = .position1
  97. }
  98. if !TabPosition.customizablePositions.contains(snoozerPosition.value.normalized) {
  99. // Move any unstored occupant at position 3 to menu before placing snoozer,
  100. // to avoid a collision when dev defaults differ from main.
  101. for item in TabItem.allCases where item != .snoozer {
  102. if position(for: item).normalized == .position3,
  103. !migratePositionExists(for: item)
  104. {
  105. setPosition(.menu, for: item)
  106. }
  107. }
  108. if tabItem(at: .position3) == nil {
  109. snoozerPosition.value = .position3
  110. }
  111. }
  112. if alarmsPosition.value == .more {
  113. alarmsPosition.value = .menu
  114. }
  115. if remotePosition.value == .more {
  116. remotePosition.value = .menu
  117. }
  118. if nightscoutPosition.value == .more {
  119. nightscoutPosition.value = .menu
  120. }
  121. }
  122. func migrateStep2() {
  123. // Migrate from old system to new position-based system
  124. if remoteType.value != .none {
  125. remotePosition.value = .position2
  126. alarmsPosition.value = .menu
  127. } else {
  128. alarmsPosition.value = .position2
  129. remotePosition.value = .menu
  130. }
  131. nightscoutPosition.value = .position4
  132. }
  133. func migrateStep1() {
  134. // Guard each field with .exists so that if the App Group suite is unreadable
  135. // (e.g. Before-First-Unlock state after a reboot), we skip the write rather
  136. // than overwriting the already-migrated Standard value with an empty default.
  137. if ObservableUserDefaults.shared.old_url.exists {
  138. Storage.shared.url.value = ObservableUserDefaults.shared.old_url.value
  139. }
  140. if ObservableUserDefaults.shared.old_device.exists {
  141. Storage.shared.device.value = ObservableUserDefaults.shared.old_device.value
  142. }
  143. if ObservableUserDefaults.shared.old_nsWriteAuth.exists {
  144. Storage.shared.nsWriteAuth.value = ObservableUserDefaults.shared.old_nsWriteAuth.value
  145. }
  146. if ObservableUserDefaults.shared.old_nsAdminAuth.exists {
  147. Storage.shared.nsAdminAuth.value = ObservableUserDefaults.shared.old_nsAdminAuth.value
  148. }
  149. // Helper: 1-to-1 type -----------------------------------------------------------------
  150. func move<T: AnyConvertible & Equatable>(
  151. _ legacy: @autoclosure () -> UserDefaultsValue<T>,
  152. into newValue: StorageValue<T>
  153. ) {
  154. let item = legacy()
  155. guard item.exists else { return }
  156. newValue.value = item.value
  157. item.setNil(key: item.key)
  158. }
  159. // Helper: Float → Double ------------------------------------------------------------
  160. func moveFloatToDouble(
  161. _ legacy: @autoclosure () -> UserDefaultsValue<Float>,
  162. into newValue: StorageValue<Double>
  163. ) {
  164. let item = legacy()
  165. guard item.exists else { return }
  166. newValue.value = Double(item.value)
  167. item.setNil(key: item.key)
  168. }
  169. // Remove this in a year later than the release of the new Alarms [BEGIN]
  170. let legacyColorBGText = UserDefaultsValue<Bool>(key: "colorBGText", default: true)
  171. if legacyColorBGText.exists {
  172. Storage.shared.colorBGText.value = legacyColorBGText.value
  173. legacyColorBGText.setNil(key: "colorBGText")
  174. }
  175. let legacyAppBadge = UserDefaultsValue<Bool>(key: "appBadge", default: true)
  176. if legacyAppBadge.exists {
  177. Storage.shared.appBadge.value = legacyAppBadge.value
  178. legacyAppBadge.setNil(key: "appBadge")
  179. }
  180. let legacyForceDarkMode = UserDefaultsValue<Bool>(key: "forceDarkMode", default: true)
  181. if legacyForceDarkMode.exists {
  182. // Migrate from Bool to AppearanceMode: true -> .dark, false -> .system
  183. Storage.shared.appearanceMode.value = legacyForceDarkMode.value ? .dark : .system
  184. legacyForceDarkMode.setNil(key: "forceDarkMode")
  185. }
  186. let legacyShowStats = UserDefaultsValue<Bool>(key: "showStats", default: true)
  187. if legacyShowStats.exists {
  188. Storage.shared.showStats.value = legacyShowStats.value
  189. legacyShowStats.setNil(key: "showStats")
  190. }
  191. let legacyUseIFCC = UserDefaultsValue<Bool>(key: "useIFCC", default: false)
  192. if legacyUseIFCC.exists {
  193. Storage.shared.useIFCC.value = legacyUseIFCC.value
  194. legacyUseIFCC.setNil(key: "useIFCC")
  195. }
  196. let legacyShowSmallGraph = UserDefaultsValue<Bool>(key: "showSmallGraph", default: true)
  197. if legacyShowSmallGraph.exists {
  198. Storage.shared.showSmallGraph.value = legacyShowSmallGraph.value
  199. legacyShowSmallGraph.setNil(key: "showSmallGraph")
  200. }
  201. let legacyScreenlockSwitchState = UserDefaultsValue<Bool>(key: "screenlockSwitchState", default: true)
  202. if legacyScreenlockSwitchState.exists {
  203. Storage.shared.screenlockSwitchState.value = legacyScreenlockSwitchState.value
  204. legacyScreenlockSwitchState.setNil(key: "screenlockSwitchState")
  205. }
  206. let legacyShowDisplayName = UserDefaultsValue<Bool>(key: "showDisplayName", default: false)
  207. if legacyShowDisplayName.exists {
  208. Storage.shared.showDisplayName.value = legacyShowDisplayName.value
  209. legacyShowDisplayName.setNil(key: "showDisplayName")
  210. }
  211. let legacySpeakBG = UserDefaultsValue<Bool>(key: "speakBG", default: false)
  212. if legacySpeakBG.exists {
  213. Storage.shared.speakBG.value = legacySpeakBG.value
  214. legacySpeakBG.setNil(key: "speakBG")
  215. }
  216. let legacySpeakBGAlways = UserDefaultsValue<Bool>(key: "speakBGAlways", default: true)
  217. if legacySpeakBGAlways.exists {
  218. Storage.shared.speakBGAlways.value = legacySpeakBGAlways.value
  219. legacySpeakBGAlways.setNil(key: "speakBGAlways")
  220. }
  221. let legacySpeakLowBG = UserDefaultsValue<Bool>(key: "speakLowBG", default: false)
  222. if legacySpeakLowBG.exists {
  223. Storage.shared.speakLowBG.value = legacySpeakLowBG.value
  224. legacySpeakLowBG.setNil(key: "speakLowBG")
  225. }
  226. let legacySpeakProactiveLowBG = UserDefaultsValue<Bool>(key: "speakProactiveLowBG", default: false)
  227. if legacySpeakProactiveLowBG.exists {
  228. Storage.shared.speakProactiveLowBG.value = legacySpeakProactiveLowBG.value
  229. legacySpeakProactiveLowBG.setNil(key: "speakProactiveLowBG")
  230. }
  231. let legacySpeakFastDropDelta = UserDefaultsValue<Float>(key: "speakFastDropDelta", default: 10.0)
  232. if legacySpeakFastDropDelta.exists {
  233. Storage.shared.speakFastDropDelta.value = Double(legacySpeakFastDropDelta.value)
  234. legacySpeakFastDropDelta.setNil(key: "speakFastDropDelta")
  235. }
  236. let legacySpeakLowBGLimit = UserDefaultsValue<Float>(key: "speakLowBGLimit", default: 72.0)
  237. if legacySpeakLowBGLimit.exists {
  238. Storage.shared.speakLowBGLimit.value = Double(legacySpeakLowBGLimit.value)
  239. legacySpeakLowBGLimit.setNil(key: "speakLowBGLimit")
  240. }
  241. let legacySpeakHighBGLimit = UserDefaultsValue<Float>(key: "speakHighBGLimit", default: 180.0)
  242. if legacySpeakHighBGLimit.exists {
  243. Storage.shared.speakHighBGLimit.value = Double(legacySpeakHighBGLimit.value)
  244. legacySpeakHighBGLimit.setNil(key: "speakHighBGLimit")
  245. }
  246. let legacySpeakHighBG = UserDefaultsValue<Bool>(key: "speakHighBG", default: false)
  247. if legacySpeakHighBG.exists {
  248. Storage.shared.speakHighBG.value = legacySpeakHighBG.value
  249. legacySpeakHighBG.setNil(key: "speakHighBG")
  250. }
  251. let legacySpeakLanguage = UserDefaultsValue<String>(key: "speakLanguage", default: "en")
  252. if legacySpeakLanguage.exists {
  253. Storage.shared.speakLanguage.value = legacySpeakLanguage.value
  254. legacySpeakLanguage.setNil(key: "speakLanguage")
  255. }
  256. move(UserDefaultsValue<Bool>(key: "persistentNotification", default: true), into: Storage.shared.persistentNotification)
  257. // ── General (done earlier, but safe to repeat) ──
  258. move(UserDefaultsValue<Bool>(key: "colorBGText", default: true), into: Storage.shared.colorBGText)
  259. move(UserDefaultsValue<Bool>(key: "appBadge", default: true), into: appBadge)
  260. // Note: forceDarkMode migration to appearanceMode is handled earlier in migrateGeneralSettings()
  261. move(UserDefaultsValue<Bool>(key: "showStats", default: true), into: showStats)
  262. move(UserDefaultsValue<Bool>(key: "useIFCC", default: false), into: useIFCC)
  263. move(UserDefaultsValue<Bool>(key: "showSmallGraph", default: true), into: showSmallGraph)
  264. move(UserDefaultsValue<Bool>(key: "screenlockSwitchState", default: false), into: screenlockSwitchState)
  265. move(UserDefaultsValue<Bool>(key: "showDisplayName", default: false), into: showDisplayName)
  266. // ── Speak-BG ──
  267. move(UserDefaultsValue<Bool>(key: "speakBG", default: false), into: speakBG)
  268. move(UserDefaultsValue<Bool>(key: "speakBGAlways", default: true), into: speakBGAlways)
  269. move(UserDefaultsValue<Bool>(key: "speakLowBG", default: false), into: speakLowBG)
  270. move(UserDefaultsValue<Bool>(key: "speakProactiveLowBG", default: false), into: speakProactiveLowBG)
  271. move(UserDefaultsValue<Bool>(key: "speakHighBG", default: false), into: speakHighBG)
  272. moveFloatToDouble(UserDefaultsValue<Float>(key: "speakLowBGLimit", default: 72.0), into: speakLowBGLimit)
  273. moveFloatToDouble(UserDefaultsValue<Float>(key: "speakHighBGLimit", default: 180.0), into: speakHighBGLimit)
  274. moveFloatToDouble(UserDefaultsValue<Float>(key: "speakFastDropDelta", default: 10.0), into: speakFastDropDelta)
  275. move(UserDefaultsValue<String>(key: "speakLanguage", default: "en"), into: speakLanguage)
  276. // ── Graph ──
  277. move(UserDefaultsValue<Bool>(key: "showDots", default: true), into: showDots)
  278. move(UserDefaultsValue<Bool>(key: "showLines", default: true), into: showLines)
  279. move(UserDefaultsValue<Bool>(key: "showValues", default: true), into: showValues)
  280. move(UserDefaultsValue<Bool>(key: "showAbsorption", default: true), into: showAbsorption)
  281. move(UserDefaultsValue<Bool>(key: "showDIAMarkers", default: true), into: showDIALines)
  282. move(UserDefaultsValue<Bool>(key: "show30MinLine", default: false), into: show30MinLine)
  283. move(UserDefaultsValue<Bool>(key: "show90MinLine", default: false), into: show90MinLine)
  284. move(UserDefaultsValue<Bool>(key: "showMidnightMarkers", default: false), into: showMidnightLines)
  285. move(UserDefaultsValue<Bool>(key: "smallGraphTreatments", default: true), into: smallGraphTreatments)
  286. move(UserDefaultsValue<Int>(key: "smallGraphHeight", default: 40), into: smallGraphHeight)
  287. move(UserDefaultsValue<Double>(key: "predictionToLoad", default: 1.0), into: predictionToLoad)
  288. move(UserDefaultsValue<Double>(key: "minBasalScale", default: 5.0), into: minBasalScale)
  289. moveFloatToDouble(UserDefaultsValue<Float>(key: "minBGScale", default: 250.0), into: minBGScale)
  290. moveFloatToDouble(UserDefaultsValue<Float>(key: "lowLine", default: 70.0), into: lowLine)
  291. moveFloatToDouble(UserDefaultsValue<Float>(key: "highLine", default: 180.0), into: highLine)
  292. move(UserDefaultsValue<Int>(key: "downloadDays", default: 1), into: downloadDays)
  293. // Remove this in a year later than the release of the new Alarms [END]
  294. // ── Watch / Calendar ────────────────────────────────────────────────
  295. move(UserDefaultsValue<Bool>(key: "writeCalendarEvent", default: false), into: writeCalendarEvent)
  296. move(UserDefaultsValue<String>(key: "calendarIdentifier", default: ""), into: calendarIdentifier)
  297. move(UserDefaultsValue<String>(key: "watchLine1", default: "%BG% %DIRECTION% %DELTA% %MINAGO%"), into: watchLine1)
  298. move(UserDefaultsValue<String>(key: "watchLine2", default: "C:%COB% I:%IOB% B:%BASAL%"), into: watchLine2)
  299. // Migration of generic alarm settings
  300. // ── AlarmConfiguration migration ─────────────────────────────────────────
  301. do {
  302. // Work on a mutable copy, then write the whole thing back once.
  303. var cfg = Storage.shared.alarmConfiguration.value
  304. let cal = Calendar.current
  305. /// Copy *one* legacy value → struct field → delete old key
  306. func move<T: AnyConvertible & Equatable>(
  307. _ legacy: @autoclosure () -> UserDefaultsValue<T>,
  308. write: (inout AlarmConfiguration, T) -> Void
  309. ) {
  310. let item = legacy()
  311. guard item.exists else { return }
  312. write(&cfg, item.value)
  313. item.setNil(key: item.key)
  314. }
  315. // 1. Override-volume toggle
  316. move(UserDefaultsValue<Bool>(key: "overrideSystemOutputVolume",
  317. default: cfg.overrideSystemOutputVolume))
  318. {
  319. $0.overrideSystemOutputVolume = $1
  320. }
  321. // 2. Forced output volume itself.
  322. // Prefer newer key (“forcedOutputVolume”); otherwise fall back.
  323. if UserDefaultsValue<Float>(key: "forcedOutputVolume",
  324. default: cfg.forcedOutputVolume).exists
  325. {
  326. move(UserDefaultsValue<Float>(key: "forcedOutputVolume",
  327. default: cfg.forcedOutputVolume))
  328. {
  329. $0.forcedOutputVolume = $1
  330. }
  331. } else {
  332. move(UserDefaultsValue<Float>(key: "systemOutputVolume",
  333. default: cfg.forcedOutputVolume))
  334. {
  335. $0.forcedOutputVolume = $1
  336. }
  337. }
  338. // 3. Play audio during phone calls
  339. move(UserDefaultsValue<Bool>(key: "alertAudioDuringPhone",
  340. default: cfg.audioDuringCalls))
  341. {
  342. $0.audioDuringCalls = $1
  343. }
  344. // 4. Auto-snooze CGM-start alarm
  345. move(UserDefaultsValue<Bool>(key: "alertAutoSnoozeCGMStart",
  346. default: cfg.autoSnoozeCGMStart))
  347. {
  348. $0.autoSnoozeCGMStart = $1
  349. }
  350. // 5. Global “Snooze all” → snoozeUntil
  351. move(UserDefaultsValue<Date?>(key: "alertSnoozeAllTime",
  352. default: cfg.snoozeUntil))
  353. {
  354. $0.snoozeUntil = $1
  355. }
  356. // 6. Global “Mute all” → muteUntil
  357. move(UserDefaultsValue<Date?>(key: "alertMuteAllTime",
  358. default: cfg.muteUntil))
  359. {
  360. $0.muteUntil = $1
  361. }
  362. // 7 & 8. Legacy quiet-hours → day/night start
  363. // (only if both dates exist and are on the same “reference” day)
  364. let qStart = UserDefaultsValue<Date?>(key: "quietHourStart", default: nil)
  365. let qEnd = UserDefaultsValue<Date?>(key: "quietHourEnd", default: nil)
  366. if let s = qStart.value, let e = qEnd.value {
  367. let compsStart = cal.dateComponents([.hour, .minute], from: s)
  368. let compsEnd = cal.dateComponents([.hour, .minute], from: e)
  369. if let sh = compsStart.hour, let sm = compsStart.minute,
  370. let eh = compsEnd.hour, let em = compsEnd.minute
  371. {
  372. cfg.nightStart = TimeOfDay(hour: sh, minute: sm)
  373. cfg.dayStart = TimeOfDay(hour: eh, minute: em)
  374. qStart.setNil(key: qStart.key)
  375. qEnd.setNil(key: qEnd.key)
  376. }
  377. }
  378. // 9. Legacy “ignore zero BG” flag → ignoreZeroBG
  379. move(UserDefaultsValue<Bool>(key: "alertIgnoreZero",
  380. default: cfg.ignoreZeroBG))
  381. {
  382. $0.ignoreZeroBG = $1
  383. }
  384. // finally persist the whole struct
  385. Storage.shared.alarmConfiguration.value = cfg
  386. }
  387. // ── Dexcom Share --------------------------------------------------------
  388. move(UserDefaultsValue<String>(key: "shareUserName", default: ""),
  389. into: Storage.shared.shareUserName)
  390. move(UserDefaultsValue<String>(key: "sharePassword", default: ""),
  391. into: Storage.shared.sharePassword)
  392. move(UserDefaultsValue<String>(key: "shareServer", default: "US"),
  393. into: Storage.shared.shareServer)
  394. // ── Graph ---------------------------------------------------------------
  395. moveFloatToDouble(
  396. UserDefaultsValue<Float>(key: "chartScaleX", default: 18.0),
  397. into: Storage.shared.chartScaleX
  398. )
  399. // ── Advanced settings ---------------------------------------------------
  400. move(UserDefaultsValue<Bool>(key: "downloadTreatments", default: true),
  401. into: Storage.shared.downloadTreatments)
  402. move(UserDefaultsValue<Bool>(key: "downloadPrediction", default: true),
  403. into: Storage.shared.downloadPrediction)
  404. move(UserDefaultsValue<Bool>(key: "graphOtherTreatments", default: true),
  405. into: Storage.shared.graphOtherTreatments)
  406. move(UserDefaultsValue<Bool>(key: "graphBasal", default: true),
  407. into: Storage.shared.graphBasal)
  408. move(UserDefaultsValue<Bool>(key: "graphBolus", default: true),
  409. into: Storage.shared.graphBolus)
  410. move(UserDefaultsValue<Bool>(key: "graphCarbs", default: true),
  411. into: Storage.shared.graphCarbs)
  412. move(UserDefaultsValue<Int>(key: "bgUpdateDelay", default: 10),
  413. into: Storage.shared.bgUpdateDelay)
  414. // ── Insert times --------------------------------------------------------
  415. move(UserDefaultsValue<TimeInterval>(key: "alertCageInsertTime", default: 0),
  416. into: Storage.shared.cageInsertTime)
  417. move(UserDefaultsValue<TimeInterval>(key: "alertSageInsertTime", default: 0),
  418. into: Storage.shared.sageInsertTime)
  419. // ── Version-cache / notification bookkeeping ---------------------------
  420. move(UserDefaultsValue<String?>(key: "cachedForVersion", default: nil),
  421. into: Storage.shared.cachedForVersion)
  422. move(UserDefaultsValue<String?>(key: "latestVersion", default: nil),
  423. into: Storage.shared.latestVersion)
  424. move(UserDefaultsValue<Date?>(key: "latestVersionChecked", default: nil),
  425. into: Storage.shared.latestVersionChecked)
  426. move(UserDefaultsValue<Bool>(key: "currentVersionBlackListed", default: false),
  427. into: Storage.shared.currentVersionBlackListed)
  428. move(UserDefaultsValue<Date?>(key: "lastBlacklistNotificationShown", default: nil),
  429. into: Storage.shared.lastBlacklistNotificationShown)
  430. move(UserDefaultsValue<Date?>(key: "lastVersionUpdateNotificationShown", default: nil),
  431. into: Storage.shared.lastVersionUpdateNotificationShown)
  432. move(UserDefaultsValue<Date?>(key: "lastExpirationNotificationShown", default: nil),
  433. into: Storage.shared.lastExpirationNotificationShown)
  434. move(UserDefaultsValue<Bool>(key: "hideInfoTable", default: false), into: Storage.shared.hideInfoTable)
  435. move(UserDefaultsValue<String>(key: "token", default: ""), into: Storage.shared.token)
  436. move(UserDefaultsValue<String>(key: "units", default: "mg/dL"), into: Storage.shared.units)
  437. move(UserDefaultsValue<[Int]>(key: "infoSort",
  438. default: InfoType.allCases.map { $0.sortOrder }),
  439. into: Storage.shared.infoSort)
  440. move(UserDefaultsValue<[Bool]>(key: "infoVisible",
  441. default: InfoType.allCases.map { $0.defaultVisible }),
  442. into: Storage.shared.infoVisible)
  443. migrateUrgentLowAlarm()
  444. migrateLowAlarm()
  445. migrateHighAlarm()
  446. migrateUrgentHighAlarm()
  447. migrateFastDropAlarm()
  448. migrateFastRiseAlarm()
  449. migrateMissedReadingAlarm()
  450. migrateNotLoopingAlarm()
  451. migrateMissedBolusAlarm()
  452. migrateSensorChangeAlarm()
  453. migratePumpChangeAlarm()
  454. migrateOverrideStartAlarm()
  455. migrateOverrideEndAlarm()
  456. migrateTempTargetStartAlarm()
  457. migrateTempTargetEndAlarm()
  458. migrateTemporaryBGAlarm()
  459. migratePumpVolumeAlarm()
  460. migrateIOBAlarm()
  461. migrateCOBAlarm()
  462. migrateBatteryAlarm()
  463. migrateBatteryDropAlarm()
  464. migrateRecBolusAlarm()
  465. }
  466. // MARK: - One-off alarm migrations
  467. /// Reads *all* `alertUrgentLow*` keys, converts them into a single `Alarm`,
  468. /// saves it to the modern `[Alarm]` store, then deletes the legacy keys.
  469. private func migrateUrgentLowAlarm() {
  470. // Did the user ever change that alert? (No key ⇒ nothing to do.)
  471. guard UserDefaultsValue<Bool>(key: "alertUrgentLowActive", default: false).exists else { return }
  472. /// Helper: fetch-then-delete a legacy value in one line.
  473. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  474. let box = UserDefaultsValue<V>(key: key, default: def)
  475. defer { box.setNil(key: key) }
  476. return box.value
  477. }
  478. // Build the new Alarm ------------------------------------------------
  479. var alarm = Alarm(type: .low)
  480. alarm.name = "Urgent Low"
  481. alarm.isEnabled = take("alertUrgentLowActive", default: false)
  482. alarm.belowBG = Double(take("alertUrgentLowBG", default: 55.0))
  483. alarm.predictiveMinutes = take("alertUrgentLowPredictiveMinutes", default: 0)
  484. alarm.snoozeDuration = take("alertUrgentLowSnooze", default: 5)
  485. alarm.snoozedUntil = take("alertUrgentLowSnoozedTime", default: nil as Date?)
  486. alarm.soundFile = SoundFile(
  487. rawValue: take("alertUrgentLowSound",
  488. default: "Emergency_Alarm_Siren"))
  489. ?? .emergencyAlarmSiren
  490. alarm.playSoundOption = PlaySoundOption(
  491. rawValue: take("alertUrgentLowAudible",
  492. default: "Always").lowercased()) ?? .always
  493. alarm.repeatSoundOption = RepeatSoundOption(
  494. rawValue: take("alertUrgentLowRepeat",
  495. default: "Always").lowercased()) ?? .always
  496. // Day / Night active-window (“Pre-Snooze”)
  497. let autoStr = take("alertUrgentLowAutosnooze", default: "Never").lowercased()
  498. let dayFlag = take("alertUrgentLowAutosnoozeDay", default: false)
  499. let nightFlag = take("alertUrgentLowAutosnoozeNight", default: false)
  500. alarm.activeOption = {
  501. if dayFlag, !nightFlag { return .day }
  502. if !dayFlag, nightFlag { return .night }
  503. switch autoStr {
  504. case "day", "at day": return .day
  505. case "night", "at night": return .night
  506. default: return .always // “Never” → always active
  507. }
  508. }()
  509. // Persist -----------------------------------------------------------------
  510. Storage.shared.alarms.value.append(alarm)
  511. }
  512. // MARK: - Low-BG alarm -------------------------------------------------------
  513. private func migrateLowAlarm() {
  514. // Bail if the old keys were never written
  515. guard UserDefaultsValue<Bool>(key: "alertLowActive", default: false).exists else { return }
  516. // tiny helper: fetch value → erase key
  517. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  518. let box = UserDefaultsValue<V>(key: key, default: def)
  519. defer { box.setNil(key: key) }
  520. return box.value
  521. }
  522. // Build the new Alarm ----------------------------------------------------
  523. var alarm = Alarm(type: .low)
  524. alarm.name = "Low"
  525. alarm.isEnabled = take("alertLowActive", default: false)
  526. alarm.belowBG = Double(take("alertLowBG", default: 70.0))
  527. // “Persistent ≥ X min” → `persistentMinutes`
  528. alarm.persistentMinutes = take("alertLowPersistent", default: 0)
  529. // “Persistence max BG drop” -- ignoring this for now
  530. // let persistentLowTriggerImmediatelyBG = UserDefaultsRepository.alertLowBG.value - UserDefaultsRepository.alertLowPersistenceMax.value
  531. // (Float(persistentLowBG) <= UserDefaultsRepository.alertLowBG.value || Float(currentBG) <= persistentLowTriggerImmediatelyBG)
  532. _ = Double(take("alertLowPersistenceMax", default: 5.0))
  533. alarm.snoozeDuration = take("alertLowSnooze", default: 5)
  534. alarm.snoozedUntil = take("alertLowSnoozedTime", default: nil as Date?)
  535. alarm.soundFile = SoundFile(
  536. rawValue: take("alertLowSound",
  537. default: "Indeed")) ?? .indeed
  538. alarm.playSoundOption = PlaySoundOption(
  539. rawValue: take("alertLowAudible",
  540. default: "Always").lowercased()) ?? .always
  541. alarm.repeatSoundOption = RepeatSoundOption(
  542. rawValue: take("alertLowRepeat",
  543. default: "Always").lowercased()) ?? .always
  544. // activeOption ← legacy “Pre-Snooze” flags / picker
  545. let autoStr = take("alertLowAutosnooze", default: "Never").lowercased()
  546. let dayFlag = take("alertLowAutosnoozeDay", default: false)
  547. let nightFlag = take("alertLowAutosnoozeNight", default: false)
  548. alarm.activeOption = {
  549. if dayFlag, !nightFlag { return .day }
  550. if !dayFlag, nightFlag { return .night }
  551. switch autoStr {
  552. case "day", "at day": return .day
  553. case "night", "at night": return .night
  554. default: return .always
  555. }
  556. }()
  557. // Done → append to the modern store
  558. Storage.shared.alarms.value.append(alarm)
  559. }
  560. // MARK: - High-BG alarm -----------------------------------------------------
  561. private func migrateHighAlarm() {
  562. // Only run if the legacy key ever existed
  563. guard UserDefaultsValue<Bool>(key: "alertHighActive", default: false).exists else { return }
  564. /// Fetch → erase helper
  565. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  566. let box = UserDefaultsValue<V>(key: key, default: def)
  567. defer { box.setNil(key: key) } // remove legacy value
  568. return box.value
  569. }
  570. // ---------- Build Alarm -----------------------------------------------
  571. var alarm = Alarm(type: .high)
  572. alarm.name = "High"
  573. alarm.isEnabled = take("alertHighActive", default: false)
  574. alarm.aboveBG = Double(take("alertHighBG", default: 180.0))
  575. alarm.persistentMinutes = take("alertHighPersistent", default: 60)
  576. alarm.snoozeDuration = take("alertHighSnooze", default: 60)
  577. alarm.snoozedUntil = take("alertHighSnoozedTime", default: nil as Date?)
  578. alarm.soundFile = SoundFile(
  579. rawValue: take("alertHighSound",
  580. default: "Time_Has_Come")) ?? .timeHasCome
  581. alarm.playSoundOption = PlaySoundOption(
  582. rawValue: take("alertHighAudible",
  583. default: "Always").lowercased()) ?? .always
  584. alarm.repeatSoundOption = RepeatSoundOption(
  585. rawValue: take("alertHighRepeat",
  586. default: "Always").lowercased()) ?? .always
  587. // ── activeOption derived from “Pre-Snooze” picker & flags
  588. let autoStr = take("alertHighAutosnooze", default: "Never").lowercased()
  589. let dayFlag = take("alertHighAutosnoozeDay", default: false)
  590. let nightFlag = take("alertHighAutosnoozeNight", default: false)
  591. alarm.activeOption = {
  592. if dayFlag, !nightFlag { return .day }
  593. if !dayFlag, nightFlag { return .night }
  594. switch autoStr {
  595. case "day", "at day": return .day
  596. case "night", "at night": return .night
  597. default: return .always
  598. }
  599. }()
  600. // ---------- Persist & we’re done --------------------------------------
  601. Storage.shared.alarms.value.append(alarm)
  602. }
  603. // MARK: - Urgent-High alarm --------------------------------------------------
  604. private func migrateUrgentHighAlarm() {
  605. // run only once, only if the user ever changed that toggle
  606. guard UserDefaultsValue<Bool>(key: "alertUrgentHighActive", default: false).exists else { return }
  607. // helper: read-then-erase a legacy value
  608. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  609. let box = UserDefaultsValue<V>(key: key, default: def)
  610. defer { box.setNil(key: key) } // wipe legacy key
  611. return box.value
  612. }
  613. // ───────── Build the Alarm ────────────────────────────────────────────
  614. var alarm = Alarm(type: .high) // we map to the existing `.high` type
  615. alarm.name = "Urgent High"
  616. alarm.isEnabled = take("alertUrgentHighActive", default: false)
  617. alarm.aboveBG = Double(take("alertUrgentHighBG", default: 250.0))
  618. alarm.snoozeDuration = take("alertUrgentHighSnooze", default: 30)
  619. alarm.snoozedUntil = take("alertUrgentHighSnoozedTime", default: nil as Date?)
  620. alarm.soundFile = SoundFile(
  621. rawValue: take("alertUrgentHighSound",
  622. default: "Pager_Beeps")) ?? .pagerBeeps
  623. alarm.playSoundOption = PlaySoundOption(
  624. rawValue: take("alertUrgentHighAudible",
  625. default: "Always").lowercased()) ?? .always
  626. alarm.repeatSoundOption = RepeatSoundOption(
  627. rawValue: take("alertUrgentHighRepeat",
  628. default: "Always").lowercased()) ?? .always
  629. // activeOption comes from the old “Pre-Snooze” picker + its day/night flags
  630. let autoStr = take("alertUrgentHighAutosnooze", default: "Never").lowercased()
  631. let dayFlag = take("alertUrgentHighAutosnoozeDay", default: false)
  632. let nightFlag = take("alertUrgentHighAutosnoozeNight", default: false)
  633. alarm.activeOption = {
  634. if dayFlag, !nightFlag { return .day }
  635. if !dayFlag, nightFlag { return .night }
  636. switch autoStr { // fall back to picker value
  637. case "day", "at day": return .day
  638. case "night", "at night": return .night
  639. default: return .always
  640. }
  641. }()
  642. // ───────── Persist in new storage ─────────────────────────────────────
  643. Storage.shared.alarms.value.append(alarm)
  644. }
  645. // MARK: - Fast-Drop alarm ----------------------------------------------------
  646. private func migrateFastDropAlarm() {
  647. guard UserDefaultsValue<Bool>(key: "alertFastDropDeltaActive",
  648. default: false).exists else { return }
  649. // helper: read-then-erase
  650. func take<V: AnyConvertible & Equatable>(_ k: String, default d: V) -> V {
  651. let box = UserDefaultsValue<V>(key: k, default: d)
  652. defer { box.setNil(key: k) }
  653. return box.value
  654. }
  655. var alarm = Alarm(type: .fastDrop)
  656. alarm.name = "Fast Drop"
  657. alarm.isEnabled = take("alertFastDropDeltaActive", default: false)
  658. // core trigger parameters
  659. alarm.delta = Double(take("alertFastDropDelta", default: 10.0))
  660. alarm.monitoringWindow = take("alertFastDropReadings", default: 3) - 1 // store #readings
  661. if take("alertFastDropUseLimit", default: false) {
  662. alarm.belowBG = Double(take("alertFastDropBelowBG", default: 120.0))
  663. }
  664. // snoozing
  665. alarm.snoozeDuration = take("alertFastDropDeltaSnooze", default: 10)
  666. alarm.snoozedUntil = take("alertFastDropSnoozedTime", default: nil as Date?)
  667. // sound + options
  668. alarm.soundFile = SoundFile(
  669. rawValue: take("alertFastDropSound", default: "Big_Clock_Ticking")
  670. ) ?? .bigClockTicking
  671. alarm.playSoundOption = PlaySoundOption(
  672. rawValue: take("alertFastDropAudible", default: "Always").lowercased()
  673. ) ?? .always
  674. alarm.repeatSoundOption = RepeatSoundOption(
  675. rawValue: take("alertFastDropRepeat", default: "Never").lowercased()
  676. ) ?? .never
  677. // activeOption from old “Pre-Snooze” picker + day/night flags
  678. let autoStr = take("alertFastDropAutosnooze", default: "Never").lowercased()
  679. let dayFlag = take("alertFastDropAutosnoozeDay", default: false)
  680. let nightFlag = take("alertFastDropAutosnoozeNight", default: false)
  681. alarm.activeOption = {
  682. if dayFlag, !nightFlag { return .day }
  683. if !dayFlag, nightFlag { return .night }
  684. switch autoStr {
  685. case "day", "at day": return .day
  686. case "night", "at night": return .night
  687. default: return .always
  688. }
  689. }()
  690. Storage.shared.alarms.value.append(alarm)
  691. }
  692. // MARK: - Fast-Rise alarm ----------------------------------------------------
  693. private func migrateFastRiseAlarm() {
  694. guard UserDefaultsValue<Bool>(key: "alertFastRiseDeltaActive",
  695. default: false).exists else { return }
  696. func take<V: AnyConvertible & Equatable>(_ k: String, default d: V) -> V {
  697. let box = UserDefaultsValue<V>(key: k, default: d)
  698. defer { box.setNil(key: k) }
  699. return box.value
  700. }
  701. var alarm = Alarm(type: .fastRise)
  702. alarm.name = "Fast Rise"
  703. alarm.isEnabled = take("alertFastRiseDeltaActive", default: false)
  704. alarm.delta = Double(take("alertFastRiseDelta", default: 10.0))
  705. alarm.monitoringWindow = take("alertFastRiseReadings", default: 3)
  706. if take("alertFastRiseUseLimit", default: false) {
  707. alarm.aboveBG = Double(take("alertFastRiseAboveBG", default: 200.0))
  708. }
  709. alarm.snoozeDuration = take("alertFastRiseDeltaSnooze", default: 10)
  710. alarm.snoozedUntil = take("alertFastRiseSnoozedTime", default: nil as Date?)
  711. alarm.soundFile = SoundFile(
  712. rawValue: take("alertFastRiseSound",
  713. default: "Cartoon_Fail_Strings_Trumpet")
  714. ) ?? .cartoonFailStringsTrumpet
  715. alarm.playSoundOption = PlaySoundOption(
  716. rawValue: take("alertFastRiseAudible", default: "Always").lowercased()
  717. ) ?? .always
  718. alarm.repeatSoundOption = RepeatSoundOption(
  719. rawValue: take("alertFastRiseRepeat", default: "Never").lowercased()
  720. ) ?? .never
  721. let autoStr = take("alertFastRiseAutosnooze", default: "Never").lowercased()
  722. let dayFlag = take("alertFastRiseAutosnoozeDay", default: false)
  723. let nightFlag = take("alertFastRiseAutosnoozeNight", default: false)
  724. alarm.activeOption = {
  725. if dayFlag, !nightFlag { return .day }
  726. if !dayFlag, nightFlag { return .night }
  727. switch autoStr {
  728. case "day", "at day": return .day
  729. case "night", "at night": return .night
  730. default: return .always
  731. }
  732. }()
  733. Storage.shared.alarms.value.append(alarm)
  734. }
  735. // MARK: - Missed-Reading alarm ---------------------------------------------
  736. private func migrateMissedReadingAlarm() {
  737. // Run only when the user has ever modified those settings
  738. guard UserDefaultsValue<Bool>(key: "alertMissedReadingActive",
  739. default: false).exists else { return }
  740. // read-then-erase helper
  741. func take<V: AnyConvertible & Equatable>(_ k: String, default d: V) -> V {
  742. let b = UserDefaultsValue<V>(key: k, default: d)
  743. defer { b.setNil(key: k) }
  744. return b.value
  745. }
  746. var alarm = Alarm(type: .missedReading)
  747. alarm.name = "Missed Reading"
  748. alarm.isEnabled = take("alertMissedReadingActive", default: false)
  749. // “No CGM data for X minutes”
  750. alarm.threshold = take("alertMissedReading", default: 31)
  751. // snoozing
  752. alarm.snoozeDuration = take("alertMissedReadingSnooze", default: 30)
  753. alarm.snoozedUntil = take("alertMissedReadingSnoozedTime",
  754. default: nil as Date?)
  755. // (legacy “is-snoozed” flag is implicit in snoozedUntil)
  756. // sound
  757. alarm.soundFile = SoundFile(
  758. rawValue: take("alertMissedReadingSound",
  759. default: "Cartoon_Tip_Toe_Sneaky_Walk")
  760. ) ?? .cartoonTipToeSneakyWalk
  761. // play / repeat options
  762. alarm.playSoundOption = PlaySoundOption(
  763. rawValue: take("alertMissedReadingAudible",
  764. default: "Always").lowercased()
  765. ) ?? .always
  766. alarm.repeatSoundOption = RepeatSoundOption(
  767. rawValue: take("alertMissedReadingRepeat",
  768. default: "Never").lowercased()
  769. ) ?? .never
  770. // activeOption ← “Pre-Snooze” picker + day/night flags
  771. let autoStr = take("alertMissedReadingAutosnooze", default: "Never")
  772. .lowercased()
  773. let dayFlag = take("alertMissedReadingAutosnoozeDay", default: false)
  774. let nightFlag = take("alertMissedReadingAutosnoozeNight", default: false)
  775. alarm.activeOption = {
  776. if dayFlag, !nightFlag { return .day }
  777. if !dayFlag, nightFlag { return .night }
  778. switch autoStr {
  779. case "day", "at day": return .day
  780. case "night", "at night": return .night
  781. default: return .always // “Never” → always on
  782. }
  783. }()
  784. // store in the new world
  785. Storage.shared.alarms.value.append(alarm)
  786. }
  787. // MARK: - “Not Looping” (legacy → Storage.shared.alarms)
  788. private func migrateNotLoopingAlarm() {
  789. // Check if the user ever configured this alarm
  790. let activeFlag = UserDefaultsValue<Bool>(key: "alertNotLoopingActive", default: false)
  791. guard activeFlag.exists else { return } // nothing to migrate
  792. // Convenience: read-then-erase
  793. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  794. let box = UserDefaultsValue<V>(key: key, default: def)
  795. defer { box.setNil(key: key) } // wipe after reading
  796. return box.value
  797. }
  798. // Build the new Alarm ---------------------------------------------------
  799. var alarm = Alarm(type: .notLooping)
  800. alarm.name = "Not Looping Alert"
  801. alarm.isEnabled = take("alertNotLoopingActive", default: false)
  802. alarm.threshold = Double(take("alertNotLooping", default: 31)) // minutes
  803. alarm.snoozeDuration = take("alertNotLoopingSnooze", default: 30)
  804. alarm.snoozedUntil = take("alertNotLoopingSnoozedTime", default: nil as Date?)
  805. alarm.soundFile = SoundFile(rawValue:
  806. take("alertNotLoopingSound",
  807. default: "Sci-Fi_Engine_Shut_Down")) ?? .sciFiEngineShutDown
  808. // ── ACTIVE-DURING (day/night) ← old **Pre-Snooze** flags --------------
  809. let actDay = take("alertNotLoopingAutosnoozeDay", default: false)
  810. let actNight = take("alertNotLoopingAutosnoozeNight", default: false)
  811. alarm.activeOption = {
  812. switch (actDay, actNight) {
  813. case (true, true): return .always
  814. case (true, false): return .day
  815. case (false, true): return .night
  816. default: return .always // “Never” in old UI
  817. }
  818. }()
  819. // ── PLAY-SOUND option ---------------------------------------------------
  820. let playStr = take("alertNotLoopingAudible", default: "Always").lowercased()
  821. let playDay = take("alertNotLoopingDayTimeAudible", default: true)
  822. let playNight = take("alertNotLoopingNightTimeAudible", default: true)
  823. alarm.playSoundOption = {
  824. if !playDay, !playNight { return .never }
  825. else if playDay, playNight { return .always }
  826. else if playDay { return .day }
  827. else { return .night }
  828. }()
  829. // ── REPEAT-SOUND option -------------------------------------------------
  830. let repStr = take("alertNotLoopingRepeat", default: "Never").lowercased()
  831. let repDay = take("alertNotLoopingDayTime", default: false)
  832. let repNight = take("alertNotLoopingNightTime", default: false)
  833. alarm.repeatSoundOption = {
  834. if repDay, repNight { return .always }
  835. else if repDay, !repNight { return .day }
  836. else if repNight, !repDay { return .night }
  837. else { return .never }
  838. }()
  839. // ── BG-limit guard ------------------------------------------------------
  840. if take("alertNotLoopingUseLimits", default: false) {
  841. alarm.belowBG = Double(take("alertNotLoopingLowerLimit", default: 100.0))
  842. alarm.aboveBG = Double(take("alertNotLoopingUpperLimit", default: 160.0))
  843. }
  844. // ── Per-alarm snooze state ---------------------------------------------
  845. if !take("alertNotLoopingIsSnoozed", default: false) {
  846. alarm.snoozedUntil = nil // ignore stored date if flag isn’t set
  847. }
  848. // Persist & finish -------------------------------------------------------
  849. var list = Storage.shared.alarms.value
  850. list.append(alarm)
  851. Storage.shared.alarms.value = list
  852. }
  853. // MARK: – Missed-Bolus alarm -------------------------------------------------
  854. private func migrateMissedBolusAlarm() {
  855. // Was the old alarm ever configured?
  856. let legacyActive = UserDefaultsValue<Bool>(key: "alertMissedBolusActive",
  857. default: false)
  858. guard legacyActive.exists else { return } // nothing to do
  859. // helper: read-then-delete ------------------------------------------------
  860. func take<V: AnyConvertible & Equatable>(_ k: String,
  861. _ def: V) -> V
  862. {
  863. let box = UserDefaultsValue<V>(key: k, default: def)
  864. defer { box.setNil(key: k) } // wipe after reading
  865. return box.value
  866. }
  867. // ────────────────────────────────────────────────────────────────────────
  868. // Build the new Alarm
  869. // ────────────────────────────────────────────────────────────────────────
  870. var alarm = Alarm(type: .missedBolus)
  871. alarm.name = "Missed Bolus Alert"
  872. alarm.isEnabled = take("alertMissedBolusActive", false)
  873. // core timings
  874. alarm.monitoringWindow = take("alertMissedBolus", 10) // delay
  875. alarm.predictiveMinutes = take("alertMissedBolusPrebolus", 20) // pre-bolus window
  876. alarm.snoozeDuration = take("alertMissedBolusSnooze", 10)
  877. // snooze-state
  878. if take("alertMissedBolusIsSnoozed", false) {
  879. alarm.snoozedUntil = take("alertMissedBolusSnoozedTime",
  880. nil as Date?)
  881. }
  882. // carb / bolus filters
  883. alarm.delta = take("alertMissedBolusIgnoreBolus", 0.5)
  884. alarm.threshold = Double(
  885. take("alertMissedBolusLowGrams", 10))
  886. alarm.aboveBG = Double(
  887. take("alertMissedBolusLowGramsBG", 70.0))
  888. // sound & tone
  889. alarm.soundFile = SoundFile(rawValue:
  890. take("alertMissedBolusSound",
  891. "Dhol_Shuffleloop")) ?? .dholShuffleloop
  892. // ── ACTIVE-DURING ← old “Pre-Snooze” flags
  893. let actDay = take("alertMissedBolusAutosnoozeDay", false)
  894. let actNight = take("alertMissedBolusAutosnoozeNight", false)
  895. alarm.activeOption = {
  896. switch (actDay, actNight) {
  897. case (true, true): return .always
  898. case (true, false): return .day
  899. case (false, true): return .night
  900. default: return .always // “Never” → always
  901. }
  902. }()
  903. // ── PLAY-SOUND ← old “PlaySound” picker
  904. let playDay = take("alertMissedBolusDayTimeAudible", true)
  905. let playNight = take("alertMissedBolusNightTimeAudible", true)
  906. alarm.playSoundOption = {
  907. switch (playDay, playNight) {
  908. case (true, true): return .always
  909. case (true, false): return .day
  910. case (false, true): return .night
  911. default: return .never
  912. }
  913. }()
  914. // ── REPEAT-SOUND ← old “Repeat Sound” picker
  915. let repDay = take("alertMissedBolusDayTime", false)
  916. let repNight = take("alertMissedBolusNightTime", false)
  917. alarm.repeatSoundOption = {
  918. switch (repDay, repNight) {
  919. case (true, true): return .always
  920. case (true, false): return .day
  921. case (false, true): return .night
  922. default: return .never
  923. }
  924. }()
  925. // (The deprecated “alertMissedBolusQuiet” key is ignored.)
  926. // ── Store & finish ------------------------------------------------------
  927. var list = Storage.shared.alarms.value
  928. list.append(alarm)
  929. Storage.shared.alarms.value = list
  930. }
  931. // ─────────────────────────────────────────────────────────────────────────────
  932. // MARK: SAGE → .sensorChange
  933. // ─────────────────────────────────────────────────────────────────────────────
  934. private func migrateSensorChangeAlarm() {
  935. // Was the old setting ever stored?
  936. let flag = UserDefaultsValue<Bool>(key: "alertSAGEActive", default: false)
  937. guard flag.exists else { return }
  938. // tiny helper that *reads + wipes* a legacy key
  939. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  940. let b = UserDefaultsValue<V>(key: k, default: def)
  941. defer { b.setNil(key: k) }
  942. return b.value
  943. }
  944. var alarm = Alarm(type: .sensorChange)
  945. alarm.name = "Sensor Change Reminder"
  946. alarm.isEnabled = take("alertSAGEActive", false)
  947. alarm.threshold = Double(take("alertSAGE", 8)) // hours
  948. alarm.snoozeDuration = take("alertSAGESnooze", 2) // hours
  949. if take("alertSAGEIsSnoozed", false) {
  950. alarm.snoozedUntil = take("alertSAGESnoozedTime", nil as Date?)
  951. }
  952. alarm.soundFile = SoundFile(rawValue:
  953. take("alertSAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou
  954. // ACTIVE (day / night)
  955. let actDay = take("alertSAGEAutosnoozeDay", false)
  956. let actNight = take("alertSAGEAutosnoozeNight", true)
  957. alarm.activeOption = {
  958. switch (actDay, actNight) {
  959. case (true, true): return .always
  960. case (true, false): return .day
  961. case (false, true): return .night
  962. default: return .always
  963. }
  964. }()
  965. // PLAY sound
  966. let playDay = take("alertSAGEDayTimeAudible", true)
  967. let playNight = take("alertSAGENightTimeAudible", true)
  968. alarm.playSoundOption = {
  969. switch (playDay, playNight) {
  970. case (true, true): return .always
  971. case (true, false): return .day
  972. case (false, true): return .night
  973. default: return .never
  974. }
  975. }()
  976. // REPEAT sound
  977. let repDay = take("alertSAGEDayTime", false)
  978. let repNight = take("alertSAGENightTime", false)
  979. alarm.repeatSoundOption = {
  980. switch (repDay, repNight) {
  981. case (true, true): return .always
  982. case (true, false): return .day
  983. case (false, true): return .night
  984. default: return .never
  985. }
  986. }()
  987. // Persist
  988. var list = Storage.shared.alarms.value
  989. list.append(alarm)
  990. Storage.shared.alarms.value = list
  991. }
  992. // ─────────────────────────────────────────────────────────────────────────────
  993. // MARK: CAGE → .pumpChange
  994. // ─────────────────────────────────────────────────────────────────────────────
  995. private func migratePumpChangeAlarm() {
  996. let flag = UserDefaultsValue<Bool>(key: "alertCAGEActive", default: false)
  997. guard flag.exists else { return }
  998. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  999. let b = UserDefaultsValue<V>(key: k, default: def)
  1000. defer { b.setNil(key: k) }
  1001. return b.value
  1002. }
  1003. var alarm = Alarm(type: .pumpChange)
  1004. alarm.name = "Pump / Cannula Change"
  1005. alarm.isEnabled = take("alertCAGEActive", false)
  1006. alarm.threshold = Double(take("alertCAGE", 4)) // hours
  1007. alarm.snoozeDuration = take("alertCAGESnooze", 2) // hours
  1008. if take("alertCAGEIsSnoozed", false) {
  1009. alarm.snoozedUntil = take("alertCAGESnoozedTime", nil as Date?)
  1010. }
  1011. alarm.soundFile = SoundFile(rawValue:
  1012. take("alertCAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou
  1013. // ACTIVE
  1014. let actDay = take("alertCAGEAutosnoozeDay", false)
  1015. let actNight = take("alertCAGEAutosnoozeNight", true)
  1016. alarm.activeOption = {
  1017. switch (actDay, actNight) {
  1018. case (true, true): return .always
  1019. case (true, false): return .day
  1020. case (false, true): return .night
  1021. default: return .always
  1022. }
  1023. }()
  1024. // PLAY
  1025. let playDay = take("alertCAGEDayTimeAudible", true)
  1026. let playNight = take("alertCAGENightTimeAudible", true)
  1027. alarm.playSoundOption = {
  1028. switch (playDay, playNight) {
  1029. case (true, true): return .always
  1030. case (true, false): return .day
  1031. case (false, true): return .night
  1032. default: return .never
  1033. }
  1034. }()
  1035. // REPEAT
  1036. let repDay = take("alertCAGEDayTime", false)
  1037. let repNight = take("alertCAGENightTime", false)
  1038. alarm.repeatSoundOption = {
  1039. switch (repDay, repNight) {
  1040. case (true, true): return .always
  1041. case (true, false): return .day
  1042. case (false, true): return .night
  1043. default: return .never
  1044. }
  1045. }()
  1046. var list = Storage.shared.alarms.value
  1047. list.append(alarm)
  1048. Storage.shared.alarms.value = list
  1049. }
  1050. // ─────────────────────────────────────────────────────────────────────────────
  1051. // MARK: Override-Start → .overrideStart
  1052. // ─────────────────────────────────────────────────────────────────────────────
  1053. private func migrateOverrideStartAlarm() {
  1054. let exists = UserDefaultsValue<Bool>(key: "alertOverrideStart", default: false)
  1055. guard exists.exists else { return } // user never touched it
  1056. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  1057. let box = UserDefaultsValue<V>(key: k, default: def)
  1058. defer { box.setNil(key: k) } // wipe after reading
  1059. return box.value
  1060. }
  1061. var alarm = Alarm(type: .overrideStart)
  1062. alarm.name = "Override Started"
  1063. alarm.isEnabled = take("alertOverrideStart", false)
  1064. alarm.snoozeDuration = 5 // legacy UI had no stepper
  1065. if take("alertOverrideStartIsSnoozed", false) {
  1066. alarm.snoozedUntil = take("alertOverrideStartSnoozedTime", nil as Date?)
  1067. }
  1068. alarm.soundFile = SoundFile(
  1069. rawValue: take("alertOverrideStartSound", "Ending_Reached")
  1070. ) ?? .endingReached
  1071. // ── ACTIVE (legacy “Pre-Snooze” day/night flags) ──────────────
  1072. let actDay = take("alertOverrideStartAutosnoozeDay", false)
  1073. let actNight = take("alertOverrideStartAutosnoozeNight", false)
  1074. alarm.activeOption = {
  1075. switch (actDay, actNight) {
  1076. case (true, true): return .always
  1077. case (true, false): return .day
  1078. case (false, true): return .night
  1079. default: return .always
  1080. }
  1081. }()
  1082. // ── PLAY (legacy “Play Sound” day/night flags) ───────────────
  1083. let playDay = take("alertOverrideStartDayTimeAudible", true)
  1084. let playNight = take("alertOverrideStartNightTimeAudible", true)
  1085. alarm.playSoundOption = {
  1086. switch (playDay, playNight) {
  1087. case (true, true): return .always
  1088. case (true, false): return .day
  1089. case (false, true): return .night
  1090. default: return .never
  1091. }
  1092. }()
  1093. // ── REPEAT (legacy “Repeat Sound” day/night flags) ───────────
  1094. let repDay = take("alertOverrideStartDayTime", false)
  1095. let repNight = take("alertOverrideStartNightTime", false)
  1096. alarm.repeatSoundOption = {
  1097. switch (repDay, repNight) {
  1098. case (true, true): return .always
  1099. case (true, false): return .day
  1100. case (false, true): return .night
  1101. default: return .never
  1102. }
  1103. }()
  1104. // ignore & wipe unused keys
  1105. _ = take("alertOverrideStartQuiet", false as Bool)
  1106. _ = take("alertOverrideStartRepeatAudible", "Always" as String)
  1107. var list = Storage.shared.alarms.value
  1108. list.append(alarm)
  1109. Storage.shared.alarms.value = list
  1110. }
  1111. // ─────────────────────────────────────────────────────────────────────────────
  1112. // MARK: Override-End → .overrideEnd
  1113. // ─────────────────────────────────────────────────────────────────────────────
  1114. private func migrateOverrideEndAlarm() {
  1115. let exists = UserDefaultsValue<Bool>(key: "alertOverrideEnd", default: false)
  1116. guard exists.exists else { return }
  1117. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  1118. let box = UserDefaultsValue<V>(key: k, default: def)
  1119. defer { box.setNil(key: k) }
  1120. return box.value
  1121. }
  1122. var alarm = Alarm(type: .overrideEnd)
  1123. alarm.name = "Override Ended"
  1124. alarm.isEnabled = take("alertOverrideEnd", false)
  1125. alarm.snoozeDuration = 5
  1126. if take("alertOverrideEndIsSnoozed", false) {
  1127. alarm.snoozedUntil = take("alertOverrideEndSnoozedTime", nil as Date?)
  1128. }
  1129. alarm.soundFile = SoundFile(
  1130. rawValue: take("alertOverrideEndSound", "Alert_Tone_Busy")
  1131. ) ?? .alertToneBusy
  1132. // ACTIVE
  1133. let actDay = take("alertOverrideEndAutosnoozeDay", false)
  1134. let actNight = take("alertOverrideEndAutosnoozeNight", false)
  1135. alarm.activeOption = {
  1136. switch (actDay, actNight) {
  1137. case (true, true): return .always
  1138. case (true, false): return .day
  1139. case (false, true): return .night
  1140. default: return .always
  1141. }
  1142. }()
  1143. // PLAY
  1144. let playDay = take("alertOverrideEndDayTimeAudible", true)
  1145. let playNight = take("alertOverrideEndNightTimeAudible", true)
  1146. alarm.playSoundOption = {
  1147. switch (playDay, playNight) {
  1148. case (true, true): return .always
  1149. case (true, false): return .day
  1150. case (false, true): return .night
  1151. default: return .never
  1152. }
  1153. }()
  1154. // REPEAT
  1155. let repDay = take("alertOverrideEndDayTime", false)
  1156. let repNight = take("alertOverrideEndNightTime", false)
  1157. alarm.repeatSoundOption = {
  1158. switch (repDay, repNight) {
  1159. case (true, true): return .always
  1160. case (true, false): return .day
  1161. case (false, true): return .night
  1162. default: return .never
  1163. }
  1164. }()
  1165. // wipe unused keys
  1166. _ = take("alertOverrideEndQuiet", false as Bool)
  1167. _ = take("alertOverrideEndRepeatAudible", "Always" as String)
  1168. var list = Storage.shared.alarms.value
  1169. list.append(alarm)
  1170. Storage.shared.alarms.value = list
  1171. }
  1172. // MARK: ––––– Temp-Target START → .tempTargetStart –––––
  1173. private func migrateTempTargetStartAlarm() {
  1174. let touched = UserDefaultsValue<Bool>(key: "alertTempTargetStart", default: false)
  1175. guard touched.exists else { return }
  1176. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  1177. let box = UserDefaultsValue<V>(key: k, default: def)
  1178. defer { box.setNil(key: k) } // scrub after read
  1179. return box.value
  1180. }
  1181. var alarm = Alarm(type: .tempTargetStart)
  1182. alarm.name = "Temp Target Started"
  1183. alarm.isEnabled = take("alertTempTargetStart", false)
  1184. alarm.snoozeDuration = 5
  1185. if take("alertTempTargetStartIsSnoozed", false) {
  1186. alarm.snoozedUntil = take("alertTempTargetStartSnoozedTime", nil as Date?)
  1187. }
  1188. alarm.soundFile = SoundFile(
  1189. rawValue: take("alertTempTargetStartSound", "Ending_Reached")
  1190. ) ?? .endingReached
  1191. // ACTIVE ← legacy Pre-Snooze day/night flags
  1192. alarm.activeOption = {
  1193. let d = take("alertTempTargetStartAutosnoozeDay", false)
  1194. let n = take("alertTempTargetStartAutosnoozeNight", false)
  1195. switch (d, n) { case (true, true): return .always
  1196. case (true, false): return .day
  1197. case (false, true): return .night
  1198. default: return .always }
  1199. }()
  1200. // PLAY
  1201. alarm.playSoundOption = {
  1202. let d = take("alertTempTargetStartDayTimeAudible", true)
  1203. let n = take("alertTempTargetStartNightTimeAudible", true)
  1204. switch (d, n) { case (true, true): return .always
  1205. case (true, false): return .day
  1206. case (false, true): return .night
  1207. default: return .never }
  1208. }()
  1209. // REPEAT
  1210. alarm.repeatSoundOption = {
  1211. let d = take("alertTempTargetStartDayTime", false)
  1212. let n = take("alertTempTargetStartNightTime", false)
  1213. switch (d, n) { case (true, true): return .always
  1214. case (true, false): return .day
  1215. case (false, true): return .night
  1216. default: return .never }
  1217. }()
  1218. // wipe “quiet / RepeatAudible” extras
  1219. _ = take("alertTempTargetStartQuiet", false as Bool)
  1220. _ = take("alertTempTargetStartRepeatAudible", "Always" as String)
  1221. var list = Storage.shared.alarms.value
  1222. list.append(alarm)
  1223. Storage.shared.alarms.value = list
  1224. }
  1225. // MARK: ––––– Temp-Target END → .tempTargetEnd –––––
  1226. private func migrateTempTargetEndAlarm() {
  1227. let touched = UserDefaultsValue<Bool>(key: "alertTempTargetEnd", default: false)
  1228. guard touched.exists else { return }
  1229. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  1230. let box = UserDefaultsValue<V>(key: k, default: def)
  1231. defer { box.setNil(key: k) }
  1232. return box.value
  1233. }
  1234. var alarm = Alarm(type: .tempTargetEnd)
  1235. alarm.name = "Temp Target Ended"
  1236. alarm.isEnabled = take("alertTempTargetEnd", false)
  1237. alarm.snoozeDuration = 5
  1238. if take("alertTempTargetEndIsSnoozed", false) {
  1239. alarm.snoozedUntil = take("alertTempTargetEndSnoozedTime", nil as Date?)
  1240. }
  1241. alarm.soundFile = SoundFile(
  1242. rawValue: take("alertTempTargetEndSound", "Alert_Tone_Busy")
  1243. ) ?? .alertToneBusy
  1244. alarm.activeOption = {
  1245. let d = take("alertTempTargetEndAutosnoozeDay", false)
  1246. let n = take("alertTempTargetEndAutosnoozeNight", false)
  1247. switch (d, n) { case (true, true): return .always
  1248. case (true, false): return .day
  1249. case (false, true): return .night
  1250. default: return .always }
  1251. }()
  1252. alarm.playSoundOption = {
  1253. let d = take("alertTempTargetEndDayTimeAudible", true)
  1254. let n = take("alertTempTargetEndNightTimeAudible", true)
  1255. switch (d, n) { case (true, true): return .always
  1256. case (true, false): return .day
  1257. case (false, true): return .night
  1258. default: return .never }
  1259. }()
  1260. alarm.repeatSoundOption = {
  1261. let d = take("alertTempTargetEndDayTime", false)
  1262. let n = take("alertTempTargetEndNightTime", false)
  1263. switch (d, n) { case (true, true): return .always
  1264. case (true, false): return .day
  1265. case (false, true): return .night
  1266. default: return .never }
  1267. }()
  1268. _ = take("alertTempTargetEndQuiet", false as Bool)
  1269. _ = take("alertTempTargetEndRepeatAudible", "Always" as String)
  1270. var list = Storage.shared.alarms.value
  1271. list.append(alarm)
  1272. Storage.shared.alarms.value = list
  1273. }
  1274. // MARK: ––––– TEMPORARY BG LIMIT → .temporary –––––
  1275. private func migrateTemporaryBGAlarm() {
  1276. let flag = UserDefaultsValue<Bool>(key: "alertTemporaryActive", default: false)
  1277. guard flag.exists else { return }
  1278. func take<V: AnyConvertible & Equatable>(_ k: String, _ d: V) -> V {
  1279. let box = UserDefaultsValue<V>(key: k, default: d)
  1280. defer { box.setNil(key: k) }
  1281. return box.value
  1282. }
  1283. var alarm = Alarm(type: .temporary)
  1284. alarm.name = "Temporary BG Limit"
  1285. alarm.isEnabled = take("alertTemporaryActive", false)
  1286. // limit direction ↓ / ↑
  1287. let limit = Double(take("alertTemporaryBG", 90.0 as Float))
  1288. if take("alertTemporaryBelow", true) {
  1289. alarm.belowBG = limit
  1290. } else {
  1291. alarm.aboveBG = limit
  1292. }
  1293. // audio & repeat
  1294. alarm.soundFile = SoundFile(rawValue:
  1295. take("alertTemporarySound", "Indeed")) ?? .indeed
  1296. alarm.playSoundOption = take("alertTemporaryBGAudible", true) ? .always : .never
  1297. alarm.repeatSoundOption = take("alertTemporaryBGRepeat", false) ? .always : .never
  1298. Storage.shared.alarms.value.append(alarm)
  1299. }
  1300. // MARK: ––––– PUMP RESERVOIR LEVEL → .pump –––––
  1301. private func migratePumpVolumeAlarm() {
  1302. let flag = UserDefaultsValue<Bool>(key: "alertPump", default: false)
  1303. guard flag.exists else { return }
  1304. func take<V: AnyConvertible & Equatable>(_ k: String, _ d: V) -> V {
  1305. let box = UserDefaultsValue<V>(key: k, default: d)
  1306. defer { box.setNil(key: k) }
  1307. return box.value
  1308. }
  1309. var alarm = Alarm(type: .pump)
  1310. alarm.name = "Pump Reservoir"
  1311. alarm.isEnabled = take("alertPump", false)
  1312. alarm.threshold = Double(take("alertPumpAt", 10)) // units left
  1313. // Snooze — stored in hours, so keep as-is.
  1314. alarm.snoozeDuration = take("alertPumpSnoozeHours", 5)
  1315. if take("alertPumpIsSnoozed", false) {
  1316. alarm.snoozedUntil = take("alertPumpSnoozedTime", nil as Date?)
  1317. }
  1318. alarm.soundFile = SoundFile(rawValue:
  1319. take("alertPumpSound", "Marimba_Descend")) ?? .marimbaDescend
  1320. // PLAY-sound option (day / night / never)
  1321. alarm.playSoundOption = {
  1322. let d = take("alertPumpDayTimeAudible", true)
  1323. let n = take("alertPumpNightTimeAudible", true)
  1324. switch (d, n) {
  1325. case (true, true): return .always
  1326. case (true, false): return .day
  1327. case (false, true): return .night
  1328. default: return .never
  1329. }
  1330. }()
  1331. // REPEAT-sound option – derived from legacy picker flags
  1332. alarm.repeatSoundOption = {
  1333. let d = take("alertPumpDayTime", false)
  1334. let n = take("alertPumpNightTime", false)
  1335. switch (d, n) {
  1336. case (true, true): return .always
  1337. case (true, false): return .day
  1338. case (false, true): return .night
  1339. default: return .never
  1340. }
  1341. }()
  1342. // ACTIVE day/night ← old “Pre-Snooze” flags
  1343. alarm.activeOption = {
  1344. let d = take("alertPumpAutosnoozeDay", false)
  1345. let n = take("alertPumpAutosnoozeNight", false)
  1346. switch (d, n) {
  1347. case (true, true): return .always
  1348. case (true, false): return .day
  1349. case (false, true): return .night
  1350. default: return .always
  1351. }
  1352. }()
  1353. // Discard no-longer-needed extras
  1354. _ = take("alertPumpQuiet", false as Bool)
  1355. _ = take("alertPumpRepeat", "Never" as String)
  1356. _ = take("alertPumpAudible", "Always" as String)
  1357. _ = take("alertPumpAutosnooze", "Never" as String)
  1358. Storage.shared.alarms.value.append(alarm)
  1359. }
  1360. // -----------------------------------------------------------------------------
  1361. // MARK: - Helpers (place them once, near the top of the migrate() file)
  1362. // -----------------------------------------------------------------------------
  1363. /// Legacy picker + day/night flags → PlaySoundOption
  1364. private func playOption(from picker: String,
  1365. dayFlag: Bool,
  1366. nightFlag: Bool) -> PlaySoundOption
  1367. {
  1368. let p = picker.lowercased()
  1369. if p == "never" { return .never }
  1370. if p == "always" { return .always }
  1371. switch (dayFlag, nightFlag) {
  1372. case (true, true): return .always
  1373. case (true, false): return .day
  1374. case (false, true): return .night
  1375. default: return .never
  1376. }
  1377. }
  1378. /// Legacy picker + day/night flags → RepeatSoundOption
  1379. private func repeatOption(from picker: String,
  1380. dayFlag: Bool,
  1381. nightFlag: Bool) -> RepeatSoundOption
  1382. {
  1383. let p = picker.lowercased()
  1384. if p == "never" { return .never }
  1385. if p == "always" { return .always }
  1386. switch (dayFlag, nightFlag) {
  1387. case (true, true): return .always
  1388. case (true, false): return .day
  1389. case (false, true): return .night
  1390. default: return .never
  1391. }
  1392. }
  1393. /// Convenience: load-then-erase a legacy `UserDefaultsValue`
  1394. private func take<V: AnyConvertible & Equatable>(
  1395. _ key: String,
  1396. _ defaultValue: V
  1397. ) -> V {
  1398. let box = UserDefaultsValue<V>(key: key, default: defaultValue)
  1399. defer { box.setNil(key: key) }
  1400. return box.value
  1401. }
  1402. // -----------------------------------------------------------------------------
  1403. // MARK: - IOB Alarm migration
  1404. // -----------------------------------------------------------------------------
  1405. private func migrateIOBAlarm() {
  1406. // Only migrate if the user ever touched IOB alarm settings
  1407. guard UserDefaultsValue<Bool>(key: "alertIOB", default: false).exists else {
  1408. return
  1409. }
  1410. var alarm = Alarm(type: .iob)
  1411. alarm.name = "High IOB"
  1412. alarm.isEnabled = take("alertIOB", false)
  1413. alarm.aboveBG = nil // BG band not used
  1414. alarm.threshold = Double(take("alertIOBAt", 1.5)) // units threshold
  1415. alarm.bolusCountThreshold = take("alertIOBNumber", 3)
  1416. alarm.bolusWindowMinutes = take("alertIOBBolusesWithin", 60)
  1417. alarm.delta = Double(take("alertIOBMaxBoluses", 10)) // max total bolus
  1418. alarm.snoozeDuration = take("alertIOBSnoozeHours", 1) * 60 // hours → minutes
  1419. alarm.snoozedUntil = take("alertIOBSnoozedTime", nil as Date?)
  1420. alarm.soundFile = SoundFile(rawValue:
  1421. take("alertIOBSound", "Alert_Tone_Ringtone_1"))
  1422. ?? .alertToneRingtone1
  1423. // Audio options
  1424. alarm.playSoundOption = playOption(
  1425. from: take("alertIOBAudible", "Always"),
  1426. dayFlag: take("alertIOBDayTimeAudible", true),
  1427. nightFlag: take("alertIOBNightTimeAudible", true)
  1428. )
  1429. alarm.repeatSoundOption = repeatOption(
  1430. from: take("alertIOBRepeat", "Always"),
  1431. dayFlag: take("alertIOBDayTime", true),
  1432. nightFlag: take("alertIOBNightTime", true)
  1433. )
  1434. // Active (day/night) option comes from the legacy “Pre-Snooze” picker,
  1435. // but the IOB alarm never had one, so treat it as always-on.
  1436. alarm.activeOption = .always
  1437. // Persist
  1438. Storage.shared.alarms.value.append(alarm)
  1439. }
  1440. // -----------------------------------------------------------------------------
  1441. // MARK: - COB Alarm migration
  1442. // -----------------------------------------------------------------------------
  1443. private func migrateCOBAlarm() {
  1444. // Only migrate if the user ever touched COB alarm settings
  1445. guard UserDefaultsValue<Bool>(key: "alertCOB", default: false).exists else {
  1446. return
  1447. }
  1448. var alarm = Alarm(type: .cob)
  1449. alarm.name = "High COB"
  1450. alarm.isEnabled = take("alertCOB", false)
  1451. alarm.threshold = Double(take("alertCOBAt", 50)) // grams threshold
  1452. alarm.snoozeDuration = take("alertCOBSnoozeHours", 1) * 60 // hours → minutes
  1453. alarm.snoozedUntil = take("alertCOBSnoozedTime", nil as Date?)
  1454. alarm.soundFile = SoundFile(rawValue:
  1455. take("alertCOBSound", "Alert_Tone_Ringtone_2"))
  1456. ?? .alertToneRingtone2
  1457. // Audio options
  1458. alarm.playSoundOption = playOption(
  1459. from: take("alertCOBAudible", "Always"),
  1460. dayFlag: take("alertCOBDayTimeAudible", true),
  1461. nightFlag: take("alertCOBNightTimeAudible", true)
  1462. )
  1463. alarm.repeatSoundOption = repeatOption(
  1464. from: take("alertCOBRepeat", "Always"),
  1465. dayFlag: take("alertCOBDayTime", true),
  1466. nightFlag: take("alertCOBNightTime", true)
  1467. )
  1468. alarm.activeOption = .always // same reason as above
  1469. // Persist
  1470. Storage.shared.alarms.value.append(alarm)
  1471. }
  1472. // =============================================================================
  1473. // BATTERY-LEVEL alarm (old keys → .battery)
  1474. // =============================================================================
  1475. private func migrateBatteryAlarm() {
  1476. guard UserDefaultsValue<Bool>(key: "alertBatteryActive",
  1477. default: false).exists else { return }
  1478. var alarm = Alarm(type: .battery)
  1479. alarm.name = "Low Battery"
  1480. alarm.isEnabled = take("alertBatteryActive", false)
  1481. alarm.threshold = Double(take("alertBatteryLevel", 25)) // %
  1482. alarm.snoozeDuration = take("alertBatterySnoozeHours", 1) * 60
  1483. alarm.snoozedUntil = take("alertBatterySnoozedTime", nil as Date?)
  1484. alarm.soundFile = SoundFile(rawValue:
  1485. take("alertBatterySound", "Machine_Charge"))
  1486. ?? .machineCharge
  1487. // ── audio – legacy had a simple Bool “repeat / no-repeat”
  1488. let rpt = take("alertBatteryRepeat", true)
  1489. alarm.playSoundOption = .always // no day/night picker in legacy UI
  1490. alarm.repeatSoundOption = rpt ? .always : .never
  1491. alarm.activeOption = .always // no day/night activation picker
  1492. Storage.shared.alarms.value.append(alarm)
  1493. }
  1494. // =============================================================================
  1495. // BATTERY-DROP alarm (old keys → .batteryDrop)
  1496. // =============================================================================
  1497. private func migrateBatteryDropAlarm() {
  1498. guard UserDefaultsValue<Bool>(key: "alertBatteryDropActive",
  1499. default: false).exists else { return }
  1500. var alarm = Alarm(type: .batteryDrop)
  1501. alarm.name = "Battery Drop"
  1502. alarm.isEnabled = take("alertBatteryDropActive", false)
  1503. alarm.delta = Double(take("alertBatteryDropPercentage", 5)) // % drop
  1504. alarm.monitoringWindow = take("alertBatteryDropPeriod", 15) // min
  1505. alarm.snoozeDuration = take("alertBatteryDropSnoozeHours", 1) * 60
  1506. alarm.snoozedUntil = take("alertBatteryDropSnoozedTime", nil as Date?)
  1507. alarm.soundFile = SoundFile(rawValue:
  1508. take("alertBatteryDropSound", "Machine_Charge"))
  1509. ?? .machineCharge
  1510. let rpt = take("alertBatteryDropRepeat", true)
  1511. alarm.playSoundOption = .always
  1512. alarm.repeatSoundOption = rpt ? .always : .never
  1513. alarm.activeOption = .always
  1514. Storage.shared.alarms.value.append(alarm)
  1515. }
  1516. // =============================================================================
  1517. // REC-BOLUS alarm (old keys → .recBolus)
  1518. // =============================================================================
  1519. private func migrateRecBolusAlarm() {
  1520. guard UserDefaultsValue<Bool>(key: "alertRecBolusActive",
  1521. default: false).exists else { return }
  1522. var alarm = Alarm(type: .recBolus)
  1523. alarm.name = "Recommended Bolus"
  1524. alarm.isEnabled = take("alertRecBolusActive", false)
  1525. alarm.delta = take("alertRecBolusLevel", 1.0) // units
  1526. alarm.snoozeDuration = take("alertRecBolusSnooze", 5) // min
  1527. alarm.snoozedUntil = take("alertRecBolusSnoozedTime", nil as Date?)
  1528. alarm.soundFile = SoundFile(rawValue:
  1529. take("alertRecBolusSound", "Dhol_Shuffleloop"))
  1530. ?? .dholShuffleloop
  1531. let rpt = take("alertRecBolusRepeat", false)
  1532. alarm.playSoundOption = .always
  1533. alarm.repeatSoundOption = rpt ? .always : .never
  1534. alarm.activeOption = .always
  1535. Storage.shared.alarms.value.append(alarm)
  1536. }
  1537. // MARK: - Migration helpers (can be removed when step 5 is removed)
  1538. /// Check whether a tab item's position was explicitly stored in UserDefaults
  1539. /// (as opposed to using the StorageValue default).
  1540. private func migratePositionExists(for item: TabItem) -> Bool {
  1541. switch item {
  1542. case .home: return homePosition.exists
  1543. case .alarms: return alarmsPosition.exists
  1544. case .remote: return remotePosition.exists
  1545. case .nightscout: return nightscoutPosition.exists
  1546. case .snoozer: return snoozerPosition.exists
  1547. case .stats: return statisticsPosition.exists
  1548. case .treatments: return treatmentsPosition.exists
  1549. }
  1550. }
  1551. }