Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions TidepoolService.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
A9DAAD3322E7CA1A00E76C9F /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DAAD3222E7CA1A00E76C9F /* LocalizedString.swift */; };
A9DAAD3422E7CA1A00E76C9F /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DAAD3222E7CA1A00E76C9F /* LocalizedString.swift */; };
A9DAAD3622E7CAC100E76C9F /* TidepoolService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DAAD3522E7CAC100E76C9F /* TidepoolService.swift */; };
1EF2F4D6B0FAF3E69B0B20B1 /* TidepoolService+ConfigurationSharing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C0C86423CEC0DDC37825A6 /* TidepoolService+ConfigurationSharing.swift */; };
A9DAAD3922E7DEE000E76C9F /* TidepoolService+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DAAD3822E7DEE000E76C9F /* TidepoolService+UI.swift */; };
A9DAAD5B22E7E6BE00E76C9F /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAAD5A22E7E6BE00E76C9F /* LoopKit.framework */; };
A9DAAD5D22E7E73000E76C9F /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAAD5C22E7E73000E76C9F /* LoopKit.framework */; };
Expand Down Expand Up @@ -188,6 +189,7 @@
A9DAAD2A22E7988900E76C9F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A9DAAD3222E7CA1A00E76C9F /* LocalizedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedString.swift; sourceTree = "<group>"; };
A9DAAD3522E7CAC100E76C9F /* TidepoolService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidepoolService.swift; sourceTree = "<group>"; };
B1C0C86423CEC0DDC37825A6 /* TidepoolService+ConfigurationSharing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TidepoolService+ConfigurationSharing.swift"; sourceTree = "<group>"; };
A9DAAD3822E7DEE000E76C9F /* TidepoolService+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TidepoolService+UI.swift"; sourceTree = "<group>"; };
A9DAAD5A22E7E6BE00E76C9F /* LoopKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A9DAAD5C22E7E73000E76C9F /* LoopKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -370,6 +372,7 @@
A9057686271F770F0030C3B1 /* IdentifiableDatum.swift */,
A9F9F318271A05B100D19374 /* IdentifiableHKDatum.swift */,
A9DAAD3522E7CAC100E76C9F /* TidepoolService.swift */,
B1C0C86423CEC0DDC37825A6 /* TidepoolService+ConfigurationSharing.swift */,
A913B37B24200C86000805C4 /* Extensions */,
B66D1FAD2E6A8D8300471149 /* Localizable.xcstrings */,
A9DAAD4122E7DF9B00E76C9F /* Localizable.strings */,
Expand Down Expand Up @@ -742,6 +745,7 @@
A9057687271F770F0030C3B1 /* IdentifiableDatum.swift in Sources */,
A97651752421AA10002EB5D4 /* OSLog.swift in Sources */,
A9DAAD3622E7CAC100E76C9F /* TidepoolService.swift in Sources */,
1EF2F4D6B0FAF3E69B0B20B1 /* TidepoolService+ConfigurationSharing.swift in Sources */,
A9D58155272B266700AA5C18 /* Date.swift in Sources */,
A9752A99270B93A700E50750 /* GlucoseRangeSchedule.swift in Sources */,
);
Expand Down Expand Up @@ -1126,8 +1130,11 @@
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator watchos watchsimulator";
SUPPORTS_MACCATALYST = NO;
TARGETED_DEVICE_FAMILY = "1,2";
WATCHOS_DEPLOYMENT_TARGET = 10.6;
};
name = Debug;
};
Expand All @@ -1154,8 +1161,11 @@
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator watchos watchsimulator";
SUPPORTS_MACCATALYST = NO;
TARGETED_DEVICE_FAMILY = "1,2";
WATCHOS_DEPLOYMENT_TARGET = 10.6;
};
name = Release;
};
Expand Down Expand Up @@ -1420,8 +1430,11 @@
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator watchos watchsimulator";
SUPPORTS_MACCATALYST = NO;
TARGETED_DEVICE_FAMILY = "1,2";
WATCHOS_DEPLOYMENT_TARGET = 10.6;
};
name = Testflight;
};
Expand Down
60 changes: 60 additions & 0 deletions TidepoolServiceKit/TidepoolService+ConfigurationSharing.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// TidepoolService+ConfigurationSharing.swift
// TidepoolServiceKit
//
// What another controller needs to upload into this user's data set: the session, the host
// identity the data set is keyed on, and the data set itself. The session goes WITH its refresh
// token: an access token lives minutes (12 in Tidepool's development realm), a loan lasts hours,
// and the other controller may be out of this one's reach. Tidepool's realm does not rotate refresh
// tokens (revokeRefreshToken false), so both controllers renew independently without either
// invalidating the other's. The other controller never revokes it: its logout only drops the session.
//

