diff --git a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt index 911f76fc..ff3cfb65 100644 --- a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt +++ b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt @@ -5,7 +5,9 @@ import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import com.threegap.bitnagil.presentation.home.HomeScreen +import com.threegap.bitnagil.presentation.intro.IntroScreenContainer import com.threegap.bitnagil.presentation.login.LoginScreenContainer +import com.threegap.bitnagil.presentation.splash.SplashScreenContainer @Composable fun MainNavHost( @@ -17,6 +19,17 @@ fun MainNavHost( startDestination = navigator.startDestination, modifier = modifier, ) { + composable { + SplashScreenContainer( + navigateToIntro = { navigator.navController.navigate(Route.Intro) }, + navigateToHome = { navigator.navController.navigate(Route.Home) }, + ) + } + + composable { + IntroScreenContainer() + } + composable { LoginScreenContainer() } diff --git a/app/src/main/java/com/threegap/bitnagil/MainNavigator.kt b/app/src/main/java/com/threegap/bitnagil/MainNavigator.kt index efd9f459..ed4106d4 100644 --- a/app/src/main/java/com/threegap/bitnagil/MainNavigator.kt +++ b/app/src/main/java/com/threegap/bitnagil/MainNavigator.kt @@ -8,7 +8,7 @@ import androidx.navigation.compose.rememberNavController class MainNavigator( val navController: NavHostController, ) { - val startDestination = Route.Login + val startDestination = Route.Splash } @Composable diff --git a/app/src/main/java/com/threegap/bitnagil/Route.kt b/app/src/main/java/com/threegap/bitnagil/Route.kt index 9ba31063..0d9ecfa8 100644 --- a/app/src/main/java/com/threegap/bitnagil/Route.kt +++ b/app/src/main/java/com/threegap/bitnagil/Route.kt @@ -4,6 +4,12 @@ import kotlinx.serialization.Serializable @Serializable sealed interface Route { + @Serializable + data object Splash : Route + + @Serializable + data object Intro : Route + @Serializable data object Login : Route 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 6067bcd9..70f7540d 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 @@ -6,6 +6,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) 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 0d26a905..cd9f4861 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,12 +3,24 @@ 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 { 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 06edb560..67dd07c2 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,5 +1,7 @@ package com.threegap.bitnagil.data.auth.datasource interface AuthLocalDataSource { + suspend fun hasToken(): Boolean + suspend fun updateAuthToken(accessToken: String, 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 ebb0f58b..b0ae1640 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 @@ -8,6 +8,8 @@ class AuthLocalDataSourceImpl @Inject constructor( private val authTokenDataStore: AuthTokenDataStore, ) : AuthLocalDataSource { + override suspend fun hasToken(): Boolean = authTokenDataStore.hasToken() + override suspend fun updateAuthToken(accessToken: String, refreshToken: String): Result = runCatching { authTokenDataStore.updateAuthToken( 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 0cb2ed6a..4221c4ec 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 @@ -16,6 +16,8 @@ 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) } 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 da1187a0..9ff3573e 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,5 +5,7 @@ import com.threegap.bitnagil.domain.auth.model.AuthSession interface AuthRepository { suspend fun login(socialAccessToken: String, socialType: String): Result + suspend fun hasToken(): Boolean + suspend fun updateAuthToken(accessToken: String, refreshToken: String): Result } 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 new file mode 100644 index 00000000..45f89d4c --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/HasTokenUseCase.kt @@ -0,0 +1,10 @@ +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/intro/IntroScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/intro/IntroScreen.kt new file mode 100644 index 00000000..11a7a668 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/intro/IntroScreen.kt @@ -0,0 +1,32 @@ +package com.threegap.bitnagil.presentation.intro + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview + +@Composable +fun IntroScreenContainer() { + IntroScreen() +} + +@Composable +private fun IntroScreen(modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .background(Color.White), + ) { + Text("인트로 화면") + } +} + +@Preview +@Composable +private fun IntroScreenPreview() { + IntroScreen() +} 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 new file mode 100644 index 00000000..7777e324 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt @@ -0,0 +1,49 @@ +package com.threegap.bitnagil.presentation.splash + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import com.threegap.bitnagil.presentation.splash.model.SplashSideEffect +import org.orbitmvi.orbit.compose.collectSideEffect + +@Composable +fun SplashScreenContainer( + navigateToIntro: () -> Unit, + navigateToHome: () -> Unit, + viewModel: SplashViewModel = hiltViewModel(), +) { + viewModel.collectSideEffect { sideEffect -> + when (sideEffect) { + is SplashSideEffect.NavigateToIntro -> navigateToIntro() + is SplashSideEffect.NavigateToHome -> navigateToHome() + } + } + + SplashScreen() +} + +@Composable +private fun SplashScreen( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + ) { + Text( + text = "야무진 로고 추가 예정", + modifier = Modifier + .align(Alignment.Center), + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun SplashScreenPreview() { + SplashScreen() +} 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 new file mode 100644 index 00000000..a850b8d8 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt @@ -0,0 +1,66 @@ +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.presentation.common.mviviewmodel.MviViewModel +import com.threegap.bitnagil.presentation.splash.model.SplashIntent +import com.threegap.bitnagil.presentation.splash.model.SplashSideEffect +import com.threegap.bitnagil.presentation.splash.model.SplashState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.orbitmvi.orbit.syntax.simple.SimpleSyntax +import javax.inject.Inject + +@HiltViewModel +class SplashViewModel @Inject constructor( + private val savedStateHandle: SavedStateHandle, + private val hasTokenUseCase: HasTokenUseCase, +) : MviViewModel( + initState = SplashState(), + savedStateHandle = savedStateHandle, +) { + + init { + checkAutoLogin() + } + + override suspend fun SimpleSyntax.reduceState( + intent: SplashIntent, + state: SplashState, + ): SplashState? = + when (intent) { + is SplashIntent.SetLoading -> { + state.copy(isLoading = intent.isLoading) + } + + is SplashIntent.NavigateToIntro -> { + sendSideEffect(SplashSideEffect.NavigateToIntro) + state.copy(isLoading = false) + } + + is SplashIntent.NavigateToHome -> { + sendSideEffect(SplashSideEffect.NavigateToHome) + state.copy(isLoading = false) + } + } + + private fun checkAutoLogin() { + viewModelScope.launch { + sendIntent(SplashIntent.SetLoading(true)) + val delayDeferred = async { delay(2000L) } + val tokenDeferred = async { hasTokenUseCase() } + + delayDeferred.await() + val hasToken = tokenDeferred.await() + + if (hasToken) { + sendIntent(SplashIntent.NavigateToHome) + } 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 new file mode 100644 index 00000000..f51b055f --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashIntent.kt @@ -0,0 +1,9 @@ +package com.threegap.bitnagil.presentation.splash.model + +import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent + +sealed class SplashIntent : MviIntent { + data class SetLoading(val isLoading: Boolean) : SplashIntent() + data object NavigateToIntro : SplashIntent() + data object NavigateToHome : 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 new file mode 100644 index 00000000..1f870170 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashSideEffect.kt @@ -0,0 +1,8 @@ +package com.threegap.bitnagil.presentation.splash.model + +import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect + +sealed interface SplashSideEffect : MviSideEffect { + data object NavigateToIntro : SplashSideEffect + data object NavigateToHome : 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 new file mode 100644 index 00000000..eb4ce75b --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashState.kt @@ -0,0 +1,9 @@ +package com.threegap.bitnagil.presentation.splash.model + +import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState +import kotlinx.parcelize.Parcelize + +@Parcelize +data class SplashState( + val isLoading: Boolean = false, +) : MviState