diff --git a/app/src/main/java/com/into/websoso/core/common/util/ExtentionFuction.kt b/app/src/main/java/com/into/websoso/core/common/util/ExtentionFuction.kt index b6506be20..d0d73bdfa 100644 --- a/app/src/main/java/com/into/websoso/core/common/util/ExtentionFuction.kt +++ b/app/src/main/java/com/into/websoso/core/common/util/ExtentionFuction.kt @@ -21,9 +21,15 @@ import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.preferencesDataStoreFile +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.into.websoso.BuildConfig import com.into.websoso.core.common.ui.custom.WebsosoCustomSnackBar import com.into.websoso.core.common.ui.custom.WebsosoCustomToast +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch import java.io.Serializable fun Float.toFloatPxFromDp(): Float = this * Resources.getSystem().displayMetrics.density @@ -96,7 +102,8 @@ inline fun Intent.getAdaptedSerializableExtra(key: St getSerializableExtra(key, T::class.java) } else { @Suppress("DEPRECATION") - getSerializableExtra(key) as? T + getSerializableExtra(key) + as? T } inline fun Intent.getAdaptedParcelableExtra(key: String): T? = @@ -104,7 +111,8 @@ inline fun Intent.getAdaptedParcelableExtra(key: String getParcelableExtra(key, T::class.java) } else { @Suppress("DEPRECATION") - getParcelableExtra(key) as? T + getParcelableExtra(key) + as? T } inline fun Bundle.getAdaptedParcelable(key: String): T? = @@ -112,7 +120,8 @@ inline fun Bundle.getAdaptedParcelable(key: String): T? getParcelable(key, T::class.java) } else { @Suppress("DEPRECATION") - getParcelable(key) as? T + getParcelable(key) + as? T } fun Context.createDataStore(preferencesName: String): DataStore = @@ -137,3 +146,15 @@ fun Modifier.clickableWithoutRipple( onClick = onClick, ) } + +fun Flow.collectWithLifecycle( + lifecycleOwner: LifecycleOwner, + state: Lifecycle.State = Lifecycle.State.STARTED, + collector: suspend (T) -> Unit, +) { + lifecycleOwner.lifecycleScope.launch { + lifecycleOwner.repeatOnLifecycle(state) { + collect { collector(it) } + } + } +} 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..27372d6e8 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 @@ -29,56 +31,52 @@ import com.into.websoso.data.remote.response.UserStorageResponseDto import com.into.websoso.data.remote.response.UserStorageResponseDto.StorageNovelDto import com.into.websoso.ui.main.myPage.myActivity.model.Genres -fun UserInfoResponseDto.toData(): UserInfoEntity { - return UserInfoEntity( +fun UserInfoResponseDto.toData(): UserInfoEntity = + UserInfoEntity( userId = this.userId, nickname = this.nickname, gender = this.gender, ) -} -fun UserInfoDetailResponseDto.toData(): UserInfoDetailEntity { - return UserInfoDetailEntity( +fun UserInfoDetailResponseDto.toData(): UserInfoDetailEntity = + UserInfoDetailEntity( email = email, gender = gender, birthYear = birth, ) -} -fun BlockedUsersResponseDto.toData(): BlockedUsersEntity { - return BlockedUsersEntity(blockedUsers = blocks.map { blockedUser -> - BlockedUserEntity( - blockId = blockedUser.blockId, - userId = blockedUser.userId, - nickName = blockedUser.nickName, - avatarImage = blockedUser.avatarImage, - ) - }) -} +fun BlockedUsersResponseDto.toData(): BlockedUsersEntity = + BlockedUsersEntity( + blockedUsers = blocks.map { blockedUser -> + BlockedUserEntity( + blockId = blockedUser.blockId, + userId = blockedUser.userId, + nickName = blockedUser.nickName, + avatarImage = blockedUser.avatarImage, + ) + }, + ) -fun UserNovelStatsResponseDto.toData(): UserNovelStatsEntity { - return UserNovelStatsEntity( +fun UserNovelStatsResponseDto.toData(): UserNovelStatsEntity = + UserNovelStatsEntity( interestNovelCount = interestNovelCount, watchingNovelCount = watchingNovelCount, watchedNovelCount = watchedNovelCount, quitNovelCount = quitNovelCount, ) -} -fun UserProfileStatusResponseDto.toData(): UserProfileStatusEntity { - return UserProfileStatusEntity( +fun UserProfileStatusResponseDto.toData(): UserProfileStatusEntity = + UserProfileStatusEntity( isProfilePublic = isProfilePublic, ) -} -fun MyProfileResponseDto.toData(): MyProfileEntity { - return MyProfileEntity( +fun MyProfileResponseDto.toData(): MyProfileEntity = + MyProfileEntity( nickname = this.nickname, intro = this.intro, avatarImage = this.avatarImage, genrePreferences = this.genrePreferences, ) -} fun GenrePreferenceResponseDto.GenrePreferenceDto.toData(): GenrePreferenceEntity { val koreanGenreName = Genres.from(this.genreName)?.korean ?: this.genreName @@ -89,41 +87,37 @@ fun GenrePreferenceResponseDto.GenrePreferenceDto.toData(): GenrePreferenceEntit ) } -fun NovelPreferenceResponseDto.toData(): NovelPreferenceEntity { - return NovelPreferenceEntity( +fun NovelPreferenceResponseDto.toData(): NovelPreferenceEntity = + NovelPreferenceEntity( attractivePoints = this.attractivePoints, keywords = this.keywords.map { it.toData() }, ) -} -fun NovelPreferenceResponseDto.AttractivePointKeywordDto.toData(): NovelPreferenceEntity.KeywordEntity { - return NovelPreferenceEntity.KeywordEntity( +fun NovelPreferenceResponseDto.AttractivePointKeywordDto.toData(): NovelPreferenceEntity.KeywordEntity = + NovelPreferenceEntity.KeywordEntity( keywordName = this.keywordName, keywordCount = this.keywordCount, ) -} -fun OtherUserProfileResponseDto.toData(): OtherUserProfileEntity { - return OtherUserProfileEntity( +fun OtherUserProfileResponseDto.toData(): OtherUserProfileEntity = + OtherUserProfileEntity( nickname = this.nickname, intro = this.intro, avatarImage = this.avatarImage, isProfilePublic = this.isProfilePublic, genrePreferences = this.genrePreferences, ) -} -fun UserStorageResponseDto.toData(): UserStorageEntity { - return UserStorageEntity( +fun UserStorageResponseDto.toData(): UserStorageEntity = + UserStorageEntity( isLoadable = this.isLoadable, userNovelRating = this.userNovelRating, userNovelCount = this.userNovelCount, userNovels = this.userNovels.map { it.toData() }, ) -} -fun StorageNovelDto.toData(): StorageNovelEntity { - return StorageNovelEntity( +fun StorageNovelDto.toData(): StorageNovelEntity = + StorageNovelEntity( author = this.author, userNovelId = this.userNovelId, novelId = this.novelId, @@ -131,17 +125,15 @@ fun StorageNovelDto.toData(): StorageNovelEntity { novelImage = this.novelImage, title = this.title, ) -} -fun UserFeedsResponseDto.toData(): UserFeedsEntity { - return UserFeedsEntity( +fun UserFeedsResponseDto.toData(): UserFeedsEntity = + UserFeedsEntity( isLoadable = this.isLoadable, feeds = this.feeds.map { it.toData() }, ) -} -fun UserFeedResponseDto.toData(): UserFeedEntity { - return UserFeedEntity( +fun UserFeedResponseDto.toData(): UserFeedEntity = + UserFeedEntity( feedId = this.feedId, isSpoiler = this.isSpoiler, feedContent = this.feedContent, @@ -156,4 +148,10 @@ fun UserFeedResponseDto.toData(): UserFeedEntity { novelRating = this.novelRating, relevantCategories = this.relevantCategories, ) -} \ No newline at end of file + +fun TermsAgreementResponseDto.toData(): TermsAgreementEntity = + 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..683122488 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 @@ -25,7 +27,6 @@ import retrofit2.http.Path import retrofit2.http.Query interface UserApi { - @GET("users/me") suspend fun getUserInfo(): UserInfoResponseDto @@ -106,4 +107,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..e7ea0fac9 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,10 +19,13 @@ 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 @@ -30,6 +34,9 @@ class UserRepository private val userApi: UserApi, private val userStorage: DataStore, ) { + val isTermsAgreementChecked: Flow = userStorage.data + .map { preferences -> preferences[TERMS_AGREEMENT_CHECKED_KEY] ?: false } + suspend fun fetchUserInfo(): UserInfoEntity { val userInfo = userApi.getUserInfo().toData() saveUserInfo(userInfo.userId, userInfo.nickname, userInfo.gender) @@ -164,8 +171,35 @@ class UserRepository } } + suspend fun saveTermsAgreements( + serviceAgreed: Boolean, + privacyAgreed: Boolean, + marketingAgreed: Boolean, + ) { + userApi.patchTermsAgreement( + TermsAgreementRequestDto(serviceAgreed, privacyAgreed, marketingAgreed), + ) + saveTermsAgreementChecked(serviceAgreed, privacyAgreed) + } + + 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") 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 af5877c1d..8f601c7fb 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 @@ -19,6 +19,7 @@ import com.into.websoso.core.common.ui.model.ResultFrom.NormalExploreBack import com.into.websoso.core.common.ui.model.ResultFrom.Notification 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 @@ -27,6 +28,7 @@ import com.into.websoso.ui.main.home.adpater.PopularFeedsAdapter import com.into.websoso.ui.main.home.adpater.PopularNovelsAdapter import com.into.websoso.ui.main.home.adpater.RecommendedNovelsByUserTasteAdapter import com.into.websoso.ui.main.home.adpater.UserInterestFeedAdapter +import com.into.websoso.ui.main.home.dialog.TermsAgreementDialogFragment import com.into.websoso.ui.normalExplore.NormalExploreActivity import com.into.websoso.ui.notification.NotificationActivity import com.into.websoso.ui.novelDetail.NovelDetailActivity @@ -171,6 +173,12 @@ class HomeFragment : BaseFragment(R.layout.fragment_home) { } updateFCMToken(isFirstLaunch = false) } + + homeViewModel.showTermsAgreementDialog.collectWithLifecycle(viewLifecycleOwner) { shouldShow -> + if (shouldShow) { + showTermsAgreementDialog() + } + } } private fun updateUserInterestFeedsVisibility(isUserInterestEmpty: Boolean) { @@ -310,6 +318,11 @@ class HomeFragment : BaseFragment(R.layout.fragment_home) { } } + private fun showTermsAgreementDialog() { + val dialog = TermsAgreementDialogFragment.newInstance() + dialog.show(parentFragmentManager, TermsAgreementDialogFragment.TERMS_AGREEMENT_TAG) + } + companion object { private const val TODAY_POPULAR_NOVEL_MARGIN = 15 private const val USER_INTEREST_MARGIN = 14 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 81cd79261..8451d6e04 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,10 +15,14 @@ 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 @@ -29,6 +34,7 @@ class HomeViewModel 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 @@ -36,9 +42,16 @@ class HomeViewModel 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) { @@ -219,6 +232,27 @@ class HomeViewModel } } + 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) + } + } + } + fun updateFCMToken(token: String) { viewModelScope.launch { runCatching { diff --git a/app/src/main/java/com/into/websoso/ui/main/home/dialog/TermsAgreementDialogFragment.kt b/app/src/main/java/com/into/websoso/ui/main/home/dialog/TermsAgreementDialogFragment.kt new file mode 100644 index 000000000..35893d6bd --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/main/home/dialog/TermsAgreementDialogFragment.kt @@ -0,0 +1,39 @@ +package com.into.websoso.ui.main.home.dialog + +import android.os.Bundle +import android.view.View +import com.into.websoso.R +import com.into.websoso.core.common.ui.base.BaseDialogFragment +import com.into.websoso.databinding.DialogTermsAgreementPopupMenuBinding +import com.into.websoso.ui.termsAgreement.TermsAgreementDialogBottomSheet + +class TermsAgreementDialogFragment : + BaseDialogFragment(R.layout.dialog_terms_agreement_popup_menu) { + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + super.onViewCreated(view, savedInstanceState) + isCancelable = false + onTermsAgreementPopupMenuUpdateClick() + } + + private fun onTermsAgreementPopupMenuUpdateClick() { + binding.tvTermsAgreementPopupMenuUpdate.setOnClickListener { + showTermsAgreementBottomSheet() + dismiss() + } + } + + private fun showTermsAgreementBottomSheet() { + TermsAgreementDialogBottomSheet + .newInstance(isFromHome = true) + .show(parentFragmentManager, "TermsAgreementDialogBottomSheet") + } + + companion object { + const val TERMS_AGREEMENT_TAG = "TermsAgreementDialog" + + fun newInstance(): TermsAgreementDialogFragment = TermsAgreementDialogFragment() + } +} diff --git a/app/src/main/java/com/into/websoso/ui/onboarding/OnboardingActivity.kt b/app/src/main/java/com/into/websoso/ui/onboarding/OnboardingActivity.kt index dcf5090a6..20925b8e1 100644 --- a/app/src/main/java/com/into/websoso/ui/onboarding/OnboardingActivity.kt +++ b/app/src/main/java/com/into/websoso/ui/onboarding/OnboardingActivity.kt @@ -12,6 +12,7 @@ import com.into.websoso.core.common.util.SingleEventHandler import com.into.websoso.databinding.ActivityOnboardingBinding import com.into.websoso.ui.onboarding.model.OnboardingPage import com.into.websoso.ui.onboarding.welcome.WelcomeActivity +import com.into.websoso.ui.termsAgreement.TermsAgreementDialogBottomSheet import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -26,6 +27,7 @@ class OnboardingActivity : BaseActivity(R.layout.acti setupViewPager() setupObserver() onSkipGeneralButtonClick() + showTermsAgreementDialog() } private fun bindViewModel() { @@ -80,6 +82,12 @@ class OnboardingActivity : BaseActivity(R.layout.acti } } + private fun showTermsAgreementDialog() { + TermsAgreementDialogBottomSheet + .newInstance() + .show(supportFragmentManager, "TermsAgreementDialog") + } + companion object { private const val ANIMATION_PROPERTY_NAME = "progress" private const val ANIMATION_DURATION_TIME = 200L 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 new file mode 100644 index 000000000..675dba6cd --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/termsAgreement/TermsAgreementDialogBottomSheet.kt @@ -0,0 +1,164 @@ +package com.into.websoso.ui.termsAgreement + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.into.websoso.R +import com.into.websoso.R.drawable.bg_novel_rating_date_primary_100_radius_12dp +import com.into.websoso.R.drawable.bg_profile_edit_gray_70_radius_12dp +import com.into.websoso.R.drawable.ic_terms_agreement_selected +import com.into.websoso.R.drawable.ic_terms_agreement_unselected +import com.into.websoso.R.string.string_terms_agreement_complete +import com.into.websoso.R.string.string_terms_agreement_next +import com.into.websoso.core.common.ui.base.BaseBottomSheetDialog +import com.into.websoso.core.common.util.collectWithLifecycle +import com.into.websoso.databinding.DialogTermsAgreementBinding +import com.into.websoso.ui.termsAgreement.model.AgreementType +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class TermsAgreementDialogBottomSheet : BaseBottomSheetDialog(R.layout.dialog_terms_agreement) { + private val termsAgreementViewModel: TermsAgreementViewModel by viewModels() + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + super.onViewCreated(view, savedInstanceState) + + setupBottomSheetDialog() + onRequiredTermsAgreementClick() + onTermsAgreementToggleClick() + onTermsAgreementCompleteButtonClick() + setupViewModel() + updateCompleteButtonText() + } + + private fun setupBottomSheetDialog() { + (dialog as BottomSheetDialog).apply { + behavior.state = BottomSheetBehavior.STATE_EXPANDED + behavior.isDraggable = false + behavior.isHideable = false + setCancelable(false) + } + } + + private fun onRequiredTermsAgreementClick() { + binding.tvTermsAgreementService.setOnClickListener { + startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse(getString(R.string.terms_agreement_service)), + ), + ) + } + + binding.tvTermsAgreementPrivacy.setOnClickListener { + startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse(getString(R.string.terms_agreement_privacy)), + ), + ) + } + } + + private fun onTermsAgreementToggleClick() { + binding.ivTermsAgreementAll.setOnClickListener { termsAgreementViewModel.updateTermsAgreementsAll() } + + binding.ivTermsAgreementService.setOnClickListener { + termsAgreementViewModel.updateTermsAgreements( + AgreementType.SERVICE, + ) + } + + binding.ivTermsAgreementPrivacy.setOnClickListener { + termsAgreementViewModel.updateTermsAgreements( + AgreementType.PRIVACY, + ) + } + + binding.ivTermsAgreementMarketing.setOnClickListener { + termsAgreementViewModel.updateTermsAgreements( + AgreementType.MARKETING, + ) + } + } + + private fun onTermsAgreementCompleteButtonClick() { + binding.btnTermsAgreementComplete.setOnClickListener { sendTermsAgreement() } + } + + private fun sendTermsAgreement() { + if (!termsAgreementViewModel.isRequiredAgreementsChecked.value) return // 필수 항목 미체크 시 요청 안 함 + + termsAgreementViewModel.saveTermsAgreements() + } + + private fun setupViewModel() { + termsAgreementViewModel.agreementStatus.collectWithLifecycle(viewLifecycleOwner) { status -> + updateAgreementIcons(status) + updateAllAgreementIcon(status.values.all { it }) + } + + termsAgreementViewModel.isRequiredAgreementsChecked.collectWithLifecycle(viewLifecycleOwner) { + updateCompleteButtonState(it) + } + + termsAgreementViewModel.saveAgreementResult.collectWithLifecycle(viewLifecycleOwner) { result -> + result?.onSuccess { + dismiss() + } + } + } + + private fun updateAgreementIcons(status: Map) { + binding.apply { + ivTermsAgreementService.setImageResource(getToggleIcon(status[AgreementType.SERVICE] == true)) + ivTermsAgreementPrivacy.setImageResource(getToggleIcon(status[AgreementType.PRIVACY] == true)) + ivTermsAgreementMarketing.setImageResource(getToggleIcon(status[AgreementType.MARKETING] == true)) + } + } + + private fun getToggleIcon(isChecked: Boolean): Int = if (isChecked) ic_terms_agreement_selected else ic_terms_agreement_unselected + + private fun updateAllAgreementIcon(isChecked: Boolean) { + binding.ivTermsAgreementAll.setImageResource( + if (isChecked) ic_terms_agreement_selected else ic_terms_agreement_unselected, + ) + } + + private fun updateCompleteButtonState(isEnabled: Boolean) { + binding.btnTermsAgreementComplete.setBackgroundResource( + when (isEnabled) { + true -> bg_novel_rating_date_primary_100_radius_12dp + false -> bg_profile_edit_gray_70_radius_12dp + }, + ) + } + + private fun updateCompleteButtonText() { + val isFromHome = arguments?.getBoolean(IS_FROM_HOME_TAG, false) ?: false + + binding.btnTermsAgreementComplete.text = + if (isFromHome) { + getString(string_terms_agreement_complete) + } else { + getString(string_terms_agreement_next) + } + } + + companion object { + private const val IS_FROM_HOME_TAG = "IS_FROM_HOME" + + fun newInstance(isFromHome: Boolean = false): TermsAgreementDialogBottomSheet = + TermsAgreementDialogBottomSheet().apply { + arguments = bundleOf(IS_FROM_HOME_TAG to isFromHome) + } + } +} 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 new file mode 100644 index 000000000..b0fdfbbbe --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/termsAgreement/TermsAgreementViewModel.kt @@ -0,0 +1,83 @@ +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 +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +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( + private val userRepository: UserRepository, + ) : ViewModel() { + private val _agreementStatus = MutableStateFlow( + mapOf( + AgreementType.SERVICE to false, + AgreementType.PRIVACY to false, + AgreementType.MARKETING to false, + ), + ) + val agreementStatus: StateFlow> = _agreementStatus.asStateFlow() + + 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) } + .stateIn(viewModelScope, SharingStarted.Lazily, false) + + private fun isRequiredAgreementChecked(status: Map): Boolean = + status[AgreementType.SERVICE] == true && status[AgreementType.PRIVACY] == true + + fun updateTermsAgreementsAll() { + val newStatus = _agreementStatus.value.values.any { !it } + _agreementStatus.update { it.mapValues { _ -> newStatus } } + } + + fun updateTermsAgreements(agreementType: AgreementType) { + _agreementStatus.value[agreementType]?.let { currentValue -> + _agreementStatus.value = _agreementStatus.value.toMutableMap().apply { + this[agreementType] = !currentValue + } + } + } + + 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 + } + } + } diff --git a/app/src/main/java/com/into/websoso/ui/termsAgreement/model/AgreementType.kt b/app/src/main/java/com/into/websoso/ui/termsAgreement/model/AgreementType.kt new file mode 100644 index 000000000..2812ccc1b --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/termsAgreement/model/AgreementType.kt @@ -0,0 +1,7 @@ +package com.into.websoso.ui.termsAgreement.model + +enum class AgreementType { + SERVICE, + PRIVACY, + MARKETING, +} diff --git a/app/src/main/res/drawable/ic_terms_agreement_selected.xml b/app/src/main/res/drawable/ic_terms_agreement_selected.xml new file mode 100644 index 000000000..ddef5db7d --- /dev/null +++ b/app/src/main/res/drawable/ic_terms_agreement_selected.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_terms_agreement_unselected.xml b/app/src/main/res/drawable/ic_terms_agreement_unselected.xml new file mode 100644 index 000000000..623fec56a --- /dev/null +++ b/app/src/main/res/drawable/ic_terms_agreement_unselected.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/layout/dialog_terms_agreement.xml b/app/src/main/res/layout/dialog_terms_agreement.xml new file mode 100644 index 000000000..62495d70c --- /dev/null +++ b/app/src/main/res/layout/dialog_terms_agreement.xml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_terms_agreement_popup_menu.xml b/app/src/main/res/layout/dialog_terms_agreement_popup_menu.xml new file mode 100644 index 000000000..35fec7f4a --- /dev/null +++ b/app/src/main/res/layout/dialog_terms_agreement_popup_menu.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/deepLinks.xml b/app/src/main/res/values/deepLinks.xml index 76a1fd97d..81b542356 100644 --- a/app/src/main/res/values/deepLinks.xml +++ b/app/src/main/res/values/deepLinks.xml @@ -5,4 +5,7 @@ https://websoso.notion.site/143600bd74688050be18f4da31d9403e?pvs=4 https://websoso.notion.site/143600bd746880668556fb005fcef491?pvs=4 https://play.google.com/store/apps/details?id=com.into.websoso - \ No newline at end of file + https://www.notion.so/websoso/143600bd74688050be18f4da31d9403e?pvs=4 + https://www.notion.so/kimmjabc/18e9e64a45328048842ed4dd7b17e5b3?pvs=25 + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 744551e38..cad0992cf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -418,4 +418,19 @@ 업데이트 알림 웹소소 세계에 변화가 생겼어요!\n지금 업데이트해보세요. 업데이트 + + + 서비스 이용약관 동의 (필수) + 개인정보 수집 및 이용 안내 (필수) + 마켓팅 정보 수신 동의 (선택) + 다음으로 + 완료 + 웹소소 세계로 들어가는 중... + 아래 약관 내용에 동의 후 서비스 이용이 가능해요 + 전체 동의 + + + 동의하러 가기 + 더 안전하고 원활한 웹소소를 위해\n업데이트된 약관에 동의해주세요. + 약관 동의가 필요해요!