Skip to content

feat: account switcher#75

Merged
sozercan merged 3 commits intomainfrom
brand-accounts
Jan 16, 2026
Merged

feat: account switcher#75
sozercan merged 3 commits intomainfrom
brand-accounts

Conversation

@sozercan
Copy link
Owner

@sozercan sozercan commented Jan 16, 2026

Description

fixes #20

AI Prompt (Optional)

🤖 AI Prompt Used
<!-- Paste your prompt here, or write "N/A - Manual implementation" -->

AI Tool:

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to change)
  • 📚 Documentation update
  • 🎨 UI/UX improvement
  • ♻️ Refactoring (no functional changes)
  • 🧪 Test update
  • 🔧 Build/CI configuration

Related Issues

Changes Made

Testing

  • Unit tests pass (xcodebuild test -only-testing:KasetTests)
  • Manual testing performed
  • UI tested on macOS 26+

Checklist

  • My code follows the project's style guidelines
  • I have run swiftlint --strict && swiftformat .
  • I have added tests that prove my fix/feature works
  • New and existing unit tests pass locally
  • I have updated documentation if needed
  • I have checked for any performance implications
  • My changes generate no new warnings

Screenshots

Additional Notes

Signed-off-by: Sertac Ozercan <sozercan@gmail.com>
Copilot AI review requested due to automatic review settings January 16, 2026 06:11
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements an account switcher feature that allows users to switch between their primary Google account and associated brand accounts (YouTube channels) within the Kaset app. The implementation adds UI components for account selection, integrates with the YouTube Music API's account management endpoints, and ensures content refreshes when switching accounts.

Changes:

  • Added account management infrastructure (AccountService, UserAccount model, AccountsListParser)
  • Implemented UI components for account switching (SidebarProfileView, AccountSwitcherPopover, AccountRowView, ToastView)
  • Enhanced API client to support brand account context in requests via onBehalfOfUser parameter
  • Added comprehensive unit tests and UI tests for the new functionality
  • Updated API explorer tool to support brand account discovery and testing

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
Core/Services/Auth/AccountService.swift Service managing account state, switching, and persistence
Core/Models/UserAccount.swift Model representing user accounts (primary/brand)
Core/Models/AccountsListResponse.swift Response model for accounts list API
Core/Services/API/Parsers/AccountsListParser.swift Parser for accounts list API responses
Views/macOS/SidebarProfileView.swift Profile section at bottom of sidebar with account switcher trigger
Views/macOS/AccountSwitcherPopover.swift Popover UI for switching between accounts
Views/macOS/AccountRowView.swift Individual account row component
Views/macOS/ToastView.swift Toast notification for errors
Core/Services/API/YTMusicClient.swift Enhanced to support brand account requests
Views/macOS/MainWindow.swift Integrated account service and refresh logic
Tests/KasetTests/AccountServiceTests.swift Unit tests for AccountService
Tests/KasetTests/UserAccountTests.swift Unit tests for UserAccount model
Tests/KasetTests/AccountsListParserTests.swift Unit tests for parser
Tests/KasetUITests/AccountSwitcherUITests.swift UI tests for account switcher
Tools/api-explorer.swift Enhanced with brand account support
docs/api-discovery.md Documentation for brand account API

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@available(macOS 26.0, *)
#Preview("Account Without Handle") {
let account = UserAccount(
id: "nohanlde",
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'nohanlde' to 'nohandle'.

Suggested change
id: "nohanlde",
id: "nohandle",

Copilot uses AI. Check for mistakes.
Comment on lines +163 to +173
.onChange(of: self.accountService.currentAccount?.id) { _, newAccountId in
// Refresh all content when user switches accounts
guard newAccountId != nil else { return }
DiagnosticsLogger.auth.info("Account switched, refreshing content...")
// Clear API cache to ensure fresh data for new account
Task { @MainActor in
APICache.shared.invalidateAll()
URLCache.shared.removeAllCachedResponses()
await self.refreshAllContent()
}
}
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onChange handler triggers on initial account load after login, not just switches. This causes unnecessary cache invalidation and content refresh when the app starts. Consider tracking previous account ID to only refresh on actual switches, or check if this is the first account load.

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +41
private(set) var accounts: [UserAccount] = []

/// Currently selected/active account.
private(set) var currentAccount: UserAccount?
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AccountService test helpers expose internal state via setAccountsForTesting and setCurrentAccountForTesting methods, but tests don't cover the actual fetchAccounts() method that populates this state from the API. Consider adding tests that verify the full flow including API response parsing and account selection logic.

Copilot uses AI. Check for mistakes.
/// Provider for the current brand account ID.
/// Set this after initialization to enable brand account API requests.
/// Returns nil for primary account, brand ID string for brand accounts.
var brandIdProvider: (() -> String?)?
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The brandIdProvider closure property is not marked as @Sendable despite being used in an async context within the buildContext() method. While the current implementation using a weak reference in KasetApp.swift is safe, consider marking this as @Sendable to enforce thread-safety requirements and prevent future issues if the implementation changes.

Suggested change
var brandIdProvider: (() -> String?)?
var brandIdProvider: (@Sendable () -> String?)?

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +111
var hashData = jsonData
if !brandId.isEmpty {
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appending brandId as raw UTF-8 bytes to the hash data could cause collisions if endpoint/body combinations produce JSON that ends with bytes matching a brandId prefix. Consider using a separator or structured format (e.g., JSON with explicit brandId field) to ensure unambiguous cache key generation.

Suggested change
var hashData = jsonData
if !brandId.isEmpty {
var hashData = Data()
hashData.append(jsonData)
if !brandId.isEmpty {
// Use a NUL byte as a separator to avoid ambiguity between JSON and brandId bytes
hashData.append(0)

Copilot uses AI. Check for mistakes.
Signed-off-by: Sertac Ozercan <sozercan@gmail.com>
Signed-off-by: Sertac Ozercan <sozercan@gmail.com>
@sozercan sozercan merged commit 1cb3df3 into main Jan 16, 2026
6 checks passed
@sozercan sozercan deleted the brand-accounts branch January 16, 2026 06:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Problem with Multiple Brand account on Youtube Music

2 participants