diff --git a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt index d378f812..5d40ed48 100644 --- a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt +++ b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt @@ -38,6 +38,16 @@ fun MainNavHost( popUpTo { inclusive = true } } }, + navigateToTermsAgreement = { + navigator.navController.navigate(Route.TermsAgreement) { + popUpTo { inclusive = true } + } + }, + navigateToOnboarding = { + navigator.navController.navigate(Route.OnBoarding()) { + popUpTo { inclusive = true } + } + }, navigateToHome = navigator::navigateToHomeAndClearStack, ) } diff --git a/core/datastore/src/main/java/com/threegap/bitnagil/datastore/auth/storage/AuthTokenDataStore.kt b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/auth/storage/AuthTokenDataStore.kt index 70f7540d..4c927996 100644 --- a/core/datastore/src/main/java/com/threegap/bitnagil/datastore/auth/storage/AuthTokenDataStore.kt +++ b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/auth/storage/AuthTokenDataStore.kt @@ -5,14 +5,8 @@ import kotlinx.coroutines.flow.Flow interface AuthTokenDataStore { val tokenFlow: Flow - - suspend fun hasToken(): Boolean - suspend fun updateAuthToken(accessToken: String, refreshToken: String) - suspend fun updateAccessToken(accessToken: String) - suspend fun updateRefreshToken(refreshToken: String) - suspend fun clearAuthToken() } diff --git a/core/datastore/src/main/java/com/threegap/bitnagil/datastore/auth/storage/AuthTokenDataStoreImpl.kt b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/auth/storage/AuthTokenDataStoreImpl.kt index cd9f4861..b1d4e6a7 100644 --- a/core/datastore/src/main/java/com/threegap/bitnagil/datastore/auth/storage/AuthTokenDataStoreImpl.kt +++ b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/auth/storage/AuthTokenDataStoreImpl.kt @@ -3,24 +3,12 @@ package com.threegap.bitnagil.datastore.auth.storage import androidx.datastore.core.DataStore import com.threegap.bitnagil.datastore.auth.model.AuthToken import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull class AuthTokenDataStoreImpl( private val dataStore: DataStore, ) : AuthTokenDataStore { override val tokenFlow: Flow = dataStore.data - override suspend fun hasToken(): Boolean { - return try { - val currentToken = dataStore.data.firstOrNull() - currentToken?.let { - !it.accessToken.isNullOrEmpty() && !it.refreshToken.isNullOrEmpty() - } ?: false - } catch (e: Exception) { - false - } - } - override suspend fun updateAuthToken(accessToken: String, refreshToken: String) { try { dataStore.updateData { @@ -58,8 +46,4 @@ class AuthTokenDataStoreImpl( throw e } } - - companion object { - private const val TAG = "AuthTokenDataStore" - } } diff --git a/core/network/src/main/java/com/threegap/bitnagil/network/auth/TokenAuthenticator.kt b/core/network/src/main/java/com/threegap/bitnagil/network/auth/TokenAuthenticator.kt index 5417781d..7988e4b7 100644 --- a/core/network/src/main/java/com/threegap/bitnagil/network/auth/TokenAuthenticator.kt +++ b/core/network/src/main/java/com/threegap/bitnagil/network/auth/TokenAuthenticator.kt @@ -19,6 +19,7 @@ class TokenAuthenticator( private val authMutex = Mutex() override fun authenticate(route: Route?, response: Response): Request? { + if (response.request.header(AUTO_LOGIN_HEADER) != null) return null if (!shouldRetry(response)) return null val currentToken = runBlocking { tokenProvider.getAccessToken() } @@ -82,6 +83,7 @@ class TokenAuthenticator( private fun buildRequestWithToken(originalRequest: Request, token: String): Request { return originalRequest.newBuilder() .header(AUTHORIZATION, "$TOKEN_PREFIX $token") + .removeHeader(AUTO_LOGIN_HEADER) .build() } @@ -96,5 +98,6 @@ class TokenAuthenticator( private const val AUTHORIZATION = "Authorization" private const val TOKEN_PREFIX = "Bearer" private const val SUCCESS_CODE = "CO000" + private const val AUTO_LOGIN_HEADER = "Auto-Login" } } diff --git a/data/src/main/java/com/threegap/bitnagil/data/auth/datasource/AuthLocalDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/auth/datasource/AuthLocalDataSource.kt index 4bf7bce4..b746fca5 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/auth/datasource/AuthLocalDataSource.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/auth/datasource/AuthLocalDataSource.kt @@ -1,9 +1,7 @@ package com.threegap.bitnagil.data.auth.datasource interface AuthLocalDataSource { - suspend fun hasToken(): Boolean - + suspend fun getRefreshToken(): String? suspend fun updateAuthToken(accessToken: String, refreshToken: String): Result - suspend fun clearAuthToken(): Result } diff --git a/data/src/main/java/com/threegap/bitnagil/data/auth/datasource/AuthRemoteDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/auth/datasource/AuthRemoteDataSource.kt index dd4dce44..c72bd0f6 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/auth/datasource/AuthRemoteDataSource.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/auth/datasource/AuthRemoteDataSource.kt @@ -6,10 +6,8 @@ import com.threegap.bitnagil.data.auth.model.response.LoginResponseDto interface AuthRemoteDataSource { suspend fun login(socialAccessToken: String, loginRequestDto: LoginRequestDto): Result - suspend fun submitAgreement(termsAgreementRequestDto: TermsAgreementRequestDto): Result - suspend fun logout(): Result - suspend fun withdrawal(): Result + suspend fun reissueToken(refreshToken: String): Result } diff --git a/data/src/main/java/com/threegap/bitnagil/data/auth/datasourceimpl/AuthLocalDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/auth/datasourceimpl/AuthLocalDataSourceImpl.kt index cd1fc995..04454327 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/auth/datasourceimpl/AuthLocalDataSourceImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/auth/datasourceimpl/AuthLocalDataSourceImpl.kt @@ -2,13 +2,17 @@ package com.threegap.bitnagil.data.auth.datasourceimpl import com.threegap.bitnagil.data.auth.datasource.AuthLocalDataSource import com.threegap.bitnagil.datastore.auth.storage.AuthTokenDataStore +import kotlinx.coroutines.flow.firstOrNull import javax.inject.Inject class AuthLocalDataSourceImpl @Inject constructor( private val authTokenDataStore: AuthTokenDataStore, ) : AuthLocalDataSource { - override suspend fun hasToken(): Boolean = authTokenDataStore.hasToken() + override suspend fun getRefreshToken(): String? = + runCatching { + authTokenDataStore.tokenFlow.firstOrNull()?.refreshToken + }.getOrNull() override suspend fun updateAuthToken(accessToken: String, refreshToken: String): Result = runCatching { diff --git a/data/src/main/java/com/threegap/bitnagil/data/auth/datasourceimpl/AuthRemoteDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/auth/datasourceimpl/AuthRemoteDataSourceImpl.kt index 844158da..9cae33e2 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/auth/datasourceimpl/AuthRemoteDataSourceImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/auth/datasourceimpl/AuthRemoteDataSourceImpl.kt @@ -31,4 +31,9 @@ class AuthRemoteDataSourceImpl @Inject constructor( safeUnitApiCall { authService.postWithdrawal() } + + override suspend fun reissueToken(refreshToken: String): Result = + safeApiCall { + authService.postReissueToken(refreshToken) + } } diff --git a/data/src/main/java/com/threegap/bitnagil/data/auth/repositoryimpl/AuthRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/auth/repositoryimpl/AuthRepositoryImpl.kt index 065cc652..2dadacc3 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/auth/repositoryimpl/AuthRepositoryImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/auth/repositoryimpl/AuthRepositoryImpl.kt @@ -18,11 +18,6 @@ class AuthRepositoryImpl @Inject constructor( authRemoteDataSource.login(socialAccessToken, LoginRequestDto(socialType)) .map { it.toDomain() } - override suspend fun hasToken(): Boolean = authLocalDataSource.hasToken() - - override suspend fun updateAuthToken(accessToken: String, refreshToken: String): Result = - authLocalDataSource.updateAuthToken(accessToken, refreshToken) - override suspend fun submitAgreement(termsAgreement: TermsAgreement): Result = authRemoteDataSource.submitAgreement( termsAgreement.toDto(), @@ -39,4 +34,16 @@ class AuthRepositoryImpl @Inject constructor( if (it.isSuccess) authLocalDataSource.clearAuthToken() } } + + override suspend fun reissueToken(refreshToken: String): Result = + authRemoteDataSource.reissueToken(refreshToken).map { it.toDomain() } + + override suspend fun getRefreshToken(): String? = + authLocalDataSource.getRefreshToken() + + override suspend fun updateAuthToken(accessToken: String, refreshToken: String): Result = + authLocalDataSource.updateAuthToken(accessToken, refreshToken) + + override suspend fun clearAuthToken(): Result = + authLocalDataSource.clearAuthToken() } diff --git a/data/src/main/java/com/threegap/bitnagil/data/auth/service/AuthService.kt b/data/src/main/java/com/threegap/bitnagil/data/auth/service/AuthService.kt index 2548f1c8..58aa0a71 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/auth/service/AuthService.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/auth/service/AuthService.kt @@ -27,4 +27,10 @@ interface AuthService { @POST("/api/v1/auth/logout") suspend fun postLogout(): BaseResponse + + @POST("/api/v1/auth/token/reissue") + @Headers("No-Service-Token: true", "Auto-Login: true") + suspend fun postReissueToken( + @Header("Refresh-Token") refreshToken: String, + ): BaseResponse } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/auth/model/UserRole.kt b/domain/src/main/java/com/threegap/bitnagil/domain/auth/model/UserRole.kt index 42f224ec..9abe25fd 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/auth/model/UserRole.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/auth/model/UserRole.kt @@ -3,6 +3,9 @@ package com.threegap.bitnagil.domain.auth.model enum class UserRole { USER, GUEST, + ONBOARDING, + WITHDRAWN, + UNKNOWN, ; fun isGuest() = this == GUEST @@ -12,7 +15,9 @@ enum class UserRole { when (value) { "USER" -> USER "GUEST" -> GUEST - else -> throw IllegalArgumentException("Unknown role: $value") + "ONBOARDING" -> ONBOARDING + "WITHDRAWN" -> WITHDRAWN + else -> UNKNOWN } } } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/auth/repository/AuthRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/auth/repository/AuthRepository.kt index f872955f..cee11a5d 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/auth/repository/AuthRepository.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/auth/repository/AuthRepository.kt @@ -5,14 +5,12 @@ import com.threegap.bitnagil.domain.auth.model.TermsAgreement interface AuthRepository { suspend fun login(socialAccessToken: String, socialType: String): Result - - suspend fun hasToken(): Boolean - - suspend fun updateAuthToken(accessToken: String, refreshToken: String): Result - suspend fun submitAgreement(termsAgreement: TermsAgreement): Result - suspend fun logout(): Result - suspend fun withdrawal(): Result + + suspend fun reissueToken(refreshToken: String): Result + suspend fun getRefreshToken(): String? + suspend fun updateAuthToken(accessToken: String, refreshToken: String): Result + suspend fun clearAuthToken(): Result } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/AutoLoginUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/AutoLoginUseCase.kt new file mode 100644 index 00000000..c219598c --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/AutoLoginUseCase.kt @@ -0,0 +1,29 @@ +package com.threegap.bitnagil.domain.auth.usecase + +import com.threegap.bitnagil.domain.auth.model.UserRole +import com.threegap.bitnagil.domain.auth.repository.AuthRepository +import javax.inject.Inject + +class AutoLoginUseCase @Inject constructor( + private val authRepository: AuthRepository, +) { + suspend operator fun invoke(): UserRole { + val refreshToken = authRepository.getRefreshToken() + if (refreshToken.isNullOrEmpty()) return UserRole.UNKNOWN + + return authRepository.reissueToken(refreshToken) + .onSuccess { authSession -> + authRepository.updateAuthToken( + accessToken = authSession.accessToken, + refreshToken = authSession.refreshToken, + ) + } + .onFailure { + authRepository.clearAuthToken() + } + .fold( + onSuccess = { authSession -> authSession.role }, + onFailure = { UserRole.UNKNOWN }, + ) + } +} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/HasTokenUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/HasTokenUseCase.kt deleted file mode 100644 index 45f89d4c..00000000 --- a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/HasTokenUseCase.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.threegap.bitnagil.domain.auth.usecase - -import com.threegap.bitnagil.domain.auth.repository.AuthRepository -import javax.inject.Inject - -class HasTokenUseCase @Inject constructor( - private val authRepository: AuthRepository, -) { - suspend operator fun invoke(): Boolean = authRepository.hasToken() -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt index 110cff37..d6234a20 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt @@ -29,12 +29,16 @@ import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun SplashScreenContainer( navigateToIntro: () -> Unit, + navigateToTermsAgreement: () -> Unit, + navigateToOnboarding: () -> Unit, navigateToHome: () -> Unit, viewModel: SplashViewModel = hiltViewModel(), ) { viewModel.collectSideEffect { sideEffect -> when (sideEffect) { is SplashSideEffect.NavigateToIntro -> navigateToIntro() + is SplashSideEffect.NavigateToTermsAgreement -> navigateToTermsAgreement() + is SplashSideEffect.NavigateToOnboarding -> navigateToOnboarding() is SplashSideEffect.NavigateToHome -> navigateToHome() } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt index 12ffb675..47acbbe0 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt @@ -2,7 +2,8 @@ package com.threegap.bitnagil.presentation.splash import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.threegap.bitnagil.domain.auth.usecase.HasTokenUseCase +import com.threegap.bitnagil.domain.auth.model.UserRole +import com.threegap.bitnagil.domain.auth.usecase.AutoLoginUseCase import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel import com.threegap.bitnagil.presentation.splash.model.SplashIntent import com.threegap.bitnagil.presentation.splash.model.SplashSideEffect @@ -10,22 +11,21 @@ import com.threegap.bitnagil.presentation.splash.model.SplashState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull import org.orbitmvi.orbit.syntax.simple.SimpleSyntax import javax.inject.Inject @HiltViewModel class SplashViewModel @Inject constructor( - private val savedStateHandle: SavedStateHandle, - private val hasTokenUseCase: HasTokenUseCase, + savedStateHandle: SavedStateHandle, + private val autoLoginUseCase: AutoLoginUseCase, ) : MviViewModel( initState = SplashState(), savedStateHandle = savedStateHandle, ) { - private var hasToken: Boolean? = null - init { - checkTokenStatus() + performAutoLogin() } override suspend fun SimpleSyntax.reduceState( @@ -33,8 +33,11 @@ class SplashViewModel @Inject constructor( state: SplashState, ): SplashState? = when (intent) { - is SplashIntent.SetTokenChecked -> { - state.copy(isTokenChecked = intent.hasToken != null) + is SplashIntent.SetUserRole -> { + state.copy( + userRole = intent.userRole, + isAutoLoginCompleted = true, + ) } is SplashIntent.NavigateToIntro -> { @@ -46,23 +49,34 @@ class SplashViewModel @Inject constructor( sendSideEffect(SplashSideEffect.NavigateToHome) null } + + is SplashIntent.NavigateToTermsAgreement -> { + sendSideEffect(SplashSideEffect.NavigateToTermsAgreement) + null + } + + is SplashIntent.NavigateToOnboarding -> { + sendSideEffect(SplashSideEffect.NavigateToOnboarding) + null + } } - private fun checkTokenStatus() { + private fun performAutoLogin() { viewModelScope.launch { try { - hasToken = hasTokenUseCase() - sendIntent(SplashIntent.SetTokenChecked(hasToken)) + val userRole = withTimeoutOrNull(5000) { + autoLoginUseCase() + } + sendIntent(SplashIntent.SetUserRole(userRole)) } catch (e: Exception) { - hasToken = false - sendIntent(SplashIntent.SetTokenChecked(false)) + sendIntent(SplashIntent.SetUserRole(null)) } } } fun onAnimationCompleted() { - val tokenResult = hasToken - if (tokenResult == null) { + val splashState = container.stateFlow.value + if (!splashState.isAutoLoginCompleted) { viewModelScope.launch { delay(100) onAnimationCompleted() @@ -70,10 +84,11 @@ class SplashViewModel @Inject constructor( return } - if (tokenResult) { - sendIntent(SplashIntent.NavigateToHome) - } else { - sendIntent(SplashIntent.NavigateToIntro) + when (splashState.userRole) { + UserRole.GUEST -> sendIntent(SplashIntent.NavigateToTermsAgreement) + UserRole.USER -> sendIntent(SplashIntent.NavigateToHome) + UserRole.ONBOARDING -> sendIntent(SplashIntent.NavigateToOnboarding) + else -> sendIntent(SplashIntent.NavigateToIntro) } } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashIntent.kt index dd91788b..72ed4161 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashIntent.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashIntent.kt @@ -1,9 +1,12 @@ package com.threegap.bitnagil.presentation.splash.model +import com.threegap.bitnagil.domain.auth.model.UserRole import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent sealed class SplashIntent : MviIntent { - data class SetTokenChecked(val hasToken: Boolean?) : SplashIntent() + data class SetUserRole(val userRole: UserRole?) : SplashIntent() data object NavigateToIntro : SplashIntent() data object NavigateToHome : SplashIntent() + data object NavigateToTermsAgreement : SplashIntent() + data object NavigateToOnboarding : SplashIntent() } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashSideEffect.kt index 1f870170..1479561f 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashSideEffect.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashSideEffect.kt @@ -5,4 +5,6 @@ import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect sealed interface SplashSideEffect : MviSideEffect { data object NavigateToIntro : SplashSideEffect data object NavigateToHome : SplashSideEffect + data object NavigateToTermsAgreement : SplashSideEffect + data object NavigateToOnboarding : SplashSideEffect } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashState.kt index 448f9119..61cbb355 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashState.kt @@ -1,9 +1,11 @@ package com.threegap.bitnagil.presentation.splash.model +import com.threegap.bitnagil.domain.auth.model.UserRole import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState import kotlinx.parcelize.Parcelize @Parcelize data class SplashState( - val isTokenChecked: Boolean = false, + val userRole: UserRole? = null, + val isAutoLoginCompleted: Boolean = false, ) : MviState