diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/TokenRefreshHandler.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/TokenRefreshHandler.kt index b327525..76f3778 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/TokenRefreshHandler.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/TokenRefreshHandler.kt @@ -3,18 +3,23 @@ package dev.datlag.aniflow.other import dev.datlag.aniflow.model.saveFirstOrNull import dev.datlag.aniflow.settings.DataStoreUserSettings import dev.datlag.aniflow.settings.Settings +import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import org.publicvalue.multiplatform.oidc.OpenIdConnectClient import org.publicvalue.multiplatform.oidc.tokenstore.OauthTokens import org.publicvalue.multiplatform.oidc.types.remote.AccessTokenResponse +import kotlin.time.Duration.Companion.minutes class TokenRefreshHandler( private val storeUserSettings: Settings.PlatformUserSettings ) { private val mutex = Mutex() + private var lastRefresh: Int = 0 suspend fun getAccessToken(): String? { return storeUserSettings.aniList.saveFirstOrNull()?.accessToken @@ -26,7 +31,8 @@ class TokenRefreshHandler( suspend fun refreshAndSaveToken(refreshCall: suspend (String) -> AccessTokenResponse, oldAccessToken: String): OauthTokens { mutex.withLock { - val currentTokens = storeUserSettings.aniList.saveFirstOrNull()?.let { + val storeData = storeUserSettings.aniList.saveFirstOrNull() + val currentTokens = storeData?.let { OauthTokens( accessToken = it.accessToken ?: return@let null, refreshToken = it.refreshToken, @@ -34,16 +40,23 @@ class TokenRefreshHandler( ) } - return if (currentTokens != null && currentTokens.accessToken != oldAccessToken) { + val nowMinus10Minutes = Clock.System.now().minus(10.minutes).epochSeconds + val requiresRefresh = lastRefresh <= nowMinus10Minutes || nowMinus10Minutes > (storeData?.expires ?: 0) + + return if (currentTokens != null && currentTokens.accessToken != oldAccessToken && !requiresRefresh) { currentTokens } else { - val refreshToken = storeUserSettings.aniListRefreshToken.firstOrNull() + val refreshToken = storeUserSettings.aniListRefreshToken.saveFirstOrNull() val newTokens = refreshCall(refreshToken ?: "") storeUserSettings.setAniListTokens( access = newTokens.access_token, refresh = newTokens.refresh_token, - id = newTokens.id_token + id = newTokens.id_token, + expires = (newTokens.expires_in ?: newTokens.refresh_token_expires_in)?.let { + Clock.System.now().epochSeconds + it + }?.toInt() ) + lastRefresh = Clock.System.now().epochSeconds.toInt() OauthTokens( accessToken = newTokens.access_token, diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeComponent.kt index 960fafe..dd345af 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeComponent.kt @@ -11,6 +11,7 @@ import dev.datlag.aniflow.trace.TraceStateMachine import dev.datlag.aniflow.ui.navigation.Component import dev.datlag.aniflow.ui.navigation.ContentHolderComponent import io.ktor.utils.io.* +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow interface HomeComponent : ContentHolderComponent { @@ -18,7 +19,7 @@ interface HomeComponent : ContentHolderComponent { val trendingState: StateFlow val popularSeasonState: StateFlow val popularNextSeasonState: StateFlow - val traceState: StateFlow + val traceState: Flow fun details(medium: Medium) fun trace(channel: ByteArray) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreen.kt index 0f671e3..282f5c5 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreen.kt @@ -52,7 +52,7 @@ private fun MainView(component: HomeComponent, modifier: Modifier = Modifier) { val imagePicker = rememberImagePickerState { it?.let(component::trace) } - val traceState by component.traceState.collectAsStateWithLifecycle() + val traceState by component.traceState.collectAsStateWithLifecycle(TraceStateMachine.State.Waiting) LaunchedEffect(listState, traceState) { FABConfig.state.value = FABConfig.Scan( diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreenComponent.kt index 2d8509a..e064d83 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreenComponent.kt @@ -16,10 +16,7 @@ import dev.datlag.aniflow.ui.navigation.Component import dev.datlag.aniflow.ui.navigation.screen.medium.MediumScreenComponent import dev.datlag.tooling.compose.ioDispatcher import dev.datlag.tooling.decompose.ioScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.* import org.kodein.di.DI import org.kodein.di.instance @@ -66,12 +63,8 @@ class HomeScreenComponent( ) private val traceStateMachine by di.instance() - override val traceState: StateFlow = traceStateMachine.state.flowOn( + override val traceState: Flow = traceStateMachine.state.flowOn( context = ioDispatcher() - ).stateIn( - scope = ioScope(), - started = SharingStarted.WhileSubscribed(), - initialValue = TraceStateMachine.State.Waiting ) @Composable diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt index 043eafb..771d6fe 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt @@ -331,7 +331,10 @@ class MediumScreenComponent( userSettings.setAniListTokens( access = it.access_token, refresh = it.refresh_token, - id = it.id_token + id = it.id_token, + expires = (it.expires_in ?: it.refresh_token_expires_in)?.let { time -> + Clock.System.now().epochSeconds + time + }?.toInt() ) } diff --git a/settings/build.gradle.kts b/settings/build.gradle.kts index 08eb418..1c3b872 100644 --- a/settings/build.gradle.kts +++ b/settings/build.gradle.kts @@ -22,6 +22,7 @@ kotlin { implementation(libs.serialization.protobuf) implementation(libs.tooling) + implementation(libs.datetime) } } } diff --git a/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/DataStoreUserSettings.kt b/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/DataStoreUserSettings.kt index c29dec7..8e63a01 100644 --- a/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/DataStoreUserSettings.kt +++ b/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/DataStoreUserSettings.kt @@ -3,13 +3,16 @@ package dev.datlag.aniflow.settings import androidx.datastore.core.DataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.datetime.Clock class DataStoreUserSettings( private val dataStore: DataStore ) : Settings.PlatformUserSettings { override val aniList: Flow = dataStore.data.map { it.aniList } override val aniListRefreshToken: Flow = aniList.map { it.refreshToken } - override val isAniListLoggedIn: Flow = aniList.map { it.accessToken != null } + override val isAniListLoggedIn: Flow = aniList.map { + it.accessToken != null && Clock.System.now().epochSeconds < (it.expires ?: 0) + } override suspend fun setAniListAccessToken(token: String) { dataStore.updateData { @@ -41,13 +44,19 @@ class DataStoreUserSettings( } } - override suspend fun setAniListTokens(access: String, refresh: String?, id: String?) { + override suspend fun setAniListTokens( + access: String, + refresh: String?, + id: String?, + expires: Int? + ) { dataStore.updateData { it.copy( aniList = it.aniList.copy( accessToken = access, refreshToken = refresh, - idToken = id + idToken = id, + expires = expires ) ) } diff --git a/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/Settings.kt b/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/Settings.kt index 3ab62c3..23aab0d 100644 --- a/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/Settings.kt +++ b/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/Settings.kt @@ -12,6 +12,11 @@ data object Settings { suspend fun setAniListAccessToken(token: String) suspend fun setAniListRefreshToken(token: String) suspend fun setAniListIdToken(token: String) - suspend fun setAniListTokens(access: String, refresh: String?, id: String?) + suspend fun setAniListTokens( + access: String, + refresh: String?, + id: String?, + expires: Int? + ) } } \ No newline at end of file diff --git a/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/UserSettings.kt b/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/UserSettings.kt index 0f3eb74..b130cb4 100644 --- a/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/UserSettings.kt +++ b/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/UserSettings.kt @@ -10,7 +10,8 @@ data class UserSettings( @ProtoNumber(1) val aniList: AniList = AniList( accessToken = null, refreshToken = null, - idToken = null + idToken = null, + expires = null ) ) { @Serializable @@ -18,5 +19,6 @@ data class UserSettings( @ProtoNumber(1) val accessToken: String?, @ProtoNumber(2) val refreshToken: String?, @ProtoNumber(3) val idToken: String?, + @ProtoNumber(4) val expires: Int? ) }