- Add OAuth 2.0 PKCE authentication for Spotify Web API - Create SpotifyNowPlayingMonitor for polling current track - Add Settings tab with music source toggle (Apple Music/Spotify) - Store tokens securely in Keychain - Display current track on Glass as NOW_PLAYING card 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
62 lines
1.9 KiB
Swift
62 lines
1.9 KiB
Swift
//
|
|
// KeychainHelper.swift
|
|
// iris
|
|
//
|
|
// Secure token storage using iOS Keychain.
|
|
//
|
|
|
|
import Foundation
|
|
import Security
|
|
|
|
enum KeychainHelper {
|
|
private static let service = "sh.nym.iris.spotify"
|
|
|
|
static func save(key: String, data: Data) throws {
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: key
|
|
]
|
|
SecItemDelete(query as CFDictionary)
|
|
|
|
let attributes: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: key,
|
|
kSecValueData as String: data,
|
|
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
|
|
]
|
|
let status = SecItemAdd(attributes as CFDictionary, nil)
|
|
guard status == errSecSuccess else {
|
|
throw KeychainError.saveFailed(status)
|
|
}
|
|
}
|
|
|
|
static func load(key: String) -> Data? {
|
|
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?
|
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
guard status == errSecSuccess else { return nil }
|
|
return result as? Data
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
enum KeychainError: Error {
|
|
case saveFailed(OSStatus)
|
|
}
|
|
}
|