diff --git a/app/src/main/java/com/into/websoso/data/mapper/UserMapper.kt b/app/src/main/java/com/into/websoso/data/mapper/UserMapper.kt index 94ea7de30..d31a64026 100644 --- a/app/src/main/java/com/into/websoso/data/mapper/UserMapper.kt +++ b/app/src/main/java/com/into/websoso/data/mapper/UserMapper.kt @@ -6,6 +6,7 @@ import com.into.websoso.data.model.GenrePreferenceEntity import com.into.websoso.data.model.MyProfileEntity import com.into.websoso.data.model.NovelPreferenceEntity import com.into.websoso.data.model.OtherUserProfileEntity +import com.into.websoso.data.model.TermsAgreementEntity import com.into.websoso.data.model.UserFeedsEntity import com.into.websoso.data.model.UserFeedsEntity.UserFeedEntity import com.into.websoso.data.model.UserInfoDetailEntity @@ -19,6 +20,7 @@ import com.into.websoso.data.remote.response.GenrePreferenceResponseDto import com.into.websoso.data.remote.response.MyProfileResponseDto import com.into.websoso.data.remote.response.NovelPreferenceResponseDto import com.into.websoso.data.remote.response.OtherUserProfileResponseDto +import com.into.websoso.data.remote.response.TermsAgreementResponseDto import com.into.websoso.data.remote.response.UserFeedsResponseDto import com.into.websoso.data.remote.response.UserFeedsResponseDto.UserFeedResponseDto import com.into.websoso.data.remote.response.UserInfoDetailResponseDto @@ -156,4 +158,12 @@ fun UserFeedResponseDto.toData(): UserFeedEntity { novelRating = this.novelRating, relevantCategories = this.relevantCategories, ) -} \ No newline at end of file +} + +fun TermsAgreementResponseDto.toData(): TermsAgreementEntity { + return TermsAgreementEntity( + serviceAgreed = this.serviceAgreed, + privacyAgreed = this.privacyAgreed, + marketingAgreed = this.marketingAgreed, + ) +} diff --git a/app/src/main/java/com/into/websoso/data/model/TermsAgreementEntity.kt b/app/src/main/java/com/into/websoso/data/model/TermsAgreementEntity.kt new file mode 100644 index 000000000..ea976c779 --- /dev/null +++ b/app/src/main/java/com/into/websoso/data/model/TermsAgreementEntity.kt @@ -0,0 +1,7 @@ +package com.into.websoso.data.model + +data class TermsAgreementEntity( + val serviceAgreed: Boolean, + val privacyAgreed: Boolean, + val marketingAgreed: Boolean, +) diff --git a/app/src/main/java/com/into/websoso/data/remote/api/UserApi.kt b/app/src/main/java/com/into/websoso/data/remote/api/UserApi.kt index 2632fd5ed..8a1994dfe 100644 --- a/app/src/main/java/com/into/websoso/data/remote/api/UserApi.kt +++ b/app/src/main/java/com/into/websoso/data/remote/api/UserApi.kt @@ -1,5 +1,6 @@ package com.into.websoso.data.remote.api +import com.into.websoso.data.remote.request.TermsAgreementRequestDto import com.into.websoso.data.remote.request.UserInfoRequestDto import com.into.websoso.data.remote.request.UserProfileEditRequestDto import com.into.websoso.data.remote.request.UserProfileStatusRequestDto @@ -8,6 +9,7 @@ import com.into.websoso.data.remote.response.GenrePreferenceResponseDto import com.into.websoso.data.remote.response.MyProfileResponseDto import com.into.websoso.data.remote.response.NovelPreferenceResponseDto import com.into.websoso.data.remote.response.OtherUserProfileResponseDto +import com.into.websoso.data.remote.response.TermsAgreementResponseDto import com.into.websoso.data.remote.response.UserFeedsResponseDto import com.into.websoso.data.remote.response.UserInfoDetailResponseDto import com.into.websoso.data.remote.response.UserInfoResponseDto @@ -106,4 +108,12 @@ interface UserApi { @Query("lastFeedId") lastFeedId: Long, @Query("size") size: Int, ): UserFeedsResponseDto + + @PATCH("users/terms-settings") + suspend fun patchTermsAgreement( + @Body termsAgreementRequestDto: TermsAgreementRequestDto, + ) + + @GET("users/terms-settings") + suspend fun getTermsAgreement(): TermsAgreementResponseDto } diff --git a/app/src/main/java/com/into/websoso/data/remote/request/TermsAgreementRequestDto.kt b/app/src/main/java/com/into/websoso/data/remote/request/TermsAgreementRequestDto.kt new file mode 100644 index 000000000..58cfe6f03 --- /dev/null +++ b/app/src/main/java/com/into/websoso/data/remote/request/TermsAgreementRequestDto.kt @@ -0,0 +1,14 @@ +package com.into.websoso.data.remote.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class TermsAgreementRequestDto( + @SerialName("serviceAgreed") + val serviceAgreed: Boolean, + @SerialName("privacyAgreed") + val privacyAgreed: Boolean, + @SerialName("marketingAgreed") + val marketingAgreed: Boolean, +) diff --git a/app/src/main/java/com/into/websoso/data/remote/response/TermsAgreementResponseDto.kt b/app/src/main/java/com/into/websoso/data/remote/response/TermsAgreementResponseDto.kt new file mode 100644 index 000000000..eace932b4 --- /dev/null +++ b/app/src/main/java/com/into/websoso/data/remote/response/TermsAgreementResponseDto.kt @@ -0,0 +1,14 @@ +package com.into.websoso.data.remote.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class TermsAgreementResponseDto( + @SerialName("serviceAgreed") + val serviceAgreed: Boolean, + @SerialName("privacyAgreed") + val privacyAgreed: Boolean, + @SerialName("marketingAgreed") + val marketingAgreed: Boolean, +) diff --git a/app/src/main/java/com/into/websoso/data/repository/UserRepository.kt b/app/src/main/java/com/into/websoso/data/repository/UserRepository.kt index faf813b4d..e9b730424 100644 --- a/app/src/main/java/com/into/websoso/data/repository/UserRepository.kt +++ b/app/src/main/java/com/into/websoso/data/repository/UserRepository.kt @@ -11,6 +11,7 @@ import com.into.websoso.data.model.GenrePreferenceEntity import com.into.websoso.data.model.MyProfileEntity import com.into.websoso.data.model.NovelPreferenceEntity import com.into.websoso.data.model.OtherUserProfileEntity +import com.into.websoso.data.model.TermsAgreementEntity import com.into.websoso.data.model.UserFeedsEntity import com.into.websoso.data.model.UserInfoDetailEntity import com.into.websoso.data.model.UserInfoEntity @@ -18,159 +19,195 @@ import com.into.websoso.data.model.UserNovelStatsEntity import com.into.websoso.data.model.UserProfileStatusEntity import com.into.websoso.data.model.UserStorageEntity import com.into.websoso.data.remote.api.UserApi +import com.into.websoso.data.remote.request.TermsAgreementRequestDto import com.into.websoso.data.remote.request.UserInfoRequestDto import com.into.websoso.data.remote.request.UserProfileEditRequestDto import com.into.websoso.data.remote.request.UserProfileStatusRequestDto +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import javax.inject.Inject class UserRepository - @Inject - constructor( - private val userApi: UserApi, - private val userStorage: DataStore, +@Inject +constructor( + private val userApi: UserApi, + private val userStorage: DataStore, +) { + suspend fun fetchUserInfo(): UserInfoEntity { + val userInfo = userApi.getUserInfo().toData() + saveUserInfo(userInfo.userId, userInfo.nickname, userInfo.gender) + return userInfo + } + + private suspend fun saveUserInfo( + userId: Long, + nickname: String, + gender: String, ) { - suspend fun fetchUserInfo(): UserInfoEntity { - val userInfo = userApi.getUserInfo().toData() - saveUserInfo(userInfo.userId, userInfo.nickname, userInfo.gender) - return userInfo + userStorage.edit { preferences -> + preferences[USER_ID_KEY] = userId.toString() + preferences[USER_NICKNAME_KEY] = nickname + preferences[USER_GENDER_KEY] = gender } + } - private suspend fun saveUserInfo( - userId: Long, - nickname: String, - gender: String, - ) { - userStorage.edit { preferences -> - preferences[USER_ID_KEY] = userId.toString() - preferences[USER_NICKNAME_KEY] = nickname - preferences[USER_GENDER_KEY] = gender - } - } + suspend fun fetchUserInfoDetail(): UserInfoDetailEntity = userApi.getUserInfoDetail().toData() - suspend fun fetchUserInfoDetail(): UserInfoDetailEntity = userApi.getUserInfoDetail().toData() + suspend fun fetchBlockedUsers(): BlockedUsersEntity = userApi.getBlockedUser().toData() - suspend fun fetchBlockedUsers(): BlockedUsersEntity = userApi.getBlockedUser().toData() + suspend fun deleteBlockedUser(blockId: Long) { + userApi.deleteBlockedUser(blockId) + } - suspend fun deleteBlockedUser(blockId: Long) { - userApi.deleteBlockedUser(blockId) - } + suspend fun saveBlockUser(userId: Long) { + userApi.postBlockUser(userId) + } - suspend fun saveBlockUser(userId: Long) { - userApi.postBlockUser(userId) - } + suspend fun fetchUserNovelStats(userId: Long): UserNovelStatsEntity = + userApi.getUserNovelStats(userId).toData() + + suspend fun fetchUserProfileStatus(): UserProfileStatusEntity = + userApi.getProfileStatus().toData() - suspend fun fetchUserNovelStats(userId: Long): UserNovelStatsEntity = userApi.getUserNovelStats(userId).toData() + suspend fun saveUserProfileStatus(isProfilePublic: Boolean) { + userApi.patchProfileStatus(UserProfileStatusRequestDto(isProfilePublic)) + } - suspend fun fetchUserProfileStatus(): UserProfileStatusEntity = userApi.getProfileStatus().toData() + suspend fun saveUserInfoDetail( + gender: String, + birthYear: Int, + ) { + userApi.putUserInfo(UserInfoRequestDto(gender, birthYear)) + } - suspend fun saveUserProfileStatus(isProfilePublic: Boolean) { - userApi.patchProfileStatus(UserProfileStatusRequestDto(isProfilePublic)) - } + suspend fun fetchMyProfile(): MyProfileEntity = userApi.getMyProfile().toData() - suspend fun saveUserInfoDetail( - gender: String, - birthYear: Int, - ) { - userApi.putUserInfo(UserInfoRequestDto(gender, birthYear)) + suspend fun fetchGenrePreference(userId: Long): List = + userApi.getGenrePreference(userId).genrePreferences.map { + it.toData() } - suspend fun fetchMyProfile(): MyProfileEntity = userApi.getMyProfile().toData() - - suspend fun fetchGenrePreference(userId: Long): List = - userApi.getGenrePreference(userId).genrePreferences.map { - it.toData() - } + suspend fun fetchNovelPreferences(userId: Long): NovelPreferenceEntity = + userApi.getNovelPreferences(userId).toData() - suspend fun fetchNovelPreferences(userId: Long): NovelPreferenceEntity = userApi.getNovelPreferences(userId).toData() + suspend fun fetchOtherUserProfile(userId: Long): OtherUserProfileEntity = + userApi.getOtherUserProfile(userId).toData() - suspend fun fetchOtherUserProfile(userId: Long): OtherUserProfileEntity = userApi.getOtherUserProfile(userId).toData() + suspend fun saveEditingUserProfile( + avatarId: Int?, + nickname: String?, + intro: String?, + genrePreferences: List, + ) { + userApi.patchProfile(UserProfileEditRequestDto(avatarId, nickname, intro, genrePreferences)) + } - suspend fun saveEditingUserProfile( - avatarId: Int?, - nickname: String?, - intro: String?, - genrePreferences: List, - ) { - userApi.patchProfile(UserProfileEditRequestDto(avatarId, nickname, intro, genrePreferences)) + suspend fun saveNovelDetailFirstLaunched(value: Boolean) { + userStorage.edit { preferences -> + preferences[NOVEL_DETAIL_FIRST_LAUNCHED_KEY] = value } + } - suspend fun saveNovelDetailFirstLaunched(value: Boolean) { - userStorage.edit { preferences -> - preferences[NOVEL_DETAIL_FIRST_LAUNCHED_KEY] = value - } - } + suspend fun fetchUserId(): Long { + val preferences = userStorage.data.first() + return preferences[USER_ID_KEY]?.toLongOrNull() ?: DEFAULT_USER_ID + } - suspend fun fetchUserId(): Long { - val preferences = userStorage.data.first() - return preferences[USER_ID_KEY]?.toLongOrNull() ?: DEFAULT_USER_ID - } + suspend fun fetchIsLogin(): Boolean = fetchUserId() != DEFAULT_USER_ID - suspend fun fetchIsLogin(): Boolean = fetchUserId() != DEFAULT_USER_ID + suspend fun fetchGender(): String { + val preferences = userStorage.data.first() + return preferences[USER_GENDER_KEY] ?: DEFAULT_USER_GENDER + } - suspend fun fetchGender(): String { - val preferences = userStorage.data.first() - return preferences[USER_GENDER_KEY] ?: DEFAULT_USER_GENDER + suspend fun saveGender(gender: String) { + userStorage.edit { preferences -> + preferences[USER_GENDER_KEY] = gender } + } - suspend fun saveGender(gender: String) { - userStorage.edit { preferences -> - preferences[USER_GENDER_KEY] = gender - } - } + suspend fun fetchNovelDetailFirstLaunched() = + userStorage.data.first()[NOVEL_DETAIL_FIRST_LAUNCHED_KEY] ?: true + + suspend fun fetchNicknameValidity(nickname: String): Boolean = + userApi.getNicknameValidity(nickname).isValid + + suspend fun fetchUserStorage( + userId: Long, + readStatus: String, + lastUserNovelId: Long, + size: Int, + sortType: String, + ): UserStorageEntity = + userApi + .getUserStorage( + userId = userId, + readStatus = readStatus, + lastUserNovelId = lastUserNovelId, + size = size, + sortType = sortType, + ).toData() + + suspend fun fetchUserFeeds( + userId: Long, + lastFeedId: Long, + size: Int, + ): UserFeedsEntity = userApi.getUserFeeds(userId, lastFeedId, size).toData() + + suspend fun fetchMyActivities( + lastFeedId: Long, + size: Int, + ): UserFeedsEntity { + val myUserId = fetchUserId() + return fetchUserFeeds(myUserId, lastFeedId, size) + } - suspend fun fetchNovelDetailFirstLaunched() = userStorage.data.first()[NOVEL_DETAIL_FIRST_LAUNCHED_KEY] ?: true - - suspend fun fetchNicknameValidity(nickname: String): Boolean = userApi.getNicknameValidity(nickname).isValid - - suspend fun fetchUserStorage( - userId: Long, - readStatus: String, - lastUserNovelId: Long, - size: Int, - sortType: String, - ): UserStorageEntity = - userApi - .getUserStorage( - userId = userId, - readStatus = readStatus, - lastUserNovelId = lastUserNovelId, - size = size, - sortType = sortType, - ).toData() - - suspend fun fetchUserFeeds( - userId: Long, - lastFeedId: Long, - size: Int, - ): UserFeedsEntity = userApi.getUserFeeds(userId, lastFeedId, size).toData() - - suspend fun fetchMyActivities( - lastFeedId: Long, - size: Int, - ): UserFeedsEntity { - val myUserId = fetchUserId() - return fetchUserFeeds(myUserId, lastFeedId, size) - } + suspend fun fetchUserDeviceIdentifier(): String { + val preferences = userStorage.data.first() + return preferences[USER_DEVICE_IDENTIFIER_KEY] ?: "" + } - suspend fun fetchUserDeviceIdentifier(): String { - val preferences = userStorage.data.first() - return preferences[USER_DEVICE_IDENTIFIER_KEY] ?: "" + suspend fun saveUserDeviceIdentifier(deviceIdentifier: String) { + userStorage.edit { preferences -> + preferences[USER_DEVICE_IDENTIFIER_KEY] = deviceIdentifier } + } - suspend fun saveUserDeviceIdentifier(deviceIdentifier: String) { - userStorage.edit { preferences -> - preferences[USER_DEVICE_IDENTIFIER_KEY] = deviceIdentifier - } - } + val isTermsAgreementChecked: Flow = userStorage.data + .map { preferences -> preferences[TERMS_AGREEMENT_CHECKED_KEY] ?: false } + + suspend fun saveTermsAgreements( + serviceAgreed: Boolean, + privacyAgreed: Boolean, + marketingAgreed: Boolean, + ) { + userApi.patchTermsAgreement( + TermsAgreementRequestDto(serviceAgreed, privacyAgreed, marketingAgreed) + ) + saveTermsAgreementChecked(serviceAgreed, privacyAgreed) + } - companion object { - val NOVEL_DETAIL_FIRST_LAUNCHED_KEY = booleanPreferencesKey("NOVEL_DETAIL_FIRST_LAUNCHED") - val USER_ID_KEY = stringPreferencesKey("USER_ID") - val USER_NICKNAME_KEY = stringPreferencesKey("USER_NICKNAME") - val USER_GENDER_KEY = stringPreferencesKey("USER_GENDER") - val USER_DEVICE_IDENTIFIER_KEY = stringPreferencesKey("USER_DEVICE_IDENTIFIER") - const val DEFAULT_USER_ID = -1L - const val DEFAULT_USER_GENDER = "F" + suspend fun fetchTermsAgreements(): TermsAgreementEntity { + val termsAgreement = userApi.getTermsAgreement().toData() + saveTermsAgreementChecked(termsAgreement.serviceAgreed, termsAgreement.privacyAgreed) + return termsAgreement + } + + private suspend fun saveTermsAgreementChecked(serviceAgreed: Boolean, privacyAgreed: Boolean) { + userStorage.edit { preferences -> + preferences[TERMS_AGREEMENT_CHECKED_KEY] = serviceAgreed && privacyAgreed } } + + companion object { + val NOVEL_DETAIL_FIRST_LAUNCHED_KEY = booleanPreferencesKey("NOVEL_DETAIL_FIRST_LAUNCHED") + val TERMS_AGREEMENT_CHECKED_KEY = booleanPreferencesKey("terms_agreement_checked") + val USER_ID_KEY = stringPreferencesKey("USER_ID") + val USER_NICKNAME_KEY = stringPreferencesKey("USER_NICKNAME") + val USER_GENDER_KEY = stringPreferencesKey("USER_GENDER") + val USER_DEVICE_IDENTIFIER_KEY = stringPreferencesKey("USER_DEVICE_IDENTIFIER") + const val DEFAULT_USER_ID = -1L + const val DEFAULT_USER_GENDER = "F" + } +} diff --git a/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt b/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt index 09f37a42a..d704fe6a5 100644 --- a/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt +++ b/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt @@ -17,6 +17,7 @@ import com.into.websoso.core.common.ui.model.ResultFrom.FeedDetailRemoved import com.into.websoso.core.common.ui.model.ResultFrom.NormalExploreBack import com.into.websoso.core.common.ui.model.ResultFrom.NovelDetailBack import com.into.websoso.core.common.ui.model.ResultFrom.ProfileEditSuccess +import com.into.websoso.core.common.util.collectWithLifecycle import com.into.websoso.core.common.util.tracker.Tracker import com.into.websoso.databinding.FragmentHomeBinding import com.into.websoso.ui.feedDetail.FeedDetailActivity @@ -91,7 +92,6 @@ class HomeFragment : BaseFragment(R.layout.fragment_home) { onPostInterestNovelClick() onSettingPreferenceGenreClick() onNotificationButtonClick() - showTermsAgreementDialog() tracker.trackEvent("home") } @@ -156,6 +156,12 @@ class HomeFragment : BaseFragment(R.layout.fragment_home) { showNotificationPermissionDialog() } } + + homeViewModel.showTermsAgreementDialog.collectWithLifecycle(viewLifecycleOwner) { shouldShow -> + if (shouldShow) { + showTermsAgreementDialog() + } + } } private fun updateUserInterestFeedsVisibility(isUserInterestEmpty: Boolean) { diff --git a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt index 9ee6339c3..67f30eedd 100644 --- a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt +++ b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import com.into.websoso.data.model.PopularFeedsEntity import com.into.websoso.data.model.PopularNovelsEntity import com.into.websoso.data.model.RecommendedNovelsByUserTasteEntity +import com.into.websoso.data.model.TermsAgreementEntity import com.into.websoso.data.model.UserInterestFeedMessage import com.into.websoso.data.model.UserInterestFeedMessage.NO_INTEREST_NOVELS import com.into.websoso.data.model.UserInterestFeedsEntity @@ -14,208 +15,242 @@ import com.into.websoso.data.repository.FeedRepository import com.into.websoso.data.repository.NotificationRepository import com.into.websoso.data.repository.NovelRepository import com.into.websoso.data.repository.PushMessageRepository +import com.into.websoso.data.repository.UserRepository import com.into.websoso.ui.main.home.model.HomeUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class HomeViewModel - @Inject - constructor( - private val novelRepository: NovelRepository, - private val feedRepository: FeedRepository, - private val pushMessageRepository: PushMessageRepository, - private val notificationRepository: NotificationRepository, - ) : ViewModel() { - private val _uiState: MutableLiveData = MutableLiveData(HomeUiState()) - val uiState: LiveData get() = _uiState - - private val _isNotificationPermissionFirstLaunched: MutableLiveData = MutableLiveData() - val isNotificationPermissionFirstLaunched: LiveData get() = _isNotificationPermissionFirstLaunched - - init { - updateHomeData(true) - updateNotificationUnread() - } +@Inject +constructor( + private val novelRepository: NovelRepository, + private val feedRepository: FeedRepository, + private val pushMessageRepository: PushMessageRepository, + private val notificationRepository: NotificationRepository, + private val userRepository: UserRepository, +) : ViewModel() { + private val _uiState: MutableLiveData = MutableLiveData(HomeUiState()) + val uiState: LiveData get() = _uiState - private fun updateHomeData(isLogin: Boolean) { - viewModelScope.launch { - if (isLogin) { - fetchUserHomeData() - checkIsNotificationPermissionFirstLaunched() - } else { - fetchGuestData() - } + private val _isNotificationPermissionFirstLaunched: MutableLiveData = MutableLiveData() + val isNotificationPermissionFirstLaunched: LiveData get() = _isNotificationPermissionFirstLaunched + + private val _termsAgreementState = MutableStateFlow(null) + val termsAgreementState: StateFlow = _termsAgreementState.asStateFlow() + + private val _showTermsAgreementDialog = MutableStateFlow(false) + val showTermsAgreementDialog: StateFlow = _showTermsAgreementDialog.asStateFlow() + + init { + updateHomeData(true) + updateNotificationUnread() + checkTermsAgreement() + } + + private fun updateHomeData(isLogin: Boolean) { + + viewModelScope.launch { + if (isLogin) { + fetchUserHomeData() + checkIsNotificationPermissionFirstLaunched() + } else { + fetchGuestData() } } + } + + private suspend fun fetchUserHomeData() { + viewModelScope.launch { + runCatching { + val results = listOf( + async { runCatching { novelRepository.fetchPopularNovels() } }, + async { runCatching { feedRepository.fetchPopularFeeds() } }, + async { runCatching { feedRepository.fetchUserInterestFeeds() } }, + async { runCatching { novelRepository.fetchRecommendedNovelsByUserTaste() } }, + ).awaitAll() - private suspend fun fetchUserHomeData() { - viewModelScope.launch { - runCatching { - val results = listOf( - async { runCatching { novelRepository.fetchPopularNovels() } }, - async { runCatching { feedRepository.fetchPopularFeeds() } }, - async { runCatching { feedRepository.fetchUserInterestFeeds() } }, - async { runCatching { novelRepository.fetchRecommendedNovelsByUserTaste() } }, - ).awaitAll() - - // 실패가 하나라도 있다면 상위 onFailure로 예외 전파 - val failures = results.filter { it.isFailure } - if (failures.isNotEmpty()) { - throw failures.first().exceptionOrNull() - ?: IllegalStateException("Unknown error") - } - - val popularNovels = results[0].getOrNull() as? PopularNovelsEntity - ?: PopularNovelsEntity(emptyList()) - val popularFeeds = results[1].getOrNull() as? PopularFeedsEntity - ?: PopularFeedsEntity(emptyList()) - val userInterestFeeds = results[2].getOrNull() as? UserInterestFeedsEntity - ?: UserInterestFeedsEntity(emptyList(), "") - val recommendedNovels = - results[3].getOrNull() as? RecommendedNovelsByUserTasteEntity - ?: RecommendedNovelsByUserTasteEntity(emptyList()) - - _uiState.value = uiState.value?.copy( - loading = false, - error = false, - popularNovels = popularNovels.popularNovels, - popularFeeds = popularFeeds.popularFeeds.chunked(3), - isInterestNovel = isUserInterestedInNovels(userInterestFeeds.message), - userInterestFeeds = userInterestFeeds.userInterestFeeds, - recommendedNovelsByUserTaste = recommendedNovels.tasteNovels, - ) - }.onFailure { - _uiState.value = uiState.value?.copy( - loading = false, - error = true, - ) + // 실패가 하나라도 있다면 상위 onFailure로 예외 전파 + val failures = results.filter { it.isFailure } + if (failures.isNotEmpty()) { + throw failures.first().exceptionOrNull() + ?: IllegalStateException("Unknown error") } + + val popularNovels = results[0].getOrNull() as? PopularNovelsEntity + ?: PopularNovelsEntity(emptyList()) + val popularFeeds = results[1].getOrNull() as? PopularFeedsEntity + ?: PopularFeedsEntity(emptyList()) + val userInterestFeeds = results[2].getOrNull() as? UserInterestFeedsEntity + ?: UserInterestFeedsEntity(emptyList(), "") + val recommendedNovels = + results[3].getOrNull() as? RecommendedNovelsByUserTasteEntity + ?: RecommendedNovelsByUserTasteEntity(emptyList()) + + _uiState.value = uiState.value?.copy( + loading = false, + error = false, + popularNovels = popularNovels.popularNovels, + popularFeeds = popularFeeds.popularFeeds.chunked(3), + isInterestNovel = isUserInterestedInNovels(userInterestFeeds.message), + userInterestFeeds = userInterestFeeds.userInterestFeeds, + recommendedNovelsByUserTaste = recommendedNovels.tasteNovels, + ) + }.onFailure { + _uiState.value = uiState.value?.copy( + loading = false, + error = true, + ) } } + } - private fun checkIsNotificationPermissionFirstLaunched() { - viewModelScope.launch { - runCatching { - pushMessageRepository.fetchNotificationPermissionFirstLaunched() - }.onSuccess { isFirstLaunched -> - _isNotificationPermissionFirstLaunched.value = isFirstLaunched - } + private fun checkIsNotificationPermissionFirstLaunched() { + viewModelScope.launch { + runCatching { + pushMessageRepository.fetchNotificationPermissionFirstLaunched() + }.onSuccess { isFirstLaunched -> + _isNotificationPermissionFirstLaunched.value = isFirstLaunched } } + } - fun updateIsNotificationPermissionFirstLaunched(isFirstLaunched: Boolean) { - viewModelScope.launch { - runCatching { - pushMessageRepository.saveNotificationPermissionFirstLaunched(isFirstLaunched) - }.onSuccess { - _isNotificationPermissionFirstLaunched.value = isFirstLaunched - } + fun updateIsNotificationPermissionFirstLaunched(isFirstLaunched: Boolean) { + viewModelScope.launch { + runCatching { + pushMessageRepository.saveNotificationPermissionFirstLaunched(isFirstLaunched) + }.onSuccess { + _isNotificationPermissionFirstLaunched.value = isFirstLaunched } } + } - private suspend fun fetchGuestData() { - viewModelScope.launch { - runCatching { - listOf( - async { novelRepository.fetchPopularNovels() }, - async { feedRepository.fetchPopularFeeds() }, - ).awaitAll() - }.onSuccess { responses -> - val popularNovels = responses[0] as PopularNovelsEntity - val popularFeeds = responses[1] as PopularFeedsEntity - - _uiState.value = uiState.value?.copy( - loading = false, - popularNovels = popularNovels.popularNovels, - popularFeeds = popularFeeds.popularFeeds.chunked(3), - ) - }.onFailure { - _uiState.value = uiState.value?.copy( - loading = false, - error = true, - ) - } + private suspend fun fetchGuestData() { + viewModelScope.launch { + runCatching { + listOf( + async { novelRepository.fetchPopularNovels() }, + async { feedRepository.fetchPopularFeeds() }, + ).awaitAll() + }.onSuccess { responses -> + val popularNovels = responses[0] as PopularNovelsEntity + val popularFeeds = responses[1] as PopularFeedsEntity + + _uiState.value = uiState.value?.copy( + loading = false, + popularNovels = popularNovels.popularNovels, + popularFeeds = popularFeeds.popularFeeds.chunked(3), + ) + }.onFailure { + _uiState.value = uiState.value?.copy( + loading = false, + error = true, + ) } } + } - fun updateFeed() { - viewModelScope.launch { - runCatching { - listOf( - async { feedRepository.fetchPopularFeeds() }, - async { feedRepository.fetchUserInterestFeeds() }, - ).awaitAll() - }.onSuccess { responses -> - val popularFeeds = responses[0] as PopularFeedsEntity - val userInterestFeeds = responses[1] as UserInterestFeedsEntity - - _uiState.value = uiState.value?.copy( - popularFeeds = popularFeeds.popularFeeds.chunked(3), - isInterestNovel = isUserInterestedInNovels(userInterestFeeds.message), - userInterestFeeds = userInterestFeeds.userInterestFeeds, - ) - }.onFailure { - _uiState.value = uiState.value?.copy( - error = true, - ) - } + fun updateFeed() { + viewModelScope.launch { + runCatching { + listOf( + async { feedRepository.fetchPopularFeeds() }, + async { feedRepository.fetchUserInterestFeeds() }, + ).awaitAll() + }.onSuccess { responses -> + val popularFeeds = responses[0] as PopularFeedsEntity + val userInterestFeeds = responses[1] as UserInterestFeedsEntity + + _uiState.value = uiState.value?.copy( + popularFeeds = popularFeeds.popularFeeds.chunked(3), + isInterestNovel = isUserInterestedInNovels(userInterestFeeds.message), + userInterestFeeds = userInterestFeeds.userInterestFeeds, + ) + }.onFailure { + _uiState.value = uiState.value?.copy( + error = true, + ) } } + } - fun updateNovel() { - viewModelScope.launch { - runCatching { - listOf( - async { novelRepository.fetchPopularNovels() }, - async { novelRepository.fetchRecommendedNovelsByUserTaste() }, - ).awaitAll() - }.onSuccess { responses -> - val popularNovels = responses[0] as PopularNovelsEntity - val recommendedNovels = responses[1] as RecommendedNovelsByUserTasteEntity - - _uiState.value = uiState.value?.copy( - popularNovels = popularNovels.popularNovels, - recommendedNovelsByUserTaste = recommendedNovels.tasteNovels, - ) - }.onFailure { - _uiState.value = uiState.value?.copy( - error = true, - ) - } + fun updateNovel() { + viewModelScope.launch { + runCatching { + listOf( + async { novelRepository.fetchPopularNovels() }, + async { novelRepository.fetchRecommendedNovelsByUserTaste() }, + ).awaitAll() + }.onSuccess { responses -> + val popularNovels = responses[0] as PopularNovelsEntity + val recommendedNovels = responses[1] as RecommendedNovelsByUserTasteEntity + + _uiState.value = uiState.value?.copy( + popularNovels = popularNovels.popularNovels, + recommendedNovelsByUserTaste = recommendedNovels.tasteNovels, + ) + }.onFailure { + _uiState.value = uiState.value?.copy( + error = true, + ) } } + } - private fun updateNotificationUnread() { - viewModelScope.launch { - runCatching { - notificationRepository.fetchNotificationUnread() - }.onSuccess { isNotificationUnread -> - _uiState.value = uiState.value?.copy( - isNotificationUnread = isNotificationUnread, - ) - }.onFailure { - _uiState.value = uiState.value?.copy( - error = true, - ) - } + private fun updateNotificationUnread() { + viewModelScope.launch { + runCatching { + notificationRepository.fetchNotificationUnread() + }.onSuccess { isNotificationUnread -> + _uiState.value = uiState.value?.copy( + isNotificationUnread = isNotificationUnread, + ) + }.onFailure { + _uiState.value = uiState.value?.copy( + error = true, + ) } } + } + + private fun isUserInterestedInNovels(userInterestFeedMessage: String): Boolean = + when (UserInterestFeedMessage.fromMessage(userInterestFeedMessage)) { + NO_INTEREST_NOVELS -> false + else -> true + } - private fun isUserInterestedInNovels(userInterestFeedMessage: String): Boolean = - when (UserInterestFeedMessage.fromMessage(userInterestFeedMessage)) { - NO_INTEREST_NOVELS -> false - else -> true + fun updateFCMToken(token: String) { + viewModelScope.launch { + runCatching { + pushMessageRepository.saveUserFCMToken(token) } + } + } - fun updateFCMToken(token: String) { - viewModelScope.launch { - runCatching { - pushMessageRepository.saveUserFCMToken(token) + private fun checkTermsAgreement() { + viewModelScope.launch { + userRepository.isTermsAgreementChecked.collect { checked -> + if (!checked) { + updateTermsAgreement() } } } } + + private fun updateTermsAgreement() { + viewModelScope.launch { + runCatching { userRepository.fetchTermsAgreements() } + .onSuccess { terms -> + + _termsAgreementState.value = terms + _showTermsAgreementDialog.value = !(terms.serviceAgreed && terms.privacyAgreed) + } + } + } +} diff --git a/app/src/main/java/com/into/websoso/ui/termsAgreement/TermsAgreementDialogBottomSheet.kt b/app/src/main/java/com/into/websoso/ui/termsAgreement/TermsAgreementDialogBottomSheet.kt index a476cd5ad..ecb11c798 100644 --- a/app/src/main/java/com/into/websoso/ui/termsAgreement/TermsAgreementDialogBottomSheet.kt +++ b/app/src/main/java/com/into/websoso/ui/termsAgreement/TermsAgreementDialogBottomSheet.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.View +import android.widget.Toast import androidx.core.os.bundleOf import androidx.fragment.app.viewModels import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -94,13 +95,13 @@ class TermsAgreementDialogBottomSheet : } private fun onTermsAgreementCompleteButtonClick() { - binding.btnTermsAgreementComplete.setOnClickListener { dismissIfAgreementsCompleted() } + binding.btnTermsAgreementComplete.setOnClickListener { sendTermsAgreement() } } - private fun dismissIfAgreementsCompleted() { - if (termsAgreementViewModel.isRequiredAgreementsChecked.value) { - dismiss() - } + private fun sendTermsAgreement() { + if (!termsAgreementViewModel.isRequiredAgreementsChecked.value) return // 필수 항목 미체크 시 요청 안 함 + + termsAgreementViewModel.saveTermsAgreements() } private fun setupViewModel() { @@ -112,6 +113,12 @@ class TermsAgreementDialogBottomSheet : termsAgreementViewModel.isRequiredAgreementsChecked.collectWithLifecycle(viewLifecycleOwner) { updateCompleteButtonState(it) } + + termsAgreementViewModel.saveAgreementResult.collectWithLifecycle(viewLifecycleOwner) { result -> + result?.onSuccess { + dismiss() + } + } } private fun updateAgreementIcons(status: Map) { diff --git a/app/src/main/java/com/into/websoso/ui/termsAgreement/TermsAgreementViewModel.kt b/app/src/main/java/com/into/websoso/ui/termsAgreement/TermsAgreementViewModel.kt index b91f3b9a2..ccfcc8e90 100644 --- a/app/src/main/java/com/into/websoso/ui/termsAgreement/TermsAgreementViewModel.kt +++ b/app/src/main/java/com/into/websoso/ui/termsAgreement/TermsAgreementViewModel.kt @@ -2,6 +2,8 @@ package com.into.websoso.ui.termsAgreement import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.into.websoso.data.remote.request.TermsAgreementRequestDto +import com.into.websoso.data.repository.UserRepository import com.into.websoso.ui.termsAgreement.model.AgreementType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -11,11 +13,13 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class TermsAgreementViewModel @Inject constructor() : ViewModel() { - +class TermsAgreementViewModel @Inject constructor( + private val userRepository: UserRepository, +) : ViewModel() { private val _agreementStatus = MutableStateFlow( mapOf( AgreementType.SERVICE to false, @@ -25,9 +29,11 @@ class TermsAgreementViewModel @Inject constructor() : ViewModel() { ) val agreementStatus: StateFlow> = _agreementStatus.asStateFlow() - val isAllChecked: StateFlow = agreementStatus - .map { it.values.all { checked -> checked } } - .stateIn(viewModelScope, SharingStarted.Lazily, false) + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading.asStateFlow() + + private val _saveAgreementResult = MutableStateFlow?>(null) + val saveAgreementResult: StateFlow?> = _saveAgreementResult.asStateFlow() val isRequiredAgreementsChecked: StateFlow = agreementStatus .map { isRequiredAgreementChecked(it) } @@ -49,4 +55,28 @@ class TermsAgreementViewModel @Inject constructor() : ViewModel() { } } } + + fun saveTermsAgreements() { + if (!isRequiredAgreementsChecked.value) return + + viewModelScope.launch { + _isLoading.value = true + + val agreementRequest = TermsAgreementRequestDto( + serviceAgreed = _agreementStatus.value[AgreementType.SERVICE] == true, + privacyAgreed = _agreementStatus.value[AgreementType.PRIVACY] == true, + marketingAgreed = _agreementStatus.value[AgreementType.MARKETING] == true, + ) + + _saveAgreementResult.value = runCatching { + userRepository.saveTermsAgreements( + agreementRequest.serviceAgreed, + agreementRequest.privacyAgreed, + agreementRequest.marketingAgreed, + ) + } + + _isLoading.value = false + } + } }