import Foundation
import LoopKit
import TidepoolKit

extension TidepoolService: DeviceConfigurationSharing {
public func exportConfiguration() -> SharedDeviceConfiguration {
var state: [String: Any] = [:]
if let session {
let shared = TSession(environment: session.environment, accessToken: session.accessToken,
accessTokenExpiration: session.accessTokenExpiration, refreshToken: session.refreshToken,
userId: session.userId, username: session.username, userRoles: session.userRoles,
trace: session.trace, createdDate: session.createdDate)
state["session"] = try? JSONEncoder().encode(shared)
}
state["hostIdentifier"] = hostIdentifier
state["hostVersion"] = hostVersion
if case .fetched(let dataSetId) = dataSetIdCacheStatus {
state["dataSetId"] = dataSetId
}
return SharedDeviceConfiguration(managerIdentifier: pluginIdentifier, asOf: Date(), state: state)
}

public convenience init?(adopting configuration: SharedDeviceConfiguration, localState: [String: Any]?) {
guard configuration.managerIdentifier == Self.serviceIdentifier,
let data = configuration.state["session"] as? Data,
let session = try? JSONDecoder().decode(TSession.self, from: data),
let hostIdentifier = configuration.state["hostIdentifier"] as? String,
let hostVersion = configuration.state["hostVersion"] as? String else {
return nil
}
self.init(hostIdentifier: hostIdentifier, hostVersion: hostVersion)
sessionStorage = InMemorySessionStorage()
isOnboarded = true
isConfiguredByAnotherController = true
if let dataSetId = configuration.state["dataSetId"] as? String {
dataSetIdCacheStatus = .fetched(dataSetId)
}
self.session = session
Task { await tapi.setSession(session) }
}
}

/// An adopted service's session store: memory only, so this controller's keychain is never written.
private final class InMemorySessionStorage: SessionStorage {
private var sessions: [String: TSession] = [:]
func setSession(_ session: TSession?, for service: String) throws { sessions[service] = session }
func getSession(for service: String) throws -> TSession? { sessions[service] }
}
9 changes: 6 additions & 3 deletions TidepoolServiceKit/TidepoolService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {

private var lastPumpSettingsDatum: TPumpSettingsDatum?

private var hostIdentifier: String?
private var hostVersion: String?
var hostIdentifier: String?
var hostVersion: String?

private let log = OSLog(category: "TidepoolService")
private let tidepoolKitLog = OSLog(category: "TidepoolKit")
Expand Down Expand Up @@ -139,6 +139,9 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
}

public var isOnboarded = false // No distinction between created and onboarded

/// Built from another controller's export: its session lives in memory, never the keychain.
public internal(set) var isConfiguredByAnotherController = false

public func markAsDepedency(_ isDependency: Bool) {
self.isDependency = isDependency
Expand Down Expand Up @@ -222,7 +225,7 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
case fetched(String)
}

private var dataSetIdCacheStatus: DataSetIdCacheStatus?
var dataSetIdCacheStatus: DataSetIdCacheStatus?

private func clearCachedDataSetId() {
dataSetIdCacheStatus = nil
Expand Down