diff --git a/README.md b/README.md index 8a6f02e..4f88c1a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# TidepoolService +# Tidepool Service -Integration of the Tidepool service into Loop. +A remote data service plugin for [Loop](https://github.com/LoopKit/Loop) that uploads to the Tidepool platform using [TidepoolKit](https://github.com/tidepool-org/TidepoolKit) diff --git a/TidepoolService.xcodeproj/project.pbxproj b/TidepoolService.xcodeproj/project.pbxproj index 43cbce5..a5a1a85 100644 --- a/TidepoolService.xcodeproj/project.pbxproj +++ b/TidepoolService.xcodeproj/project.pbxproj @@ -14,7 +14,6 @@ A9151365244E2A9E00116932 /* TidepoolKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9BF371C2418195C008D7F34 /* TidepoolKit.framework */; }; A9151366244E2A9E00116932 /* TidepoolKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9BF371C2418195C008D7F34 /* TidepoolKit.framework */; }; A9151368244E2A9E00116932 /* TidepoolServiceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAACFF22E7987800E76C9F /* TidepoolServiceKit.framework */; }; - A92E770122E9181500591027 /* TidepoolServiceSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A92E770022E9181500591027 /* TidepoolServiceSetupViewController.swift */; }; A9309CA72435987000E02268 /* SyncCarbObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9309CA62435987000E02268 /* SyncCarbObject.swift */; }; A9309CAF2436C52900E02268 /* StoredGlucoseSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9309CAE2436C52900E02268 /* StoredGlucoseSample.swift */; }; A94AE4E8235A89B5005CA320 /* TidepoolServiceKitPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = A94AE4E6235A89B5005CA320 /* TidepoolServiceKitPlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -51,7 +50,6 @@ A9DAAD3422E7CA1A00E76C9F /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DAAD3222E7CA1A00E76C9F /* LocalizedString.swift */; }; A9DAAD3622E7CAC100E76C9F /* TidepoolService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DAAD3522E7CAC100E76C9F /* TidepoolService.swift */; }; A9DAAD3922E7DEE000E76C9F /* TidepoolService+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DAAD3822E7DEE000E76C9F /* TidepoolService+UI.swift */; }; - A9DAAD3B22E7DEF100E76C9F /* TidepoolServiceSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DAAD3A22E7DEF100E76C9F /* TidepoolServiceSettingsViewController.swift */; }; A9DAAD3F22E7DF9B00E76C9F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A9DAAD4122E7DF9B00E76C9F /* Localizable.strings */; }; A9DAAD4D22E7DFD400E76C9F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A9DAAD4F22E7DFD400E76C9F /* Localizable.strings */; }; A9DAAD5B22E7E6BE00E76C9F /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAAD5A22E7E6BE00E76C9F /* LoopKit.framework */; }; @@ -73,6 +71,11 @@ C12E4BBB288F2215009C98A2 /* TidepoolServiceKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAACFF22E7987800E76C9F /* TidepoolServiceKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C12E4BBE288F2215009C98A2 /* TidepoolServiceKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAAD1B22E7988900E76C9F /* TidepoolServiceKitUI.framework */; platformFilter = ios; }; C12E4BBF288F2215009C98A2 /* TidepoolServiceKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAAD1B22E7988900E76C9F /* TidepoolServiceKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + C1861B83297B4496008F69AE /* TidepoolKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9BF371C2418195C008D7F34 /* TidepoolKit.framework */; }; + C1861B84297B4496008F69AE /* TidepoolKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A9BF371C2418195C008D7F34 /* TidepoolKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + C1C9414629F0CB21008D3E05 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C9414529F0CB21008D3E05 /* UIImage.swift */; }; + C1D0B62929848A460098D215 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62829848A460098D215 /* SettingsView.swift */; }; + C1D0B62C29848BEB0098D215 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62B29848BEB0098D215 /* Image.swift */; }; E93BA06224A29C9C00C5D7E6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E93BA06124A29C9C00C5D7E6 /* Assets.xcassets */; }; /* End PBXBuildFile section */ @@ -137,6 +140,7 @@ files = ( C12E4BBB288F2215009C98A2 /* TidepoolServiceKit.framework in Embed Frameworks */, C12E4BBF288F2215009C98A2 /* TidepoolServiceKitUI.framework in Embed Frameworks */, + C1861B84297B4496008F69AE /* TidepoolKit.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -147,7 +151,6 @@ 1D70C41326F28CC900C62570 /* URLProtocolMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLProtocolMock.swift; sourceTree = ""; }; A9057686271F770F0030C3B1 /* IdentifiableDatum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifiableDatum.swift; sourceTree = ""; }; A913B37C24200C97000805C4 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; - A92E770022E9181500591027 /* TidepoolServiceSetupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidepoolServiceSetupViewController.swift; sourceTree = ""; }; A9309CA62435987000E02268 /* SyncCarbObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncCarbObject.swift; sourceTree = ""; }; A9309CAE2436C52900E02268 /* StoredGlucoseSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredGlucoseSample.swift; sourceTree = ""; }; A94AE4E4235A89B5005CA320 /* TidepoolServiceKitPlugin.loopplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TidepoolServiceKitPlugin.loopplugin; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -192,7 +195,6 @@ A9DAAD3222E7CA1A00E76C9F /* LocalizedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedString.swift; sourceTree = ""; }; A9DAAD3522E7CAC100E76C9F /* TidepoolService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidepoolService.swift; sourceTree = ""; }; A9DAAD3822E7DEE000E76C9F /* TidepoolService+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TidepoolService+UI.swift"; sourceTree = ""; }; - A9DAAD3A22E7DEF100E76C9F /* TidepoolServiceSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidepoolServiceSettingsViewController.swift; sourceTree = ""; }; A9DAAD4222E7DFA500E76C9F /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; A9DAAD4322E7DFA600E76C9F /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; A9DAAD4422E7DFA700E76C9F /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; @@ -225,6 +227,26 @@ A9E8C610272C76A500016E2E /* TimeInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInterval.swift; sourceTree = ""; }; A9F9F316271A046E00D19374 /* StoredCarbEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredCarbEntry.swift; sourceTree = ""; }; A9F9F318271A05B100D19374 /* IdentifiableHKDatum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifiableHKDatum.swift; sourceTree = ""; }; + C12522E1298309B5006EA1CD /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; + C1317D4129830A0800625B94 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; + C18B726B299581C600F138D3 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; + C192C60B29C78711001EFEA6 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; + C199E4D929C64072003D32F7 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; + C199E4DA29C64072003D32F7 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; + C1A3529629C640A5002322A5 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = ""; }; + C1A3529729C640A5002322A5 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = ""; }; + C1B0CFE129C786BF0045B04D /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; + C1B267AA2995824000BCB7C1 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; + C1C9414529F0CB21008D3E05 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; + C1D0B62829848A460098D215 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + C1D0B62B29848BEB0098D215 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; + C1DEE89E298309EA0008194D /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; + C1DEE89F298309EA0008194D /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; + C1E5A6E529C7870100703C90 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; + C1E693D729C786E200410918 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; + C1E8ADD92995822300AB9EEB /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = ""; }; + C1E8ADDA2995822300AB9EEB /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = ""; }; + C1F4FD6029C7869800D7ACBC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; E93BA06124A29C9C00C5D7E6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; /* End PBXFileReference section */ @@ -235,6 +257,7 @@ files = ( C12E4BBA288F2215009C98A2 /* TidepoolServiceKit.framework in Frameworks */, C12E4BBE288F2215009C98A2 /* TidepoolServiceKitUI.framework in Frameworks */, + C1861B83297B4496008F69AE /* TidepoolKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -405,13 +428,13 @@ A9DAAD1C22E7988900E76C9F /* TidepoolServiceKitUI */ = { isa = PBXGroup; children = ( + C1D0B62A29848BD90098D215 /* Extensions */, A9DAAD1D22E7988900E76C9F /* TidepoolServiceKitUI.h */, A9DAAD1E22E7988900E76C9F /* Info.plist */, A9DAAD6C22E7EA8F00E76C9F /* IdentifiableClass.swift */, A9DAAD6E22E7EA9700E76C9F /* NibLoadable.swift */, A9DAAD3822E7DEE000E76C9F /* TidepoolService+UI.swift */, - A9DAAD3A22E7DEF100E76C9F /* TidepoolServiceSettingsViewController.swift */, - A92E770022E9181500591027 /* TidepoolServiceSetupViewController.swift */, + C1D0B62829848A460098D215 /* SettingsView.swift */, E93BA06124A29C9C00C5D7E6 /* Assets.xcassets */, A9DAAD4F22E7DFD400E76C9F /* Localizable.strings */, ); @@ -457,6 +480,15 @@ path = Extensions; sourceTree = ""; }; + C1D0B62A29848BD90098D215 /* Extensions */ = { + isa = PBXGroup; + children = ( + C1D0B62B29848BEB0098D215 /* Image.swift */, + C1C9414529F0CB21008D3E05 /* UIImage.swift */, + ); + path = Extensions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -630,6 +662,17 @@ ru, es, Base, + da, + sk, + tr, + ro, + cs, + he, + fi, + ja, + "pt-BR", + sv, + vi, ); mainGroup = A9DAACD522E7978800E76C9F; productRefGroup = A9DAACE022E7978800E76C9F /* Products */; @@ -749,12 +792,13 @@ buildActionMask = 2147483647; files = ( A9DAAD6D22E7EA8F00E76C9F /* IdentifiableClass.swift in Sources */, + C1C9414629F0CB21008D3E05 /* UIImage.swift in Sources */, + C1D0B62C29848BEB0098D215 /* Image.swift in Sources */, + C1D0B62929848A460098D215 /* SettingsView.swift in Sources */, A97651762421AA11002EB5D4 /* OSLog.swift in Sources */, A9DAAD3422E7CA1A00E76C9F /* LocalizedString.swift in Sources */, A9DAAD3922E7DEE000E76C9F /* TidepoolService+UI.swift in Sources */, A9DAAD6F22E7EA9700E76C9F /* NibLoadable.swift in Sources */, - A92E770122E9181500591027 /* TidepoolServiceSetupViewController.swift in Sources */, - A9DAAD3B22E7DEF100E76C9F /* TidepoolServiceSettingsViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -821,6 +865,12 @@ A9DAAD4922E7DFAA00E76C9F /* ru */, A9DAAD4A22E7DFAB00E76C9F /* es */, A99222FA235A879600C11C04 /* Base */, + C1DEE89E298309EA0008194D /* sk */, + C18B726B299581C600F138D3 /* da */, + C1E8ADD92995822300AB9EEB /* ro */, + C1B267AA2995824000BCB7C1 /* tr */, + C199E4D929C64072003D32F7 /* cs */, + C1A3529629C640A5002322A5 /* he */, ); name = Localizable.strings; sourceTree = ""; @@ -838,6 +888,17 @@ A9DAAD5722E7DFE100E76C9F /* ru */, A9DAAD5822E7DFE200E76C9F /* es */, A99222FB235A87B100C11C04 /* Base */, + C12522E1298309B5006EA1CD /* da */, + C1DEE89F298309EA0008194D /* sk */, + C1317D4129830A0800625B94 /* tr */, + C1E8ADDA2995822300AB9EEB /* ro */, + C199E4DA29C64072003D32F7 /* cs */, + C1A3529729C640A5002322A5 /* he */, + C1F4FD6029C7869800D7ACBC /* fi */, + C1B0CFE129C786BF0045B04D /* ja */, + C1E693D729C786E200410918 /* pt-BR */, + C1E5A6E529C7870100703C90 /* sv */, + C192C60B29C78711001EFEA6 /* vi */, ); name = Localizable.strings; sourceTree = ""; @@ -990,7 +1051,7 @@ GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; LOCALIZED_STRING_MACRO_NAMES = ( NSLocalizedString, CFLocalizedString, @@ -1094,7 +1155,7 @@ GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; LOCALIZED_STRING_MACRO_NAMES = ( NSLocalizedString, CFLocalizedString, diff --git a/TidepoolService.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme b/TidepoolService.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme index a364522..4bd32d3 100644 --- a/TidepoolService.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme +++ b/TidepoolService.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme @@ -1,6 +1,6 @@ + + + + @@ -63,17 +72,6 @@ - - - - - - - - String? { object(forInfoDictionaryKey: key) as? String } } + +public extension Bundle { + + // TidepoolServiceClientId should be set in the hosting app's info plist + // TidepoolServiceRedirectURI generally does not need to be set, and the default can be used. + + var tidepoolServiceClientId: String { + return object(forInfoDictionaryKey: "TidepoolServiceClientId") as? String ?? "client-id-not-in-info-plist" + } + + var tidepoolServiceRedirectURL: URL { + if let str = object(forInfoDictionaryKey: "TidepoolServiceRedirectURL") as? String, let url = URL(string: str) { + return url + } + return URL(string: "org.tidepool.tidepoolkit.auth://redirect")! + } +} diff --git a/TidepoolServiceKit/Extensions/DoseEntry.swift b/TidepoolServiceKit/Extensions/DoseEntry.swift index 103ab2c..c9f7c85 100644 --- a/TidepoolServiceKit/Extensions/DoseEntry.swift +++ b/TidepoolServiceKit/Extensions/DoseEntry.swift @@ -29,72 +29,101 @@ import TidepoolKit */ extension DoseEntry: IdentifiableDatum { - func data(for userId: String) -> [TDatum] { + func data(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { guard syncIdentifier != nil else { return [] } switch type { case .basal: - return dataForBasal(for: userId) + return dataForBasal(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) case .bolus: - return dataForBolus(for: userId) + return dataForBolus(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) case .resume: return [] case .suspend: - return dataForSuspend(for: userId) + return dataForSuspend(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) case .tempBasal: - return dataForTempBasal(for: userId) + return dataForTempBasal(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) } } var syncIdentifierAsString: String { syncIdentifier!.md5hash! } // Actual sync identifier may be human readable and of variable length - private func dataForBasal(for userId: String) -> [TDatum] { - guard let datumScheduledBasalRate = datumScheduledBasalRate else { - return [] + private func dataForBasal(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { + if automatic != true { + return dataForBasalManual(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) + } else { + return dataForBasalAutomatic(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) } + } + + private func dataForBasalManual(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { var payload = datumPayload - payload["deliveredUnits"] = programmedUnits + payload["deliveredUnits"] = datumBasalDeliveredUnits var datum = TScheduledBasalDatum(time: datumTime, duration: datumDuration, rate: datumScheduledBasalRate, scheduleName: StoredSettings.activeScheduleNameDefault, insulinFormulation: datumInsulinFormulation) + + let origin = datumOrigin(for: resolvedIdentifier(for: TScheduledBasalDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) datum = datum.adornWith(id: datumId(for: userId, type: TScheduledBasalDatum.self), annotations: datumAnnotations, payload: payload, - origin: datumOrigin(for: TScheduledBasalDatum.self)) + origin: origin) + return [datum] + } + + private func dataForBasalAutomatic(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { + var payload = datumPayload + payload["deliveredUnits"] = datumBasalDeliveredUnits + + var datum = TAutomatedBasalDatum(time: datumTime, + duration: !isMutable ? datumDuration : 0, + expectedDuration: !isMutable && datumDuration < basalDatumExpectedDuration ? basalDatumExpectedDuration : nil, + rate: datumScheduledBasalRate, + scheduleName: StoredSettings.activeScheduleNameDefault, + insulinFormulation: datumInsulinFormulation) + let origin = datumOrigin(for: resolvedIdentifier(for: TAutomatedBasalDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) + datum = datum.adornWith(id: datumId(for: userId, type: TAutomatedBasalDatum.self), + annotations: datumAnnotations, + payload: payload, + origin: origin) return [datum] } - private func dataForBolus(for userId: String) -> [TDatum] { + private func dataForBolus(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { if manuallyEntered { - return dataForBolusManuallyEntered(for: userId) + return dataForBolusManuallyEntered(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) + + } else if automatic != true { - return dataForBolusManual(for: userId) - } else { - return dataForBolusAutomatic(for: userId) + return dataForBolusManual(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) + } else { + return dataForBolusAutomatic(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) } } - private func dataForBolusManuallyEntered(for userId: String) ->[TDatum] { + private func dataForBolusManuallyEntered(for userId: String, hostIdentifier: String, hostVersion: String) ->[TDatum] { var payload = datumPayload payload["duration"] = datumDuration.milliseconds var datum = TInsulinDatum(time: datumTime, dose: TInsulinDatum.Dose(total: deliveredUnits ?? programmedUnits), formulation: datumInsulinFormulation) + + let origin = datumOrigin(for: resolvedIdentifier(for: TInsulinDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) datum = datum.adornWith(id: datumId(for: userId, type: TInsulinDatum.self), annotations: datumAnnotations, payload: payload, - origin: datumOrigin(for: TInsulinDatum.self)) + origin: origin) return [datum] } - private func dataForBolusManual(for userId: String) -> [TDatum] { + private func dataForBolusManual(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { var payload = datumPayload payload["duration"] = datumDuration.milliseconds @@ -105,14 +134,15 @@ extension DoseEntry: IdentifiableDatum { normal: !isMutable ? deliveredUnits : programmedUnits, expectedNormal: !isMutable && programmedUnits != deliveredUnits ? programmedUnits : nil, insulinFormulation: datumInsulinFormulation) + let origin = datumOrigin(for: resolvedIdentifier(for: TNormalBolusDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) datum = datum.adornWith(id: datumId(for: userId, type: TNormalBolusDatum.self), annotations: datumAnnotations, payload: payload, - origin: datumOrigin(for: TNormalBolusDatum.self)) + origin: origin) return [datum] } - private func dataForBolusAutomatic(for userId: String) -> [TDatum] { + private func dataForBolusAutomatic(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { var payload = datumPayload payload["duration"] = datumDuration.milliseconds @@ -123,33 +153,35 @@ extension DoseEntry: IdentifiableDatum { normal: !isMutable ? deliveredUnits : programmedUnits, expectedNormal: !isMutable && programmedUnits != deliveredUnits ? programmedUnits : nil, insulinFormulation: datumInsulinFormulation) + let origin = datumOrigin(for: resolvedIdentifier(for: TAutomatedBolusDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) datum = datum.adornWith(id: datumId(for: userId, type: TAutomatedBolusDatum.self), annotations: datumAnnotations, payload: payload, - origin: datumOrigin(for: TAutomatedBolusDatum.self)) + origin: origin) return [datum] } - private func dataForSuspend(for userId: String) -> [TDatum] { + private func dataForSuspend(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { var datum = TSuspendedBasalDatum(time: datumTime, duration: datumDuration) datum.suppressed = datumSuppressed + let origin = datumOrigin(for: resolvedIdentifier(for: TSuspendedBasalDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) datum = datum.adornWith(id: datumId(for: userId, type: TSuspendedBasalDatum.self), annotations: datumAnnotations, payload: datumPayload, - origin: datumOrigin(for: TSuspendedBasalDatum.self)) + origin: origin) return [datum] } - private func dataForTempBasal(for userId: String) -> [TDatum] { + private func dataForTempBasal(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { if automatic == false { - return dataForTempBasalManual(for: userId) + return dataForTempBasalManual(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) } else { - return dataForTempBasalAutomatic(for: userId) + return dataForTempBasalAutomatic(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) } } - private func dataForTempBasalManual(for userId: String) -> [TDatum] { + private func dataForTempBasalManual(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { var payload = datumPayload payload["deliveredUnits"] = deliveredUnits @@ -159,14 +191,15 @@ extension DoseEntry: IdentifiableDatum { rate: datumRate, insulinFormulation: datumInsulinFormulation) datum.suppressed = datumSuppressed + let origin = datumOrigin(for: resolvedIdentifier(for: TTemporaryBasalDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) datum = datum.adornWith(id: datumId(for: userId, type: TTemporaryBasalDatum.self), annotations: datumAnnotations, payload: payload, - origin: datumOrigin(for: TTemporaryBasalDatum.self)) + origin: origin) return [datum] } - private func dataForTempBasalAutomatic(for userId: String) -> [TDatum] { + private func dataForTempBasalAutomatic(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { var payload = datumPayload payload["deliveredUnits"] = deliveredUnits @@ -177,10 +210,11 @@ extension DoseEntry: IdentifiableDatum { scheduleName: StoredSettings.activeScheduleNameDefault, insulinFormulation: datumInsulinFormulation) datum.suppressed = datumSuppressed + let origin = datumOrigin(for: resolvedIdentifier(for: TAutomatedBasalDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) datum = datum.adornWith(id: datumId(for: userId, type: TAutomatedBasalDatum.self), annotations: datumAnnotations, payload: payload, - origin: datumOrigin(for: TAutomatedBasalDatum.self)) + origin: origin) return [datum] } @@ -190,10 +224,34 @@ extension DoseEntry: IdentifiableDatum { private var datumRate: Double { unitsPerHour } - private var datumScheduledBasalRate: Double? { scheduledBasalRate?.doubleValue(for: .internationalUnitsPerHour) } + + private var datumBasalDeliveredUnits: Double? { + guard type == .basal || type == .tempBasal else { + return nil + } + + if let deliveredUnits = deliveredUnits { + return deliveredUnits + } + + if unit == .units { + return programmedUnits + } + + return nil + } + + private var datumScheduledBasalRate: Double { + + if let rate = scheduledBasalRate?.doubleValue(for: .internationalUnitsPerHour) { + return rate + } + + return unitsPerHour + } private var datumSuppressed: TScheduledBasalDatum.Suppressed? { - guard let datumScheduledBasalRate = datumScheduledBasalRate else { + guard type == .tempBasal || type == .suspend else { return nil } return TScheduledBasalDatum.Suppressed(rate: datumScheduledBasalRate, diff --git a/TidepoolServiceKit/Extensions/PersistedPumpEvent.swift b/TidepoolServiceKit/Extensions/PersistedPumpEvent.swift index cfbf88e..b926997 100644 --- a/TidepoolServiceKit/Extensions/PersistedPumpEvent.swift +++ b/TidepoolServiceKit/Extensions/PersistedPumpEvent.swift @@ -39,24 +39,24 @@ import TidepoolKit */ extension PersistedPumpEvent: IdentifiableDatum { - func data(for userId: String) -> [TDatum] { + func data(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { guard let type = type, syncIdentifier != nil else { return [] } switch type { case .alarm: - return dataForAlarm(for: userId) + return dataForAlarm(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) case .alarmClear: - return dataForAlarmClear(for: userId) + return dataForAlarmClear(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) case .prime: - return dataForPrime(for: userId) + return dataForPrime(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) case .resume: - return dataForResume(for: userId) + return dataForResume(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) case .rewind: - return dataForRewind(for: userId) + return dataForRewind(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) case .suspend: - return dataForSuspend(for: userId) + return dataForSuspend(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) default: return [] } @@ -66,10 +66,10 @@ extension PersistedPumpEvent: IdentifiableDatum { var syncIdentifierAsString: String { syncIdentifier! } - private func dataForAlarm(for userId: String) -> [TDatum] { + private func dataForAlarm(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { var data: [TDatum] = [] if dose?.type == .suspend { - data.append(contentsOf: dataForSuspend(for: userId)) + data.append(contentsOf: dataForSuspend(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)) } var payload = datumPayload @@ -78,46 +78,48 @@ extension PersistedPumpEvent: IdentifiableDatum { } var datum = TAlarmDeviceEventDatum(time: date, alarmType: datumAlarmType ?? .other) + let origin = datumOrigin(for: resolvedIdentifier(for: TAlarmDeviceEventDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) datum = datum.adornWith(id: datumId(for: userId, type: TAlarmDeviceEventDatum.self), payload: payload, - origin: datumOrigin(for: TAlarmDeviceEventDatum.self)) + origin: origin) data.append(datum) if dose?.type == .resume { - data.append(contentsOf: dataForResume(for: userId)) + data.append(contentsOf: dataForResume(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)) } return data } - private func dataForAlarmClear(for userId: String) -> [TDatum] { + private func dataForAlarmClear(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { if dose?.type == .suspend { - return dataForSuspend(for: userId) + return dataForSuspend(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) } else if dose?.type == .resume { - return dataForResume(for: userId) + return dataForResume(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) } else { return [] } } - private func dataForPrime(for userId: String) -> [TDatum] { + private func dataForPrime(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { var data: [TDatum] = [] if dose?.type == .suspend { - data.append(contentsOf: dataForSuspend(for: userId)) + data.append(contentsOf: dataForSuspend(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)) } var datum = TPrimeDeviceEventDatum(time: date, target: .tubing) // Default to tubing until we have further information + let origin = datumOrigin(for: resolvedIdentifier(for: TPrimeDeviceEventDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) datum = datum.adornWith(id: datumId(for: userId, type: TPrimeDeviceEventDatum.self), payload: datumPayload, - origin: datumOrigin(for: TPrimeDeviceEventDatum.self)) + origin: origin) data.append(datum) if dose?.type == .resume { - data.append(contentsOf: dataForResume(for: userId)) + data.append(contentsOf: dataForResume(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)) } return data } - private func dataForResume(for userId: String) -> [TDatum] { + private func dataForResume(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { guard let dose = dose else { return [] } @@ -128,31 +130,33 @@ extension PersistedPumpEvent: IdentifiableDatum { var datum = TStatusDeviceEventDatum(time: datumTime, name: .resumed, reason: reason) + let origin = datumOrigin(for: resolvedIdentifier(for: TStatusDeviceEventDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) datum = datum.adornWith(id: datumId(for: userId, type: TStatusDeviceEventDatum.self), payload: datumPayload, - origin: datumOrigin(for: TStatusDeviceEventDatum.self)) + origin: origin) return [datum] } - private func dataForRewind(for userId: String) -> [TDatum] { + private func dataForRewind(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { var data: [TDatum] = [] if dose?.type == .suspend { - data.append(contentsOf: dataForSuspend(for: userId)) + data.append(contentsOf: dataForSuspend(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)) } var datum = TReservoirChangeDeviceEventDatum(time: date) + let origin = datumOrigin(for: resolvedIdentifier(for: TReservoirChangeDeviceEventDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) datum = datum.adornWith(id: datumId(for: userId, type: TReservoirChangeDeviceEventDatum.self), payload: datumPayload, - origin: datumOrigin(for: TReservoirChangeDeviceEventDatum.self)) + origin: origin) data.append(datum) if dose?.type == .resume { - data.append(contentsOf: dataForResume(for: userId)) + data.append(contentsOf: dataForResume(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)) } return data } - private func dataForSuspend(for userId: String) -> [TDatum] { + private func dataForSuspend(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { guard let dose = dose else { return [] } @@ -163,9 +167,10 @@ extension PersistedPumpEvent: IdentifiableDatum { var datum = TStatusDeviceEventDatum(time: datumTime, name: .suspended, reason: reason) + let origin = datumOrigin(for: resolvedIdentifier(for: TStatusDeviceEventDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) datum = datum.adornWith(id: datumId(for: userId, type: TStatusDeviceEventDatum.self), payload: datumPayload, - origin: datumOrigin(for: TStatusDeviceEventDatum.self)) + origin: origin) return [datum] } diff --git a/TidepoolServiceKit/Extensions/StoredDosingDecision.swift b/TidepoolServiceKit/Extensions/StoredDosingDecision.swift index cabc6f3..7710caf 100644 --- a/TidepoolServiceKit/Extensions/StoredDosingDecision.swift +++ b/TidepoolServiceKit/Extensions/StoredDosingDecision.swift @@ -46,7 +46,7 @@ import TidepoolKit */ extension StoredDosingDecision: IdentifiableDatum { - func datumDosingDecision(for userId: String) -> TDosingDecisionDatum { + func datumDosingDecision(for userId: String, hostIdentifier: String, hostVersion: String) -> TDosingDecisionDatum { var associations: [TAssociation] = [] if let id = settings?.datumId(for: userId, type: TPumpSettingsDatum.self) { associations.append(TAssociation(type: .datum, id: id, reason: "pumpSettings")) @@ -77,36 +77,39 @@ extension StoredDosingDecision: IdentifiableDatum { errors: datumErrors, scheduleTimeZoneOffset: datumScheduleTimeZoneOffset, units: datumUnits) + let origin = datumOrigin(for: resolvedIdentifier(for: TDosingDecisionDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) return datum.adornWith(id: datumId(for: userId, type: TDosingDecisionDatum.self), timeZone: datumTimeZone, timeZoneOffset: datumTimeZoneOffset, associations: associations, payload: datumPayload, - origin: datumOrigin(for: TDosingDecisionDatum.self)) + origin: origin) } - func datumControllerStatus(for userId: String) -> TControllerStatusDatum { + func datumControllerStatus(for userId: String, hostIdentifier: String, hostVersion: String) -> TControllerStatusDatum { let datum = TControllerStatusDatum(time: datumTime, battery: datumControllerBattery) + let origin = datumOrigin(for: resolvedIdentifier(for: TControllerStatusDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) return datum.adornWith(id: datumId(for: userId, type: TControllerStatusDatum.self), timeZone: datumTimeZone, timeZoneOffset: datumTimeZoneOffset, payload: datumPayload, - origin: datumOrigin(for: TControllerStatusDatum.self)) + origin: origin) } - func datumPumpStatus(for userId: String) -> TPumpStatusDatum { + func datumPumpStatus(for userId: String, hostIdentifier: String, hostVersion: String) -> TPumpStatusDatum { let datum = TPumpStatusDatum(time: datumTime, basalDelivery: datumBasalDelivery, battery: datumPumpBattery, bolusDelivery: datumBolusDelivery, deliveryIndeterminant: datumDeliveryIndeterminant, reservoir: datumReservoir) + let origin = datumOrigin(for: resolvedIdentifier(for: TPumpStatusDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) return datum.adornWith(id: datumId(for: userId, type: TPumpStatusDatum.self), timeZone: datumTimeZone, timeZoneOffset: datumTimeZoneOffset, payload: datumPayload, - origin: datumOrigin(for: TPumpStatusDatum.self)) + origin: origin) } var syncIdentifierAsString: String { syncIdentifier.uuidString } diff --git a/TidepoolServiceKit/Extensions/StoredGlucoseSample.swift b/TidepoolServiceKit/Extensions/StoredGlucoseSample.swift index 51ca716..445055c 100644 --- a/TidepoolServiceKit/Extensions/StoredGlucoseSample.swift +++ b/TidepoolServiceKit/Extensions/StoredGlucoseSample.swift @@ -30,7 +30,7 @@ import TidepoolKit */ extension StoredGlucoseSample: IdentifiableHKDatum { - func datum(for userId: String) -> TDatum? { + func datum(for userId: String, hostIdentifier: String, hostVersion: String) -> TDatum? { guard let id = datumId(for: userId) else { return nil } @@ -44,7 +44,8 @@ extension StoredGlucoseSample: IdentifiableHKDatum { datum = TCBGDatum(time: datumTime, value: datumValue, units: datumUnits, trend: datumTrend, trendRate: datumTrendRate) } - return datum.adornWith(id: id, deviceId: datumDeviceId, annotations: datumAnnotations, payload: datumPayload, origin: datumOrigin) + let origin = datumOrigin(for: resolvedIdentifier, hostIdentifier: hostIdentifier, hostVersion: hostVersion) + return datum.adornWith(id: id, deviceId: datumDeviceId, annotations: datumAnnotations, payload: datumPayload, origin: origin) } private var datumTime: Date { startDate } diff --git a/TidepoolServiceKit/Extensions/StoredSettings.swift b/TidepoolServiceKit/Extensions/StoredSettings.swift index db78109..e3993b6 100644 --- a/TidepoolServiceKit/Extensions/StoredSettings.swift +++ b/TidepoolServiceKit/Extensions/StoredSettings.swift @@ -49,18 +49,19 @@ import TidepoolKit */ extension StoredSettings: IdentifiableDatum { - func datumControllerSettings(for userId: String) -> TControllerSettingsDatum { + func datumControllerSettings(for userId: String, hostIdentifier: String, hostVersion: String) -> TControllerSettingsDatum { let datum = TControllerSettingsDatum(time: datumTime, device: datumControllerDevice, notifications: datumControllerNotifications) + let origin = datumOrigin(for: resolvedIdentifier(for: TControllerSettingsDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) return datum.adornWith(id: datumId(for: userId, type: TControllerSettingsDatum.self), timeZone: datumTimeZone, timeZoneOffset: datumTimeZoneOffset, payload: datumPayload, - origin: datumOrigin(for: TControllerSettingsDatum.self)) + origin: origin) } - func datumCGMSettings(for userId: String) -> TCGMSettingsDatum { + func datumCGMSettings(for userId: String, hostIdentifier: String, hostVersion: String) -> TCGMSettingsDatum { let datum = TCGMSettingsDatum(time: datumTime, firmwareVersion: datumCGMFirmwareVersion, hardwareVersion: datumCGMHardwareVersion, @@ -73,14 +74,15 @@ extension StoredSettings: IdentifiableDatum { units: datumCGMUnits, defaultAlerts: nil, // TODO: https://tidepool.atlassian.net/browse/LOOP-3929 scheduledAlerts: nil) // TODO: https://tidepool.atlassian.net/browse/LOOP-3929 + let origin = datumOrigin(for: resolvedIdentifier(for: TCGMSettingsDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) return datum.adornWith(id: datumId(for: userId, type: TCGMSettingsDatum.self), timeZone: datumTimeZone, timeZoneOffset: datumTimeZoneOffset, payload: datumPayload, - origin: datumOrigin(for: TCGMSettingsDatum.self)) + origin: origin) } - func datumPumpSettings(for userId: String) -> TPumpSettingsDatum { + func datumPumpSettings(for userId: String, hostIdentifier: String, hostVersion: String) -> TPumpSettingsDatum { let datum = TPumpSettingsDatum(time: datumTime, activeScheduleName: datumPumpActiveScheduleName, automatedDelivery: datumPumpAutomatedDelivery, @@ -106,14 +108,15 @@ extension StoredSettings: IdentifiableDatum { serialNumber: datumPumpSerialNumber, softwareVersion: datumPumpSoftwareVersion, units: datumPumpUnits) + let origin = datumOrigin(for: resolvedIdentifier(for: TPumpSettingsDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) return datum.adornWith(id: datumId(for: userId, type: TPumpSettingsDatum.self), timeZone: datumTimeZone, timeZoneOffset: datumTimeZoneOffset, payload: datumPayload, - origin: datumOrigin(for: TPumpSettingsDatum.self)) + origin: origin) } - func datumPumpSettingsOverrideDeviceEvent(for userId: String) -> TPumpSettingsOverrideDeviceEventDatum? { + func datumPumpSettingsOverrideDeviceEvent(for userId: String, hostIdentifier: String, hostVersion: String) -> TPumpSettingsOverrideDeviceEventDatum? { guard let activeOverride = activeOverride else { return nil } @@ -128,11 +131,12 @@ extension StoredSettings: IdentifiableDatum { carbohydrateRatioScaleFactor: activeOverride.datumCarbohydrateRatioScaleFactor, insulinSensitivityScaleFactor: activeOverride.datumInsulinSensitivityScaleFactor, units: activeOverride.datumUnits) + let origin = datumOrigin(for: resolvedIdentifier(for: TPumpSettingsOverrideDeviceEventDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) return datum.adornWith(id: datumId(for: userId, type: TPumpSettingsOverrideDeviceEventDatum.self), timeZone: datumTimeZone, timeZoneOffset: datumTimeZoneOffset, payload: datumPayload, - origin: datumOrigin(for: TPumpSettingsOverrideDeviceEventDatum.self)) + origin: origin) } var syncIdentifierAsString: String { syncIdentifier.uuidString } @@ -174,12 +178,6 @@ extension StoredSettings: IdentifiableDatum { private var datumCGMUnits: TCGMSettingsDatum.Units { .milligramsPerDeciliter } private var datumPumpActiveScheduleName: String? { - guard basalRateSchedule != nil || - glucoseTargetRangeSchedule != nil || - carbRatioSchedule != nil || - insulinSensitivitySchedule != nil else { - return nil - } return Self.activeScheduleNameDefault } diff --git a/TidepoolServiceKit/Extensions/SyncAlertObject.swift b/TidepoolServiceKit/Extensions/SyncAlertObject.swift index 106b08c..ef27910 100644 --- a/TidepoolServiceKit/Extensions/SyncAlertObject.swift +++ b/TidepoolServiceKit/Extensions/SyncAlertObject.swift @@ -26,7 +26,7 @@ import TidepoolKit */ extension SyncAlertObject: IdentifiableDatum { - func datum(for userId: String) -> TAlertDatum? { + func datum(for userId: String, hostIdentifier: String, hostVersion: String) -> TAlertDatum? { guard triggered else { // If alert not yet triggered due to delay, then ignore return nil } @@ -42,7 +42,7 @@ extension SyncAlertObject: IdentifiableDatum { retractedTime: datumRetractedTime) return datum.adornWith(id: datumId(for: userId), payload: datumPayload, - origin: datumOrigin) + origin: datumOrigin(for: resolvedIdentifier, hostIdentifier: hostIdentifier, hostVersion: hostVersion)) } var syncIdentifierAsString: String { syncIdentifier.uuidString } diff --git a/TidepoolServiceKit/Extensions/SyncCarbObject.swift b/TidepoolServiceKit/Extensions/SyncCarbObject.swift index a865d0b..5203776 100644 --- a/TidepoolServiceKit/Extensions/SyncCarbObject.swift +++ b/TidepoolServiceKit/Extensions/SyncCarbObject.swift @@ -33,16 +33,22 @@ import TidepoolKit // TODO: Consider adding syncVersion to new update backend API (or just keep in payload) extension SyncCarbObject: IdentifiableHKDatum { - func datum(for userId: String) -> TFoodDatum? { + func datum(for userId: String, hostIdentifier: String, hostVersion: String) -> TFoodDatum? { guard let id = datumId(for: userId) else { return nil } - return TFoodDatum(time: datumTime, name: datumName, nutrition: datumNutrition).adornWith(id: id, payload: datumPayload, origin: datumOrigin) + let origin = datumOrigin(for: resolvedIdentifier, hostIdentifier: hostIdentifier, hostVersion: hostVersion) + return TFoodDatum(time: datumTime, name: datumName, nutrition: datumNutrition).adornWith(id: id, payload: datumPayload, origin: origin) } private var datumTime: Date { startDate } - private var datumName: String? { foodType } + private var datumName: String? { + guard let foodType else { + return nil + } + return foodType.isEmpty ? nil : foodType + } private var datumNutrition: TFoodDatum.Nutrition { return TFoodDatum.Nutrition(carbohydrate: datumCarbohydrate, estimatedAbsorptionDuration: absorptionTime) diff --git a/TidepoolServiceKit/IdentifiableDatum.swift b/TidepoolServiceKit/IdentifiableDatum.swift index e555919..4167d50 100644 --- a/TidepoolServiceKit/IdentifiableDatum.swift +++ b/TidepoolServiceKit/IdentifiableDatum.swift @@ -29,18 +29,10 @@ extension IdentifiableDatum { return "\(userId):\(resolvedIdentifier)".md5hash! } - var datumOrigin: TOrigin { - return datumOrigin(for: resolvedIdentifier) - } - - func datumOrigin(for type: T.Type) -> TOrigin { - return datumOrigin(for: resolvedIdentifier(for: type)) - } - - private func datumOrigin(for resolvedIdentifier: String) -> TOrigin { + func datumOrigin(for resolvedIdentifier: String, hostIdentifier: String, hostVersion: String) -> TOrigin { return TOrigin(id: resolvedIdentifier, - name: Bundle.main.bundleIdentifier, - version: Bundle.main.semanticVersion, + name: hostIdentifier, + version: hostVersion, type: .application) } diff --git a/TidepoolServiceKit/IdentifiableHKDatum.swift b/TidepoolServiceKit/IdentifiableHKDatum.swift index 724a893..09048fb 100644 --- a/TidepoolServiceKit/IdentifiableHKDatum.swift +++ b/TidepoolServiceKit/IdentifiableHKDatum.swift @@ -30,26 +30,18 @@ extension IdentifiableHKDatum { return "\(userId):\(resolvedIdentifier)".md5hash } - var datumOrigin: TOrigin? { - return datumOrigin(for: resolvedIdentifier) - } - - func datumOrigin(for type: T.Type) -> TOrigin? { - return datumOrigin(for: resolvedIdentifier(for: type)) - } - - private func datumOrigin(for resolvedIdentifier: String?) -> TOrigin? { + func datumOrigin(for resolvedIdentifier: String?, hostIdentifier: String, hostVersion: String) -> TOrigin? { guard let resolvedIdentifier = resolvedIdentifier else { return nil } - if !provenanceIdentifier.isEmpty, provenanceIdentifier != Bundle.main.bundleIdentifier { + if !provenanceIdentifier.isEmpty, provenanceIdentifier != hostIdentifier { return TOrigin(id: resolvedIdentifier, name: provenanceIdentifier, type: .application) } else { return TOrigin(id: resolvedIdentifier, - name: Bundle.main.bundleIdentifier, - version: Bundle.main.semanticVersion, + name: hostIdentifier, + version: hostVersion, type: .application) } } diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index 7363938..5bc5233 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -12,34 +12,46 @@ import TidepoolKit public enum TidepoolServiceError: Error { case configuration + case missingDataSetId } +extension TidepoolServiceError: LocalizedError { + public var errorDescription: String? { + switch self { + case .configuration: return LocalizedString("Configuration Error", comment: "Error string for TidepoolServiceError.configuration") + case .missingDataSetId: return LocalizedString("Missing DataSet Id", comment: "Error string for TidepoolServiceError.missingDataSetId") + } + } +} + + + public protocol SessionStorage { func setSession(_ session: TSession?, for service: String) throws func getSession(for service: String) throws -> TSession? } -public final class TidepoolService: Service, TAPIObserver { +public final class TidepoolService: Service, TAPIObserver, ObservableObject { public static let serviceIdentifier = "TidepoolService" public static let localizedTitle = LocalizedString("Tidepool", comment: "The title of the Tidepool service") - public weak var serviceDelegate: ServiceDelegate? + public weak var serviceDelegate: ServiceDelegate? { + didSet { + self.hostIdentifier = serviceDelegate?.hostIdentifier + self.hostVersion = serviceDelegate?.hostVersion + } + } - public lazy var sessionStorage: SessionStorage? = KeychainManager() + public lazy var sessionStorage: SessionStorage = KeychainManager() - public let tapi: TAPI + public let tapi: TAPI = TAPI(clientId: Bundle.main.tidepoolServiceClientId, redirectURL: Bundle.main.tidepoolServiceRedirectURL) public private (set) var error: Error? private let id: String - private var dataSetId: String? { - didSet { - completeUpdate() - } - } private var lastControllerSettingsDatum: TControllerSettingsDatum? @@ -49,50 +61,56 @@ public final class TidepoolService: Service, TAPIObserver { private var lastPumpSettingsOverrideDeviceEventDatum: TPumpSettingsOverrideDeviceEventDatum? + private var hostIdentifier: String? + private var hostVersion: String? + private let log = OSLog(category: "TidepoolService") private let tidepoolKitLog = OSLog(category: "TidepoolKit") - public init(automaticallyFetchEnvironments: Bool = true) { + public init(hostIdentifier: String, hostVersion: String) { self.id = UUID().uuidString - self.tapi = TAPI(automaticallyFetchEnvironments: automaticallyFetchEnvironments) + self.hostIdentifier = hostIdentifier + self.hostVersion = hostVersion - // TODO: REMOVE BEFORE SHIPPING - https://tidepool.atlassian.net/browse/LOOP-4060 - if tapi.defaultEnvironment == nil { - tapi.defaultEnvironment = TEnvironment(host: "external.integration.tidepool.org", port: 443) + Task { + await tapi.setLogging(self) + await tapi.addObserver(self) } - - tapi.logging = self - tapi.addObserver(self) - } - - deinit { - tapi.removeObserver(self) } public init?(rawState: RawStateValue) { - self.tapi = TAPI() + self.isOnboarded = true // Assume when restoring from state, that we're onboarded guard let id = rawState["id"] as? String else { return nil } do { self.id = id - self.dataSetId = rawState["dataSetId"] as? String + if let dataSetId = rawState["dataSetId"] as? String { + self.dataSetIdCacheStatus = .fetched(dataSetId) + } self.lastControllerSettingsDatum = (rawState["lastControllerSettingsDatum"] as? Data).flatMap { try? Self.decoder.decode(TControllerSettingsDatum.self, from: $0) } self.lastCGMSettingsDatum = (rawState["lastCGMSettingsDatum"] as? Data).flatMap { try? Self.decoder.decode(TCGMSettingsDatum.self, from: $0) } self.lastPumpSettingsDatum = (rawState["lastPumpSettingsDatum"] as? Data).flatMap { try? Self.decoder.decode(TPumpSettingsDatum.self, from: $0) } self.lastPumpSettingsOverrideDeviceEventDatum = (rawState["lastPumpSettingsOverrideDeviceEventDatum"] as? Data).flatMap { try? Self.decoder.decode(TPumpSettingsOverrideDeviceEventDatum.self, from: $0) } - tapi.session = try sessionStorage?.getSession(for: sessionService) + self.session = try sessionStorage.getSession(for: sessionService) + Task { + await tapi.setSession(session) + await tapi.setLogging(self) + await tapi.addObserver(self) + } } catch let error { + tidepoolKitLog.error("Error initializing TidepoolService %{public}@", error.localizedDescription) self.error = error + return nil } - tapi.logging = self - tapi.addObserver(self) } public var rawState: RawStateValue { var rawValue: RawStateValue = [:] rawValue["id"] = id - rawValue["dataSetId"] = dataSetId + if case .fetched(let dataSetId) = dataSetIdCacheStatus { + rawValue["dataSetId"] = dataSetId + } rawValue["lastControllerSettingsDatum"] = lastControllerSettingsDatum.flatMap { try? Self.encoder.encode($0) } rawValue["lastCGMSettingsDatum"] = lastCGMSettingsDatum.flatMap { try? Self.encoder.encode($0) } rawValue["lastPumpSettingsDatum"] = lastPumpSettingsDatum.flatMap { try? Self.encoder.encode($0) } @@ -100,90 +118,141 @@ public final class TidepoolService: Service, TAPIObserver { return rawValue } - public let isOnboarded = true // No distinction between created and onboarded + public var isOnboarded = false // No distinction between created and onboarded + + @Published public var session: TSession? public func apiDidUpdateSession(_ session: TSession?) { - if session == nil { - self.dataSetId = nil + guard session != self.session else { + return } + + // If userId changed, then current dataSetId is invalid + if session?.userId != self.session?.userId { + clearCachedDataSetId() + } + + self.session = session + do { - try sessionStorage?.setSession(session, for: sessionService) + try sessionStorage.setSession(session, for: sessionService) } catch let error { self.error = error } - } - public func completeCreate(completion: @escaping (Error?) -> Void) { - DispatchQueue.global(qos: .background).async { - self.getDataSet(completion: completion) + if session == nil { + clearCachedDataSetId() + let content = Alert.Content(title: LocalizedString("Tidepool Service Authorization", comment: "The title for an alert generated when TidepoolService is no longer authorized."), + body: LocalizedString("Tidepool service is no longer authorized. Please navigate to Tidepool Service settings and reauthenticate.", comment: "The body text for an alert generated when TidepoolService is no longer authorized."), + acknowledgeActionButtonLabel: LocalizedString("OK", comment: "Alert acknowledgment OK button")) + serviceDelegate?.issueAlert(Alert(identifier: Alert.Identifier(managerIdentifier: "TidepoolService", + alertIdentifier: "authentication-needed"), + foregroundContent: content, backgroundContent: content, + trigger: .immediate)) } } + public func completeCreate() async throws { + self.isOnboarded = true + } + public func completeUpdate() { serviceDelegate?.serviceDidUpdateState(self) } - public func completeDelete() { - DispatchQueue.global(qos: .background).async { - self.tapi.logout() { _ in } + public func deleteService() { + Task { + await self.tapi.logout() } serviceDelegate?.serviceWantsDeletion(self) } - private func getDataSet(completion: @escaping (Error?) -> Void) { - guard let clientName = Bundle.main.bundleIdentifier else { - completion(TidepoolServiceError.configuration) - return - } - tapi.listDataSets(filter: TDataSet.Filter(clientName: clientName, deleted: false)) { result in - switch result { - case .failure(let error): - completion(error) - case .success(let dataSets): - if !dataSets.isEmpty { - if dataSets.count > 1 { - self.log.error("Found multiple matching data sets; expected zero or one") - } - self.dataSetId = dataSets.first?.uploadId - completion(nil) - } else { - self.createDataSet(completion: completion) - } + private var sessionService: String { "org.tidepool.TidepoolService.\(id)" } + + private var userId: String? { session?.userId } + + private static var encoder: PropertyListEncoder = { + let encoder = PropertyListEncoder() + encoder.outputFormat = .binary + return encoder + }() + + private static var decoder = PropertyListDecoder() + + // MARK: - DataSetId + + enum DataSetIdCacheStatus { + case inProgress(Task) + case fetched(String) + } + + private var dataSetIdCacheStatus: DataSetIdCacheStatus? + + private func clearCachedDataSetId() { + dataSetIdCacheStatus = nil + } + + // This is the main accessor for data set id. It will trigger a fetch or creation + // of the Loop data set associated with the currently logged in account, and will + // handle caching and minimizing the number of network requests. + public func getCachedDataSetId() async throws -> String { + if let fetchStatus = dataSetIdCacheStatus { + switch fetchStatus { + case .fetched(let dataSetId): + return dataSetId + case .inProgress(let task): + return try await task.value } } + + let task: Task = Task { + return try await fetchDataSetId() + } + + dataSetIdCacheStatus = .inProgress(task) + let dataSetId = try await task.value + dataSetIdCacheStatus = .fetched(dataSetId) + return dataSetId } - private func createDataSet(completion: @escaping (Error?) -> Void) { - guard let clientName = Bundle.main.bundleIdentifier, let clientVersion = Bundle.main.semanticVersion else { - completion(TidepoolServiceError.configuration) - return + private func fetchDataSetId() async throws -> String { + guard let clientName = hostIdentifier else { + throw TidepoolServiceError.configuration } - let dataSet = TDataSet(client: TDataSet.Client(name: clientName, version: clientVersion), - dataSetType: .continuous, - deduplicator: TDataSet.Deduplicator(name: .dataSetDeleteOrigin), - deviceTags: [.bgm, .cgm, .insulinPump]) - tapi.createDataSet(dataSet) { result in - switch result { - case .failure(let error): - completion(error) - case .success(let dataSet): - self.dataSetId = dataSet.uploadId - completion(nil) + + let dataSets = try await tapi.listDataSets(filter: TDataSet.Filter(clientName: clientName, deleted: false)) + + if !dataSets.isEmpty { + if dataSets.count > 1 { + self.log.error("Found multiple matching data sets; expected zero or one") + } + + guard let dataSetId = dataSets.first?.uploadId else { + throw TidepoolServiceError.missingDataSetId + } + return dataSetId + } else { + let dataSet = try await self.createDataSet() + guard let dataSetId = dataSet.id else { + throw TidepoolServiceError.missingDataSetId } + return dataSetId } } - private var sessionService: String { "org.tidepool.TidepoolService.\(id)" } + private func createDataSet() async throws -> TDataSet { + guard let clientName = hostIdentifier, let clientVersion = hostVersion else { + throw TidepoolServiceError.configuration + } - private var userId: String? { tapi.session?.userId } + let dataSet = TDataSet(client: TDataSet.Client(name: clientName, version: clientVersion), + dataSetType: .continuous, + deduplicator: TDataSet.Deduplicator(name: .dataSetDeleteOrigin), + deviceTags: [.bgm, .cgm, .insulinPump]) - private static var encoder: PropertyListEncoder = { - let encoder = PropertyListEncoder() - encoder.outputFormat = .binary - return encoder - }() + return try await tapi.createDataSet(dataSet) + } - private static var decoder = PropertyListDecoder() } extension TidepoolService: TLogging { @@ -214,41 +283,36 @@ extension TidepoolService: RemoteDataService { public var alertDataLimit: Int? { return 1000 } public func uploadAlertData(_ stored: [SyncAlertObject], completion: @escaping (_ result: Result) -> Void) { - guard let userId = userId else { + guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { completion(.failure(TidepoolServiceError.configuration)) return } - createData(stored.compactMap { $0.datum(for: userId) }, completion: completion) + Task { + do { + let result = try await createData(stored.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) + completion(.success(result)) + } catch { + completion(.failure(error)) + } + } } public var carbDataLimit: Int? { return 1000 } public func uploadCarbData(created: [SyncCarbObject], updated: [SyncCarbObject], deleted: [SyncCarbObject], completion: @escaping (Result) -> Void) { - guard let userId = userId else { + guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { completion(.failure(TidepoolServiceError.configuration)) return } - createData(created.compactMap { $0.datum(for: userId) }) { result in - switch result { - case .failure(let error): + Task { + do { + let createdUploaded = try await createData(created.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) + let updatedUploaded = try await updateData(updated.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) + let deletedUploaded = try await deleteData(withSelectors: deleted.compactMap { $0.selector }) + completion(.success(createdUploaded || updatedUploaded || deletedUploaded)) + } catch { completion(.failure(error)) - case .success(let createdUploaded): - self.updateData(updated.compactMap { $0.datum(for: userId) }) { result in - switch result { - case .failure(let error): - completion(.failure(error)) - case .success(let updatedUploaded): - self.deleteData(withSelectors: deleted.compactMap { $0.selector }) { result in - switch result { - case .failure(let error): - completion(.failure(error)) - case .success(let deletedUploaded): - completion(.success(createdUploaded || updatedUploaded || deletedUploaded)) - } - } - } - } } } } @@ -256,23 +320,18 @@ extension TidepoolService: RemoteDataService { public var doseDataLimit: Int? { return 1000 } public func uploadDoseData(created: [DoseEntry], deleted: [DoseEntry], completion: @escaping (_ result: Result) -> Void) { - guard let userId = userId else { + guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { completion(.failure(TidepoolServiceError.configuration)) return } - createData(created.flatMap { $0.data(for: userId) }) { result in - switch result { - case .failure(let error): + + Task { + do { + let createdUploaded = try await createData(created.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) + let deletedUploaded = try await deleteData(withSelectors: deleted.flatMap { $0.selectors }) + completion(.success(createdUploaded || deletedUploaded)) + } catch { completion(.failure(error)) - case .success(let createdUploaded): - self.deleteData(withSelectors: deleted.flatMap { $0.selectors }) { result in - switch result { - case .failure(let error): - completion(.failure(error)) - case .success(let deletedUploaded): - completion(.success(createdUploaded || deletedUploaded)) - } - } } } } @@ -280,20 +339,28 @@ extension TidepoolService: RemoteDataService { public var dosingDecisionDataLimit: Int? { return 50 } // Each can be up to 20K bytes of serialized JSON, target ~1M or less public func uploadDosingDecisionData(_ stored: [StoredDosingDecision], completion: @escaping (_ result: Result) -> Void) { - guard let userId = userId else { + guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { completion(.failure(TidepoolServiceError.configuration)) return } - createData(calculateDosingDecisionData(stored, for: userId), completion: completion) + + Task { + do { + let result = try await createData(calculateDosingDecisionData(stored, for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)) + completion(.success(result)) + } catch { + completion(.failure(error)) + } + } } - func calculateDosingDecisionData(_ stored: [StoredDosingDecision], for userId: String) -> [TDatum] { + func calculateDosingDecisionData(_ stored: [StoredDosingDecision], for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { var created: [TDatum] = [] stored.forEach { - let dosingDecisionDatum = $0.datumDosingDecision(for: userId) - let controllerStatusDatum = $0.datumControllerStatus(for: userId) - let pumpStatusDatum = $0.datumPumpStatus(for:userId) + let dosingDecisionDatum = $0.datumDosingDecision(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) + let controllerStatusDatum = $0.datumControllerStatus(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) + let pumpStatusDatum = $0.datumPumpStatus(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) var dosingDecisionAssociations: [TAssociation] = [] var controllerStatusAssociations: [TAssociation] = [] @@ -336,56 +403,66 @@ extension TidepoolService: RemoteDataService { public var glucoseDataLimit: Int? { return 1000 } public func uploadGlucoseData(_ stored: [StoredGlucoseSample], completion: @escaping (Result) -> Void) { - guard let userId = userId else { + guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { completion(.failure(TidepoolServiceError.configuration)) return } - createData(stored.compactMap { $0.datum(for: userId) }, completion: completion) + + Task { + do { + let result = try await createData(stored.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) + completion(.success(result)) + } catch { + completion(.failure(error)) + } + } } public var pumpDataEventLimit: Int? { return 1000 } public func uploadPumpEventData(_ stored: [PersistedPumpEvent], completion: @escaping (_ result: Result) -> Void) { - guard let userId = userId else { + guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { completion(.failure(TidepoolServiceError.configuration)) return } - createData(stored.flatMap { $0.data(for: userId) }, completion: completion) + + Task { + do { + let result = try await createData(stored.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) + completion(.success(result)) + } catch { + completion(.failure(error)) + } + } } public var settingsDataLimit: Int? { return 400 } // Each can be up to 2.5K bytes of serialized JSON, target ~1M or less public func uploadSettingsData(_ stored: [StoredSettings], completion: @escaping (_ result: Result) -> Void) { - guard let userId = userId else { + guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { completion(.failure(TidepoolServiceError.configuration)) return } - let (created, updated, lastControllerSettingsDatum, lastCGMSettingsDatum, lastPumpSettingsDatum, lastPumpSettingsOverrideDeviceEventDatum) = calculateSettingsData(stored, for: userId) + let (created, updated, lastControllerSettingsDatum, lastCGMSettingsDatum, lastPumpSettingsDatum, lastPumpSettingsOverrideDeviceEventDatum) = calculateSettingsData(stored, for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) - createData(created) { result in - switch result { - case .failure(let error): + Task { + do { + let createdUploaded = try await createData(created) + let updatedUploaded = try await updateData(updated) + self.lastControllerSettingsDatum = lastControllerSettingsDatum + self.lastCGMSettingsDatum = lastCGMSettingsDatum + self.lastPumpSettingsDatum = lastPumpSettingsDatum + self.lastPumpSettingsOverrideDeviceEventDatum = lastPumpSettingsOverrideDeviceEventDatum + self.completeUpdate() + completion(.success(createdUploaded || updatedUploaded)) + } catch { completion(.failure(error)) - case .success(let createdUploaded): - self.updateData(updated) { result in - switch result { - case .failure(let error): - completion(.failure(error)) - case .success(let updatedUploaded): - self.lastControllerSettingsDatum = lastControllerSettingsDatum - self.lastCGMSettingsDatum = lastCGMSettingsDatum - self.lastPumpSettingsDatum = lastPumpSettingsDatum - self.lastPumpSettingsOverrideDeviceEventDatum = lastPumpSettingsOverrideDeviceEventDatum - self.completeUpdate() - completion(.success(createdUploaded || updatedUploaded)) - } - } } } } - func calculateSettingsData(_ stored: [StoredSettings], for userId: String) -> ([TDatum], [TDatum], TControllerSettingsDatum?, TCGMSettingsDatum?, TPumpSettingsDatum?, TPumpSettingsOverrideDeviceEventDatum?) { + func calculateSettingsData(_ stored: [StoredSettings], for userId: String, hostIdentifier: String, hostVersion: String) -> ([TDatum], [TDatum], TControllerSettingsDatum?, TCGMSettingsDatum?, TPumpSettingsDatum?, TPumpSettingsOverrideDeviceEventDatum?) { var created: [TDatum] = [] var updated: [TDatum] = [] var lastControllerSettingsDatum = lastControllerSettingsDatum @@ -403,16 +480,16 @@ extension TidepoolService: RemoteDataService { // Calculate the data - let controllerSettingsDatum = $0.datumControllerSettings(for: userId) + let controllerSettingsDatum = $0.datumControllerSettings(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) let controllerSettingsDatumIsEffectivelyEquivalent = TControllerSettingsDatum.areEffectivelyEquivalent(old: lastControllerSettingsDatum, new: controllerSettingsDatum) - let cgmSettingsDatum = $0.datumCGMSettings(for: userId) + let cgmSettingsDatum = $0.datumCGMSettings(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) let cgmSettingsDatumIsEffectivelyEquivalent = TCGMSettingsDatum.areEffectivelyEquivalent(old: lastCGMSettingsDatum, new: cgmSettingsDatum) - let pumpSettingsDatum = $0.datumPumpSettings(for: userId) + let pumpSettingsDatum = $0.datumPumpSettings(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) let pumpSettingsDatumIsEffectivelyEquivalent = TPumpSettingsDatum.areEffectivelyEquivalent(old: lastPumpSettingsDatum, new: pumpSettingsDatum) - let pumpSettingsOverrideDeviceEventDatum = $0.datumPumpSettingsOverrideDeviceEvent(for: userId) + let pumpSettingsOverrideDeviceEventDatum = $0.datumPumpSettingsOverrideDeviceEvent(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) let pumpSettingsOverrideDeviceEventDatumIsEffectivelyEquivalent = TPumpSettingsOverrideDeviceEventDatum.areEffectivelyEquivalent(old: lastPumpSettingsOverrideDeviceEventDatum, new: pumpSettingsOverrideDeviceEventDatum) // Associate the data @@ -483,68 +560,66 @@ extension TidepoolService: RemoteDataService { return (created, updated, lastControllerSettingsDatum, lastCGMSettingsDatum, lastPumpSettingsDatum, lastPumpSettingsOverrideDeviceEventDatum) } - private func createData(_ data: [TDatum], completion: @escaping (Result) -> Void) { + private func createData(_ data: [TDatum]) async throws -> Bool { if let error = error { - completion(.failure(error)) - return - } - guard let dataSetId = dataSetId else { - completion(.failure(TidepoolServiceError.configuration)) - return + throw error } - tapi.createData(data, dataSetId: dataSetId) { error in - if let error = error { - self.log.error("Failed to create data - %{public}@", error.errorDescription!) - completion(.failure(error)) - return - } - completion(.success(!data.isEmpty)) + let dataSetId = try await getCachedDataSetId() + + do { + try await tapi.createData(data, dataSetId: dataSetId) + return !data.isEmpty + } catch { + self.log.error("Failed to create data - %{public}@", error.localizedDescription) + self.log.error("Failed data: %{public}@", String(describing: data)) + throw error } } - private func updateData(_ data: [TDatum], completion: @escaping (Result) -> Void) { + private func updateData(_ data: [TDatum]) async throws -> Bool { if let error = error { - completion(.failure(error)) - return - } - guard let dataSetId = dataSetId else { - completion(.failure(TidepoolServiceError.configuration)) - return + throw error } + let dataSetId = try await getCachedDataSetId() + // TODO: This implementation is incorrect and will not record the correct history when data is updated. Currently waiting on // https://tidepool.atlassian.net/browse/BACK-815 for backend to support new API to capture full history of data changes. // This work will be covered in https://tidepool.atlassian.net/browse/LOOP-3943. For now just call createData with the // updated data as it will just overwrite the previous data with the updated data. - tapi.createData(data, dataSetId: dataSetId) { error in - if let error = error { - self.log.error("Failed to update data - %{public}@", error.errorDescription!) - completion(.failure(error)) - return - } - completion(.success(!data.isEmpty)) + do { + try await tapi.createData(data, dataSetId: dataSetId) + return !data.isEmpty + } catch { + self.log.error("Failed to update data - %{public}@", error.localizedDescription) + throw error } } - private func deleteData(withSelectors selectors: [TDatum.Selector], completion: @escaping (Result) -> Void) { + private func deleteData(withSelectors selectors: [TDatum.Selector]) async throws -> Bool { if let error = error { - completion(.failure(error)) - return + throw error } - guard let dataSetId = dataSetId else { - completion(.failure(TidepoolServiceError.configuration)) - return + + let dataSetId = try await getCachedDataSetId() + + do { + try await tapi.deleteData(withSelectors: selectors, dataSetId: dataSetId) + return !selectors.isEmpty + } catch { + self.log.error("Failed to delete data - %{public}@", error.localizedDescription) + throw error } + } - tapi.deleteData(withSelectors: selectors, dataSetId: dataSetId) { error in - if let error = error { - self.log.error("Failed to delete data - %{public}@", error.errorDescription!) - completion(.failure(error)) - return - } - completion(.success(!selectors.isEmpty)) + public func commandFromPushNotification(_ notification: [String: AnyObject]) async throws -> RemoteCommand { + + enum TidepoolPushNotificationError: LocalizedError { + case remoteCommandsNotSupported } + + throw TidepoolPushNotificationError.remoteCommandsNotSupported } } @@ -564,14 +639,6 @@ extension KeychainManager: SessionStorage { } } -extension TidepoolServiceError: LocalizedError { - public var errorDescription: String? { - switch self { - case .configuration: return NSLocalizedString("Configuration Error", comment: "Error string for configuration error") - } - } -} - fileprivate protocol EffectivelyEquivalent { func isEffectivelyEquivalent(to other: Self) -> Bool var isEffectivelyEmpty: Bool { get } diff --git a/TidepoolServiceKit/cs.lproj/Localizable.strings b/TidepoolServiceKit/cs.lproj/Localizable.strings new file mode 100644 index 0000000..d7434dc --- /dev/null +++ b/TidepoolServiceKit/cs.lproj/Localizable.strings @@ -0,0 +1,6 @@ +/* Error string for configuration error */ +"Configuration Error" = "Chyba konfigurace"; + +/* The title of the Tidepool service */ +"Tidepool" = "Tidepool"; + diff --git a/TidepoolServiceKit/da.lproj/Localizable.strings b/TidepoolServiceKit/da.lproj/Localizable.strings new file mode 100644 index 0000000..e8a6bdd --- /dev/null +++ b/TidepoolServiceKit/da.lproj/Localizable.strings @@ -0,0 +1,6 @@ +/* Error string for configuration error */ +"Configuration Error" = "Konfigurationsfejl"; + +/* The title of the Tidepool service */ +"Tidepool" = "Tidepool"; + diff --git a/TidepoolServiceKit/de.lproj/Localizable.strings b/TidepoolServiceKit/de.lproj/Localizable.strings index 2e91324..cf08a93 100644 --- a/TidepoolServiceKit/de.lproj/Localizable.strings +++ b/TidepoolServiceKit/de.lproj/Localizable.strings @@ -1,2 +1,6 @@ +/* Error string for configuration error */ +"Configuration Error" = "Konfigurationsfehler"; + /* The title of the Tidepool service */ "Tidepool" = "Tidepool"; + diff --git a/TidepoolServiceKit/es.lproj/Localizable.strings b/TidepoolServiceKit/es.lproj/Localizable.strings index 2e91324..490d1c1 100644 --- a/TidepoolServiceKit/es.lproj/Localizable.strings +++ b/TidepoolServiceKit/es.lproj/Localizable.strings @@ -1,2 +1,6 @@ +/* Error string for configuration error */ +"Configuration Error" = "Error de configuración"; + /* The title of the Tidepool service */ "Tidepool" = "Tidepool"; + diff --git a/TidepoolServiceKit/fr.lproj/Localizable.strings b/TidepoolServiceKit/fr.lproj/Localizable.strings index 2e91324..e927160 100644 --- a/TidepoolServiceKit/fr.lproj/Localizable.strings +++ b/TidepoolServiceKit/fr.lproj/Localizable.strings @@ -1,2 +1,6 @@ +/* Error string for configuration error */ +"Configuration Error" = "Erreur de configuration"; + /* The title of the Tidepool service */ "Tidepool" = "Tidepool"; + diff --git a/TidepoolServiceKit/he.lproj/Localizable.strings b/TidepoolServiceKit/he.lproj/Localizable.strings new file mode 100644 index 0000000..b519394 --- /dev/null +++ b/TidepoolServiceKit/he.lproj/Localizable.strings @@ -0,0 +1,3 @@ +/* The title of the Tidepool service */ +"Tidepool" = "טיידפול"; + diff --git a/TidepoolServiceKit/it.lproj/Localizable.strings b/TidepoolServiceKit/it.lproj/Localizable.strings index 2e91324..e6d4e6a 100644 --- a/TidepoolServiceKit/it.lproj/Localizable.strings +++ b/TidepoolServiceKit/it.lproj/Localizable.strings @@ -1,2 +1,6 @@ +/* Error string for configuration error */ +"Configuration Error" = "Errore di configurazione"; + /* The title of the Tidepool service */ "Tidepool" = "Tidepool"; + diff --git a/TidepoolServiceKit/nb.lproj/Localizable.strings b/TidepoolServiceKit/nb.lproj/Localizable.strings index 2e91324..2d5b426 100644 --- a/TidepoolServiceKit/nb.lproj/Localizable.strings +++ b/TidepoolServiceKit/nb.lproj/Localizable.strings @@ -1,2 +1,6 @@ +/* Error string for configuration error */ +"Configuration Error" = "Konfigurasjonsfeil"; + /* The title of the Tidepool service */ "Tidepool" = "Tidepool"; + diff --git a/TidepoolServiceKit/nl.lproj/Localizable.strings b/TidepoolServiceKit/nl.lproj/Localizable.strings index 2e91324..1da58ef 100644 --- a/TidepoolServiceKit/nl.lproj/Localizable.strings +++ b/TidepoolServiceKit/nl.lproj/Localizable.strings @@ -1,2 +1,6 @@ +/* Error string for configuration error */ +"Configuration Error" = "Configuratiefout"; + /* The title of the Tidepool service */ "Tidepool" = "Tidepool"; + diff --git a/TidepoolServiceKit/pl.lproj/Localizable.strings b/TidepoolServiceKit/pl.lproj/Localizable.strings index 2e91324..f715aa5 100644 --- a/TidepoolServiceKit/pl.lproj/Localizable.strings +++ b/TidepoolServiceKit/pl.lproj/Localizable.strings @@ -1,2 +1,6 @@ +/* Error string for configuration error */ +"Configuration Error" = "Błąd konfiguracji"; + /* The title of the Tidepool service */ "Tidepool" = "Tidepool"; + diff --git a/TidepoolServiceKit/ro.lproj/Localizable.strings b/TidepoolServiceKit/ro.lproj/Localizable.strings new file mode 100644 index 0000000..c6fe344 --- /dev/null +++ b/TidepoolServiceKit/ro.lproj/Localizable.strings @@ -0,0 +1,6 @@ +/* Error string for configuration error */ +"Configuration Error" = "Eroare de configurare"; + +/* The title of the Tidepool service */ +"Tidepool" = "Tidepool"; + diff --git a/TidepoolServiceKit/ru.lproj/Localizable.strings b/TidepoolServiceKit/ru.lproj/Localizable.strings index 2e91324..f2db765 100644 --- a/TidepoolServiceKit/ru.lproj/Localizable.strings +++ b/TidepoolServiceKit/ru.lproj/Localizable.strings @@ -1,2 +1,6 @@ +/* Error string for configuration error */ +"Configuration Error" = "Ошибка конфигурации"; + /* The title of the Tidepool service */ "Tidepool" = "Tidepool"; + diff --git a/TidepoolServiceKit/sk.lproj/Localizable.strings b/TidepoolServiceKit/sk.lproj/Localizable.strings new file mode 100644 index 0000000..236375c --- /dev/null +++ b/TidepoolServiceKit/sk.lproj/Localizable.strings @@ -0,0 +1,6 @@ +/* Error string for configuration error */ +"Configuration Error" = "Chyba konfigurácie"; + +/* The title of the Tidepool service */ +"Tidepool" = "Tidepool"; + diff --git a/TidepoolServiceKit/tr.lproj/Localizable.strings b/TidepoolServiceKit/tr.lproj/Localizable.strings new file mode 100644 index 0000000..90f5438 --- /dev/null +++ b/TidepoolServiceKit/tr.lproj/Localizable.strings @@ -0,0 +1,6 @@ +/* Error string for configuration error */ +"Configuration Error" = "Yapılandırma hatası"; + +/* The title of the Tidepool service */ +"Tidepool" = "Tidepool"; + diff --git a/TidepoolServiceKit/zh-Hans.lproj/Localizable.strings b/TidepoolServiceKit/zh-Hans.lproj/Localizable.strings index 2e91324..74ea404 100644 --- a/TidepoolServiceKit/zh-Hans.lproj/Localizable.strings +++ b/TidepoolServiceKit/zh-Hans.lproj/Localizable.strings @@ -1,2 +1,3 @@ /* The title of the Tidepool service */ "Tidepool" = "Tidepool"; + diff --git a/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift b/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift index 72d68a0..56d6a9c 100644 --- a/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift +++ b/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift @@ -16,6 +16,9 @@ import TidepoolKit @testable import TidepoolServiceKit class DoseEntryDataTests: XCTestCase { + let hostIdentifier = "com.apple.dt.xctest.tool" + let hostVersion = "1.0.0" + func testDataBasal() { let doseEntry = DoseEntry(type: .basal, startDate: Self.dateFormatter.date(from: "2020-01-02T03:00:23Z")!, @@ -29,13 +32,14 @@ class DoseEntryDataTests: XCTestCase { insulinType: .novolog, automatic: true, manuallyEntered: false) - let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: hostIdentifier, hostVersion: hostVersion) XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { - "deliveryType" : "scheduled", + "deliveryType" : "automated", "duration" : 1500000, - "id" : "ecabf24a123e1d8028a6e41beb00dd13", + "expectedDuration" : 1800000, + "id" : "f839af02f6832d7c81d636dbbbadbc01", "insulinFormulation" : { "simple" : { "actingType" : "rapid", @@ -43,9 +47,10 @@ class DoseEntryDataTests: XCTestCase { } }, "origin" : { - "id" : "ab0a722d639669875017a899a5214677:basal/scheduled", + "id" : "ab0a722d639669875017a899a5214677:basal/automated", "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "type" : "application", + "version" : "1.0.0" }, "payload" : { "deliveredUnits" : 0.75, @@ -74,7 +79,7 @@ class DoseEntryDataTests: XCTestCase { insulinType: .apidra, automatic: false, manuallyEntered: true) - let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: hostIdentifier, hostVersion: hostVersion) XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { @@ -92,7 +97,8 @@ class DoseEntryDataTests: XCTestCase { "origin" : { "id" : "ab0a722d639669875017a899a5214677:insulin", "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "type" : "application", + "version" : "1.0.0" }, "payload" : { "duration" : 30000, @@ -119,7 +125,7 @@ class DoseEntryDataTests: XCTestCase { insulinType: .apidra, automatic: false, manuallyEntered: false) - let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: hostIdentifier, hostVersion: hostVersion) XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { @@ -135,7 +141,8 @@ class DoseEntryDataTests: XCTestCase { "origin" : { "id" : "ab0a722d639669875017a899a5214677:bolus/normal", "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "type" : "application", + "version" : "1.0.0" }, "payload" : { "duration" : 30000, @@ -164,7 +171,7 @@ class DoseEntryDataTests: XCTestCase { automatic: false, manuallyEntered: false, isMutable: true) - let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: hostIdentifier, hostVersion: hostVersion) XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { @@ -184,7 +191,8 @@ class DoseEntryDataTests: XCTestCase { "origin" : { "id" : "ab0a722d639669875017a899a5214677:bolus/normal", "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "type" : "application", + "version" : "1.0.0" }, "payload" : { "duration" : 30000, @@ -212,7 +220,7 @@ class DoseEntryDataTests: XCTestCase { insulinType: .apidra, automatic: true, manuallyEntered: false) - let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: hostIdentifier, hostVersion: hostVersion) XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { @@ -228,7 +236,8 @@ class DoseEntryDataTests: XCTestCase { "origin" : { "id" : "ab0a722d639669875017a899a5214677:bolus/automated", "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "type" : "application", + "version" : "1.0.0" }, "payload" : { "duration" : 30000, @@ -257,7 +266,7 @@ class DoseEntryDataTests: XCTestCase { automatic: true, manuallyEntered: false, isMutable: true) - let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: hostIdentifier, hostVersion: hostVersion) XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { @@ -277,7 +286,8 @@ class DoseEntryDataTests: XCTestCase { "origin" : { "id" : "ab0a722d639669875017a899a5214677:bolus/automated", "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "type" : "application", + "version" : "1.0.0" }, "payload" : { "duration" : 30000, @@ -305,7 +315,7 @@ class DoseEntryDataTests: XCTestCase { insulinType: nil, automatic: true, manuallyEntered: false) - let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: hostIdentifier, hostVersion: hostVersion) XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ @@ -326,7 +336,7 @@ class DoseEntryDataTests: XCTestCase { insulinType: .fiasp, automatic: true, manuallyEntered: false) - let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: hostIdentifier, hostVersion: hostVersion) XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { @@ -336,7 +346,8 @@ class DoseEntryDataTests: XCTestCase { "origin" : { "id" : "ab0a722d639669875017a899a5214677:basal/suspend", "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "type" : "application", + "version" : "1.0.0" }, "payload" : { "syncIdentifier" : "18CF3948-0B3D-4B12-8BFE-14986B0E6784" @@ -368,7 +379,7 @@ class DoseEntryDataTests: XCTestCase { insulinType: .fiasp, automatic: false, manuallyEntered: false) - let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: hostIdentifier, hostVersion: hostVersion) XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { @@ -385,7 +396,8 @@ class DoseEntryDataTests: XCTestCase { "origin" : { "id" : "ab0a722d639669875017a899a5214677:basal/temp", "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "type" : "application", + "version" : "1.0.0" }, "payload" : { "deliveredUnits" : 0.5, @@ -420,7 +432,7 @@ class DoseEntryDataTests: XCTestCase { automatic: false, manuallyEntered: false, isMutable: true) - let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: hostIdentifier, hostVersion: hostVersion) XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { @@ -441,7 +453,8 @@ class DoseEntryDataTests: XCTestCase { "origin" : { "id" : "ab0a722d639669875017a899a5214677:basal/temp", "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "type" : "application", + "version" : "1.0.0" }, "payload" : { "deliveredUnits" : 0.5, @@ -474,7 +487,7 @@ class DoseEntryDataTests: XCTestCase { scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), insulinType: .fiasp, manuallyEntered: false) - let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: hostIdentifier, hostVersion: hostVersion) XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { @@ -491,7 +504,8 @@ class DoseEntryDataTests: XCTestCase { "origin" : { "id" : "ab0a722d639669875017a899a5214677:basal/automated", "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "type" : "application", + "version" : "1.0.0" }, "payload" : { "deliveredUnits" : 0.5, @@ -526,7 +540,7 @@ class DoseEntryDataTests: XCTestCase { insulinType: .fiasp, manuallyEntered: false, isMutable: true) - let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: hostIdentifier, hostVersion: hostVersion) XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { @@ -547,7 +561,8 @@ class DoseEntryDataTests: XCTestCase { "origin" : { "id" : "ab0a722d639669875017a899a5214677:basal/automated", "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "type" : "application", + "version" : "1.0.0" }, "payload" : { "deliveredUnits" : 0.5, diff --git a/TidepoolServiceKitTests/Extensions/PersistedPumpEventTests.swift b/TidepoolServiceKitTests/Extensions/PersistedPumpEventTests.swift index 7f2aeb6..348a7d3 100644 --- a/TidepoolServiceKitTests/Extensions/PersistedPumpEventTests.swift +++ b/TidepoolServiceKitTests/Extensions/PersistedPumpEventTests.swift @@ -24,7 +24,7 @@ class PersistedPumpEventTests: XCTestCase { type: .alarm, automatic: true, alarmType: .other("Test Alarm")) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { @@ -32,8 +32,9 @@ class PersistedPumpEventTests: XCTestCase { "id" : "a07718a631a79cbe9dfafdc7aa3bc227", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/alarm", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "otherAlarmType" : "Test Alarm", @@ -60,15 +61,16 @@ class PersistedPumpEventTests: XCTestCase { type: .alarm, automatic: true, alarmType: .other("Test Alarm")) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { "id" : "383e8a915ae51534a2907f9d9a527e5b", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/status", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "ab0a722d639669875017a899a5214677" @@ -86,8 +88,9 @@ class PersistedPumpEventTests: XCTestCase { "id" : "a07718a631a79cbe9dfafdc7aa3bc227", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/alarm", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "otherAlarmType" : "Test Alarm", @@ -114,7 +117,7 @@ class PersistedPumpEventTests: XCTestCase { type: .alarm, automatic: true, alarmType: .other("Test Alarm")) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { @@ -122,8 +125,9 @@ class PersistedPumpEventTests: XCTestCase { "id" : "a07718a631a79cbe9dfafdc7aa3bc227", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/alarm", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "otherAlarmType" : "Test Alarm", @@ -137,8 +141,9 @@ class PersistedPumpEventTests: XCTestCase { "id" : "383e8a915ae51534a2907f9d9a527e5b", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/status", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "ab0a722d639669875017a899a5214677" @@ -165,7 +170,7 @@ class PersistedPumpEventTests: XCTestCase { raw: "18CF3948-0B3D-4B12-8BFE-14986B0E6784".data(using: .utf8), title: nil, type: .alarmClear) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertTrue(data.isEmpty) } @@ -179,15 +184,16 @@ class PersistedPumpEventTests: XCTestCase { raw: "18CF3948-0B3D-4B12-8BFE-14986B0E6784".data(using: .utf8), title: nil, type: .alarmClear) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { "id" : "383e8a915ae51534a2907f9d9a527e5b", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/status", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "ab0a722d639669875017a899a5214677" @@ -215,15 +221,16 @@ class PersistedPumpEventTests: XCTestCase { raw: "18CF3948-0B3D-4B12-8BFE-14986B0E6784".data(using: .utf8), title: nil, type: .alarmClear) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { "id" : "383e8a915ae51534a2907f9d9a527e5b", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/status", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "ab0a722d639669875017a899a5214677" @@ -262,7 +269,7 @@ class PersistedPumpEventTests: XCTestCase { title: nil, type: .basal, automatic: true) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ @@ -292,7 +299,7 @@ class PersistedPumpEventTests: XCTestCase { title: nil, type: .basal, automatic: false) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ @@ -310,15 +317,16 @@ class PersistedPumpEventTests: XCTestCase { raw: "18CF3948-0B3D-4B12-8BFE-14986B0E6784".data(using: .utf8), title: nil, type: .prime) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { "id" : "00e23a994ef6b0393a8c31db6bc5b264", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/prime", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "ab0a722d639669875017a899a5214677" @@ -343,15 +351,16 @@ class PersistedPumpEventTests: XCTestCase { raw: "18CF3948-0B3D-4B12-8BFE-14986B0E6784".data(using: .utf8), title: nil, type: .prime) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { "id" : "383e8a915ae51534a2907f9d9a527e5b", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/status", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "ab0a722d639669875017a899a5214677" @@ -368,8 +377,9 @@ class PersistedPumpEventTests: XCTestCase { "id" : "00e23a994ef6b0393a8c31db6bc5b264", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/prime", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "ab0a722d639669875017a899a5214677" @@ -394,15 +404,16 @@ class PersistedPumpEventTests: XCTestCase { raw: "18CF3948-0B3D-4B12-8BFE-14986B0E6784".data(using: .utf8), title: nil, type: .prime) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { "id" : "00e23a994ef6b0393a8c31db6bc5b264", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/prime", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "ab0a722d639669875017a899a5214677" @@ -416,8 +427,9 @@ class PersistedPumpEventTests: XCTestCase { "id" : "383e8a915ae51534a2907f9d9a527e5b", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/status", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "ab0a722d639669875017a899a5214677" @@ -444,15 +456,16 @@ class PersistedPumpEventTests: XCTestCase { raw: "18CF3948-0B3D-4B12-8BFE-14986B0E6784".data(using: .utf8), title: nil, type: .resume) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { "id" : "383e8a915ae51534a2907f9d9a527e5b", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/status", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "ab0a722d639669875017a899a5214677" @@ -479,15 +492,16 @@ class PersistedPumpEventTests: XCTestCase { raw: "18CF3948-0B3D-4B12-8BFE-14986B0E6784".data(using: .utf8), title: nil, type: .rewind) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { "id" : "16d248ca92e6a625d0fc0b344916bee7", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/reservoirChange", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "ab0a722d639669875017a899a5214677" @@ -511,15 +525,16 @@ class PersistedPumpEventTests: XCTestCase { raw: "18CF3948-0B3D-4B12-8BFE-14986B0E6784".data(using: .utf8), title: nil, type: .rewind) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { "id" : "383e8a915ae51534a2907f9d9a527e5b", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/status", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "ab0a722d639669875017a899a5214677" @@ -536,8 +551,9 @@ class PersistedPumpEventTests: XCTestCase { "id" : "16d248ca92e6a625d0fc0b344916bee7", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/reservoirChange", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "ab0a722d639669875017a899a5214677" @@ -561,15 +577,16 @@ class PersistedPumpEventTests: XCTestCase { raw: "18CF3948-0B3D-4B12-8BFE-14986B0E6784".data(using: .utf8), title: nil, type: .rewind) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { "id" : "16d248ca92e6a625d0fc0b344916bee7", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/reservoirChange", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "ab0a722d639669875017a899a5214677" @@ -582,8 +599,9 @@ class PersistedPumpEventTests: XCTestCase { "id" : "383e8a915ae51534a2907f9d9a527e5b", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/status", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "ab0a722d639669875017a899a5214677" @@ -612,15 +630,16 @@ class PersistedPumpEventTests: XCTestCase { title: nil, type: .suspend, automatic: true) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ { "id" : "383e8a915ae51534a2907f9d9a527e5b", "origin" : { "id" : "ab0a722d639669875017a899a5214677:deviceEvent/status", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "ab0a722d639669875017a899a5214677" @@ -659,7 +678,7 @@ class PersistedPumpEventTests: XCTestCase { title: nil, type: .tempBasal, automatic: true) - let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let data = pumpEvent.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(data), encoding: .utf8), """ [ diff --git a/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift b/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift index 404f370..6e664f6 100644 --- a/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift +++ b/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift @@ -14,7 +14,7 @@ import TidepoolKit class StoredDosingDecisionTests: XCTestCase { func testDatumDosingDecision() { - let data = try! Self.encoder.encode(StoredDosingDecision.test.datumDosingDecision(for: "1234567890")) + let data = try! Self.encoder.encode(StoredDosingDecision.test.datumDosingDecision(for: "1234567890", hostIdentifier: "Loop", hostVersion: "1.2.3")) XCTAssertEqual(String(data: data, encoding: .utf8), """ { "associations" : [ @@ -112,8 +112,9 @@ class StoredDosingDecisionTests: XCTestCase { }, "origin" : { "id" : "2A67A303-5203-4CB8-8263-79498265368E:dosingDecision", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "originalFood" : { "nutrition" : { @@ -169,7 +170,7 @@ class StoredDosingDecisionTests: XCTestCase { } func testDatumControllerStatus() { - let data = try! Self.encoder.encode(StoredDosingDecision.test.datumControllerStatus(for: "1234567890")) + let data = try! Self.encoder.encode(StoredDosingDecision.test.datumControllerStatus(for: "1234567890", hostIdentifier: "Loop", hostVersion: "1.2.3")) XCTAssertEqual(String(data: data, encoding: .utf8), """ { "battery" : { @@ -180,8 +181,9 @@ class StoredDosingDecisionTests: XCTestCase { "id" : "ac3b6bdb9665f62eac07bf476f53795d", "origin" : { "id" : "2A67A303-5203-4CB8-8263-79498265368E:controllerStatus", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "2A67A303-5203-4CB8-8263-79498265368E" @@ -196,7 +198,7 @@ class StoredDosingDecisionTests: XCTestCase { } func testDatumPumpStatus() { - let data = try! Self.encoder.encode(StoredDosingDecision.test.datumPumpStatus(for: "1234567890")) + let data = try! Self.encoder.encode(StoredDosingDecision.test.datumPumpStatus(for: "1234567890", hostIdentifier: "Loop", hostVersion: "1.2.3")) XCTAssertEqual(String(data: data, encoding: .utf8), """ { "basalDelivery" : { @@ -212,8 +214,9 @@ class StoredDosingDecisionTests: XCTestCase { "id" : "4df2e9d703df3217bd3b834845acfe4d", "origin" : { "id" : "2A67A303-5203-4CB8-8263-79498265368E:pumpStatus", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "2A67A303-5203-4CB8-8263-79498265368E" diff --git a/TidepoolServiceKitTests/Extensions/StoredGlucoseSampleTests.swift b/TidepoolServiceKitTests/Extensions/StoredGlucoseSampleTests.swift index 8e5d970..18641d5 100644 --- a/TidepoolServiceKitTests/Extensions/StoredGlucoseSampleTests.swift +++ b/TidepoolServiceKitTests/Extensions/StoredGlucoseSampleTests.swift @@ -27,7 +27,7 @@ class StoredGlucoseSampleTests: XCTestCase { wasUserEntered: false, device: nil, healthKitEligibleDate: nil) - let datum = sample.datum(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let datum = sample.datum(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(datum), encoding: .utf8), """ { "id" : "4cf2a0566365e60b3f9618f39de149b8", @@ -65,7 +65,7 @@ class StoredGlucoseSampleTests: XCTestCase { wasUserEntered: true, device: nil, healthKitEligibleDate: nil) - let datum = sample.datum(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let datum = sample.datum(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(datum), encoding: .utf8), """ { "id" : "4cf2a0566365e60b3f9618f39de149b8", @@ -103,7 +103,7 @@ class StoredGlucoseSampleTests: XCTestCase { wasUserEntered: false, device: nil, healthKitEligibleDate: nil) - let datum = sample.datum(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let datum = sample.datum(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(datum), encoding: .utf8), """ { "id" : "4cf2a0566365e60b3f9618f39de149b8", @@ -142,7 +142,7 @@ class StoredGlucoseSampleTests: XCTestCase { wasUserEntered: false, device: nil, healthKitEligibleDate: nil) - let datum = sample.datum(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let datum = sample.datum(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(datum), encoding: .utf8), """ { "annotations" : [ @@ -188,7 +188,7 @@ class StoredGlucoseSampleTests: XCTestCase { wasUserEntered: false, device: nil, healthKitEligibleDate: nil) - let datum = sample.datum(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let datum = sample.datum(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(datum), encoding: .utf8), """ { "annotations" : [ diff --git a/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift b/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift index 011aaae..c08ce84 100644 --- a/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift +++ b/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift @@ -14,7 +14,7 @@ import TidepoolKit class StoredSettingsTests: XCTestCase { func testDatumControllerSettings() { - let data = try! Self.encoder.encode(StoredSettings.test.datumControllerSettings(for: "1234567890")) + let data = try! Self.encoder.encode(StoredSettings.test.datumControllerSettings(for: "1234567890", hostIdentifier: "Loop", hostVersion: "1.2.3")) XCTAssertEqual(String(data: data, encoding: .utf8), """ { "device" : { @@ -41,8 +41,9 @@ class StoredSettingsTests: XCTestCase { }, "origin" : { "id" : "2A67A303-1234-4CB8-1234-79498265368E:controllerSettings", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "2A67A303-1234-4CB8-1234-79498265368E" @@ -57,7 +58,7 @@ class StoredSettingsTests: XCTestCase { } func testDatumCGMSettings() { - let data = try! Self.encoder.encode(StoredSettings.test.datumCGMSettings(for: "1234567890")) + let data = try! Self.encoder.encode(StoredSettings.test.datumCGMSettings(for: "1234567890", hostIdentifier: "Loop", hostVersion: "1.2.3")) XCTAssertEqual(String(data: data, encoding: .utf8), """ { "firmwareVersion" : "CGM Firmware Version", @@ -70,8 +71,9 @@ class StoredSettingsTests: XCTestCase { "name" : "CGM Name", "origin" : { "id" : "2A67A303-1234-4CB8-1234-79498265368E:cgmSettings", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "syncIdentifier" : "2A67A303-1234-4CB8-1234-79498265368E" @@ -89,7 +91,7 @@ class StoredSettingsTests: XCTestCase { } func testDatumPumpSettings() { - let data = try! Self.encoder.encode(StoredSettings.test.datumPumpSettings(for: "1234567890")) + let data = try! Self.encoder.encode(StoredSettings.test.datumPumpSettings(for: "1234567890", hostIdentifier: "Loop", hostVersion: "1.2.3")) XCTAssertEqual(String(data: data, encoding: .utf8), """ { "activeSchedule" : "Default", @@ -209,8 +211,9 @@ class StoredSettingsTests: XCTestCase { "name" : "Pump Name", "origin" : { "id" : "2A67A303-1234-4CB8-1234-79498265368E:pumpSettings", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "overridePresets" : { "Apple" : { @@ -245,7 +248,7 @@ class StoredSettingsTests: XCTestCase { } func testDatumPumpSettingsOverrideDeviceEvent() { - let data = try! Self.encoder.encode(StoredSettings.test.datumPumpSettingsOverrideDeviceEvent(for: "1234567890")) + let data = try! Self.encoder.encode(StoredSettings.test.datumPumpSettingsOverrideDeviceEvent(for: "1234567890", hostIdentifier: "Loop", hostVersion: "1.2.3")) XCTAssertEqual(String(data: data, encoding: .utf8), """ { "basalRateScaleFactor" : 0.5, @@ -259,8 +262,9 @@ class StoredSettingsTests: XCTestCase { "method" : "manual", "origin" : { "id" : "2A67A303-1234-4CB8-1234-79498265368E:deviceEvent/pumpSettingsOverride", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "overrideType" : "preprandial", "payload" : { diff --git a/TidepoolServiceKitTests/Extensions/SyncAlertObjectTests.swift b/TidepoolServiceKitTests/Extensions/SyncAlertObjectTests.swift index 3e5819a..66a1dbe 100644 --- a/TidepoolServiceKitTests/Extensions/SyncAlertObjectTests.swift +++ b/TidepoolServiceKitTests/Extensions/SyncAlertObjectTests.swift @@ -30,7 +30,7 @@ class SyncAlertObjectTests: XCTestCase { acknowledgedDate: Self.dateFormatter.date(from: "2020-01-02T03:05:34Z")!, retractedDate: Self.dateFormatter.date(from: "2020-01-02T03:06:45Z")!, syncIdentifier: UUID(uuidString: "2A67A303-1234-4CB8-8263-79498265368E")!) - let datum = object.datum(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let datum = object.datum(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(datum), encoding: .utf8), """ { "acknowledgedTime" : "2020-01-02T03:05:34.000Z", @@ -39,8 +39,9 @@ class SyncAlertObjectTests: XCTestCase { "name" : "ManagerId.AlertId", "origin" : { "id" : "2A67A303-1234-4CB8-8263-79498265368E", - "name" : "com.apple.dt.xctest.tool", - "type" : "application" + "name" : "Loop", + "type" : "application", + "version" : "1.2.3" }, "payload" : { "metadata" : { diff --git a/TidepoolServiceKitTests/Extensions/SyncCarbObjectTests.swift b/TidepoolServiceKitTests/Extensions/SyncCarbObjectTests.swift index ba1de80..8dea646 100644 --- a/TidepoolServiceKitTests/Extensions/SyncCarbObjectTests.swift +++ b/TidepoolServiceKitTests/Extensions/SyncCarbObjectTests.swift @@ -30,7 +30,7 @@ class SyncCarbObjectDatumTests: XCTestCase { operation: .update, addedDate: Self.dateFormatter.date(from: "2020-01-02T03:05:23Z")!, supercededDate: nil) - let datum = object.datum(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011") + let datum = object.datum(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(String(data: try! Self.encoder.encode(datum), encoding: .utf8), """ { "id" : "6bcc86152b10e405e126714fb583b783", diff --git a/TidepoolServiceKitTests/TidepoolServiceTests.swift b/TidepoolServiceKitTests/TidepoolServiceTests.swift index 40949ed..7ace7f1 100644 --- a/TidepoolServiceKitTests/TidepoolServiceTests.swift +++ b/TidepoolServiceKitTests/TidepoolServiceTests.swift @@ -19,7 +19,7 @@ class TidepoolServiceTests: XCTestCase { override func setUp() { super.setUp() - tidepoolService = TidepoolService(automaticallyFetchEnvironments: false) + tidepoolService = TidepoolService(hostIdentifier: "Loop", hostVersion: "1.2.3") userID = "1234567890" } @@ -32,7 +32,7 @@ class TidepoolServiceTests: XCTestCase { basalDeliveryState: nil, bolusState: .noBolus, insulinType: nil))] - let created = tidepoolService.calculateDosingDecisionData(dosingDecisions, for: userID) + let created = tidepoolService.calculateDosingDecisionData(dosingDecisions, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(created.count, 3) XCTAssertEqual((created[0] as! TDosingDecisionDatum).reason, "Test") XCTAssertEqual((created[0] as! TDosingDecisionDatum).associations!.count, 2) @@ -52,7 +52,7 @@ class TidepoolServiceTests: XCTestCase { let settings = [StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #1"), cgmDevice: HKDevice(name: "CGM #1"), pumpDevice: HKDevice(name: "Pump #1"))] - let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID) + let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(created.count, 3) XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1") XCTAssertEqual((created[0] as! TControllerSettingsDatum).associations!.count, 2) @@ -80,7 +80,7 @@ class TidepoolServiceTests: XCTestCase { StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #2"), cgmDevice: HKDevice(name: "CGM #1"), pumpDevice: HKDevice(name: "Pump #1"))] - let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID) + let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(created.count, 4) XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1") XCTAssertEqual((created[1] as! TCGMSettingsDatum).name, "CGM #1") @@ -103,7 +103,7 @@ class TidepoolServiceTests: XCTestCase { StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #1"), cgmDevice: HKDevice(name: "CGM #2"), pumpDevice: HKDevice(name: "Pump #1"))] - let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID) + let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(created.count, 4) XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1") XCTAssertEqual((created[1] as! TCGMSettingsDatum).name, "CGM #1") @@ -126,7 +126,7 @@ class TidepoolServiceTests: XCTestCase { StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #1"), cgmDevice: HKDevice(name: "CGM #1"), pumpDevice: HKDevice(name: "Pump #2"))] - let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID) + let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(created.count, 4) XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1") XCTAssertEqual((created[1] as! TCGMSettingsDatum).name, "CGM #1") @@ -149,7 +149,7 @@ class TidepoolServiceTests: XCTestCase { StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #2"), cgmDevice: HKDevice(name: "CGM #2"), pumpDevice: HKDevice(name: "Pump #2"))] - let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID) + let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(created.count, 6) XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1") XCTAssertEqual((created[1] as! TCGMSettingsDatum).name, "CGM #1") @@ -186,7 +186,7 @@ class TidepoolServiceTests: XCTestCase { StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #2"), cgmDevice: HKDevice(name: "CGM #2"), pumpDevice: HKDevice(name: "Pump #2"))] - let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID) + let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(created.count, 6) XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1") XCTAssertEqual((created[1] as! TCGMSettingsDatum).name, "CGM #1") @@ -218,7 +218,7 @@ class TidepoolServiceTests: XCTestCase { StoredSettings(scheduleOverride: scheduleOverride, controllerDevice: StoredSettings.ControllerDevice(name: "Controller #1"), pumpDevice: HKDevice(name: "Pump #1"))] - let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID) + let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(created.count, 3) XCTAssertEqual((created[0] as! TPumpSettingsDatum).name, "Pump #1") XCTAssertNil((created[0] as! TPumpSettingsDatum).associations) @@ -242,7 +242,7 @@ class TidepoolServiceTests: XCTestCase { StoredSettings(pumpDevice: HKDevice(name: "Pump #1")), StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #1"), pumpDevice: HKDevice(name: "Pump #1"))] - let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID) + let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(created.count, 4) XCTAssertEqual((created[0] as! TPumpSettingsDatum).name, "Pump #1") XCTAssertNil((created[0] as! TPumpSettingsDatum).associations) diff --git a/TidepoolServiceKitUI/Extensions/Image.swift b/TidepoolServiceKitUI/Extensions/Image.swift new file mode 100644 index 0000000..9f378da --- /dev/null +++ b/TidepoolServiceKitUI/Extensions/Image.swift @@ -0,0 +1,23 @@ +// +// Image.swift +// TidepoolServiceKitUI +// +// Created by Pete Schwamb on 1/27/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI + +private class FrameworkBundle { + static let main = Bundle(for: FrameworkBundle.self) +} + +extension Image { + init(frameworkImage name: String, decorative: Bool = false) { + if decorative { + self.init(decorative: name, bundle: FrameworkBundle.main) + } else { + self.init(name, bundle: FrameworkBundle.main) + } + } +} diff --git a/TidepoolServiceKitUI/Extensions/UIImage.swift b/TidepoolServiceKitUI/Extensions/UIImage.swift new file mode 100644 index 0000000..1152daa --- /dev/null +++ b/TidepoolServiceKitUI/Extensions/UIImage.swift @@ -0,0 +1,20 @@ +// +// UIImage.swift +// TidepoolServiceKitUI +// +// Created by Pete Schwamb on 4/19/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import Foundation +import UIKit + +private class FrameworkBundle { + static let main = Bundle(for: FrameworkBundle.self) +} + +extension UIImage { + convenience init(frameworkImage name: String) { + self.init(named: name, in: FrameworkBundle.main, compatibleWith: nil)! + } +} diff --git a/TidepoolServiceKitUI/SettingsView.swift b/TidepoolServiceKitUI/SettingsView.swift new file mode 100644 index 0000000..3280516 --- /dev/null +++ b/TidepoolServiceKitUI/SettingsView.swift @@ -0,0 +1,216 @@ +// +// SettingsView.swift +// TidepoolServiceKitUI +// +// Created by Pete Schwamb on 1/27/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import TidepoolKit +import TidepoolServiceKit + +public struct SettingsView: View { + + @State private var isEnvironmentActionSheetPresented = false + @State private var showingDeletionConfirmation = false + + @State private var error: Error? + @State private var isLoggingIn = false + @State private var selectedEnvironment: TEnvironment + @State private var environments: [TEnvironment] = [TEnvironment.productionEnvironment] + @State private var environmentFetchError: Error? + + @ObservedObject private var service: TidepoolService + + private let login: ((TEnvironment) async throws -> Void)? + private let dismiss: (() -> Void)? + + var isLoggedIn: Bool { + return service.session != nil + } + + public init(service: TidepoolService, login: ((TEnvironment) async throws -> Void)?, dismiss: (() -> Void)?) + { + let tapi = service.tapi + self.service = service + let defaultEnvironment = tapi.defaultEnvironment + self._selectedEnvironment = State(initialValue: service.session?.environment ?? defaultEnvironment ?? TEnvironment.productionEnvironment) + self.login = login + self.dismiss = dismiss + } + + public var body: some View { + ZStack { + Color(.secondarySystemBackground) + .edgesIgnoringSafeArea(.all) + GeometryReader { geometry in + ScrollView { + VStack(spacing: 20) { + HStack() { + Spacer() + closeButton + .padding() + } + Spacer() + logo + .padding(.horizontal, 30) + .padding(.bottom) + if selectedEnvironment != TEnvironment.productionEnvironment { + VStack { + Text(LocalizedString("Environment", comment: "Label title for displaying selected Tidepool server environment.")) + .bold() + Text(selectedEnvironment.description) + + if isLoggedIn { + Button(LocalizedString("Revoke token", comment: "Button title to revoke oauth tokens"), action: { + Task { + do { + try await service.tapi.revokeTokens() + } catch { + self.error = error + } + } + }) + } + } + } + if let username = service.session?.username { + VStack { + Text(LocalizedString("Logged in as", comment: "LoginViewModel description text when logged in")) + .bold() + Text(username) + } + } else { + Text(LocalizedString("You are not logged in.", comment: "LoginViewModel description text when not logged in")) + .padding() + } + + if let error { + VStack(alignment: .leading) { + Text(error.localizedDescription) + .font(.callout) + .foregroundColor(.red) + } + .padding() + } + Spacer() + if isLoggedIn { + deleteServiceButton + } else { + loginButton + } + } + .padding() + .frame(minHeight: geometry.size.height) + } + } + } + .alert(LocalizedString("Are you sure you want to delete this service?", comment: "Confirmation message for deleting a service"), isPresented: $showingDeletionConfirmation) + { + Button(LocalizedString("Delete Service", comment: "Button title to delete a service"), role: .destructive) { + service.deleteService() + dismiss?() + } + } + .task { + do { + environments = try await TEnvironment.fetchEnvironments() + } catch { + + } + } + + } + + private var logo: some View { + Image(frameworkImage: "Tidepool Logo", decorative: true) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 150) + .onLongPressGesture(minimumDuration: 2) { + if !isLoggedIn { + UINotificationFeedbackGenerator().notificationOccurred(.warning) + isEnvironmentActionSheetPresented = true + } + } + .actionSheet(isPresented: $isEnvironmentActionSheetPresented) { environmentActionSheet } + } + + private var environmentActionSheet: ActionSheet { + var buttons: [ActionSheet.Button] = environments.map { environment in + .default(Text(environment.description)) { + error = nil + selectedEnvironment = environment + } + } + buttons.append(.cancel()) + + + return ActionSheet(title: Text(LocalizedString("Environment", comment: "Tidepool login environment action sheet title")), + message: Text(selectedEnvironment.description), buttons: buttons) + } + + private var loginButton: some View { + Button(action: { + loginButtonTapped() + }) { + if isLoggingIn { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } else { + Text(LocalizedString("Login", comment: "Tidepool login button title")) + } + } + .buttonStyle(ActionButtonStyle()) + .disabled(isLoggingIn) + } + + + private var deleteServiceButton: some View { + Button(action: { + showingDeletionConfirmation = true + }) { + Text(LocalizedString("Delete Service", comment: "Delete Tidepool service button title")) + } + .buttonStyle(ActionButtonStyle(.secondary)) + .disabled(isLoggingIn) + } + + private func loginButtonTapped() { + guard !isLoggingIn else { + return + } + + error = nil + isLoggingIn = true + + Task { + do { + try await login?(selectedEnvironment) + isLoggingIn = false + } catch { + self.error = error + isLoggingIn = false + } + } + } + + private var closeButton: some View { + Button(action: { + dismiss?() + }) { + Text(closeButtonTitle) + .fontWeight(.regular) + } + } + + private var closeButtonTitle: String { LocalizedString("Close", comment: "Close navigation button title of an onboarding section page view") } +} + +struct SettingsView_Previews: PreviewProvider { + @MainActor + static var previews: some View { + SettingsView(service: TidepoolService(hostIdentifier: "Previews", hostVersion: "1.0"), login: nil, dismiss: nil) + } +} diff --git a/TidepoolServiceKitUI/TidepoolService+UI.swift b/TidepoolServiceKitUI/TidepoolService+UI.swift index 833c372..c8a14cf 100644 --- a/TidepoolServiceKitUI/TidepoolService+UI.swift +++ b/TidepoolServiceKitUI/TidepoolService+UI.swift @@ -7,19 +7,61 @@ // import SwiftUI +import LoopKit import LoopKitUI +import TidepoolKit import TidepoolServiceKit extension TidepoolService: ServiceUI { public static var image: UIImage? { - UIImage(named: "Tidepool Logo", in: Bundle(for: TidepoolServiceSettingsViewController.self), compatibleWith: nil)! + UIImage(frameworkImage: "Tidepool Logo") } - public static func setupViewController(colorPalette: LoopUIColorPalette) -> SetupUIResult { - return .userInteractionRequired(ServiceNavigationController(rootViewController: TidepoolServiceSetupViewController(service: TidepoolService()))) + public static func setupViewController(colorPalette: LoopUIColorPalette, pluginHost: PluginHost) -> SetupUIResult { + + let navController = ServiceNavigationController() + navController.isNavigationBarHidden = true + + Task { + let service = TidepoolService(hostIdentifier: pluginHost.hostIdentifier, hostVersion: pluginHost.hostVersion) + + let settingsView = await SettingsView(service: service, login: { environment in + try await service.tapi.login(environment: environment, presenting: navController) + try await service.completeCreate() + await navController.notifyServiceCreatedAndOnboarded(service) + }, dismiss: { + Task { + await navController.notifyComplete() + } + }) + + let hostingController = await UIHostingController(rootView: settingsView) + await navController.pushViewController(hostingController, animated: false) + } + + return .userInteractionRequired(navController) } public func settingsViewController(colorPalette: LoopUIColorPalette) -> ServiceViewController { - return ServiceNavigationController(rootViewController: TidepoolServiceSettingsViewController(service: self)) + + let navController = ServiceNavigationController() + navController.isNavigationBarHidden = true + + Task { + let settingsView = await SettingsView(service: self, login: { [weak self] environment in + if let self { + try await self.tapi.login(environment: environment, presenting: navController) + } + }, dismiss: { + Task { + await navController.notifyComplete() + } + }) + + let hostingController = await UIHostingController(rootView: settingsView) + await navController.pushViewController(hostingController, animated: false) + } + + return navController } } diff --git a/TidepoolServiceKitUI/TidepoolServiceSettingsViewController.swift b/TidepoolServiceKitUI/TidepoolServiceSettingsViewController.swift deleted file mode 100644 index 5ed345d..0000000 --- a/TidepoolServiceKitUI/TidepoolServiceSettingsViewController.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// TidepoolServiceSettingsViewController.swift -// TidepoolServiceKitUI -// -// Created by Darin Krauss on 7/23/19. -// Copyright © 2019 Tidepool Project. All rights reserved. -// - -import LoopKitUI -import TidepoolServiceKit - -final class TidepoolServiceSettingsViewController: UITableViewController { - - private let service: TidepoolService - - init(service: TidepoolService) { - self.service = service - - super.init(style: .grouped) - - title = NSLocalizedString("Tidepool Service", comment: "The title of the Tidepool Service settings screen") - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className) - - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done)) - } - - @objc private func done() { - service.completeUpdate() - notifyComplete() - } - - private func confirmDeletion(completion: (() -> Void)? = nil) { - let alert = UIAlertController(serviceDeletionHandler: { - self.service.completeDelete() - self.notifyComplete() - }) - - present(alert, animated: true, completion: completion) - } - - private func notifyComplete() { - if let serviceNavigationController = navigationController as? ServiceNavigationController { - serviceNavigationController.notifyComplete() - } - } - - // MARK: - Data Source - - private enum Section: Int, CaseIterable { - case deleteService - } - - // MARK: - UITableViewDataSource - - override func numberOfSections(in tableView: UITableView) -> Int { - return Section.allCases.count - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - switch Section(rawValue: section)! { - case .deleteService: - return 1 - } - } - - override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - switch Section(rawValue: section)! { - case .deleteService: - return " " // Use an empty string for more dramatic spacing - } - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - switch Section(rawValue: indexPath.section)! { - case .deleteService: - let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell - cell.textLabel?.text = LocalizedString("Delete Service", comment: "Button title to delete a service") - cell.textLabel?.textAlignment = .center - cell.tintColor = .systemRed - return cell - } - } - - // MARK: - UITableViewDelegate - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - switch Section(rawValue: indexPath.section)! { - case .deleteService: - confirmDeletion { - tableView.deselectRow(at: indexPath, animated: true) - } - } - } - -} - -extension TextButtonTableViewCell: IdentifiableClass {} - -fileprivate extension UIAlertController { - - convenience init(serviceDeletionHandler handler: @escaping () -> Void) { - self.init( - title: nil, - message: LocalizedString("Are you sure you want to delete this service?", comment: "Confirmation message for deleting a service"), - preferredStyle: .actionSheet - ) - - addAction(UIAlertAction( - title: LocalizedString("Delete Service", comment: "Button title to delete a service"), - style: .destructive, - handler: { _ in - handler() - } - )) - - let cancel = LocalizedString("Cancel", comment: "The title of the cancel action in an action sheet") - addAction(UIAlertAction(title: cancel, style: .cancel, handler: nil)) - } - -} diff --git a/TidepoolServiceKitUI/TidepoolServiceSetupViewController.swift b/TidepoolServiceKitUI/TidepoolServiceSetupViewController.swift deleted file mode 100644 index c817ade..0000000 --- a/TidepoolServiceKitUI/TidepoolServiceSetupViewController.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// TidepoolServiceSetupViewController.swift -// TidepoolServiceKitUI -// -// Created by Darin Krauss on 7/24/19. -// Copyright © 2019 Tidepool Project. All rights reserved. -// - -import LoopKitUI -import TidepoolKit -import TidepoolServiceKit - -final class TidepoolServiceSetupViewController: UIViewController { - - private let service: TidepoolService - - init(service: TidepoolService) { - self.service = service - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - navigationController?.setNavigationBarHidden(true, animated: false) - - var loginSignupViewController = service.tapi.loginSignupViewController() - loginSignupViewController.loginSignupDelegate = self - loginSignupViewController.view.frame = CGRect(origin: CGPoint(), size: view.frame.size) - - addChild(loginSignupViewController) - view.addSubview(loginSignupViewController.view) - - loginSignupViewController.didMove(toParent: self) - } -} - -extension TidepoolServiceSetupViewController: TLoginSignupDelegate { - func loginSignupDidComplete(completion: @escaping (Error?) -> Void) { - service.completeCreate { error in - guard error == nil else { - completion(error) - return - } - DispatchQueue.main.async { - if let serviceNavigationController = self.navigationController as? ServiceNavigationController { - serviceNavigationController.notifyServiceCreatedAndOnboarded(self.service) - serviceNavigationController.notifyComplete() - } - completion(nil) - } - } - } - - func loginSignupCancelled() { - DispatchQueue.main.async { - if let serviceNavigationController = self.navigationController as? ServiceNavigationController { - serviceNavigationController.notifyComplete() - } - } - } -} diff --git a/TidepoolServiceKitUI/cs.lproj/Localizable.strings b/TidepoolServiceKitUI/cs.lproj/Localizable.strings new file mode 100644 index 0000000..193f975 --- /dev/null +++ b/TidepoolServiceKitUI/cs.lproj/Localizable.strings @@ -0,0 +1,15 @@ +/* No comment provided by engineer. */ +"Account" = "Účet"; + +/* Confirmation message for deleting a service */ +"Are you sure you want to delete this service?" = "Opravdu chcete tuto službu smazat?"; + +/* Button title to delete a service */ +"Delete Service" = "Smazat službu"; + +/* No comment provided by engineer. */ +"Done" = "Hotovo"; + +/* No comment provided by engineer. */ +"Tidepool " = "Tidepool"; + diff --git a/TidepoolServiceKitUI/da.lproj/Localizable.strings b/TidepoolServiceKitUI/da.lproj/Localizable.strings new file mode 100644 index 0000000..3401ba1 --- /dev/null +++ b/TidepoolServiceKitUI/da.lproj/Localizable.strings @@ -0,0 +1,15 @@ +/* No comment provided by engineer. */ +"Account" = "Konto"; + +/* Confirmation message for deleting a service */ +"Are you sure you want to delete this service?" = "Er du sikker på, du vil slette denne service?"; + +/* Button title to delete a service */ +"Delete Service" = "Slet service"; + +/* No comment provided by engineer. */ +"Done" = "Udført"; + +/* No comment provided by engineer. */ +"Tidepool " = "Tidepool "; + diff --git a/TidepoolServiceKitUI/de.lproj/Localizable.strings b/TidepoolServiceKitUI/de.lproj/Localizable.strings index b5534f7..0104156 100644 --- a/TidepoolServiceKitUI/de.lproj/Localizable.strings +++ b/TidepoolServiceKitUI/de.lproj/Localizable.strings @@ -1,2 +1,15 @@ +/* No comment provided by engineer. */ +"Account" = "Konto"; + +/* Confirmation message for deleting a service */ +"Are you sure you want to delete this service?" = "Bist Du sicher, dass Du diesen Dienst löschen möchtest?"; + /* Button title to delete a service */ -"Delete Service" = "Delete Service"; +"Delete Service" = "Dienst löschen"; + +/* No comment provided by engineer. */ +"Done" = "Fertig"; + +/* No comment provided by engineer. */ +"Tidepool " = "Tidepool "; + diff --git a/TidepoolServiceKitUI/es.lproj/Localizable.strings b/TidepoolServiceKitUI/es.lproj/Localizable.strings index b5534f7..b01207f 100644 --- a/TidepoolServiceKitUI/es.lproj/Localizable.strings +++ b/TidepoolServiceKitUI/es.lproj/Localizable.strings @@ -1,2 +1,15 @@ +/* No comment provided by engineer. */ +"Account" = "Cuenta"; + +/* Confirmation message for deleting a service */ +"Are you sure you want to delete this service?" = "¿Está seguro de que desea eliminar este servicio?"; + /* Button title to delete a service */ -"Delete Service" = "Delete Service"; +"Delete Service" = "Eliminar servicio"; + +/* No comment provided by engineer. */ +"Done" = "Completado"; + +/* No comment provided by engineer. */ +"Tidepool " = "Tidepool"; + diff --git a/TidepoolServiceKitUI/fi.lproj/Localizable.strings b/TidepoolServiceKitUI/fi.lproj/Localizable.strings new file mode 100644 index 0000000..b24cc23 --- /dev/null +++ b/TidepoolServiceKitUI/fi.lproj/Localizable.strings @@ -0,0 +1,9 @@ +/* Confirmation message for deleting a service */ +"Are you sure you want to delete this service?" = "Haluatko varmasti poistaa tämän palvelun?"; + +/* Button title to delete a service */ +"Delete Service" = "Poista palvelu"; + +/* No comment provided by engineer. */ +"Done" = "Valmis"; + diff --git a/TidepoolServiceKitUI/fr.lproj/Localizable.strings b/TidepoolServiceKitUI/fr.lproj/Localizable.strings index b5534f7..a70870e 100644 --- a/TidepoolServiceKitUI/fr.lproj/Localizable.strings +++ b/TidepoolServiceKitUI/fr.lproj/Localizable.strings @@ -1,2 +1,9 @@ +/* Confirmation message for deleting a service */ +"Are you sure you want to delete this service?" = "Voulez-vous vraiment supprimer ce service?"; + /* Button title to delete a service */ -"Delete Service" = "Delete Service"; +"Delete Service" = "Supprimer le service"; + +/* No comment provided by engineer. */ +"Done" = "Terminé"; + diff --git a/TidepoolServiceKitUI/he.lproj/Localizable.strings b/TidepoolServiceKitUI/he.lproj/Localizable.strings new file mode 100644 index 0000000..b826cdd --- /dev/null +++ b/TidepoolServiceKitUI/he.lproj/Localizable.strings @@ -0,0 +1,12 @@ +/* No comment provided by engineer. */ +"Account" = "חשבון"; + +/* Confirmation message for deleting a service */ +"Are you sure you want to delete this service?" = "Are you sure you want to delete this service?"; + +/* Button title to delete a service */ +"Delete Service" = "Delete Service"; + +/* No comment provided by engineer. */ +"Tidepool " = "טיידפול"; + diff --git a/TidepoolServiceKitUI/it.lproj/Localizable.strings b/TidepoolServiceKitUI/it.lproj/Localizable.strings index b5534f7..4a30abe 100644 --- a/TidepoolServiceKitUI/it.lproj/Localizable.strings +++ b/TidepoolServiceKitUI/it.lproj/Localizable.strings @@ -1,2 +1,9 @@ +/* Confirmation message for deleting a service */ +"Are you sure you want to delete this service?" = "Sei sicuro di voler eliminare questo servizio?"; + /* Button title to delete a service */ -"Delete Service" = "Delete Service"; +"Delete Service" = "Elimina Servizio"; + +/* No comment provided by engineer. */ +"Done" = "Fine"; + diff --git a/TidepoolServiceKitUI/ja.lproj/Localizable.strings b/TidepoolServiceKitUI/ja.lproj/Localizable.strings new file mode 100644 index 0000000..9ddaba1 --- /dev/null +++ b/TidepoolServiceKitUI/ja.lproj/Localizable.strings @@ -0,0 +1,3 @@ +/* Button title to delete a service */ +"Delete Service" = "Delete Service"; + diff --git a/TidepoolServiceKitUI/nb.lproj/Localizable.strings b/TidepoolServiceKitUI/nb.lproj/Localizable.strings index b5534f7..a53ab86 100644 --- a/TidepoolServiceKitUI/nb.lproj/Localizable.strings +++ b/TidepoolServiceKitUI/nb.lproj/Localizable.strings @@ -1,2 +1,15 @@ +/* No comment provided by engineer. */ +"Account" = "Konto"; + +/* Confirmation message for deleting a service */ +"Are you sure you want to delete this service?" = "Er du sikker på at du vil slette denne tjenesten?"; + /* Button title to delete a service */ -"Delete Service" = "Delete Service"; +"Delete Service" = "Slett tjeneste"; + +/* No comment provided by engineer. */ +"Done" = "Ferdig"; + +/* No comment provided by engineer. */ +"Tidepool " = "Tidepool"; + diff --git a/TidepoolServiceKitUI/nl.lproj/Localizable.strings b/TidepoolServiceKitUI/nl.lproj/Localizable.strings index b5534f7..f3a3271 100644 --- a/TidepoolServiceKitUI/nl.lproj/Localizable.strings +++ b/TidepoolServiceKitUI/nl.lproj/Localizable.strings @@ -1,2 +1,15 @@ +/* No comment provided by engineer. */ +"Account" = "Account"; + +/* Confirmation message for deleting a service */ +"Are you sure you want to delete this service?" = "Weet je zeker dat je deze service wil verwijderen?"; + /* Button title to delete a service */ -"Delete Service" = "Delete Service"; +"Delete Service" = "Service Verwijderen"; + +/* No comment provided by engineer. */ +"Done" = "Gereed"; + +/* No comment provided by engineer. */ +"Tidepool " = "Tidepool "; + diff --git a/TidepoolServiceKitUI/pl.lproj/Localizable.strings b/TidepoolServiceKitUI/pl.lproj/Localizable.strings index b5534f7..e9a8063 100644 --- a/TidepoolServiceKitUI/pl.lproj/Localizable.strings +++ b/TidepoolServiceKitUI/pl.lproj/Localizable.strings @@ -1,2 +1,15 @@ +/* No comment provided by engineer. */ +"Account" = "Konto"; + +/* Confirmation message for deleting a service */ +"Are you sure you want to delete this service?" = "Czy na pewno chcesz usunąć tę usługę?"; + /* Button title to delete a service */ -"Delete Service" = "Delete Service"; +"Delete Service" = "Usuń usługę"; + +/* No comment provided by engineer. */ +"Done" = "Gotowe"; + +/* No comment provided by engineer. */ +"Tidepool " = "Tidepool"; + diff --git a/TidepoolServiceKitUI/pt-BR.lproj/Localizable.strings b/TidepoolServiceKitUI/pt-BR.lproj/Localizable.strings new file mode 100644 index 0000000..9ddaba1 --- /dev/null +++ b/TidepoolServiceKitUI/pt-BR.lproj/Localizable.strings @@ -0,0 +1,3 @@ +/* Button title to delete a service */ +"Delete Service" = "Delete Service"; + diff --git a/TidepoolServiceKitUI/ro.lproj/Localizable.strings b/TidepoolServiceKitUI/ro.lproj/Localizable.strings new file mode 100644 index 0000000..d437506 --- /dev/null +++ b/TidepoolServiceKitUI/ro.lproj/Localizable.strings @@ -0,0 +1,15 @@ +/* No comment provided by engineer. */ +"Account" = "Cont"; + +/* Confirmation message for deleting a service */ +"Are you sure you want to delete this service?" = "Ești sigur că vrei să ștergi acest serviciu?"; + +/* Button title to delete a service */ +"Delete Service" = "Șterge serviciul"; + +/* No comment provided by engineer. */ +"Done" = "Realizat"; + +/* No comment provided by engineer. */ +"Tidepool " = "Tidepool"; + diff --git a/TidepoolServiceKitUI/ru.lproj/Localizable.strings b/TidepoolServiceKitUI/ru.lproj/Localizable.strings index b5534f7..21cbf77 100644 --- a/TidepoolServiceKitUI/ru.lproj/Localizable.strings +++ b/TidepoolServiceKitUI/ru.lproj/Localizable.strings @@ -1,2 +1,15 @@ +/* No comment provided by engineer. */ +"Account" = "Аккаунт"; + +/* Confirmation message for deleting a service */ +"Are you sure you want to delete this service?" = "Вы уверены, что хотите удалить этот сервис?"; + /* Button title to delete a service */ -"Delete Service" = "Delete Service"; +"Delete Service" = "Удалить сервис"; + +/* No comment provided by engineer. */ +"Done" = "Готово"; + +/* No comment provided by engineer. */ +"Tidepool " = "Tidepool "; + diff --git a/TidepoolServiceKitUI/sk.lproj/Localizable.strings b/TidepoolServiceKitUI/sk.lproj/Localizable.strings new file mode 100644 index 0000000..7356cb8 --- /dev/null +++ b/TidepoolServiceKitUI/sk.lproj/Localizable.strings @@ -0,0 +1,9 @@ +/* Confirmation message for deleting a service */ +"Are you sure you want to delete this service?" = "Naozaj chcete odstrániť túto službu?"; + +/* Button title to delete a service */ +"Delete Service" = "Odstrániť službu"; + +/* No comment provided by engineer. */ +"Done" = "Hotovo"; + diff --git a/TidepoolServiceKitUI/sv.lproj/Localizable.strings b/TidepoolServiceKitUI/sv.lproj/Localizable.strings new file mode 100644 index 0000000..91e0e34 --- /dev/null +++ b/TidepoolServiceKitUI/sv.lproj/Localizable.strings @@ -0,0 +1,9 @@ +/* Confirmation message for deleting a service */ +"Are you sure you want to delete this service?" = "Vill du ta bort den här tjänsten?"; + +/* Button title to delete a service */ +"Delete Service" = "Ta bort tjänst"; + +/* No comment provided by engineer. */ +"Done" = "Färdig"; + diff --git a/TidepoolServiceKitUI/tr.lproj/Localizable.strings b/TidepoolServiceKitUI/tr.lproj/Localizable.strings new file mode 100644 index 0000000..29d32df --- /dev/null +++ b/TidepoolServiceKitUI/tr.lproj/Localizable.strings @@ -0,0 +1,15 @@ +/* No comment provided by engineer. */ +"Account" = "Hesabı"; + +/* Confirmation message for deleting a service */ +"Are you sure you want to delete this service?" = "Bu servisi silmek istediğinizden emin misiniz?"; + +/* Button title to delete a service */ +"Delete Service" = "Servisi Sil"; + +/* No comment provided by engineer. */ +"Done" = "Tamamlandı"; + +/* No comment provided by engineer. */ +"Tidepool " = "Tidepool"; + diff --git a/TidepoolServiceKitUI/vi.lproj/Localizable.strings b/TidepoolServiceKitUI/vi.lproj/Localizable.strings new file mode 100644 index 0000000..9ddaba1 --- /dev/null +++ b/TidepoolServiceKitUI/vi.lproj/Localizable.strings @@ -0,0 +1,3 @@ +/* Button title to delete a service */ +"Delete Service" = "Delete Service"; + diff --git a/TidepoolServiceKitUI/zh-Hans.lproj/Localizable.strings b/TidepoolServiceKitUI/zh-Hans.lproj/Localizable.strings index b5534f7..9ddaba1 100644 --- a/TidepoolServiceKitUI/zh-Hans.lproj/Localizable.strings +++ b/TidepoolServiceKitUI/zh-Hans.lproj/Localizable.strings @@ -1,2 +1,3 @@ /* Button title to delete a service */ "Delete Service" = "Delete Service"; +