diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/trakt/TraktAuthStorage.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/trakt/TraktAuthStorage.android.kt index cc8e84365..d694069b1 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/trakt/TraktAuthStorage.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/trakt/TraktAuthStorage.android.kt @@ -14,13 +14,13 @@ internal actual object TraktAuthStorage { preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE) } - actual fun loadPayload(): String? = - preferences?.getString(ProfileScopedKey.of(payloadKey), null) + actual fun loadPayload(profileId: Int): String? = + preferences?.getString(ProfileScopedKey.of(payloadKey, profileId), null) - actual fun savePayload(payload: String) { + actual fun savePayload(profileId: Int, payload: String) { preferences ?.edit() - ?.putString(ProfileScopedKey.of(payloadKey), payload) + ?.putString(ProfileScopedKey.of(payloadKey, profileId), payload) ?.apply() } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/SyncManager.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/SyncManager.kt index f81af882a..703b88c48 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/SyncManager.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/SyncManager.kt @@ -427,7 +427,7 @@ object SyncManager { continue } - TraktAuthRepository.ensureLoaded() + TraktAuthRepository.ensureLoaded(profileId) TraktSettingsRepository.ensureLoaded() val traktAuthenticated = TraktAuthRepository.isAuthenticated.value diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryRepository.kt index 06c102f8c..cb9fbebe3 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryRepository.kt @@ -151,7 +151,7 @@ object LibraryRepository { TraktSettingsRepository.onProfileChanged() if (!loadFromDisk(profileId)) return - TraktAuthRepository.onProfileChanged() + TraktAuthRepository.onProfileChanged(profileId) TraktLibraryRepository.onProfileChanged() if (TraktAuthRepository.isAuthenticated.value) { TraktLibraryRepository.preloadListTabsAsync() diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt index fd76b3fef..7944af4c7 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt @@ -154,7 +154,7 @@ object ProfileRepository { persist() WatchedRepository.onProfileChanged(profileIndex) TraktSettingsRepository.onProfileChanged() - TraktAuthRepository.onProfileChanged() + TraktAuthRepository.onProfileChanged(profileIndex) LibraryRepository.onProfileChanged(profileIndex) WatchProgressRepository.onProfileChanged(profileIndex) AddonRepository.onProfileChanged(profileIndex) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthRepository.kt index 20fa9eb50..a9e3308eb 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthRepository.kt @@ -48,33 +48,37 @@ object TraktAuthRepository { val isAuthenticated: StateFlow = _isAuthenticated.asStateFlow() private var hasLoaded = false + private var currentProfileId: Int = 1 + private var profileGeneration: Long = 0L private var authState = TraktAuthState() - fun ensureLoaded() { - if (hasLoaded) return - loadFromDisk() + fun ensureLoaded(profileId: Int = ProfileRepository.activeProfileId) { + if (hasLoaded && currentProfileId == profileId) return + loadFromDisk(profileId) } - fun onProfileChanged() { - loadFromDisk() + fun onProfileChanged(profileId: Int) { + loadFromDisk(profileId) } fun clearLocalState() { hasLoaded = false + currentProfileId = 1 + profileGeneration += 1L authState = TraktAuthState() publish() } - fun snapshot(): TraktAuthUiState { - ensureLoaded() + fun snapshot(profileId: Int = ProfileRepository.activeProfileId): TraktAuthUiState { + ensureLoaded(profileId) return _uiState.value } fun hasRequiredCredentials(): Boolean = TraktConfig.CLIENT_ID.isNotBlank() && TraktConfig.CLIENT_SECRET.isNotBlank() - fun onConnectRequested(): String? { - ensureLoaded() + fun onConnectRequested(profileId: Int = ProfileRepository.activeProfileId): String? { + ensureLoaded(profileId) if (!hasRequiredCredentials()) { publish(errorMessage = localizedString(Res.string.trakt_missing_credentials)) return null @@ -85,7 +89,7 @@ object TraktAuthRepository { pendingAuthorizationState = oauthState, pendingAuthorizationStartedAtMillis = TraktPlatformClock.nowEpochMs(), ) - persist() + persist(profileId) publish( statusMessage = localizedString(Res.string.trakt_complete_sign_in_browser), errorMessage = null, @@ -94,21 +98,21 @@ object TraktAuthRepository { return buildAuthorizationUrl(oauthState) } - fun pendingAuthorizationUrl(): String? { - ensureLoaded() + fun pendingAuthorizationUrl(profileId: Int = ProfileRepository.activeProfileId): String? { + ensureLoaded(profileId) val oauthState = authState.pendingAuthorizationState ?: return null return buildAuthorizationUrl(oauthState) } - fun onCancelAuthorization() { - ensureLoaded() + fun onCancelAuthorization(profileId: Int = ProfileRepository.activeProfileId) { + ensureLoaded(profileId) clearPendingAuthorization() - persist() + persist(profileId) publish(statusMessage = null, errorMessage = null) } - fun onCancelDeviceFlow() { - onCancelAuthorization() + fun onCancelDeviceFlow(profileId: Int = ProfileRepository.activeProfileId) { + onCancelAuthorization(profileId) } fun onAuthLaunchFailed(reason: String) { @@ -116,7 +120,8 @@ object TraktAuthRepository { } fun onAuthCallbackReceived(callbackUrl: String) { - ensureLoaded() + val profileId = ProfileRepository.activeProfileId + ensureLoaded(profileId) if (!callbackUrl.startsWith("${TraktConfig.REDIRECT_URI}?", ignoreCase = true) && !callbackUrl.equals(TraktConfig.REDIRECT_URI, ignoreCase = true) ) { @@ -124,15 +129,15 @@ object TraktAuthRepository { } scope.launch { - completeAuthorizationFromCallback(callbackUrl) + completeAuthorizationFromCallback(callbackUrl, profileId) } } - suspend fun authorizedHeaders(): Map? { - ensureLoaded() + suspend fun authorizedHeaders(profileId: Int = currentProfileId): Map? { + ensureLoaded(profileId) if (!authState.isAuthenticated) return null - val hasValidToken = refreshTokenIfNeeded(force = false) + val hasValidToken = refreshTokenIfNeeded(force = false, profileId = profileId) if (!hasValidToken) return null val accessToken = authState.accessToken?.trim().orEmpty() @@ -145,9 +150,9 @@ object TraktAuthRepository { ) } - suspend fun refreshUserSettings(): String? { - ensureLoaded() - val headers = authorizedHeaders() ?: return null + suspend fun refreshUserSettings(profileId: Int = currentProfileId): String? { + ensureLoaded(profileId) + val headers = authorizedHeaders(profileId) ?: return null val response = runCatching { httpGetTextWithHeaders( url = "$BASE_URL/users/settings", @@ -166,19 +171,19 @@ object TraktAuthRepository { username = parsed.user?.username, userSlug = parsed.user?.ids?.slug, ) - persist() + persist(profileId) publish() return authState.username } - fun onDisconnectRequested() { - ensureLoaded() + fun onDisconnectRequested(profileId: Int = currentProfileId) { + ensureLoaded(profileId) scope.launch { - disconnect() + disconnect(profileId) } } - private suspend fun completeAuthorizationFromCallback(callbackUrl: String) { + private suspend fun completeAuthorizationFromCallback(callbackUrl: String, profileId: Int = currentProfileId) { publish(isLoading = true, errorMessage = null) val parsedUrl = runCatching { Url(callbackUrl) } @@ -189,7 +194,7 @@ object TraktAuthRepository { if (parsedUrl == null) { clearPendingAuthorization() - persist() + persist(profileId) publish( isLoading = false, errorMessage = localizedString(Res.string.trakt_invalid_callback), @@ -202,7 +207,7 @@ object TraktAuthRepository { val errorDescription = parsedUrl.parameters["error_description"] ?: localizedString(Res.string.trakt_authorization_denied) clearPendingAuthorization() - persist() + persist(profileId) publish( isLoading = false, errorMessage = errorDescription, @@ -213,7 +218,7 @@ object TraktAuthRepository { val code = parsedUrl.parameters["code"].orEmpty().trim() if (code.isBlank()) { clearPendingAuthorization() - persist() + persist(profileId) publish( isLoading = false, errorMessage = localizedString(Res.string.trakt_missing_auth_code), @@ -225,7 +230,7 @@ object TraktAuthRepository { val callbackState = parsedUrl.parameters["state"].orEmpty().trim() if (!expectedState.isNullOrBlank() && callbackState != expectedState) { clearPendingAuthorization() - persist() + persist(profileId) publish( isLoading = false, errorMessage = localizedString(Res.string.trakt_invalid_callback_state), @@ -233,10 +238,10 @@ object TraktAuthRepository { return } - exchangeAuthorizationCode(code) + exchangeAuthorizationCode(code, profileId) } - private suspend fun exchangeAuthorizationCode(code: String) { + private suspend fun exchangeAuthorizationCode(code: String, profileId: Int = currentProfileId) { val body = json.encodeToString( TraktAuthorizationCodeRequest( code = code, @@ -259,7 +264,7 @@ object TraktAuthRepository { if (response == null) { clearPendingAuthorization() - persist() + persist(profileId) publish(isLoading = false, errorMessage = localizedString(Res.string.trakt_sign_in_complete_failed)) return } @@ -270,7 +275,7 @@ object TraktAuthRepository { if (parsed == null) { clearPendingAuthorization() - persist() + persist(profileId) publish(isLoading = false, errorMessage = localizedString(Res.string.trakt_invalid_token_response)) return } @@ -284,8 +289,8 @@ object TraktAuthRepository { pendingAuthorizationState = null, pendingAuthorizationStartedAtMillis = null, ) - persist() - refreshUserSettings() + persist(profileId) + refreshUserSettings(profileId) publish( isLoading = false, statusMessage = localizedString(Res.string.trakt_connected_status), @@ -293,7 +298,7 @@ object TraktAuthRepository { ) } - private suspend fun disconnect() { + private suspend fun disconnect(profileId: Int = currentProfileId) { publish(isLoading = true, errorMessage = null) val token = authState.accessToken?.takeIf { it.isNotBlank() } @@ -317,9 +322,9 @@ object TraktAuthRepository { } } - TraktCredentialSync.deleteRemote() + TraktCredentialSync.deleteRemote(profileId) authState = TraktAuthState() - persist() + persist(profileId) publish( isLoading = false, statusMessage = localizedString(Res.string.trakt_disconnected_status), @@ -327,9 +332,8 @@ object TraktAuthRepository { ) } - private suspend fun refreshTokenIfNeeded(force: Boolean): Boolean = refreshMutex.withLock { + private suspend fun refreshTokenIfNeeded(force: Boolean, profileId: Int = currentProfileId): Boolean = refreshMutex.withLock { if (!hasRequiredCredentials()) return@withLock false - val profileId = ProfileRepository.activeProfileId val refreshToken = authState.refreshToken?.takeIf { it.isNotBlank() } ?: return@withLock false @@ -391,14 +395,14 @@ object TraktAuthRepository { createdAt = parsed.createdAt, expiresIn = parsed.expiresIn, ) - persist() + persist(profileId) publish() true } private suspend fun invalidateCredentials(profileId: Int) { authState = TraktAuthState() - persist() + persist(profileId) publish( isLoading = false, statusMessage = null, @@ -407,9 +411,11 @@ object TraktAuthRepository { TraktCredentialSync.deleteRemote(profileId) } - private fun loadFromDisk() { + private fun loadFromDisk(profileId: Int) { + currentProfileId = profileId + profileGeneration += 1L hasLoaded = true - val payload = TraktAuthStorage.loadPayload().orEmpty().trim() + val payload = TraktAuthStorage.loadPayload(profileId).orEmpty().trim() authState = if (payload.isBlank()) { TraktAuthState() } else { @@ -460,8 +466,8 @@ object TraktAuthRepository { ) } - private fun persist() { - TraktAuthStorage.savePayload(json.encodeToString(authState)) + private fun persist(profileId: Int = currentProfileId) { + TraktAuthStorage.savePayload(profileId, json.encodeToString(authState)) } private fun buildAuthorizationUrl(state: String): String { @@ -486,6 +492,7 @@ object TraktAuthRepository { return nowSeconds >= (expiresAtSeconds - 60) } + private fun localizedString(resource: StringResource): String = runBlocking { getString(resource) } } internal enum class TraktTokenRefreshResponseAction { @@ -549,4 +556,3 @@ private data class TraktUserDto( private data class TraktUserIdsDto( val slug: String? = null, ) - private fun localizedString(resource: StringResource): String = runBlocking { getString(resource) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthStorage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthStorage.kt index 3527be204..12cb2420f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthStorage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthStorage.kt @@ -1,6 +1,6 @@ package com.nuvio.app.features.trakt internal expect object TraktAuthStorage { - fun loadPayload(): String? - fun savePayload(payload: String) + fun loadPayload(profileId: Int): String? + fun savePayload(profileId: Int, payload: String) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watched/WatchedRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watched/WatchedRepository.kt index 27ee20761..cb2cae032 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watched/WatchedRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watched/WatchedRepository.kt @@ -270,7 +270,7 @@ object WatchedRepository { ) suspend fun pullFromServer(profileId: Int) { - TraktAuthRepository.ensureLoaded() + TraktAuthRepository.ensureLoaded(profileId) TraktSettingsRepository.ensureLoaded() refreshForSource( profileId = profileId, @@ -283,7 +283,7 @@ object WatchedRepository { } suspend fun forceSnapshotRefreshFromServer(profileId: Int) { - TraktAuthRepository.ensureLoaded() + TraktAuthRepository.ensureLoaded(profileId) TraktSettingsRepository.ensureLoaded() refreshForSource( profileId = profileId, @@ -300,7 +300,7 @@ object WatchedRepository { source: WatchProgressSource, forceSnapshot: Boolean = true, ): Boolean { - TraktAuthRepository.ensureLoaded() + TraktAuthRepository.ensureLoaded(profileId) TraktSettingsRepository.ensureLoaded() if (ProfileRepository.activeProfileId != profileId) { log.d { "Skipping watched refresh for inactive profile $profileId" } diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/trakt/TraktAuthStorage.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/trakt/TraktAuthStorage.ios.kt index 3ccbaff4e..916fedb0f 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/trakt/TraktAuthStorage.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/trakt/TraktAuthStorage.ios.kt @@ -6,10 +6,10 @@ import platform.Foundation.NSUserDefaults internal actual object TraktAuthStorage { private const val payloadKey = "trakt_auth_payload" - actual fun loadPayload(): String? = - NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(payloadKey)) + actual fun loadPayload(profileId: Int): String? = + NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(payloadKey, profileId)) - actual fun savePayload(payload: String) { - NSUserDefaults.standardUserDefaults.setObject(payload, forKey = ProfileScopedKey.of(payloadKey)) + actual fun savePayload(profileId: Int, payload: String) { + NSUserDefaults.standardUserDefaults.setObject(payload, forKey = ProfileScopedKey.of(payloadKey, profileId)) } }