From dc824ca2fcafdcd1ae56194496b69cf7f82fe047 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Wed, 19 Apr 2023 14:36:35 -0500 Subject: [PATCH 1/7] Updating to new TidepoolKit with keycloak based auth --- TidepoolService.xcodeproj/project.pbxproj | 10 +- TidepoolServiceKit/TidepoolService.swift | 297 +++++++++--------- TidepoolServiceKitUI/SettingsView.swift | 229 +++++++++++--- TidepoolServiceKitUI/TidepoolService+UI.swift | 68 +++- ...depoolServiceSettingsHostController.swift} | 32 +- 5 files changed, 400 insertions(+), 236 deletions(-) rename TidepoolServiceKitUI/{TidepoolServiceSetupViewController.swift => TidepoolServiceSettingsHostController.swift} (63%) diff --git a/TidepoolService.xcodeproj/project.pbxproj b/TidepoolService.xcodeproj/project.pbxproj index 1dfde98..bec85e8 100644 --- a/TidepoolService.xcodeproj/project.pbxproj +++ b/TidepoolService.xcodeproj/project.pbxproj @@ -14,7 +14,7 @@ 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 */; }; + A92E770122E9181500591027 /* TidepoolServiceSettingsHostController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A92E770022E9181500591027 /* TidepoolServiceSettingsHostController.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, ); }; }; @@ -151,7 +151,7 @@ 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 = ""; }; + A92E770022E9181500591027 /* TidepoolServiceSettingsHostController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidepoolServiceSettingsHostController.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; }; @@ -434,10 +434,10 @@ A9DAAD6C22E7EA8F00E76C9F /* IdentifiableClass.swift */, A9DAAD6E22E7EA9700E76C9F /* NibLoadable.swift */, A9DAAD3822E7DEE000E76C9F /* TidepoolService+UI.swift */, - A92E770022E9181500591027 /* TidepoolServiceSetupViewController.swift */, + A92E770022E9181500591027 /* TidepoolServiceSettingsHostController.swift */, + C1D0B62829848A460098D215 /* SettingsView.swift */, E93BA06124A29C9C00C5D7E6 /* Assets.xcassets */, A9DAAD4F22E7DFD400E76C9F /* Localizable.strings */, - C1D0B62829848A460098D215 /* SettingsView.swift */, ); path = TidepoolServiceKitUI; sourceTree = ""; @@ -798,7 +798,7 @@ A9DAAD3422E7CA1A00E76C9F /* LocalizedString.swift in Sources */, A9DAAD3922E7DEE000E76C9F /* TidepoolService+UI.swift in Sources */, A9DAAD6F22E7EA9700E76C9F /* NibLoadable.swift in Sources */, - A92E770122E9181500591027 /* TidepoolServiceSetupViewController.swift in Sources */, + A92E770122E9181500591027 /* TidepoolServiceSettingsHostController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index bba8e64..1f269ae 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -19,17 +19,18 @@ public protocol SessionStorage { 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? { - didSet { - self.hostIdentifier = serviceDelegate?.hostIdentifier - self.hostVersion = serviceDelegate?.hostVersion - } + public weak var serviceDelegate: ServiceDelegate? + + public func setServiceDelegate(_ delegate: ServiceDelegate) { + serviceDelegate = delegate + self.hostIdentifier = serviceDelegate?.hostIdentifier + self.hostVersion = serviceDelegate?.hostVersion } public lazy var sessionStorage: SessionStorage = KeychainManager() @@ -62,7 +63,7 @@ public final class TidepoolService: Service, TAPIObserver { public init(hostIdentifier: String, hostVersion: String, automaticallyFetchEnvironments: Bool = true) { self.id = UUID().uuidString - self.tapi = TAPI(automaticallyFetchEnvironments: automaticallyFetchEnvironments) + self.tapi = TAPI(clientId: "diy-loop", redirectURL: URL(string: "org.loopkit.Loop://tidepool_service_redirect")!, automaticallyFetchEnvironments: automaticallyFetchEnvironments) self.hostIdentifier = hostIdentifier self.hostVersion = hostVersion @@ -71,16 +72,14 @@ public final class TidepoolService: Service, TAPIObserver { tapi.defaultEnvironment = TEnvironment(host: "app.tidepool.org", port: 443) } - tapi.logging = self - tapi.addObserver(self) - } - - deinit { - tapi.removeObserver(self) + Task { + await tapi.setLogging(self) + await tapi.addObserver(self) + } } public init?(rawState: RawStateValue) { - self.tapi = TAPI() + self.tapi = TAPI(clientId: "diy-loop", redirectURL: URL(string: "org.loopkit.Loop://tidepool_service_redirect")!) guard let id = rawState["id"] as? String else { return nil } @@ -91,13 +90,16 @@ public final class TidepoolService: Service, TAPIObserver { 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 } - tapi.logging = self - tapi.addObserver(self) } public var rawState: RawStateValue { @@ -113,20 +115,30 @@ public final class TidepoolService: Service, TAPIObserver { public let isOnboarded = true // No distinction between created and onboarded - public func apiDidUpdateSession(_ session: TSession?) { - if session == nil { - self.dataSetId = nil - } - do { - try sessionStorage.setSession(session, for: sessionService) - } catch let error { - self.error = error + private var session: TSession? { + didSet { + if session == nil { + self.dataSetId = nil + } + do { + try sessionStorage.setSession(session, for: sessionService) + } catch let error { + self.error = error + } } } + public func apiDidUpdateSession(_ session: TSession?) { + self.session = session + } + public func completeCreate(completion: @escaping (Error?) -> Void) { - DispatchQueue.global(qos: .background).async { - self.getDataSet(completion: completion) + Task { + do { + try await self.getDataSet() + } catch { + completion(error) + } } } @@ -134,59 +146,46 @@ public final class TidepoolService: Service, TAPIObserver { 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) { + private func getDataSet() async throws { guard let clientName = hostIdentifier else { - completion(TidepoolServiceError.configuration) - return + throw TidepoolServiceError.configuration } - 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) - } + + 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") } + self.dataSetId = dataSets.first?.uploadId + } else { + try await self.createDataSet() } } - private func createDataSet(completion: @escaping (Error?) -> Void) { + private func createDataSet() async throws { guard let clientName = hostIdentifier, let clientVersion = hostVersion else { - completion(TidepoolServiceError.configuration) - return + 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 newDataSet = try await tapi.createDataSet(dataSet) + self.dataSetId = newDataSet.uploadId } private var sessionService: String { "org.tidepool.TidepoolService.\(id)" } - private var userId: String? { tapi.session?.userId } + private var userId: String? { session?.userId } private static var encoder: PropertyListEncoder = { let encoder = PropertyListEncoder() @@ -229,7 +228,14 @@ extension TidepoolService: RemoteDataService { completion(.failure(TidepoolServiceError.configuration)) return } - createData(stored.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }, 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 } @@ -240,26 +246,14 @@ extension TidepoolService: RemoteDataService { return } - createData(created.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) { 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, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) { 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)) - } - } - } - } } } } @@ -271,19 +265,14 @@ extension TidepoolService: RemoteDataService { completion(.failure(TidepoolServiceError.configuration)) return } - createData(created.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) { 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)) - } - } } } } @@ -295,7 +284,15 @@ extension TidepoolService: RemoteDataService { completion(.failure(TidepoolServiceError.configuration)) return } - createData(calculateDosingDecisionData(stored, for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion), 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, hostIdentifier: String, hostVersion: String) -> [TDatum] { @@ -351,7 +348,15 @@ extension TidepoolService: RemoteDataService { completion(.failure(TidepoolServiceError.configuration)) return } - createData(stored.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }, 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 } @@ -361,7 +366,15 @@ extension TidepoolService: RemoteDataService { completion(.failure(TidepoolServiceError.configuration)) return } - createData(stored.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }, 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 @@ -374,24 +387,18 @@ extension TidepoolService: RemoteDataService { 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)) - } - } } } } @@ -494,67 +501,59 @@ 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 + throw error } guard let dataSetId = dataSetId else { - completion(.failure(TidepoolServiceError.configuration)) - return + throw TidepoolServiceError.configuration } - 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)) + 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 + throw error } guard let dataSetId = dataSetId else { - completion(.failure(TidepoolServiceError.configuration)) - return + throw TidepoolServiceError.configuration } // 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 + throw TidepoolServiceError.configuration } - 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)) + 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 } } } @@ -578,7 +577,7 @@ extension KeychainManager: SessionStorage { extension TidepoolServiceError: LocalizedError { public var errorDescription: String? { switch self { - case .configuration: return NSLocalizedString("Configuration Error", comment: "Error string for configuration error") + case .configuration: return LocalizedString("Configuration Error", comment: "Error string for configuration error") } } } diff --git a/TidepoolServiceKitUI/SettingsView.swift b/TidepoolServiceKitUI/SettingsView.swift index ea7d76e..c2c5037 100644 --- a/TidepoolServiceKitUI/SettingsView.swift +++ b/TidepoolServiceKitUI/SettingsView.swift @@ -7,70 +7,199 @@ // import SwiftUI +import TidepoolKit -struct SettingsView: View { - @Environment(\.dismissAction) private var dismiss - - var accountLogin: String - var environment: String? - var didRequestDelete: () -> Void - - @State private var showingAlert = false - - var body: some View { - VStack { - Text("Tidepool ") - .font(.largeTitle) - .fontWeight(.semibold) - Image(frameworkImage: "Tidepool Logo", decorative: true) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 150, height: 150) - .padding(.bottom) - - Text("Account") - .font(.headline) - Text(accountLogin) - .padding(.bottom) - - if let environment { - Text("Environment") - .font(.headline) - Text(environment) - .padding(.bottom) - } +@MainActor +public struct SettingsView: View { + + @State private var isEnvironmentActionSheetPresented = false + @State private var showingDeletionConfirmation = false + + @State private var message = "" + @State private var isLoggingIn = false + @State private var selectedEnvironment: TEnvironment - Spacer() + var session: TSession? + let environments: [TEnvironment] + let login: ((TEnvironment) async throws -> Void)? + let dismiss: (() -> Void)? + let deleteService: (() -> Void)? - Button(action: { - showingAlert = true - } ) { - Text("Delete Service") - .foregroundColor(.red) + var isLoggedIn: Bool { + return session != nil + } + + public init( + session: TSession?, + defaultEnvironment: TEnvironment?, + environments: [TEnvironment], + login: ((TEnvironment) async throws -> Void)?, + dismiss: (() -> Void)?, + deleteService: (() -> Void)?) + { + self._selectedEnvironment = State(initialValue: session?.environment ?? defaultEnvironment ?? environments.first!) + self.environments = environments + self.login = login + self.dismiss = dismiss + self.deleteService = deleteService + } + + public var body: some View { + ZStack { + Color(.secondarySystemBackground) + .edgesIgnoringSafeArea(.all) + GeometryReader { geometry in + ScrollView { + VStack { + HStack() { + Spacer() + closeButton + .padding() + } + Spacer() + logo + .padding(.horizontal, 30) + .padding(.bottom) + Text(NSLocalizedString("Environment", comment: "Label title for displaying selected Tidepool server environment.")) + .bold() + Text(selectedEnvironment.description) + if isLoggedIn { + Text(NSLocalizedString("You are logged in.", comment: "LoginViewModel description text when logged in")) + .padding() + } else { + Text(NSLocalizedString("You are not logged in.", comment: "LoginViewModel description text when not logged in")) + .padding() + } + + VStack(alignment: .leading) { + messageView + } + .padding() + Spacer() + if isLoggedIn { + deleteServiceButton + } else { + loginButton + } + } + .padding() + .frame(minHeight: geometry.size.height) + } } } - .padding([.leading, .trailing]) - .navigationBarTitle("") - .navigationBarItems(trailing: dismissButton) - .alert(LocalizedString("Are you sure you want to delete this service?", comment: "Confirmation message for deleting a service"), isPresented: $showingAlert) + .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) { - didRequestDelete() - dismiss() + deleteService?() + dismiss?() + } + } + + } + + private var logo: some View { + Image(frameworkImage: "Tidepool Logo", decorative: true) + .resizable() + .aspectRatio(contentMode: .fit) + .onLongPressGesture(minimumDuration: 2) { + 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)) { + selectedEnvironment = environment + } + } + buttons.append(.cancel()) + + + return ActionSheet(title: Text(NSLocalizedString("Environment", comment: "Tidepool login environment action sheet title")), + message: Text(selectedEnvironment.description), buttons: buttons) + } + + private var messageView: some View { + Text(message) + .font(.callout) + .foregroundColor(.red) + } + + private var loginButton: some View { + Button(action: { + loginButtonTapped() + }) { + if isLoggingIn { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } else { + Text(NSLocalizedString("Login", comment: "Tidepool login button title")) } } + .buttonStyle(ActionButtonStyle()) + .disabled(isLoggingIn) } - private var dismissButton: some View { - Button(action: dismiss) { - Text("Done").bold() + + private var deleteServiceButton: some View { + Button(action: { + showingDeletionConfirmation = true + }) { + Text(NSLocalizedString("Logout", comment: "Tidepool logout button title")) } - }} + .buttonStyle(ActionButtonStyle(.secondary)) + .disabled(isLoggingIn) + } + + private func loginButtonTapped() { + guard !isLoggingIn else { + return + } + + isLoggingIn = true + + Task { + do { + try await login?(selectedEnvironment) + dismiss?() + } catch { + setError(error) + isLoggingIn = false + } + } + } + + private func setError(_ error: Error?) { + if case .requestNotAuthenticated = error as? TError { + self.message = NSLocalizedString("Wrong username or password.", comment: "The message for the request not authenticated error") + } else { + self.message = error?.localizedDescription ?? "" + } + } + + private var closeButton: some View { + Button(action: { + dismiss?() + }) { + Text(closeButtonTitle) + .fontWeight(.regular) + } + } + + private var closeButtonTitle: String { NSLocalizedString("Close", comment: "Close navigation button title of an onboarding section page view") } +} struct SettingsView_Previews: PreviewProvider { static var previews: some View { - SettingsView(accountLogin: "test@test.com") { - print("Delete Service!") - } + SettingsView( + session: nil, + defaultEnvironment: nil, + environments: [], + login: nil, + dismiss: nil, + deleteService: nil + ) } } diff --git a/TidepoolServiceKitUI/TidepoolService+UI.swift b/TidepoolServiceKitUI/TidepoolService+UI.swift index c54e0ca..0217230 100644 --- a/TidepoolServiceKitUI/TidepoolService+UI.swift +++ b/TidepoolServiceKitUI/TidepoolService+UI.swift @@ -13,27 +13,69 @@ import TidepoolServiceKit extension TidepoolService: ServiceUI { public static var image: UIImage? { - UIImage(named: "Tidepool Logo", in: Bundle(for: TidepoolServiceSetupViewController.self), compatibleWith: nil)! + UIImage(named: "Tidepool Logo", in: Bundle(for: TidepoolServiceSettingsHostController.self), compatibleWith: nil)! } public static func setupViewController(colorPalette: LoopUIColorPalette, pluginHost: PluginHost) -> SetupUIResult { - let service = TidepoolService(hostIdentifier: pluginHost.hostIdentifier, hostVersion: pluginHost.hostVersion) - return .userInteractionRequired(ServiceNavigationController(rootViewController: TidepoolServiceSetupViewController(service: service))) + + + let navController = ServiceNavigationController() + navController.isNavigationBarHidden = true + + Task { + let service = TidepoolService(hostIdentifier: pluginHost.hostIdentifier, hostVersion: pluginHost.hostVersion) + + let tapi = service.tapi + let session = await tapi.session + let environments = await tapi.environments + + var presentingViewController: UIViewController! + + let settingsView = await SettingsView(session: session, defaultEnvironment: tapi.defaultEnvironment, environments: environments, login: { env throws in + try await tapi.login(environment: env, presenting: presentingViewController) + await navController.notifyServiceCreatedAndOnboarded(service) + }, dismiss: { + Task { + await navController.notifyComplete() + } + }, deleteService: { + service.serviceDelegate?.serviceWantsDeletion(service) + }) + + let hostingController = await TidepoolServiceSettingsHostController(rootView: settingsView, service: service) + presentingViewController = hostingController + await navController.pushViewController(hostingController, animated: false) + } + + return .userInteractionRequired(navController) } public func settingsViewController(colorPalette: LoopUIColorPalette) -> ServiceViewController { - var environment = tapi.session?.environment.host - if environment == "app.tidepool.org" { - environment = nil - } + let navController = ServiceNavigationController() + navController.isNavigationBarHidden = true + + Task { + let session = await tapi.session + let environments = await tapi.environments - let view = SettingsView(accountLogin: tapi.session?.email ?? "Unknown", environment: environment, didRequestDelete: { - self.completeDelete() - }) - let hostedView = DismissibleHostingController(rootView: view, colorPalette: colorPalette) - let navVC = ServiceNavigationController(rootViewController: hostedView) + var presentingViewController: UIViewController! + let view = await SettingsView(session: session, defaultEnvironment: tapi.defaultEnvironment, environments: environments, login: { env throws in + try await self.tapi.login(environment: env, presenting: presentingViewController) + }, dismiss: { + Task { + await navController.notifyComplete() + } + }, deleteService: { + self.serviceDelegate?.serviceWantsDeletion(self) + }) + + let hostingController = await TidepoolServiceSettingsHostController(rootView: view, service: self) + presentingViewController = hostingController + + await navController.pushViewController(hostingController, animated: false) + } - return navVC + return navController } } diff --git a/TidepoolServiceKitUI/TidepoolServiceSetupViewController.swift b/TidepoolServiceKitUI/TidepoolServiceSettingsHostController.swift similarity index 63% rename from TidepoolServiceKitUI/TidepoolServiceSetupViewController.swift rename to TidepoolServiceKitUI/TidepoolServiceSettingsHostController.swift index c817ade..bd8ac93 100644 --- a/TidepoolServiceKitUI/TidepoolServiceSetupViewController.swift +++ b/TidepoolServiceKitUI/TidepoolServiceSettingsHostController.swift @@ -9,38 +9,32 @@ import LoopKitUI import TidepoolKit import TidepoolServiceKit +import SwiftUI -final class TidepoolServiceSetupViewController: UIViewController { + +final class TidepoolServiceSettingsHostController: UIHostingController, CompletionNotifying { + + var serviceOnboardingDelegate: ServiceOnboardingDelegate? + var completionDelegate: CompletionDelegate? private let service: TidepoolService - init(service: TidepoolService) { + init(rootView: SettingsView, service: TidepoolService) { self.service = service - super.init(nibName: nil, bundle: nil) + super.init(rootView: rootView) } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + print("Here") } - 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) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } } -extension TidepoolServiceSetupViewController: TLoginSignupDelegate { +extension TidepoolServiceSettingsHostController { func loginSignupDidComplete(completion: @escaping (Error?) -> Void) { service.completeCreate { error in guard error == nil else { From afb21b6a34c119ce04606bcc1481d8d646b4d13c Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Wed, 19 Apr 2023 21:32:35 -0500 Subject: [PATCH 2/7] SettingsView using TidepoolService as ObservedObject --- TidepoolService.xcodeproj/project.pbxproj | 8 +- TidepoolServiceKit/TidepoolService.swift | 18 ++- TidepoolServiceKitUI/Extensions/UIImage.swift | 20 ++++ TidepoolServiceKitUI/SettingsView.swift | 104 +++++++++--------- TidepoolServiceKitUI/TidepoolService+UI.swift | 39 +++---- ...idepoolServiceSettingsHostController.swift | 61 ---------- 6 files changed, 96 insertions(+), 154 deletions(-) create mode 100644 TidepoolServiceKitUI/Extensions/UIImage.swift delete mode 100644 TidepoolServiceKitUI/TidepoolServiceSettingsHostController.swift diff --git a/TidepoolService.xcodeproj/project.pbxproj b/TidepoolService.xcodeproj/project.pbxproj index bec85e8..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 /* TidepoolServiceSettingsHostController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A92E770022E9181500591027 /* TidepoolServiceSettingsHostController.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, ); }; }; @@ -74,6 +73,7 @@ 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 */; }; @@ -151,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 /* TidepoolServiceSettingsHostController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidepoolServiceSettingsHostController.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; }; @@ -238,6 +237,7 @@ 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 = ""; }; @@ -434,7 +434,6 @@ A9DAAD6C22E7EA8F00E76C9F /* IdentifiableClass.swift */, A9DAAD6E22E7EA9700E76C9F /* NibLoadable.swift */, A9DAAD3822E7DEE000E76C9F /* TidepoolService+UI.swift */, - A92E770022E9181500591027 /* TidepoolServiceSettingsHostController.swift */, C1D0B62829848A460098D215 /* SettingsView.swift */, E93BA06124A29C9C00C5D7E6 /* Assets.xcassets */, A9DAAD4F22E7DFD400E76C9F /* Localizable.strings */, @@ -485,6 +484,7 @@ isa = PBXGroup; children = ( C1D0B62B29848BEB0098D215 /* Image.swift */, + C1C9414529F0CB21008D3E05 /* UIImage.swift */, ); path = Extensions; sourceTree = ""; @@ -792,13 +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 /* TidepoolServiceSettingsHostController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index 1f269ae..6d7fc48 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -63,7 +63,7 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { public init(hostIdentifier: String, hostVersion: String, automaticallyFetchEnvironments: Bool = true) { self.id = UUID().uuidString - self.tapi = TAPI(clientId: "diy-loop", redirectURL: URL(string: "org.loopkit.Loop://tidepool_service_redirect")!, automaticallyFetchEnvironments: automaticallyFetchEnvironments) + self.tapi = TAPI(clientId: "diy-loop", redirectURL: URL(string: "org.loopkit.Loop://tidepool_service_redirect")!) self.hostIdentifier = hostIdentifier self.hostVersion = hostVersion @@ -79,6 +79,7 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { } public init?(rawState: RawStateValue) { + self.isOnboarded = true // Assume when restoring from state, that we're onboarded self.tapi = TAPI(clientId: "diy-loop", redirectURL: URL(string: "org.loopkit.Loop://tidepool_service_redirect")!) guard let id = rawState["id"] as? String else { return nil @@ -113,9 +114,9 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { return rawValue } - public let isOnboarded = true // No distinction between created and onboarded + public var isOnboarded = false // No distinction between created and onboarded - private var session: TSession? { + @Published public var session: TSession? { didSet { if session == nil { self.dataSetId = nil @@ -132,14 +133,9 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { self.session = session } - public func completeCreate(completion: @escaping (Error?) -> Void) { - Task { - do { - try await self.getDataSet() - } catch { - completion(error) - } - } + public func completeCreate() async throws { + self.isOnboarded = true + try await self.getDataSet() } public func completeUpdate() { 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 index c2c5037..65df3a2 100644 --- a/TidepoolServiceKitUI/SettingsView.swift +++ b/TidepoolServiceKitUI/SettingsView.swift @@ -8,6 +8,7 @@ import SwiftUI import TidepoolKit +import TidepoolServiceKit @MainActor public struct SettingsView: View { @@ -15,33 +16,29 @@ public struct SettingsView: View { @State private var isEnvironmentActionSheetPresented = false @State private var showingDeletionConfirmation = false - @State private var message = "" + @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? - var session: TSession? - let environments: [TEnvironment] - let login: ((TEnvironment) async throws -> Void)? - let dismiss: (() -> Void)? - let deleteService: (() -> Void)? + @ObservedObject private var service: TidepoolService + + private let login: ((TEnvironment) async throws -> Void)? + private let dismiss: (() -> Void)? var isLoggedIn: Bool { - return session != nil + return service.session != nil } - public init( - session: TSession?, - defaultEnvironment: TEnvironment?, - environments: [TEnvironment], - login: ((TEnvironment) async throws -> Void)?, - dismiss: (() -> Void)?, - deleteService: (() -> Void)?) + public init(service: TidepoolService, login: ((TEnvironment) async throws -> Void)?, dismiss: (() -> Void)?) { - self._selectedEnvironment = State(initialValue: session?.environment ?? defaultEnvironment ?? environments.first!) - self.environments = environments + 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 - self.deleteService = deleteService } public var body: some View { @@ -50,7 +47,7 @@ public struct SettingsView: View { .edgesIgnoringSafeArea(.all) GeometryReader { geometry in ScrollView { - VStack { + VStack(spacing: 20) { HStack() { Spacer() closeButton @@ -60,21 +57,32 @@ public struct SettingsView: View { logo .padding(.horizontal, 30) .padding(.bottom) - Text(NSLocalizedString("Environment", comment: "Label title for displaying selected Tidepool server environment.")) - .bold() - Text(selectedEnvironment.description) - if isLoggedIn { - Text(NSLocalizedString("You are logged in.", comment: "LoginViewModel description text when logged in")) - .padding() + if selectedEnvironment != TEnvironment.productionEnvironment { + VStack { + Text(NSLocalizedString("Environment", comment: "Label title for displaying selected Tidepool server environment.")) + .bold() + Text(selectedEnvironment.description) + } + } + if let username = service.session?.username { + VStack { + Text(NSLocalizedString("Logged in as", comment: "LoginViewModel description text when logged in")) + .bold() + Text(username) + } } else { Text(NSLocalizedString("You are not logged in.", comment: "LoginViewModel description text when not logged in")) .padding() } - VStack(alignment: .leading) { - messageView + if let error { + VStack(alignment: .leading) { + Text(error.localizedDescription) + .font(.callout) + .foregroundColor(.red) + } + .padding() } - .padding() Spacer() if isLoggedIn { deleteServiceButton @@ -90,10 +98,17 @@ public struct SettingsView: View { .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) { - deleteService?() + service.deleteService() dismiss?() } } + .task { + do { + environments = try await TEnvironment.fetchEnvironments() + } catch { + + } + } } @@ -111,6 +126,7 @@ public struct SettingsView: View { private var environmentActionSheet: ActionSheet { var buttons: [ActionSheet.Button] = environments.map { environment in .default(Text(environment.description)) { + error = nil selectedEnvironment = environment } } @@ -121,12 +137,6 @@ public struct SettingsView: View { message: Text(selectedEnvironment.description), buttons: buttons) } - private var messageView: some View { - Text(message) - .font(.callout) - .foregroundColor(.red) - } - private var loginButton: some View { Button(action: { loginButtonTapped() @@ -147,7 +157,7 @@ public struct SettingsView: View { Button(action: { showingDeletionConfirmation = true }) { - Text(NSLocalizedString("Logout", comment: "Tidepool logout button title")) + Text(NSLocalizedString("Delete Service", comment: "Delete Tidepool service button title")) } .buttonStyle(ActionButtonStyle(.secondary)) .disabled(isLoggingIn) @@ -158,27 +168,21 @@ public struct SettingsView: View { return } + error = nil isLoggingIn = true Task { do { try await login?(selectedEnvironment) - dismiss?() + isLoggingIn = false + //dismiss?() } catch { - setError(error) + self.error = error isLoggingIn = false } } } - private func setError(_ error: Error?) { - if case .requestNotAuthenticated = error as? TError { - self.message = NSLocalizedString("Wrong username or password.", comment: "The message for the request not authenticated error") - } else { - self.message = error?.localizedDescription ?? "" - } - } - private var closeButton: some View { Button(action: { dismiss?() @@ -192,14 +196,8 @@ public struct SettingsView: View { } struct SettingsView_Previews: PreviewProvider { + @MainActor static var previews: some View { - SettingsView( - session: nil, - defaultEnvironment: nil, - environments: [], - login: nil, - dismiss: nil, - deleteService: nil - ) + 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 0217230..f6f16e7 100644 --- a/TidepoolServiceKitUI/TidepoolService+UI.swift +++ b/TidepoolServiceKitUI/TidepoolService+UI.swift @@ -9,41 +9,34 @@ 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: TidepoolServiceSettingsHostController.self), compatibleWith: nil)! + UIImage(frameworkImage: "Tidepool Logo") } 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 tapi = service.tapi - let session = await tapi.session - let environments = await tapi.environments - - var presentingViewController: UIViewController! - - let settingsView = await SettingsView(session: session, defaultEnvironment: tapi.defaultEnvironment, environments: environments, login: { env throws in - try await tapi.login(environment: env, presenting: presentingViewController) + 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) + //await navController.notifyComplete() }, dismiss: { Task { await navController.notifyComplete() } - }, deleteService: { - service.serviceDelegate?.serviceWantsDeletion(service) }) - let hostingController = await TidepoolServiceSettingsHostController(rootView: settingsView, service: service) - presentingViewController = hostingController + let hostingController = await UIHostingController(rootView: settingsView) await navController.pushViewController(hostingController, animated: false) } @@ -56,23 +49,19 @@ extension TidepoolService: ServiceUI { navController.isNavigationBarHidden = true Task { - let session = await tapi.session - let environments = await tapi.environments - - var presentingViewController: UIViewController! - let view = await SettingsView(session: session, defaultEnvironment: tapi.defaultEnvironment, environments: environments, login: { env throws in - try await self.tapi.login(environment: env, presenting: presentingViewController) + let settingsView = await SettingsView(service: self, login: { [weak self] environment in + if let self { + try await self.tapi.login(environment: environment, presenting: navController) + await navController.notifyServiceCreatedAndOnboarded(self) + //await navController.notifyComplete() + } }, dismiss: { Task { await navController.notifyComplete() } - }, deleteService: { - self.serviceDelegate?.serviceWantsDeletion(self) }) - let hostingController = await TidepoolServiceSettingsHostController(rootView: view, service: self) - presentingViewController = hostingController - + let hostingController = await UIHostingController(rootView: settingsView) await navController.pushViewController(hostingController, animated: false) } diff --git a/TidepoolServiceKitUI/TidepoolServiceSettingsHostController.swift b/TidepoolServiceKitUI/TidepoolServiceSettingsHostController.swift deleted file mode 100644 index bd8ac93..0000000 --- a/TidepoolServiceKitUI/TidepoolServiceSettingsHostController.swift +++ /dev/null @@ -1,61 +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 -import SwiftUI - - -final class TidepoolServiceSettingsHostController: UIHostingController, CompletionNotifying { - - var serviceOnboardingDelegate: ServiceOnboardingDelegate? - var completionDelegate: CompletionDelegate? - - private let service: TidepoolService - - init(rootView: SettingsView, service: TidepoolService) { - self.service = service - - super.init(rootView: rootView) - } - - override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { - print("Here") - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -extension TidepoolServiceSettingsHostController { - 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() - } - } - } -} From 46237570326fc56db2d4fec86196fb30d7c4e896 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Wed, 19 Apr 2023 21:38:38 -0500 Subject: [PATCH 3/7] Tweak logo size --- TidepoolServiceKitUI/SettingsView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/TidepoolServiceKitUI/SettingsView.swift b/TidepoolServiceKitUI/SettingsView.swift index 65df3a2..f1b7894 100644 --- a/TidepoolServiceKitUI/SettingsView.swift +++ b/TidepoolServiceKitUI/SettingsView.swift @@ -116,6 +116,7 @@ public struct SettingsView: View { Image(frameworkImage: "Tidepool Logo", decorative: true) .resizable() .aspectRatio(contentMode: .fit) + .frame(maxWidth: 150) .onLongPressGesture(minimumDuration: 2) { UINotificationFeedbackGenerator().notificationOccurred(.warning) isEnvironmentActionSheetPresented = true From 3afe58523a7fb6e68a3e876dd3e0327bd51bd0bf Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 20 Apr 2023 09:33:12 -0500 Subject: [PATCH 4/7] Fix issues with state restoration and re-logging in. Add alert when session loss is detected --- TidepoolServiceKit/TidepoolService.swift | 58 +++++++++++++------ TidepoolServiceKitUI/SettingsView.swift | 26 ++++++--- TidepoolServiceKitUI/TidepoolService+UI.swift | 2 - 3 files changed, 58 insertions(+), 28 deletions(-) diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index 6d7fc48..986976d 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -25,12 +25,11 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { public static let localizedTitle = LocalizedString("Tidepool", comment: "The title of the Tidepool service") - public weak var serviceDelegate: ServiceDelegate? - - public func setServiceDelegate(_ delegate: ServiceDelegate) { - serviceDelegate = delegate - self.hostIdentifier = serviceDelegate?.hostIdentifier - self.hostVersion = serviceDelegate?.hostVersion + public weak var serviceDelegate: ServiceDelegate? { + didSet { + self.hostIdentifier = serviceDelegate?.hostIdentifier + self.hostVersion = serviceDelegate?.hostVersion + } } public lazy var sessionStorage: SessionStorage = KeychainManager() @@ -96,10 +95,15 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { await tapi.setSession(session) await tapi.setLogging(self) await tapi.addObserver(self) + + if session != nil && dataSetId == nil { + try await getDataSet() + } } } catch let error { tidepoolKitLog.error("Error initializing TidepoolService %{public}@", error.localizedDescription) self.error = error + return nil } } @@ -116,26 +120,42 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { public var isOnboarded = false // No distinction between created and onboarded - @Published public var session: TSession? { - didSet { - if session == nil { - self.dataSetId = nil - } - do { - try sessionStorage.setSession(session, for: sessionService) - } catch let error { - self.error = error - } - } - } + @Published public var session: TSession? public func apiDidUpdateSession(_ session: TSession?) { + guard session != self.session else { + return + } self.session = session + + do { + try sessionStorage.setSession(session, for: sessionService) + } catch let error { + self.error = error + } + + if session == nil { + self.dataSetId = nil + 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 has lost authorization. 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)) + } else { + Task { + do { + try await getDataSet() + } catch { + self.error = error + } + } + } } public func completeCreate() async throws { self.isOnboarded = true - try await self.getDataSet() } public func completeUpdate() { diff --git a/TidepoolServiceKitUI/SettingsView.swift b/TidepoolServiceKitUI/SettingsView.swift index f1b7894..8425b51 100644 --- a/TidepoolServiceKitUI/SettingsView.swift +++ b/TidepoolServiceKitUI/SettingsView.swift @@ -59,19 +59,31 @@ public struct SettingsView: View { .padding(.bottom) if selectedEnvironment != TEnvironment.productionEnvironment { VStack { - Text(NSLocalizedString("Environment", comment: "Label title for displaying selected Tidepool server environment.")) + 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(NSLocalizedString("Logged in as", comment: "LoginViewModel description text when logged in")) + Text(LocalizedString("Logged in as", comment: "LoginViewModel description text when logged in")) .bold() Text(username) } } else { - Text(NSLocalizedString("You are not logged in.", comment: "LoginViewModel description text when not logged in")) + Text(LocalizedString("You are not logged in.", comment: "LoginViewModel description text when not logged in")) .padding() } @@ -134,7 +146,7 @@ public struct SettingsView: View { buttons.append(.cancel()) - return ActionSheet(title: Text(NSLocalizedString("Environment", comment: "Tidepool login environment action sheet title")), + return ActionSheet(title: Text(LocalizedString("Environment", comment: "Tidepool login environment action sheet title")), message: Text(selectedEnvironment.description), buttons: buttons) } @@ -146,7 +158,7 @@ public struct SettingsView: View { ProgressView() .progressViewStyle(CircularProgressViewStyle()) } else { - Text(NSLocalizedString("Login", comment: "Tidepool login button title")) + Text(LocalizedString("Login", comment: "Tidepool login button title")) } } .buttonStyle(ActionButtonStyle()) @@ -158,7 +170,7 @@ public struct SettingsView: View { Button(action: { showingDeletionConfirmation = true }) { - Text(NSLocalizedString("Delete Service", comment: "Delete Tidepool service button title")) + Text(LocalizedString("Delete Service", comment: "Delete Tidepool service button title")) } .buttonStyle(ActionButtonStyle(.secondary)) .disabled(isLoggingIn) @@ -193,7 +205,7 @@ public struct SettingsView: View { } } - private var closeButtonTitle: String { NSLocalizedString("Close", comment: "Close navigation button title of an onboarding section page view") } + private var closeButtonTitle: String { LocalizedString("Close", comment: "Close navigation button title of an onboarding section page view") } } struct SettingsView_Previews: PreviewProvider { diff --git a/TidepoolServiceKitUI/TidepoolService+UI.swift b/TidepoolServiceKitUI/TidepoolService+UI.swift index f6f16e7..3141a27 100644 --- a/TidepoolServiceKitUI/TidepoolService+UI.swift +++ b/TidepoolServiceKitUI/TidepoolService+UI.swift @@ -52,8 +52,6 @@ extension TidepoolService: ServiceUI { let settingsView = await SettingsView(service: self, login: { [weak self] environment in if let self { try await self.tapi.login(environment: environment, presenting: navController) - await navController.notifyServiceCreatedAndOnboarded(self) - //await navController.notifyComplete() } }, dismiss: { Task { From 1718bf4e7c4bc1cf45d1c8f72cfbae85dbe984d9 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 20 Apr 2023 17:26:26 -0500 Subject: [PATCH 5/7] Improve DataSetId caching --- TidepoolServiceKit/TidepoolService.swift | 147 ++++++++++++++--------- 1 file changed, 93 insertions(+), 54 deletions(-) diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index 986976d..a984ab8 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -12,8 +12,20 @@ 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? @@ -40,11 +52,6 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { private let id: String - private var dataSetId: String? { - didSet { - completeUpdate() - } - } private var lastControllerSettingsDatum: TControllerSettingsDatum? @@ -85,7 +92,9 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { } 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) } @@ -95,10 +104,6 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { await tapi.setSession(session) await tapi.setLogging(self) await tapi.addObserver(self) - - if session != nil && dataSetId == nil { - try await getDataSet() - } } } catch let error { tidepoolKitLog.error("Error initializing TidepoolService %{public}@", error.localizedDescription) @@ -110,7 +115,9 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { 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) } @@ -126,6 +133,12 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { 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 { @@ -135,7 +148,7 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { } if session == nil { - self.dataSetId = 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 has lost authorization. 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")) @@ -143,14 +156,6 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { alertIdentifier: "authentication-needed"), foregroundContent: content, backgroundContent: content, trigger: .immediate)) - } else { - Task { - do { - try await getDataSet() - } catch { - self.error = error - } - } } } @@ -169,7 +174,55 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { serviceDelegate?.serviceWantsDeletion(self) } - private func getDataSet() async throws { + 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 fetchDataSetId() async throws -> String { guard let clientName = hostIdentifier else { throw TidepoolServiceError.configuration } @@ -180,13 +233,21 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { if dataSets.count > 1 { self.log.error("Found multiple matching data sets; expected zero or one") } - self.dataSetId = dataSets.first?.uploadId + + guard let dataSetId = dataSets.first?.uploadId else { + throw TidepoolServiceError.missingDataSetId + } + return dataSetId } else { - try await self.createDataSet() + let dataSet = try await self.createDataSet() + guard let dataSetId = dataSet.id else { + throw TidepoolServiceError.missingDataSetId + } + return dataSetId } } - private func createDataSet() async throws { + private func createDataSet() async throws -> TDataSet { guard let clientName = hostIdentifier, let clientVersion = hostVersion else { throw TidepoolServiceError.configuration } @@ -195,21 +256,10 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { dataSetType: .continuous, deduplicator: TDataSet.Deduplicator(name: .dataSetDeleteOrigin), deviceTags: [.bgm, .cgm, .insulinPump]) - let newDataSet = try await tapi.createDataSet(dataSet) - self.dataSetId = newDataSet.uploadId - } - - 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 - }() + return try await tapi.createDataSet(dataSet) + } - private static var decoder = PropertyListDecoder() } extension TidepoolService: TLogging { @@ -521,9 +571,8 @@ extension TidepoolService: RemoteDataService { if let error = error { throw error } - guard let dataSetId = dataSetId else { - throw TidepoolServiceError.configuration - } + + let dataSetId = try await getCachedDataSetId() do { try await tapi.createData(data, dataSetId: dataSetId) @@ -539,9 +588,8 @@ extension TidepoolService: RemoteDataService { if let error = error { throw error } - guard let dataSetId = dataSetId else { - throw TidepoolServiceError.configuration - } + + 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. @@ -560,9 +608,8 @@ extension TidepoolService: RemoteDataService { if let error = error { throw error } - guard let dataSetId = dataSetId else { - throw TidepoolServiceError.configuration - } + + let dataSetId = try await getCachedDataSetId() do { try await tapi.deleteData(withSelectors: selectors, dataSetId: dataSetId) @@ -590,14 +637,6 @@ extension KeychainManager: SessionStorage { } } -extension TidepoolServiceError: LocalizedError { - public var errorDescription: String? { - switch self { - case .configuration: return LocalizedString("Configuration Error", comment: "Error string for configuration error") - } - } -} - fileprivate protocol EffectivelyEquivalent { func isEffectivelyEquivalent(to other: Self) -> Bool var isEffectivelyEmpty: Bool { get } From 12da214f60526c60524b026f94b932fa683ae611 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 20 Apr 2023 21:38:43 -0500 Subject: [PATCH 6/7] Do not allow environment switching when logged in --- TidepoolServiceKitUI/SettingsView.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/TidepoolServiceKitUI/SettingsView.swift b/TidepoolServiceKitUI/SettingsView.swift index 8425b51..ce69fa2 100644 --- a/TidepoolServiceKitUI/SettingsView.swift +++ b/TidepoolServiceKitUI/SettingsView.swift @@ -130,8 +130,10 @@ public struct SettingsView: View { .aspectRatio(contentMode: .fit) .frame(maxWidth: 150) .onLongPressGesture(minimumDuration: 2) { - UINotificationFeedbackGenerator().notificationOccurred(.warning) - isEnvironmentActionSheetPresented = true + if !isLoggedIn { + UINotificationFeedbackGenerator().notificationOccurred(.warning) + isEnvironmentActionSheetPresented = true + } } .actionSheet(isPresented: $isEnvironmentActionSheetPresented) { environmentActionSheet } } From 64ed1101567d1407601dfe67bea8b74e2178e30c Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 20 Apr 2023 21:43:27 -0500 Subject: [PATCH 7/7] Tweak wording --- TidepoolServiceKit/TidepoolService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index a984ab8..683e8a5 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -150,7 +150,7 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { 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 has lost authorization. Please navigate to Tidepool Service settings and reauthenticate.", comment: "The body text 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"),