Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/src/main/java/com/threegap/bitnagil/MainNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ fun MainNavHost(
popUpTo<Route.Splash> { inclusive = true }
}
},
navigateToTermsAgreement = {
navigator.navController.navigate(Route.TermsAgreement) {
popUpTo<Route.Splash> { inclusive = true }
}
},
navigateToOnboarding = {
navigator.navController.navigate(Route.OnBoarding()) {
popUpTo<Route.Splash> { inclusive = true }
}
},
navigateToHome = navigator::navigateToHomeAndClearStack,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,8 @@ import kotlinx.coroutines.flow.Flow

interface AuthTokenDataStore {
val tokenFlow: Flow<AuthToken>

suspend fun hasToken(): Boolean

suspend fun updateAuthToken(accessToken: String, refreshToken: String)

suspend fun updateAccessToken(accessToken: String)

suspend fun updateRefreshToken(refreshToken: String)

suspend fun clearAuthToken()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<AuthToken>,
) : AuthTokenDataStore {
override val tokenFlow: Flow<AuthToken> = 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 {
Expand Down Expand Up @@ -58,8 +46,4 @@ class AuthTokenDataStoreImpl(
throw e
}
}

companion object {
private const val TAG = "AuthTokenDataStore"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
Expand Down Expand Up @@ -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()
}

Expand All @@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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<Unit>

suspend fun clearAuthToken(): Result<Unit>
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import com.threegap.bitnagil.data.auth.model.response.LoginResponseDto

interface AuthRemoteDataSource {
suspend fun login(socialAccessToken: String, loginRequestDto: LoginRequestDto): Result<LoginResponseDto>

suspend fun submitAgreement(termsAgreementRequestDto: TermsAgreementRequestDto): Result<Unit>

suspend fun logout(): Result<Unit>

suspend fun withdrawal(): Result<Unit>
suspend fun reissueToken(refreshToken: String): Result<LoginResponseDto>
}
Original file line number Diff line number Diff line change
Expand Up @@ -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? {
val refreshToken = authTokenDataStore.tokenFlow.firstOrNull()?.refreshToken
return refreshToken
}

override suspend fun updateAuthToken(accessToken: String, refreshToken: String): Result<Unit> =
runCatching {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ class AuthRemoteDataSourceImpl @Inject constructor(
safeUnitApiCall {
authService.postWithdrawal()
}

override suspend fun reissueToken(refreshToken: String): Result<LoginResponseDto> =
safeApiCall {
authService.postReissueToken(refreshToken)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Unit> =
authLocalDataSource.updateAuthToken(accessToken, refreshToken)

override suspend fun submitAgreement(termsAgreement: TermsAgreement): Result<Unit> =
authRemoteDataSource.submitAgreement(
termsAgreement.toDto(),
Expand All @@ -39,4 +34,16 @@ class AuthRepositoryImpl @Inject constructor(
if (it.isSuccess) authLocalDataSource.clearAuthToken()
}
}

override suspend fun reissueToken(refreshToken: String): Result<AuthSession> =
authRemoteDataSource.reissueToken(refreshToken).map { it.toDomain() }

override suspend fun getRefreshToken(): String? =
authLocalDataSource.getRefreshToken()

override suspend fun updateAuthToken(accessToken: String, refreshToken: String): Result<Unit> =
authLocalDataSource.updateAuthToken(accessToken, refreshToken)

override suspend fun clearAuthToken(): Result<Unit> =
authLocalDataSource.clearAuthToken()
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,10 @@ interface AuthService {

@POST("/api/v1/auth/logout")
suspend fun postLogout(): BaseResponse<Unit>

@POST("/api/v1/auth/token/reissue")
@Headers("No-Service-Token: true", "Auto-Login: true")
suspend fun postReissueToken(
@Header("Refresh-Token") refreshToken: String,
): BaseResponse<LoginResponseDto>
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package com.threegap.bitnagil.domain.auth.model
enum class UserRole {
USER,
GUEST,
ONBOARDING,
WITHDRAWN,
UNKNOWN,
;

fun isGuest() = this == GUEST
Expand All @@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ import com.threegap.bitnagil.domain.auth.model.TermsAgreement

interface AuthRepository {
suspend fun login(socialAccessToken: String, socialType: String): Result<AuthSession>

suspend fun hasToken(): Boolean

suspend fun updateAuthToken(accessToken: String, refreshToken: String): Result<Unit>

suspend fun submitAgreement(termsAgreement: TermsAgreement): Result<Unit>

suspend fun logout(): Result<Unit>

suspend fun withdrawal(): Result<Unit>

suspend fun reissueToken(refreshToken: String): Result<AuthSession>
suspend fun getRefreshToken(): String?
suspend fun updateAuthToken(accessToken: String, refreshToken: String): Result<Unit>
suspend fun clearAuthToken(): Result<Unit>
}
Original file line number Diff line number Diff line change
@@ -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 },
)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -15,26 +16,27 @@ import javax.inject.Inject

@HiltViewModel
class SplashViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val hasTokenUseCase: HasTokenUseCase,
savedStateHandle: SavedStateHandle,
private val autoLoginUseCase: AutoLoginUseCase,
) : MviViewModel<SplashState, SplashSideEffect, SplashIntent>(
initState = SplashState(),
savedStateHandle = savedStateHandle,
) {

private var hasToken: Boolean? = null

init {
checkTokenStatus()
performAutoLogin()
}

override suspend fun SimpleSyntax<SplashState, SplashSideEffect>.reduceState(
intent: SplashIntent,
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 -> {
Expand All @@ -46,34 +48,40 @@ 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))
} catch (e: Exception) {
hasToken = false
sendIntent(SplashIntent.SetTokenChecked(false))
}
val userRole = autoLoginUseCase()
sendIntent(SplashIntent.SetUserRole(userRole))
}
}

fun onAnimationCompleted() {
val tokenResult = hasToken
if (tokenResult == null) {
val splashState = container.stateFlow.value
if (!splashState.isAutoLoginCompleted) {
viewModelScope.launch {
delay(100)
onAnimationCompleted()
}
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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
Loading