fix: widgets use App Group shared UserDefaults instead of Keychain
- Widgets can't access app's Keychain (different sandbox) - App writes data to shared UserDefaults (group.com.daniil.pulsehealth) - Widgets read from shared UserDefaults — no API calls needed - WidgetDataService: updates widget data + reloads timelines - DashboardView: pushes habits/tasks data to widget after load - HealthView: pushes health data to widget after load - App Group capability added to both app and widget entitlements - Widgets update every 15 minutes from cached data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,49 +1,18 @@
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
enum KeychainService {
|
||||
static let service = "com.daniil.pulsehealth"
|
||||
// Widget reads data from shared UserDefaults (App Group), not Keychain
|
||||
enum WidgetData {
|
||||
static let suiteName = "group.com.daniil.pulsehealth"
|
||||
|
||||
static func save(key: String, value: String) {
|
||||
guard let data = value.data(using: .utf8) else { return }
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key
|
||||
]
|
||||
SecItemDelete(query as CFDictionary)
|
||||
var add = query
|
||||
add[kSecValueData as String] = data
|
||||
add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
||||
SecItemAdd(add as CFDictionary, nil)
|
||||
static var shared: UserDefaults? {
|
||||
UserDefaults(suiteName: suiteName)
|
||||
}
|
||||
|
||||
static func load(key: String) -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecReturnData as String: true,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne
|
||||
]
|
||||
var result: AnyObject?
|
||||
guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
|
||||
let data = result as? Data else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
static func delete(key: String) {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key
|
||||
]
|
||||
SecItemDelete(query as CFDictionary)
|
||||
}
|
||||
|
||||
// Keys
|
||||
static let tokenKey = "auth_token"
|
||||
static let refreshTokenKey = "auth_refresh_token"
|
||||
static let healthTokenKey = "health_jwt_token"
|
||||
static let healthApiKeyKey = "health_api_key"
|
||||
static var habitsCompleted: Int { shared?.integer(forKey: "w_habits_completed") ?? 0 }
|
||||
static var habitsTotal: Int { shared?.integer(forKey: "w_habits_total") ?? 0 }
|
||||
static var tasksCount: Int { shared?.integer(forKey: "w_tasks_count") ?? 0 }
|
||||
static var steps: Int { shared?.integer(forKey: "w_steps") ?? 0 }
|
||||
static var sleep: Double { shared?.double(forKey: "w_sleep") ?? 0 }
|
||||
static var heartRate: Int { shared?.integer(forKey: "w_heart_rate") ?? 0 }
|
||||
static var readinessScore: Int { shared?.integer(forKey: "w_readiness") ?? 0 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user