From 89e985fd396a36e827b9f74ed28ca53f36bf56c7 Mon Sep 17 00:00:00 2001 From: christophergyman Date: Sat, 10 Jan 2026 20:52:31 +0000 Subject: [PATCH] Add UI to configure Spotify Client ID in Settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../iris/Spotify/SpotifyAuthManager.swift | 21 ++++++++++--- IrisCompanion/iris/Views/SettingsView.swift | 30 +++++++++++++++++-- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/IrisCompanion/iris/Spotify/SpotifyAuthManager.swift b/IrisCompanion/iris/Spotify/SpotifyAuthManager.swift index 57666aa..2bd8d80 100644 --- a/IrisCompanion/iris/Spotify/SpotifyAuthManager.swift +++ b/IrisCompanion/iris/Spotify/SpotifyAuthManager.swift @@ -11,7 +11,6 @@ import Foundation import os enum SpotifyConfig { - static let clientId = "YOUR_SPOTIFY_CLIENT_ID" static let redirectUri = "iris-spotify-auth://callback" static let scopes = "user-read-playback-state user-read-currently-playing" 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 isAuthenticating: Bool = false @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 tokensKey = "spotify_tokens" + private let clientIdKey = "spotify_client_id" private var codeVerifier: String? = nil private var authSession: ASWebAuthenticationSession? = nil + var isConfigured: Bool { + !clientId.isEmpty + } + override init() { super.init() + clientId = UserDefaults.standard.string(forKey: clientIdKey) ?? "" 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? { loadStoredTokens()?.accessToken } @@ -50,7 +63,7 @@ final class SpotifyAuthManager: NSObject, ObservableObject { var components = URLComponents(string: SpotifyConfig.authUrl)! components.queryItems = [ - URLQueryItem(name: "client_id", value: SpotifyConfig.clientId), + URLQueryItem(name: "client_id", value: clientId), URLQueryItem(name: "response_type", value: "code"), URLQueryItem(name: "redirect_uri", value: SpotifyConfig.redirectUri), URLQueryItem(name: "scope", value: SpotifyConfig.scopes), @@ -145,7 +158,7 @@ final class SpotifyAuthManager: NSObject, ObservableObject { "grant_type": "authorization_code", "code": code, "redirect_uri": SpotifyConfig.redirectUri, - "client_id": SpotifyConfig.clientId, + "client_id": clientId, "code_verifier": verifier ] @@ -172,7 +185,7 @@ final class SpotifyAuthManager: NSObject, ObservableObject { let body = [ "grant_type": "refresh_token", "refresh_token": refreshToken, - "client_id": SpotifyConfig.clientId + "client_id": clientId ] do { diff --git a/IrisCompanion/iris/Views/SettingsView.swift b/IrisCompanion/iris/Views/SettingsView.swift index ea43ff9..f8c202b 100644 --- a/IrisCompanion/iris/Views/SettingsView.swift +++ b/IrisCompanion/iris/Views/SettingsView.swift @@ -12,6 +12,8 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject private var orchestrator: ContextOrchestrator @EnvironmentObject private var spotifyAuth: SpotifyAuthManager + @State private var isConfigExpanded: Bool = false + @State private var clientIdInput: String = "" var body: some View { NavigationStack { @@ -40,6 +42,26 @@ struct SettingsView: View { } 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 { HStack { Label("Connected", systemImage: "checkmark.circle.fill") @@ -52,7 +74,7 @@ struct SettingsView: View { orchestrator.musicSource = .appleMusic } } - } else { + } else if spotifyAuth.isConfigured { Button { spotifyAuth.startAuth() } label: { @@ -65,6 +87,10 @@ struct SettingsView: View { } } .disabled(spotifyAuth.isAuthenticating) + } else { + Text("Enter your Client ID above to connect") + .font(.callout) + .foregroundStyle(.secondary) } if let error = spotifyAuth.error { @@ -75,7 +101,7 @@ struct SettingsView: View { } header: { Text("Spotify") } footer: { - if !spotifyAuth.isConnected { + if !spotifyAuth.isConnected && spotifyAuth.isConfigured { Text("Connect your Spotify account to display current track on Glass.") } }