diff --git a/app/src/main/java/io/foundy/camstudy/MainViewModel.kt b/app/src/main/java/io/foundy/camstudy/MainViewModel.kt index 745d3124..c9797aaa 100644 --- a/app/src/main/java/io/foundy/camstudy/MainViewModel.kt +++ b/app/src/main/java/io/foundy/camstudy/MainViewModel.kt @@ -4,18 +4,18 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ramcosta.composedestinations.spec.DirectionDestinationSpec import dagger.hilt.android.lifecycle.HiltViewModel -import io.foundy.auth.domain.usecase.ExistsInitInfoUseCase -import io.foundy.auth.domain.usecase.GetCurrentUserIdUseCase +import io.foundy.auth.domain.model.AuthState +import io.foundy.auth.domain.usecase.GetAuthStateStreamUseCase import io.foundy.auth.ui.destinations.LoginRouteDestination import io.foundy.home.ui.destinations.HomeRouteDestination import io.foundy.welcome.ui.destinations.WelcomeRouteDestination +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( - private val getCurrentUserIdUseCase: GetCurrentUserIdUseCase, - private val existsInitInfoUseCase: ExistsInitInfoUseCase + private val getAuthStateStreamUseCase: GetAuthStateStreamUseCase, ) : ViewModel() { private var _startDestination: DirectionDestinationSpec? = null @@ -23,22 +23,14 @@ class MainViewModel @Inject constructor( init { viewModelScope.launch { - val currentUserId = getCurrentUserIdUseCase() - if (currentUserId == null) { - _startDestination = LoginRouteDestination - } else { - val existsInitInfo = existsInitInfoUseCase() - if (existsInitInfo == null) { - // 로그인이 되어 있으나 초기 정보 입력 여부를 판단할 수 없는 경우 서버와의 연결에 실패한 것이다. - // 에러 메시지는 로그인 화면에서 보이기 때문에 따로 여기서 에러 메시지를 보이는 로직은 없다. - _startDestination = LoginRouteDestination - return@launch - } - _startDestination = if (existsInitInfo) { + _startDestination = when (val authState = getAuthStateStreamUseCase().firstOrNull()) { + is AuthState.SignedIn -> if (authState.existsInitInfo) { HomeRouteDestination } else { WelcomeRouteDestination } + + else -> LoginRouteDestination } } } diff --git a/app/src/test/java/io/foundy/camstudy/MainViewModelTest.kt b/app/src/test/java/io/foundy/camstudy/MainViewModelTest.kt index 60fc5aff..faf589b2 100644 --- a/app/src/test/java/io/foundy/camstudy/MainViewModelTest.kt +++ b/app/src/test/java/io/foundy/camstudy/MainViewModelTest.kt @@ -1,8 +1,8 @@ package io.foundy.camstudy import dagger.hilt.android.testing.HiltAndroidTest -import io.foundy.auth.domain.usecase.ExistsInitInfoUseCase -import io.foundy.auth.domain.usecase.GetCurrentUserIdUseCase +import io.foundy.auth.domain.model.AuthState +import io.foundy.auth.domain.usecase.GetAuthStateStreamUseCase import io.foundy.auth.ui.destinations.LoginRouteDestination import io.foundy.camstudy.fake.FakeAuthRepository import io.foundy.core.test.MainDispatcherRule @@ -29,18 +29,17 @@ class MainViewModelTest { private lateinit var viewModel: MainViewModel @Test - fun `should startDestination is Login when currentUserId is null`() = runTest { + fun `should startDestination is Login when not signed in`() = runTest { initViewModel() - authRepository.currentUserIdSharedFlow.emit(null) + authRepository.emitState(AuthState.NotSignedIn) assertEquals(LoginRouteDestination, viewModel.startDestination) } @Test fun `should startDestination is Home when currentUserId and init info exist`() = runTest { - authRepository.currentUserIdSharedFlow.emit("id") - authRepository.existsInitInfoTestValue = true initViewModel() + authRepository.emitState(AuthState.SignedIn(currentUserId = "id", existsInitInfo = true)) assertEquals(HomeRouteDestination, viewModel.startDestination) } @@ -48,27 +47,29 @@ class MainViewModelTest { @Test fun `should startDestination is Welcome when currentUserId exists but init info not`() = runTest { - authRepository.currentUserIdSharedFlow.emit("id") - authRepository.existsInitInfoTestValue = false initViewModel() + authRepository.emitState( + AuthState.SignedIn( + currentUserId = "id", + existsInitInfo = false + ) + ) assertEquals(WelcomeRouteDestination, viewModel.startDestination) } @Test - fun `should startDestination is Login when currentUserId exists but init info is null`() = + fun `should startDestination is Login when there is error`() = runTest { - authRepository.currentUserIdSharedFlow.emit("id") - authRepository.existsInitInfoTestValue = null initViewModel() + authRepository.emitState(AuthState.Error) assertEquals(LoginRouteDestination, viewModel.startDestination) } private fun initViewModel() { viewModel = MainViewModel( - getCurrentUserIdUseCase = GetCurrentUserIdUseCase(authRepository = authRepository), - existsInitInfoUseCase = ExistsInitInfoUseCase(authRepository = authRepository) + getAuthStateStreamUseCase = GetAuthStateStreamUseCase(authRepository = authRepository) ) } } diff --git a/app/src/test/java/io/foundy/camstudy/fake/FakeAuthRepository.kt b/app/src/test/java/io/foundy/camstudy/fake/FakeAuthRepository.kt index 48bdb4d9..822e10dc 100644 --- a/app/src/test/java/io/foundy/camstudy/fake/FakeAuthRepository.kt +++ b/app/src/test/java/io/foundy/camstudy/fake/FakeAuthRepository.kt @@ -1,19 +1,16 @@ package io.foundy.camstudy.fake +import io.foundy.auth.domain.model.AuthState import io.foundy.auth.domain.repository.AuthRepository -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow class FakeAuthRepository : AuthRepository { - val currentUserIdSharedFlow = MutableSharedFlow(replay = 1) + override val stateStream: MutableSharedFlow = MutableSharedFlow(replay = 1) - var existsInitInfoTestValue: Boolean? = false - - override val currentUserIdStream: Flow = currentUserIdSharedFlow - - override val existsInitInfo: Boolean? - get() = existsInitInfoTestValue + suspend fun emitState(newState: AuthState) { + stateStream.emit(newState) + } override suspend fun markAsUserInitialInfoExists(userId: String) { TODO("Not yet implemented") diff --git a/feature/auth/data/src/main/java/io/foundy/auth/data/repository/FirebaseAuthRepository.kt b/feature/auth/data/src/main/java/io/foundy/auth/data/repository/FirebaseAuthRepository.kt index e8561942..d8a83c79 100644 --- a/feature/auth/data/src/main/java/io/foundy/auth/data/repository/FirebaseAuthRepository.kt +++ b/feature/auth/data/src/main/java/io/foundy/auth/data/repository/FirebaseAuthRepository.kt @@ -4,6 +4,7 @@ import com.google.firebase.auth.ktx.auth import com.google.firebase.ktx.Firebase import io.foundy.auth.data.source.AuthLocalDataSource import io.foundy.auth.data.source.AuthRemoteDataSource +import io.foundy.auth.domain.model.AuthState import io.foundy.auth.domain.repository.AuthRepository import io.foundy.core.common.di.ApplicationScope import io.foundy.core.data.extension.getDataOrThrowMessage @@ -20,8 +21,7 @@ class FirebaseAuthRepository @Inject constructor( private val authLocalDataSource: AuthLocalDataSource ) : AuthRepository { - override val currentUserIdStream: MutableSharedFlow = MutableSharedFlow(replay = 1) - override var existsInitInfo: Boolean? = null + override val stateStream: MutableSharedFlow = MutableSharedFlow(replay = 1) init { Firebase.auth.addAuthStateListener { auth -> @@ -36,25 +36,35 @@ class FirebaseAuthRepository @Inject constructor( private fun fetchAndNotify(currentUserId: String?) { externalScope.launch { if (currentUserId == null) { - existsInitInfo = null - currentUserIdStream.emit(null) + stateStream.emit(AuthState.NotSignedIn) return@launch } val existsInLocal = authLocalDataSource.existsUserInitialInfo(currentUserId) if (existsInLocal) { - existsInitInfo = true + stateStream.emit( + AuthState.SignedIn( + currentUserId = currentUserId, + existsInitInfo = true + ) + ) } else { runCatching { val response = authRemoteDataSource.getUserInitialInfoExistence(currentUserId) response.getDataOrThrowMessage() }.onSuccess { exists -> - existsInitInfo = exists if (exists) { authLocalDataSource.markAsUserInitialInfoExists(currentUserId) } + stateStream.emit( + AuthState.SignedIn( + currentUserId = currentUserId, + existsInitInfo = exists + ) + ) + }.onFailure { + stateStream.emit(AuthState.Error) } } - currentUserIdStream.emit(currentUserId) } } } diff --git a/feature/auth/domain/src/main/java/io/foundy/auth/domain/model/AuthState.kt b/feature/auth/domain/src/main/java/io/foundy/auth/domain/model/AuthState.kt new file mode 100644 index 00000000..b5b003a3 --- /dev/null +++ b/feature/auth/domain/src/main/java/io/foundy/auth/domain/model/AuthState.kt @@ -0,0 +1,7 @@ +package io.foundy.auth.domain.model + +sealed class AuthState { + object NotSignedIn : AuthState() + data class SignedIn(val currentUserId: String, val existsInitInfo: Boolean) : AuthState() + object Error : AuthState() // TODO: Add error context message +} diff --git a/feature/auth/domain/src/main/java/io/foundy/auth/domain/repository/AuthRepository.kt b/feature/auth/domain/src/main/java/io/foundy/auth/domain/repository/AuthRepository.kt index fff1cddb..f7c2840e 100644 --- a/feature/auth/domain/src/main/java/io/foundy/auth/domain/repository/AuthRepository.kt +++ b/feature/auth/domain/src/main/java/io/foundy/auth/domain/repository/AuthRepository.kt @@ -1,15 +1,11 @@ package io.foundy.auth.domain.repository +import io.foundy.auth.domain.model.AuthState import kotlinx.coroutines.flow.Flow interface AuthRepository { - val currentUserIdStream: Flow - - /** - * `currentUserIdStream`이 `true`인데 이 값이 `null`인 경우 서버와의 연결을 실패한 경우이다. - */ - val existsInitInfo: Boolean? + val stateStream: Flow suspend fun markAsUserInitialInfoExists(userId: String) } diff --git a/feature/auth/domain/src/main/java/io/foundy/auth/domain/usecase/ExistsInitInfoUseCase.kt b/feature/auth/domain/src/main/java/io/foundy/auth/domain/usecase/ExistsInitInfoUseCase.kt deleted file mode 100644 index bb31c0c5..00000000 --- a/feature/auth/domain/src/main/java/io/foundy/auth/domain/usecase/ExistsInitInfoUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.foundy.auth.domain.usecase - -import io.foundy.auth.domain.repository.AuthRepository -import javax.inject.Inject - -class ExistsInitInfoUseCase @Inject constructor( - private val authRepository: AuthRepository -) { - operator fun invoke(): Boolean? { - return authRepository.existsInitInfo - } -} diff --git a/feature/auth/domain/src/main/java/io/foundy/auth/domain/usecase/GetCurrentUserIdStreamUseCase.kt b/feature/auth/domain/src/main/java/io/foundy/auth/domain/usecase/GetAuthStateStreamUseCase.kt similarity index 53% rename from feature/auth/domain/src/main/java/io/foundy/auth/domain/usecase/GetCurrentUserIdStreamUseCase.kt rename to feature/auth/domain/src/main/java/io/foundy/auth/domain/usecase/GetAuthStateStreamUseCase.kt index 0de1830a..9d60f692 100644 --- a/feature/auth/domain/src/main/java/io/foundy/auth/domain/usecase/GetCurrentUserIdStreamUseCase.kt +++ b/feature/auth/domain/src/main/java/io/foundy/auth/domain/usecase/GetAuthStateStreamUseCase.kt @@ -1,13 +1,14 @@ package io.foundy.auth.domain.usecase +import io.foundy.auth.domain.model.AuthState import io.foundy.auth.domain.repository.AuthRepository import kotlinx.coroutines.flow.Flow import javax.inject.Inject -class GetCurrentUserIdStreamUseCase @Inject constructor( +class GetAuthStateStreamUseCase @Inject constructor( private val authRepository: AuthRepository ) { - operator fun invoke(): Flow { - return authRepository.currentUserIdStream + operator fun invoke(): Flow { + return authRepository.stateStream } } diff --git a/feature/auth/domain/src/main/java/io/foundy/auth/domain/usecase/GetCurrentUserIdUseCase.kt b/feature/auth/domain/src/main/java/io/foundy/auth/domain/usecase/GetCurrentUserIdUseCase.kt index 119835e5..96b9150d 100644 --- a/feature/auth/domain/src/main/java/io/foundy/auth/domain/usecase/GetCurrentUserIdUseCase.kt +++ b/feature/auth/domain/src/main/java/io/foundy/auth/domain/usecase/GetCurrentUserIdUseCase.kt @@ -1,5 +1,6 @@ package io.foundy.auth.domain.usecase +import io.foundy.auth.domain.model.AuthState import io.foundy.auth.domain.repository.AuthRepository import kotlinx.coroutines.flow.firstOrNull import javax.inject.Inject @@ -8,6 +9,9 @@ class GetCurrentUserIdUseCase @Inject constructor( private val authRepository: AuthRepository ) { suspend operator fun invoke(): String? { - return authRepository.currentUserIdStream.firstOrNull() + return when (val authState = authRepository.stateStream.firstOrNull()) { + is AuthState.SignedIn -> authState.currentUserId + else -> null + } } } diff --git a/feature/auth/ui/src/main/java/io/foundy/auth/ui/LoginViewModel.kt b/feature/auth/ui/src/main/java/io/foundy/auth/ui/LoginViewModel.kt index 9e20b5de..6f579d1c 100644 --- a/feature/auth/ui/src/main/java/io/foundy/auth/ui/LoginViewModel.kt +++ b/feature/auth/ui/src/main/java/io/foundy/auth/ui/LoginViewModel.kt @@ -2,8 +2,8 @@ package io.foundy.auth.ui import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import io.foundy.auth.domain.usecase.ExistsInitInfoUseCase -import io.foundy.auth.domain.usecase.GetCurrentUserIdStreamUseCase +import io.foundy.auth.domain.model.AuthState +import io.foundy.auth.domain.usecase.GetAuthStateStreamUseCase import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.syntax.simple.intent import org.orbitmvi.orbit.syntax.simple.postSideEffect @@ -13,31 +13,28 @@ import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( - private val getCurrentUserIdStreamUseCase: GetCurrentUserIdStreamUseCase, - private val existsInitInfoUseCase: ExistsInitInfoUseCase + private val getAuthStateStreamUseCase: GetAuthStateStreamUseCase, ) : ViewModel(), ContainerHost { override val container = container(LoginUiState()) init { intent { - getCurrentUserIdStreamUseCase().collect { uid -> - if (uid == null) { - return@collect - } - val existsInitInfo = existsInitInfoUseCase() - if (existsInitInfo == null) { - postSideEffect( + getAuthStateStreamUseCase().collect { authState -> + when (authState) { + AuthState.Error -> postSideEffect( LoginSideEffect.Message( defaultMessageRes = R.string.failed_to_connect_server ) ) - return@collect - } - if (existsInitInfo) { - postSideEffect(LoginSideEffect.NavigateToHome) - } else { - postSideEffect(LoginSideEffect.NavigateToWelcome) + + is AuthState.SignedIn -> if (authState.existsInitInfo) { + postSideEffect(LoginSideEffect.NavigateToHome) + } else { + postSideEffect(LoginSideEffect.NavigateToWelcome) + } + + AuthState.NotSignedIn -> {} } } } diff --git a/feature/room/ui/src/test/java/io/foundy/room/ui/fake/FakeAuthRepository.kt b/feature/room/ui/src/test/java/io/foundy/room/ui/fake/FakeAuthRepository.kt index 46c45cee..8c34f77a 100644 --- a/feature/room/ui/src/test/java/io/foundy/room/ui/fake/FakeAuthRepository.kt +++ b/feature/room/ui/src/test/java/io/foundy/room/ui/fake/FakeAuthRepository.kt @@ -1,15 +1,14 @@ package io.foundy.room.ui.fake +import io.foundy.auth.domain.model.AuthState import io.foundy.auth.domain.repository.AuthRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow class FakeAuthRepository(private val userId: String = "1234") : AuthRepository { - override val currentUserIdStream: Flow - get() = flow { emit(userId) } - - override val existsInitInfo: Boolean = true + override val stateStream: Flow + get() = flow { emit(AuthState.SignedIn(currentUserId = userId, existsInitInfo = true)) } override suspend fun markAsUserInitialInfoExists(userId: String) { TODO("Not yet implemented")