Add UI to configure Spotify Client ID in Settings

- Add expandable Configuration section in Spotify settings
- Store Client ID in UserDefaults
- Show Connect button only when Client ID is configured
- Add helper text pointing to Spotify Developer Dashboard

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-10 20:52:31 +00:00
parent 11ee893367
commit 89e985fd39
2 changed files with 45 additions and 6 deletions

View File

@@ -11,7 +11,6 @@ import Foundation
import os import os
enum SpotifyConfig { enum SpotifyConfig {
static let clientId = "YOUR_SPOTIFY_CLIENT_ID"
static let redirectUri = "iris-spotify-auth://callback" static let redirectUri = "iris-spotify-auth://callback"
static let scopes = "user-read-playback-state user-read-currently-playing" static let scopes = "user-read-playback-state user-read-currently-playing"
static let authUrl = "https://accounts.spotify.com/authorize" static let authUrl = "https://accounts.spotify.com/authorize"
@@ -24,17 +23,31 @@ final class SpotifyAuthManager: NSObject, ObservableObject {
@Published private(set) var isConnected: Bool = false @Published private(set) var isConnected: Bool = false
@Published private(set) var isAuthenticating: Bool = false @Published private(set) var isAuthenticating: Bool = false
@Published private(set) var error: String? = nil @Published private(set) var error: String? = nil
@Published private(set) var clientId: String = ""
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "iris", category: "SpotifyAuth") private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "iris", category: "SpotifyAuth")
private let tokensKey = "spotify_tokens" private let tokensKey = "spotify_tokens"
private let clientIdKey = "spotify_client_id"
private var codeVerifier: String? = nil private var codeVerifier: String? = nil
private var authSession: ASWebAuthenticationSession? = nil private var authSession: ASWebAuthenticationSession? = nil
var isConfigured: Bool {
!clientId.isEmpty
}
override init() { override init() {
super.init() super.init()
clientId = UserDefaults.standard.string(forKey: clientIdKey) ?? ""
loadTokens() loadTokens()
} }
func setClientId(_ id: String) {
let trimmed = id.trimmingCharacters(in: .whitespacesAndNewlines)
clientId = trimmed
UserDefaults.standard.set(trimmed, forKey: clientIdKey)
logger.info("Spotify Client ID updated")
}
var accessToken: String? { var accessToken: String? {
loadStoredTokens()?.accessToken loadStoredTokens()?.accessToken
} }
@@ -50,7 +63,7 @@ final class SpotifyAuthManager: NSObject, ObservableObject {
var components = URLComponents(string: SpotifyConfig.authUrl)! var components = URLComponents(string: SpotifyConfig.authUrl)!
components.queryItems = [ components.queryItems = [
URLQueryItem(name: "client_id", value: SpotifyConfig.clientId), URLQueryItem(name: "client_id", value: clientId),
URLQueryItem(name: "response_type", value: "code"), URLQueryItem(name: "response_type", value: "code"),
URLQueryItem(name: "redirect_uri", value: SpotifyConfig.redirectUri), URLQueryItem(name: "redirect_uri", value: SpotifyConfig.redirectUri),
URLQueryItem(name: "scope", value: SpotifyConfig.scopes), URLQueryItem(name: "scope", value: SpotifyConfig.scopes),
@@ -145,7 +158,7 @@ final class SpotifyAuthManager: NSObject, ObservableObject {
"grant_type": "authorization_code", "grant_type": "authorization_code",
"code": code, "code": code,
"redirect_uri": SpotifyConfig.redirectUri, "redirect_uri": SpotifyConfig.redirectUri,
"client_id": SpotifyConfig.clientId, "client_id": clientId,
"code_verifier": verifier "code_verifier": verifier
] ]
@@ -172,7 +185,7 @@ final class SpotifyAuthManager: NSObject, ObservableObject {
let body = [ let body = [
"grant_type": "refresh_token", "grant_type": "refresh_token",
"refresh_token": refreshToken, "refresh_token": refreshToken,
"client_id": SpotifyConfig.clientId "client_id": clientId
] ]
do { do {

View File

@@ -12,6 +12,8 @@ import SwiftUI
struct SettingsView: View { struct SettingsView: View {
@EnvironmentObject private var orchestrator: ContextOrchestrator @EnvironmentObject private var orchestrator: ContextOrchestrator
@EnvironmentObject private var spotifyAuth: SpotifyAuthManager @EnvironmentObject private var spotifyAuth: SpotifyAuthManager
@State private var isConfigExpanded: Bool = false
@State private var clientIdInput: String = ""
var body: some View { var body: some View {
NavigationStack { NavigationStack {
@@ -40,6 +42,26 @@ struct SettingsView: View {
} }
Section { Section {
DisclosureGroup("Configuration", isExpanded: $isConfigExpanded) {
TextField("Client ID", text: $clientIdInput)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.font(.system(.body, design: .monospaced))
Text("Get your Client ID from developer.spotify.com/dashboard")
.font(.caption)
.foregroundStyle(.secondary)
Button("Save") {
spotifyAuth.setClientId(clientIdInput)
isConfigExpanded = false
}
.disabled(clientIdInput.trimmingCharacters(in: .whitespaces).isEmpty)
}
.onAppear {
clientIdInput = spotifyAuth.clientId
}
if spotifyAuth.isConnected { if spotifyAuth.isConnected {
HStack { HStack {
Label("Connected", systemImage: "checkmark.circle.fill") Label("Connected", systemImage: "checkmark.circle.fill")
@@ -52,7 +74,7 @@ struct SettingsView: View {
orchestrator.musicSource = .appleMusic orchestrator.musicSource = .appleMusic
} }
} }
} else { } else if spotifyAuth.isConfigured {
Button { Button {
spotifyAuth.startAuth() spotifyAuth.startAuth()
} label: { } label: {
@@ -65,6 +87,10 @@ struct SettingsView: View {
} }
} }
.disabled(spotifyAuth.isAuthenticating) .disabled(spotifyAuth.isAuthenticating)
} else {
Text("Enter your Client ID above to connect")
.font(.callout)
.foregroundStyle(.secondary)
} }
if let error = spotifyAuth.error { if let error = spotifyAuth.error {
@@ -75,7 +101,7 @@ struct SettingsView: View {
} header: { } header: {
Text("Spotify") Text("Spotify")
} footer: { } footer: {
if !spotifyAuth.isConnected { if !spotifyAuth.isConnected && spotifyAuth.isConfigured {
Text("Connect your Spotify account to display current track on Glass.") Text("Connect your Spotify account to display current track on Glass.")
} }
} }