feat: widget fetches calories from API independently + shared auth
Widget: - Fetches /api/fitness/entries/totals and /api/fitness/goals/for-date directly from gateway using shared session cookie - Falls back to cached data in App Group UserDefaults if network fails - Refreshes every 15 minutes via WidgetKit timeline - Each phone shows the logged-in user's own data Auth sharing: - AuthManager.syncCookieToWidget() copies the session cookie to App Group UserDefaults on login and auth check - Widget reads cookie and makes authenticated API calls - Logout clears widget auth + cached data Data in App Group (group.com.quadjourney.platform): - widget_sessionCookie: auth token for API calls - widget_totalCalories: cached fallback - widget_calorieGoal: cached fallback - widget_lastUpdate: cache timestamp HomeViewModel also writes cache on each loadTodayData() as fallback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import WidgetKit
|
||||
|
||||
@Observable
|
||||
final class AuthManager {
|
||||
@@ -10,6 +11,12 @@ final class AuthManager {
|
||||
private let api = APIClient.shared
|
||||
private let loggedInKey = "isLoggedIn"
|
||||
|
||||
// App Group for sharing auth with widget
|
||||
private static let appGroup = "group.com.quadjourney.platform"
|
||||
static var sharedDefaults: UserDefaults {
|
||||
UserDefaults(suiteName: appGroup) ?? .standard
|
||||
}
|
||||
|
||||
init() {
|
||||
isLoggedIn = UserDefaults.standard.bool(forKey: loggedInKey)
|
||||
}
|
||||
@@ -46,9 +53,11 @@ final class AuthManager {
|
||||
if response.authenticated, let user = response.user {
|
||||
currentUser = user
|
||||
isLoggedIn = true
|
||||
syncCookieToWidget()
|
||||
} else {
|
||||
isLoggedIn = false
|
||||
UserDefaults.standard.set(false, forKey: loggedInKey)
|
||||
clearWidgetAuth()
|
||||
}
|
||||
} catch {
|
||||
isLoggedIn = false
|
||||
@@ -68,6 +77,8 @@ final class AuthManager {
|
||||
currentUser = response.user
|
||||
isLoggedIn = true
|
||||
UserDefaults.standard.set(true, forKey: loggedInKey)
|
||||
syncCookieToWidget()
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
} catch let apiError as APIError {
|
||||
error = apiError.localizedDescription
|
||||
@@ -83,5 +94,26 @@ final class AuthManager {
|
||||
currentUser = nil
|
||||
isLoggedIn = false
|
||||
UserDefaults.standard.set(false, forKey: loggedInKey)
|
||||
clearWidgetAuth()
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
|
||||
// MARK: - Widget Auth Sync
|
||||
|
||||
/// Copy the session cookie to App Group UserDefaults so the widget can authenticate.
|
||||
private func syncCookieToWidget() {
|
||||
guard let url = URL(string: "https://dash.quadjourney.com"),
|
||||
let cookies = HTTPCookieStorage.shared.cookies(for: url) else { return }
|
||||
|
||||
for cookie in cookies where cookie.name == "session" {
|
||||
Self.sharedDefaults.set(cookie.value, forKey: "widget_sessionCookie")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func clearWidgetAuth() {
|
||||
Self.sharedDefaults.removeObject(forKey: "widget_sessionCookie")
|
||||
Self.sharedDefaults.removeObject(forKey: "widget_totalCalories")
|
||||
Self.sharedDefaults.removeObject(forKey: "widget_calorieGoal")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,9 +34,11 @@ final class HomeViewModel {
|
||||
calorieGoal = repo.goal?.calories ?? 2000
|
||||
isLoading = false
|
||||
|
||||
// Write to UserDefaults for widget
|
||||
UserDefaults.standard.set(totalCalories, forKey: "widget_totalCalories")
|
||||
UserDefaults.standard.set(calorieGoal, forKey: "widget_calorieGoal")
|
||||
// Write to App Group UserDefaults for widget (fallback cache)
|
||||
let shared = AuthManager.sharedDefaults
|
||||
shared.set(totalCalories, forKey: "widget_totalCalories")
|
||||
shared.set(calorieGoal, forKey: "widget_calorieGoal")
|
||||
shared.set(Date(), forKey: "widget_lastUpdate")
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user