From ad97ca5ce08f89631a95707554ac6022446be845 Mon Sep 17 00:00:00 2001 From: JSPark <48265129+pknujsp@users.noreply.github.com> Date: Tue, 12 Mar 2024 01:01:23 +0900 Subject: [PATCH] =?UTF-8?q?#219=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=A0=95=EB=B3=B4(CognitoUserDetails)=EB=A5=BC=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=EC=8B=9C=EC=97=90=20=EA=B0=80?= =?UTF-8?q?=EC=A0=B8=EC=98=A4=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/session/AccountSessionRepository.kt | 2 +- .../session/AccountSessionRepositoryImpl.kt | 43 +++++- .../mediproject/core/model/user/UserEntity.kt | 4 +- .../datasource/sign/LoginDataSource.kt | 4 +- .../datasource/sign/LoginDataSourceImpl.kt | 40 +++++- .../datasource/sign/SignupDataSource.kt | 4 +- .../feature/mypage/MyPageFragment.kt | 124 ++++++++---------- .../feature/mypage/MyPageViewModel.kt | 30 ++++- 8 files changed, 166 insertions(+), 85 deletions(-) diff --git a/core/data/src/main/java/com/android/mediproject/core/data/session/AccountSessionRepository.kt b/core/data/src/main/java/com/android/mediproject/core/data/session/AccountSessionRepository.kt index cd9f901e..8aac3273 100644 --- a/core/data/src/main/java/com/android/mediproject/core/data/session/AccountSessionRepository.kt +++ b/core/data/src/main/java/com/android/mediproject/core/data/session/AccountSessionRepository.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.StateFlow interface AccountSessionRepository { val lastSavedEmail: Flow - val userOnCurrentSession: StateFlow + val userOnCurrentSession: Flow val session: StateFlow val signedIn: Boolean diff --git a/core/data/src/main/java/com/android/mediproject/core/data/session/AccountSessionRepositoryImpl.kt b/core/data/src/main/java/com/android/mediproject/core/data/session/AccountSessionRepositoryImpl.kt index 77e8e7d4..89016b89 100644 --- a/core/data/src/main/java/com/android/mediproject/core/data/session/AccountSessionRepositoryImpl.kt +++ b/core/data/src/main/java/com/android/mediproject/core/data/session/AccountSessionRepositoryImpl.kt @@ -1,12 +1,20 @@ package com.android.mediproject.core.data.session +import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoDevice import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUserPool import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUserSession +import com.amazonaws.mobileconnectors.cognitoidentityprovider.continuations.AuthenticationContinuation +import com.amazonaws.mobileconnectors.cognitoidentityprovider.continuations.ChallengeContinuation +import com.amazonaws.mobileconnectors.cognitoidentityprovider.continuations.MultiFactorAuthenticationContinuation +import com.amazonaws.mobileconnectors.cognitoidentityprovider.handlers.AuthenticationHandler import com.android.mediproject.core.datastore.AppDataStore import com.android.mediproject.core.model.user.UserEntity import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine class AccountSessionRepositoryImpl( private val appDataStore: AppDataStore, @@ -14,8 +22,13 @@ class AccountSessionRepositoryImpl( ) : AccountSessionRepository { override val lastSavedEmail = appDataStore.userEmail.distinctUntilChanged() - private val mutableUserOnCurrentSession = MutableStateFlow(null) - override val userOnCurrentSession = mutableUserOnCurrentSession.asStateFlow() + override val userOnCurrentSession = appDataStore.userEmail.combine(appDataStore.nickName) { email, nickName -> + if (email.isEmpty() || nickName.isEmpty()) { + null + } else { + UserEntity(nickName, email) + } + } private var mutableSession = MutableStateFlow(null) override val session = mutableSession.asStateFlow() @@ -25,20 +38,40 @@ class AccountSessionRepositoryImpl( override suspend fun updateSession(session: CognitoUserSession?) { mutableSession.value = session - if (session == null) { - mutableUserOnCurrentSession.value = null appDataStore.clearMyAccountInfo() } } override suspend fun updateAccount(email: String, nickName: String) { - mutableUserOnCurrentSession.value = UserEntity(email, nickName) appDataStore.saveMyAccountInfo(email, nickName) } override suspend fun loadSession() { + suspendCoroutine { + userPool.currentUser?.getSession( + object : AuthenticationHandler { + override fun onSuccess(userSession: CognitoUserSession?, newDevice: CognitoDevice?) { + it.resume(userSession) + } + + override fun getAuthenticationDetails(authenticationContinuation: AuthenticationContinuation?, userId: String?) { + } + override fun getMFACode(continuation: MultiFactorAuthenticationContinuation?) { + } + + override fun authenticationChallenge(continuation: ChallengeContinuation?) { + } + + override fun onFailure(exception: Exception?) { + it.resume(null) + } + }, + ) + }.let { session -> + mutableSession.value = session + } } } diff --git a/core/model/src/main/java/com/android/mediproject/core/model/user/UserEntity.kt b/core/model/src/main/java/com/android/mediproject/core/model/user/UserEntity.kt index 771ed235..b4f6c1c7 100644 --- a/core/model/src/main/java/com/android/mediproject/core/model/user/UserEntity.kt +++ b/core/model/src/main/java/com/android/mediproject/core/model/user/UserEntity.kt @@ -1,6 +1,6 @@ package com.android.mediproject.core.model.user data class UserEntity( - val nickName: String, - val email: String, + val nickName: String = "", + val email: String = "", ) diff --git a/core/network/src/main/java/com/android/mediproject/core/network/datasource/sign/LoginDataSource.kt b/core/network/src/main/java/com/android/mediproject/core/network/datasource/sign/LoginDataSource.kt index a2c3ecf3..05fa3d49 100644 --- a/core/network/src/main/java/com/android/mediproject/core/network/datasource/sign/LoginDataSource.kt +++ b/core/network/src/main/java/com/android/mediproject/core/network/datasource/sign/LoginDataSource.kt @@ -1,6 +1,7 @@ package com.android.mediproject.core.network.datasource.sign import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoDevice +import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUserDetails import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUserSession interface LoginDataSource { @@ -15,5 +16,6 @@ class LoginRequest( class LoginResponse( val userSession: CognitoUserSession, - val newDevice: CognitoDevice?, + val attr: CognitoUserDetails, + val newDevice: CognitoDevice? = null, ) diff --git a/core/network/src/main/java/com/android/mediproject/core/network/datasource/sign/LoginDataSourceImpl.kt b/core/network/src/main/java/com/android/mediproject/core/network/datasource/sign/LoginDataSourceImpl.kt index 80385ace..af4c999d 100644 --- a/core/network/src/main/java/com/android/mediproject/core/network/datasource/sign/LoginDataSourceImpl.kt +++ b/core/network/src/main/java/com/android/mediproject/core/network/datasource/sign/LoginDataSourceImpl.kt @@ -1,6 +1,7 @@ package com.android.mediproject.core.network.datasource.sign import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoDevice +import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUserDetails import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUserPool import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUserSession import com.amazonaws.mobileconnectors.cognitoidentityprovider.continuations.AuthenticationContinuation @@ -8,6 +9,7 @@ import com.amazonaws.mobileconnectors.cognitoidentityprovider.continuations.Auth import com.amazonaws.mobileconnectors.cognitoidentityprovider.continuations.ChallengeContinuation import com.amazonaws.mobileconnectors.cognitoidentityprovider.continuations.MultiFactorAuthenticationContinuation import com.amazonaws.mobileconnectors.cognitoidentityprovider.handlers.AuthenticationHandler +import com.amazonaws.mobileconnectors.cognitoidentityprovider.handlers.GetDetailsHandler import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -16,15 +18,33 @@ class LoginDataSourceImpl( private val userPool: CognitoUserPool, ) : LoginDataSource { - override suspend fun login(request: LoginRequest): Result = suspendCoroutine { continuation -> + override suspend fun login(request: LoginRequest): Result { + val session = getSession(request) + session.fold( + onSuccess = { userSession -> + val attr = getUserAttr(userSession) + return attr.fold( + onSuccess = { userAttr -> + Result.success(LoginResponse(userSession, userAttr)) + }, + onFailure = { Result.failure(it) }, + ) + }, + onFailure = { return Result.failure(it) }, + ) + } + + override suspend fun logout() = userPool.currentUser.signOut() + + private suspend fun getSession(request: LoginRequest) = suspendCoroutine> { userPool.getUser(request.email).getSession( object : AuthenticationHandler { override fun onSuccess(userSession: CognitoUserSession, newDevice: CognitoDevice?) { - continuation.resume(Result.success(LoginResponse(userSession, newDevice))) + it.resume(Result.success(userSession)) } override fun onFailure(exception: Exception) { - continuation.resume(Result.failure(exception)) + it.resume(Result.failure(exception)) // UserNotConfirmedException : 이메일 인증을 하지 않았을 때 발생 } @@ -51,5 +71,17 @@ class LoginDataSourceImpl( ) } - override suspend fun logout() = userPool.currentUser.signOut() + private suspend fun getUserAttr(session: CognitoUserSession) = suspendCoroutine> { + userPool.getUser(session.username).getDetails( + object : GetDetailsHandler { + override fun onSuccess(cognitoUserDetails: CognitoUserDetails) { + it.resume(Result.success(cognitoUserDetails)) + } + + override fun onFailure(exception: Exception) { + it.resume(Result.failure(exception)) + } + }, + ) + } } diff --git a/core/network/src/main/java/com/android/mediproject/core/network/datasource/sign/SignupDataSource.kt b/core/network/src/main/java/com/android/mediproject/core/network/datasource/sign/SignupDataSource.kt index f937361d..52c8941c 100644 --- a/core/network/src/main/java/com/android/mediproject/core/network/datasource/sign/SignupDataSource.kt +++ b/core/network/src/main/java/com/android/mediproject/core/network/datasource/sign/SignupDataSource.kt @@ -10,7 +10,7 @@ interface SignupDataSource { suspend fun resendConfirmationCode(cognitoUser: CognitoUser): Result } -private const val USER_NAME = "custom:user_name" +private const val CUSTOM_USER_NAME = "custom:user_name" class SignUpRequest( val email: String, @@ -20,7 +20,7 @@ class SignUpRequest( val passwordString: String get() = password.decodeToString() val attr = CognitoUserAttributes().apply { - addAttribute(USER_NAME, nickName) + addAttribute(CUSTOM_USER_NAME, nickName) } } diff --git a/feature/mypage/src/main/java/com/android/mediproject/feature/mypage/MyPageFragment.kt b/feature/mypage/src/main/java/com/android/mediproject/feature/mypage/MyPageFragment.kt index a02df16c..7912ad06 100644 --- a/feature/mypage/src/main/java/com/android/mediproject/feature/mypage/MyPageFragment.kt +++ b/feature/mypage/src/main/java/com/android/mediproject/feature/mypage/MyPageFragment.kt @@ -11,6 +11,8 @@ import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager import com.android.mediproject.core.common.util.SystemBarStyler import com.android.mediproject.core.common.viewmodel.UiState +import com.android.mediproject.core.common.viewmodel.repeatOnStarted +import com.android.mediproject.core.model.comments.MyCommentsListResponse import com.android.mediproject.core.model.token.CurrentTokens import com.android.mediproject.core.model.token.TokenState import com.android.mediproject.core.ui.R @@ -19,17 +21,13 @@ import com.android.mediproject.feature.mypage.databinding.FragmentMyPageBinding import com.android.mediproject.feature.mypage.mypagemore.MyPageMoreBottomSheetFragment import com.android.mediproject.feature.mypage.mypagemore.MyPageMoreDialogFragment import dagger.hilt.android.AndroidEntryPoint -import com.android.mediproject.core.common.viewmodel.repeatOnStarted -import com.android.mediproject.core.model.comments.MyCommentsListResponse -import com.android.mediproject.core.model.user.UserEntity +import kotlinx.coroutines.flow.filterNotNull import javax.inject.Inject @AndroidEntryPoint -class MyPageFragment : - BaseFragment(FragmentMyPageBinding::inflate) { +class MyPageFragment : BaseFragment(FragmentMyPageBinding::inflate) { - @Inject - lateinit var systemBarStyler: SystemBarStyler + @Inject lateinit var systemBarStyler: SystemBarStyler override val fragmentViewModel: MyPageViewModel by viewModels() private val myCommentListAdapter: MyPageMyCommentAdapter by lazy { MyPageMyCommentAdapter() } private var myPageMoreBottomSheet: MyPageMoreBottomSheetFragment? = null @@ -40,20 +38,17 @@ class MyPageFragment : setFragmentResultListner() } - private fun setBinding() = - binding.apply { - viewModel = fragmentViewModel.apply { - viewLifecycleOwner.apply { - repeatOnStarted { token.collect { handleToken(it) } } - repeatOnStarted { eventFlow.collect { handleEvent(it) } } - repeatOnStarted { user.collect { handleUserState(it) } } - repeatOnStarted { myCommentsList.collect { handleMyCommentListState(it) } } - } - loadTokens() + private fun setBinding() = binding.apply { + viewModel = fragmentViewModel.apply { + viewLifecycleOwner.apply { + repeatOnStarted { eventFlow.collect { handleEvent(it) } } + repeatOnStarted { myCommentsList.collect { handleMyCommentListState(it) } } + repeatOnStarted { currentUser.filterNotNull().collect { handleUserState(it) } } } - setBarStyle() - setRecyclerView() } + setBarStyle() + setRecyclerView() + } private fun handleToken(tokenState: TokenState) { log(tokenState.toString()) @@ -65,11 +60,11 @@ class MyPageFragment : } } - private fun handleEvent(event: MyPageViewModel.MyPageEvent) = when (event) { - is MyPageViewModel.MyPageEvent.Login -> navigateWithUri("medilens://main/intro_nav/login") - is MyPageViewModel.MyPageEvent.SignUp -> navigateWithUri("medilens://main/intro_nav/signUp") - is MyPageViewModel.MyPageEvent.NavigateToMyCommentList -> navigateWithUri("medilens://main/comments_nav/myCommentsListFragment") - is MyPageViewModel.MyPageEvent.NavigateToMyPageMore -> showMyPageBottomSheet() + private fun handleEvent(event: MyPageEvent) = when (event) { + is MyPageEvent.Login -> navigateWithUri("medilens://main/intro_nav/login") + is MyPageEvent.SignUp -> navigateWithUri("medilens://main/intro_nav/signUp") + is MyPageEvent.NavigateToMyCommentList -> navigateWithUri("medilens://main/comments_nav/myCommentsListFragment") + is MyPageEvent.NavigateToMyPageMore -> showMyPageBottomSheet() } private fun showMyPageBottomSheet() { @@ -95,20 +90,19 @@ class MyPageFragment : } private fun setGuestModeScreenSpan(): SpannableStringBuilder { - val span = - SpannableStringBuilder(getString(com.android.mediproject.feature.mypage.R.string.guestDescription)).apply { - setSpan( - ForegroundColorSpan( - ContextCompat.getColor( - requireContext(), - R.color.main, - ), + val span = SpannableStringBuilder(getString(com.android.mediproject.feature.mypage.R.string.guestDescription)).apply { + setSpan( + ForegroundColorSpan( + ContextCompat.getColor( + requireContext(), + R.color.main, ), - 15, 18, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, - ) - setSpan(UnderlineSpan(), 15, 18, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - } + ), + 15, 18, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + setSpan(UnderlineSpan(), 15, 18, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } return span } @@ -119,25 +113,18 @@ class MyPageFragment : } private fun loginModeScreen() = fragmentViewModel.apply { - loadUser() loadMyCommentsList() setLoginModeScreenVisible() } - private fun handleUserState(userEntityState: UiState) { - when (userEntityState) { - is UiState.Initial -> {} - - is UiState.Loading -> setLoadingUserVisible() + private fun handleUserState(userEntityState: LoginUiState) { + setSuccessUserVisible() + binding.userDto = userEntityState.userEntity - is UiState.Success -> { - setSuccessUserVisible() - binding.userDto = userEntityState.data - } - - is UiState.Error -> { - log(userEntityState.message) - } + if (userEntityState is LoginUiState.Online) { + loginModeScreen() + } else { + guestModeScreen() } } @@ -151,6 +138,7 @@ class MyPageFragment : userNameTV.visibility = View.VISIBLE userImageIV.visibility = View.VISIBLE userLottie.visibility = View.GONE + tokenLottie.visibility = View.GONE } private fun handleMyCommentListState(commentListState: UiState>) { @@ -225,7 +213,6 @@ class MyPageFragment : private fun changeNicknameCallback() { log("MyPageDialog Callback : changeNickname() ") - fragmentViewModel.loadUser() } private fun changePasswordCallback() { @@ -252,24 +239,23 @@ class MyPageFragment : } private fun setNoShowCommentListSpan(): SpannableStringBuilder { - val span = - SpannableStringBuilder(getString(com.android.mediproject.feature.mypage.R.string.noMyComment)).apply { - setSpan( - ForegroundColorSpan( - ContextCompat.getColor( - requireContext(), - R.color.main, - ), + val span = SpannableStringBuilder(getString(com.android.mediproject.feature.mypage.R.string.noMyComment)).apply { + setSpan( + ForegroundColorSpan( + ContextCompat.getColor( + requireContext(), + R.color.main, ), - 7, 9, Spannable.SPAN_INCLUSIVE_INCLUSIVE, - ) - setSpan( - UnderlineSpan(), - 7, - 9, - Spannable.SPAN_INCLUSIVE_INCLUSIVE, - ) - } + ), + 7, 9, Spannable.SPAN_INCLUSIVE_INCLUSIVE, + ) + setSpan( + UnderlineSpan(), + 7, + 9, + Spannable.SPAN_INCLUSIVE_INCLUSIVE, + ) + } return span } diff --git a/feature/mypage/src/main/java/com/android/mediproject/feature/mypage/MyPageViewModel.kt b/feature/mypage/src/main/java/com/android/mediproject/feature/mypage/MyPageViewModel.kt index 416e140d..9925a380 100644 --- a/feature/mypage/src/main/java/com/android/mediproject/feature/mypage/MyPageViewModel.kt +++ b/feature/mypage/src/main/java/com/android/mediproject/feature/mypage/MyPageViewModel.kt @@ -10,12 +10,16 @@ import com.android.mediproject.core.data.session.AccountSessionRepository import com.android.mediproject.core.data.sign.SignRepository import com.android.mediproject.core.domain.GetCommentsUseCase import com.android.mediproject.core.model.comments.MyCommentsListResponse +import com.android.mediproject.core.model.user.UserEntity import com.android.mediproject.core.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -28,7 +32,13 @@ class MyPageViewModel @Inject constructor( @Dispatcher(MediDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, ) : BaseViewModel() { - val currentUser = accountSessionRepository.userOnCurrentSession + val currentUser = accountSessionRepository.userOnCurrentSession.map { + if (it != null) { + LoginUiState.Online(it) + } else { + LoginUiState.Offline() + } + }.stateIn(viewModelScope, SharingStarted.Lazily, null) private val _eventFlow = MutableEventFlow() val eventFlow = _eventFlow.asEventFlow() @@ -36,6 +46,16 @@ class MyPageViewModel @Inject constructor( private val _myCommentsList = MutableStateFlow>>(UiState.Initial) val myCommentsList = _myCommentsList.asStateFlow() + init { + viewModelScope.launch { + withContext(ioDispatcher) { + if (accountSessionRepository.signedIn.not()) { + accountSessionRepository.loadSession() + } + } + } + } + fun event(event: MyPageEvent) = viewModelScope.launch { _eventFlow.emit(event) } fun login() = event(MyPageEvent.Login) @@ -74,3 +94,11 @@ sealed interface MyPageEvent { data object NavigateToMyPageMore : MyPageEvent data object NavigateToMyCommentList : MyPageEvent } + + +sealed interface LoginUiState { + val userEntity: UserEntity + + class Online(override val userEntity: UserEntity) : LoginUiState + class Offline(override val userEntity: UserEntity = UserEntity()) : LoginUiState +}