diff --git a/GlucoseBar.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/GlucoseBar.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 71b513d..3ffd349 100644 --- a/GlucoseBar.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/GlucoseBar.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "41fccdc00685a58240c84eb80ad08027abc0c5ffa78f449e6a115de61c3bad3e", + "originHash" : "ffef6d90f39aa3f9bd452ee7488bed6f53dfec789d54a497e3014ed2a7837742", "pins" : [ { "identity" : "launchatlogin-modern", diff --git a/GlucoseBar.xcodeproj/xcuserdata/stokholm.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/GlucoseBar.xcodeproj/xcuserdata/stokholm.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index a698548..74d96e0 100644 --- a/GlucoseBar.xcodeproj/xcuserdata/stokholm.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/GlucoseBar.xcodeproj/xcuserdata/stokholm.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -14,8 +14,8 @@ filePath = "GlucoseBar/SettingsView.swift" startingColumnNumber = "9223372036854775807" endingColumnNumber = "9223372036854775807" - startingLineNumber = "260" - endingLineNumber = "260" + startingLineNumber = "259" + endingLineNumber = "259" landmarkName = "body" landmarkType = "24"> @@ -30,8 +30,8 @@ filePath = "GlucoseBar/SettingsView.swift" startingColumnNumber = "9223372036854775807" endingColumnNumber = "9223372036854775807" - startingLineNumber = "292" - endingLineNumber = "292" + startingLineNumber = "291" + endingLineNumber = "291" landmarkName = "body" landmarkType = "24"> diff --git a/GlucoseBar/GlucoseBarApp.swift b/GlucoseBar/GlucoseBarApp.swift index a73dad3..155e11e 100644 --- a/GlucoseBar/GlucoseBarApp.swift +++ b/GlucoseBar/GlucoseBarApp.swift @@ -124,13 +124,11 @@ struct GlucoseBarApp: App { applyColor = true } - var configuration: NSImage.SymbolConfiguration + let configuration = NSImage.SymbolConfiguration(pointSize: 16, weight: .light) if applyColor { - configuration = NSImage.SymbolConfiguration(pointSize: 16, weight: .light) - .applying(.init(paletteColors: [color])) - } else { - configuration = NSImage.SymbolConfiguration(pointSize: 16, weight: .light) + configuration.applying(.init(paletteColors: [color])) } + let image = NSImage(systemSymbolName: symbol, accessibilityDescription: accessibilityDescription) let titleImage = image?.withSymbolConfiguration(configuration) ?? NSImage(systemSymbolName: symbol, accessibilityDescription: accessibilityDescription)! diff --git a/GlucoseBar/GlucoseEntry.swift b/GlucoseBar/GlucoseEntry.swift index f04373d..fb3848f 100644 --- a/GlucoseBar/GlucoseEntry.swift +++ b/GlucoseBar/GlucoseEntry.swift @@ -87,7 +87,7 @@ public struct GlucoseEntry: Hashable { case .downDownDown: return "↓↓" case .notComputable: - return "?" + return "↔" case .rateOutOfRange: return "?" } diff --git a/GlucoseBar/GraphView.swift b/GlucoseBar/GraphView.swift index 4df7057..ef2c058 100644 --- a/GlucoseBar/GraphView.swift +++ b/GlucoseBar/GraphView.swift @@ -35,21 +35,20 @@ struct GraphView: View { func getGraphData() -> [GraphEntry] { var data: [GraphEntry] = []; if (g.entries != nil) { + for entry in g.entries! { + // Check if entry!.date is longer in the past than s.graphMinutes + if -1 * entry.date.timeIntervalSinceNow > Double(s.graphMinutes) * 60 { + continue + } - let entryCount = s.graphMinutes/5-1 - - for i in 0.. i) { - let entry = g.entries![i] - let glu = convertGlucose(s, glucose: entry.glucose) + let glu = convertGlucose(s, glucose: entry.glucose) - var delta = 0.0 - if entry.changeRate != nil { - delta = entry.changeRate! - } - - data.append(GraphEntry(date: entry.date, value: glu, trend: entry.trend ?? .notComputable, delta: delta)) + var delta = 0.0 + if entry.changeRate != nil { + delta = entry.changeRate! } + + data.append(GraphEntry(date: entry.date, value: glu, trend: entry.trend ?? .notComputable, delta: delta)) } } diff --git a/GlucoseBar/MainAppView.swift b/GlucoseBar/MainAppView.swift index 7f11efd..927e716 100644 --- a/GlucoseBar/MainAppView.swift +++ b/GlucoseBar/MainAppView.swift @@ -69,8 +69,12 @@ struct MainAppView: View { if g.error != "" && s.validSettings && vs.isOnline { VStack { Text("Error").font(.headline).frame(maxWidth: .infinity, alignment: .leading).padding(.top, 10).foregroundColor(.red) -// Text("\(g.error)").padding(.top, 10).fixedSize(horizontal: false, vertical: true).foregroundColor(.red).fontWeight(.heavy).padding(.bottom, 10) Text("Check the settings and make sure your CGM source (\(s.cgmProvider.presentable)) is responding.").fixedSize(horizontal: false, vertical: true) + + if g.provider.providerIssue != nil { + Text("Additional Info: \(g.provider.providerIssue!)").fixedSize(horizontal: false, vertical: true).padding(.top) + } + Spacer() HStack { SettingsButton() diff --git a/GlucoseBar/Providers/LibreLinkUp/LibreLinkUp.swift b/GlucoseBar/Providers/LibreLinkUp/LibreLinkUp.swift index 7d81933..670ec6f 100644 --- a/GlucoseBar/Providers/LibreLinkUp/LibreLinkUp.swift +++ b/GlucoseBar/Providers/LibreLinkUp/LibreLinkUp.swift @@ -201,13 +201,9 @@ class LibreLinkUp: Provider { if !isAuthValid() { logger.debug("calling authenticate from fetch") - authenticate(completion: { success, _ in - if success { - Task { - await self.fetch() - } - } - }) + if await authenticate() { + await self.fetch() + } return } @@ -350,7 +346,7 @@ class LibreLinkUp: Provider { // // } - private func getConnections() async { + private func getConnections() async -> (connections: [LibreLinkUp.LibreLinkUpConnectionsResponse], success: Bool) { self.logger.debug("LibreLinkUp.getConnections") let path = "/connections" @@ -363,7 +359,8 @@ class LibreLinkUp: Provider { if res.statusCode > 499 { // TODO: Handle error - return + self.logger.error("High status code, not yet handled") + return (connections: [], success: false) } do { @@ -371,33 +368,44 @@ class LibreLinkUp: Provider { switch result.status { case 0: - self.logger.info("Status 0, updating values") - // TODO: Success - self.logger.info("Connection count: \(self.connections.count)") - self.connections = result.data! - self.connectionID = self.connections.first!.patientID - self.logger.debug("got connections: \(self.connections)") - return +// DispatchQueue.main.async { + self.logger.info("Status 0, updating values") + // TODO: Success + self.logger.info("Connection count: \(self.connections.count)") + self.connections = result.data! + self.connectionID = result.data!.first?.patientID ?? ""// self.connections.first?.patientID ?? "" + self.logger.debug("got connections: \(self.connections)") +// } + return (connections: result.data!, success: true) +// return true case 920: // Version bump needed // self.logger.info("Version too low, bumping and retrying") // self.lluVersion = result.data!.minimumVersion ?? "0" // completion(false, true) - return + return (connections: [], success: false) +// return false default: self.logger.debug("Default case triggered for status: \(result.status)") } } catch { self.logger.error("failed handling decode and actions from response \(String(describing: error))") - self.logger.debug("DEBUG: \(String(data: data, encoding: .utf8)!)") - return + self.logger.error("DEBUG: \(String(data: data, encoding: .utf8)!)") + return (connections: [], success: false) +// return false } } catch { - + self.logger.error("Catastrophic failure creating request.") + return (connections: [], success: false) +// return false } + + return (connections: [], success: false) +// return false } - private func authenticate(completion: @escaping (_ success: Bool, _ tryAgain: Bool?) -> Void) { + private func authenticate() async -> Bool { self.logger.debug("LibreLinkUp.authenticate") + self.providerIssue = nil let path = "/auth/login" let requestBody = LibreLinkUpAuthRequest(email: self.username, password: self.password) @@ -408,95 +416,103 @@ class LibreLinkUp: Provider { request.httpBody = jsonData } catch { logger.info("failed marshalling json, aborting authenticate") - completion(false, false) - return + return false } - let task = URLSession.shared.dataTask(with: request) { data, response, error in - guard let data = data else { - self.logger.error("authenitcation error: \(String(describing: error))") - completion(false, false) - return - } + do { + let (data, response) = try await URLSession.shared.data(for: request) + self.logger.error("DEBUG: \(String(data: data, encoding: .utf8)!)") let res = response as! HTTPURLResponse if res.statusCode > 499 { - // TODO: Handle error - completion(false, false) - return + self.logger.error("Response code in 500 range. Dunno what to do! Code: \(res.statusCode)") + return false } - do { - let result = try JSONDecoder().decode(LibreLinkUpResponse.self, from: data) + if res.statusCode > 399 { + self.logger.error("Response code in 400 range. Something wrong? Rate limiting? Code: \(res.statusCode)") + if res.statusCode == 430 || res.statusCode == 429 { + self.logger.error("Rate limited, surfacing to user.") + self.providerIssue = "Libre LinkUp is rate limiting you. Please close GlucoseBar try again in 5 minutes or more." + } + return false + } - switch result.status { - case 0: - // TODO: Success - // Check if redirect - if let redirect = result.data!.redirect, let region = result.data!.region, redirect, !region.isEmpty { - self.apiRegion = result.data!.region! - self.authenticate(completion: completion) - return - } - - guard let authToken = result.data!.authTicket?.token, - !authToken.isEmpty else { - self.logger.error("auth response did not satisfy requirements") - completion(false, false) - return - } - - // COMPLETED AUTH - self.auth = result.data!.authTicket! - - // Getting connections - Task { - await self.getConnections() - completion(true, false) - } - return - case 2: - // Bad credentials? - self.logger.info("Bad credentials") - completion(false, false) - return - case 4: - // TODO: Request TOU - self.logger.info("TOU needs calling") - completion(false, true) - return - case 920: // Version bump needed - self.logger.info("Version too low, bumping and retrying") - self.lluVersion = result.data!.data?.minimumVersion ?? "0" - completion(false, true) - return - default: - self.logger.debug("Default case triggered for status: \(result.status)") + let result = try JSONDecoder().decode(LibreLinkUpResponse.self, from: data) + + switch result.status { + case 0: + // TODO: Success + // Check if redirect + if let redirect = result.data!.redirect, let region = result.data!.region, redirect, !region.isEmpty { + self.apiRegion = result.data!.region! + return await self.authenticate() } - } catch { - self.logger.error("failed handling decode and actions from response: \(String(describing: error))") - completion(false, false) - return + + guard let authToken = result.data!.authTicket?.token, + !authToken.isEmpty else { + self.logger.error("auth response did not satisfy requirements") + return false + } + + // COMPLETED AUTH + self.auth = result.data!.authTicket! + + // Getting connections +// Task { + let conns = await self.getConnections() + self.connections = conns.connections + return conns.success +// return await self.getConnections() +// } + case 2: + // Bad credentials? + self.logger.info("Bad credentials") + return false + case 4: + // TODO: Request TOU + self.logger.info("TOU needs calling") +// return await self.authenticate() + return false + + case 920: // Version bump needed + self.logger.info("Version too low, bumping and retrying") + self.lluVersion = result.data!.data?.minimumVersion ?? "0" + return await self.authenticate() + default: + self.logger.debug("Default case triggered for status: \(result.status)") + } + } catch { + self.logger.error("failed handling decode and actions from response: \(String(describing: error))") } - task.resume() + + return false } -// override internal func verifyCredentials(completion: @escaping (_ result: Bool) -> Void) { -// self.logger.debug("librelinkup.verifyCredentials") -// self.authenticate(completion: {authSuccess, tryAgain in -// if authSuccess { -// self.logger.debug("librelinkup auth success: \(self.connectionID)") -// completion(true) -// return -// } -// -// self.logger.debug("librelinkup auth failure") -// completion(false) -// return -// }) -// } + override internal func verifyCredentials() async -> Bool { + self.logger.debug("librelinkup.verifyCredentials") + let authSuccess = await self.authenticate() + + if authSuccess { + self.logger.debug("librelinkup auth success: \(self.connectionID)") + + let conns = await self.getConnections() +// let connectionSuccess = await self.getConnections() + if conns.success { + self.connections = conns.connections + self.logger.debug("librelinkup got connections successfully") + return true + } + + return false + } + + self.logger.debug("librelinkup auth failure") + return false + + } override public func isAuthValid() -> Bool { if auth == nil { diff --git a/GlucoseBar/Providers/Provider.swift b/GlucoseBar/Providers/Provider.swift index 816fee2..66dae55 100644 --- a/GlucoseBar/Providers/Provider.swift +++ b/GlucoseBar/Providers/Provider.swift @@ -68,16 +68,16 @@ class Provider: ObservableObject, @unchecked Sendable { return false } - @ViewBuilder - func getConnectionView(s: SettingsStore) -> some View { - @ObservedObject var settings: SettingsStore = s - - Picker("", selection: $settings.libreConnectionID) { - ForEach(self.connections, id: \.patientID) { - Text("\($0.firstName) \($0.lastName)").tag($0.patientID) - } - } - } +// @ViewBuilder +// func getConnectionView(s: SettingsStore) -> some View { +// @ObservedObject var settings: SettingsStore = s +// +// Picker("", selection: $settings.libreConnectionID) { +// ForEach(self.connections, id: \.patientID) { +// Text("\($0.firstName) \($0.lastName)").padding()//.tag($0.patientID) +// } +// } +// } internal func startTimer() { let _ = Timer.publish(every: readingInterval, on: .main, in: .default) diff --git a/GlucoseBar/Settings.swift b/GlucoseBar/Settings.swift index 4f24267..11fa465 100644 --- a/GlucoseBar/Settings.swift +++ b/GlucoseBar/Settings.swift @@ -31,6 +31,7 @@ class SettingsStore: ObservableObject, @unchecked Sendable { @Published var libreUsername: String = "your@email.com" @Published var librePassword: String = "" @Published var libreConnectionID: String = "" + @Published var libreConnections: [LibreLinkUp.LibreLinkUpConnectionsResponse] = [] @Published var showTimeSince: Bool = false @Published var showDelta: Bool = true diff --git a/GlucoseBar/SettingsView.swift b/GlucoseBar/SettingsView.swift index 5b9e85a..607c7c2 100644 --- a/GlucoseBar/SettingsView.swift +++ b/GlucoseBar/SettingsView.swift @@ -162,7 +162,7 @@ struct GeneralSettings: View { Spacer() Text("\(Bundle.main.appName) Version: \(Bundle.main.appVersionLong) (\(Bundle.main.appBuild)) ").font(.footnote).padding(2) } - }.frame(minWidth: 475, maxWidth: 475, minHeight: 395, maxHeight: 395) + }.frame(minWidth: 475, maxWidth: 475, minHeight: 400, maxHeight: 400) } } @@ -175,6 +175,10 @@ struct CGMSettings: View { @State var cgmCredentialsError: Bool = false @State var cgmCredentialsSuccess: Bool = false @State var validatedProvider: CGMProvider = .null + @State var showValidationAndErrorBox: Bool = false + + @FocusState var nsSecretFailedValidationFocus: Bool + @State var nsSecretFailedValidation: Bool = false internal var logger = Logger(subsystem: "tools.t1d.GlucoseBar", category: "provider") @@ -185,7 +189,7 @@ struct CGMSettings: View { Text("CGM Provider").font(.headline).frame(maxWidth: .infinity, alignment: .leading).padding(.top, 10) Picker("", selection: $s.cgmProvider) { ForEach(CGMProvider.allCases) { provider in - if provider != .null && provider != .librelinkup { + if provider != .null { Text(provider.presentable).tag(provider) } } @@ -240,28 +244,55 @@ struct CGMSettings: View { Text("These credentials are the ones from your primary Dexcom account. You must also have at least one follower in the Dexcom app.").font(.footnote).fixedSize(horizontal: false, vertical: true) } -// if s.cgmProvider == .librelinkup { -// HStack { -// Text("Email").frame(width: 130, alignment: .leading) -// Spacer() -// TextField("", text: $s.libreUsername).autocorrectionDisabled(true) -// .textFieldStyle(RoundedBorderTextFieldStyle()) -// } -// HStack { -// Text("Password").frame(width: 130, alignment: .leading) -// Spacer() -// SecureField("", text: $s.librePassword).textFieldStyle(RoundedBorderTextFieldStyle()) -// } -// HStack { + if s.cgmProvider == .librelinkup { + HStack { + Text("Email").frame(width: 130, alignment: .leading) + Spacer() + TextField("", text: $s.libreUsername).autocorrectionDisabled(true) + .textFieldStyle(RoundedBorderTextFieldStyle()) + } + HStack { + Text("Password").frame(width: 130, alignment: .leading) + Spacer() + SecureField("", text: $s.librePassword).textFieldStyle(RoundedBorderTextFieldStyle()) + } + HStack { + + Text("Following:").frame(width: 130, alignment: .topLeading) + Spacer() +// if cgmCredentialsSuccess && g.provider.connections.count > 0 { + ScrollView { + Picker("", selection: $s.libreConnectionID) { + + ForEach(g.provider.connections, id: \.patientID) { patient in + Text("\(patient.firstName) \(patient.lastName)").tag(patient.patientID) + } +// ForEach(CGMProvider.allCases) { provider in +//// if provider != .null { +// Text(provider.presentable).frame(width: 330, height: 15, alignment: .leading).tag(provider) +//// } +// } + }.pickerStyle(RadioGroupPickerStyle()).padding(.vertical, 10) + }.frame(width: 330, height: 75, alignment: .topLeading) + // Work around since normal selects close the settings window on us ðŸĪŠ + // Text("Following").frame(width: 130, alignment: .leading) // Spacer() -// if cgmCredentialsSuccess { + + +// Text("Hello mf'er \(g.provider.connections)") +// Picker("", selection: $s.libreConnectionID) { +// ForEach(g.provider.connections, id: \.patientID) { +// Text("\($0.firstName) \($0.lastName)").tag($0.patientID) +// } +// }.pickerStyle(SegmentedPickerStyle()) + // g.provider.getConnectionView(s: s) // } else { // Text("Please click \"Test Connection\" to display following options.").font(.footnote) // } -// } -// } + } + } Spacer() HStack { if s.cgmProvider != .simulator { @@ -290,6 +321,7 @@ struct CGMSettings: View { Button(action: { isValidating = true + showValidationAndErrorBox = true Task { validatedProvider = s.cgmProvider let providerTest = await s.testCGMProvider() @@ -298,26 +330,68 @@ struct CGMSettings: View { cgmCredentialsSuccess = providerTest isValidating = false s.validSettings = providerTest - - self.logger.debug("test complete. \(g.provider.connectionID)") } }) { Text("Test Connection") - }.disabled(isValidating) // TODO: State var for if it's currently testing + }.disabled(isValidating).sheet( + isPresented: $showValidationAndErrorBox, + onDismiss: nil + ) { + ScrollView { + VStack { + if isValidating { + ProgressView() + .controlSize(.small) + .progressViewStyle(CircularProgressViewStyle()) + Text("Validating...").padding(.top) + } + if !isValidating && g.provider.providerIssue != nil { + Image(systemName: "exclamationmark.triangle") + .resizable() + .scaledToFill() + .frame(width:50, height:50) + .foregroundColor(.orange).padding(.top) + Text("\(g.provider.providerIssue ?? "Unknown Issue").").padding(.top) + Button("Dismiss") { + showValidationAndErrorBox = false + }.padding(.top, 5) + } + if !isValidating && cgmCredentialsError && s.cgmProvider == validatedProvider && g.provider.providerIssue == nil { + Image(systemName: "exclamationmark.triangle") + .resizable() + .scaledToFill() + .frame(width:50, height:50) + .foregroundColor(.orange).padding(.top) + Text("Invalid credentials or service unreachable.").padding(.top) + if s.cgmProvider == .librelinkup { + Text("If you have had multiple authentication failures, this might just be a temporary issue. Please try again in a little while.") + .fixedSize(horizontal: false, vertical: true).font(.footnote).padding(.top) + } + Button("Dismiss") { + showValidationAndErrorBox = false + }.padding(.top, 5) + } + if !isValidating && cgmCredentialsSuccess && s.cgmProvider == validatedProvider { + Image(systemName: "checkmark.circle") + .resizable() + .scaledToFill() + .frame(width:50, height:50) + .foregroundColor(.green) + Text("Connection OK").padding(.top).foregroundColor(.green) + Button("Dismiss") { + showValidationAndErrorBox = false + }.padding(.top, 5) + } + }.padding() + }.frame(width: 300, height: 200) + } } - if g.provider.providerIssue != nil { - Text("Provider issue: \(g.provider.providerIssue ?? "Unknown")") + if !isValidating && cgmCredentialsSuccess && s.cgmProvider == validatedProvider { + Image(systemName: "checkmark.circle").foregroundColor(.green) } - if !isValidating && cgmCredentialsError && s.cgmProvider == validatedProvider { - Text("Invalid credentials or service unreachable").foregroundColor(.orange) + if (!isValidating && g.provider.providerIssue != nil) || (!isValidating && cgmCredentialsError && s.cgmProvider == validatedProvider && g.provider.providerIssue == nil) { Image(systemName: "exclamationmark.triangle").foregroundColor(.orange) } - if !isValidating && cgmCredentialsSuccess && s.cgmProvider == validatedProvider { - HStack { - Text("Connection OK").foregroundColor(.green) - Image(systemName: "checkmark.circle").foregroundColor(.green) - } - } if isValidating { ProgressView() .controlSize(.small)