diff --git a/data/src/main/java/com/nextroom/nextroom/data/datasource/AuthDataSource.kt b/data/src/main/java/com/nextroom/nextroom/data/datasource/AuthDataSource.kt index cd8089bd..9d8c08d7 100644 --- a/data/src/main/java/com/nextroom/nextroom/data/datasource/AuthDataSource.kt +++ b/data/src/main/java/com/nextroom/nextroom/data/datasource/AuthDataSource.kt @@ -6,7 +6,6 @@ import com.nextroom.nextroom.data.db.AppSettings import com.nextroom.nextroom.data.db.dataStore import com.nextroom.nextroom.data.network.ApiService import com.nextroom.nextroom.data.network.request.LoginRequest -import com.nextroom.nextroom.data.network.response.toDomain import com.nextroom.nextroom.domain.model.LoginInfo import com.nextroom.nextroom.domain.model.Result import com.nextroom.nextroom.domain.model.mapOnSuccess @@ -43,6 +42,7 @@ class AuthDataSource @Inject constructor( shopName = "", accessToken = "", refreshToken = "", + appPassword = "", ) } } diff --git a/data/src/main/java/com/nextroom/nextroom/data/datasource/SettingDataSource.kt b/data/src/main/java/com/nextroom/nextroom/data/datasource/SettingDataSource.kt index 638cbf21..31ab5304 100644 --- a/data/src/main/java/com/nextroom/nextroom/data/datasource/SettingDataSource.kt +++ b/data/src/main/java/com/nextroom/nextroom/data/datasource/SettingDataSource.kt @@ -97,11 +97,19 @@ class SettingDataSource @Inject constructor( fun saveUserEmail(userEmail: String) = runBlocking { dataStore.updateData { - it.copy(userEmail= userEmail) + it.copy(userEmail = userEmail) } } fun getUserEmail() = runBlocking { data.first().userEmail } + + suspend fun saveAppPassword(password: String) { + dataStore.updateData { + it.copy(appPassword = password) + } + } + + suspend fun getAppPassword() = data.first().appPassword } diff --git a/data/src/main/java/com/nextroom/nextroom/data/db/AppSettings.kt b/data/src/main/java/com/nextroom/nextroom/data/db/AppSettings.kt index 5340afb4..b2dbfed4 100644 --- a/data/src/main/java/com/nextroom/nextroom/data/db/AppSettings.kt +++ b/data/src/main/java/com/nextroom/nextroom/data/db/AppSettings.kt @@ -14,5 +14,6 @@ data class AppSettings( val lastLaunchDate: Long = 0L, val emailSaveChecked: Boolean = false, val userEmail: String = "", - val networkDisconnectedCount: Int = 0 + val networkDisconnectedCount: Int = 0, + val appPassword: String = "", ) diff --git a/data/src/main/java/com/nextroom/nextroom/data/repository/AdminRepositoryImpl.kt b/data/src/main/java/com/nextroom/nextroom/data/repository/AdminRepositoryImpl.kt index 2bff5477..0a7d8d7f 100644 --- a/data/src/main/java/com/nextroom/nextroom/data/repository/AdminRepositoryImpl.kt +++ b/data/src/main/java/com/nextroom/nextroom/data/repository/AdminRepositoryImpl.kt @@ -66,4 +66,12 @@ class AdminRepositoryImpl @Inject constructor( override suspend fun getUserEmail(): String { return settingDataSource.getUserEmail() } + + override suspend fun saveAppPassword(password: String) { + return settingDataSource.saveAppPassword(password) + } + + override suspend fun getAppPassword(): String { + return settingDataSource.getAppPassword() + } } diff --git a/domain/src/main/java/com/nextroom/nextroom/domain/repository/AdminRepository.kt b/domain/src/main/java/com/nextroom/nextroom/domain/repository/AdminRepository.kt index 552f681c..90004dbd 100644 --- a/domain/src/main/java/com/nextroom/nextroom/domain/repository/AdminRepository.kt +++ b/domain/src/main/java/com/nextroom/nextroom/domain/repository/AdminRepository.kt @@ -22,4 +22,6 @@ interface AdminRepository { suspend fun getUserSubscribe(): Result suspend fun getEmailSaveChecked(): Boolean suspend fun getUserEmail(): String + suspend fun saveAppPassword(password: String) + suspend fun getAppPassword(): String } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 97ec4761..d66061a7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,6 +45,7 @@ firebase-analytics="22.1.2" swiperefreshlayout = "1.1.0" firebaseCommonKtx = "21.0.0" photoview = "2.3.0" +biometric = "1.1.0" [libraries] androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activity-ktx" } @@ -96,6 +97,7 @@ timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } billing = { module = "com.android.billingclient:billing", version.ref = "billing" } androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" } firebase-common-ktx = { group = "com.google.firebase", name = "firebase-common-ktx", version.ref = "firebaseCommonKtx" } +biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" } [plugins] android-application = { id = "com.android.application", version.ref = "android" } diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index 22aeff84..9f7f93cc 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -70,6 +70,7 @@ dependencies { implementation(libs.timber) implementation(libs.billing) implementation(libs.photoview) + implementation(libs.biometric) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.analytics.ktx) diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml index dae668f4..61800c16 100644 --- a/presentation/src/main/AndroidManifest.xml +++ b/presentation/src/main/AndroidManifest.xml @@ -6,4 +6,5 @@ + \ No newline at end of file diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/extension/Bundle.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/extension/Bundle.kt new file mode 100644 index 00000000..e6ba7e44 --- /dev/null +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/extension/Bundle.kt @@ -0,0 +1,9 @@ +package com.nextroom.nextroom.presentation.extension + +import android.os.Bundle + +fun Bundle.hasResultData() = this.containsKey(BUNDLE_KEY_RESULT_DATA) + +fun Bundle.getResultData() = this.getString(BUNDLE_KEY_RESULT_DATA) + +const val BUNDLE_KEY_RESULT_DATA = "BUNDLE_KEY_RESULT_DATA" \ No newline at end of file diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/AdminMainEvent.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/AdminMainEvent.kt index 35949e01..1ca62a2f 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/AdminMainEvent.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/AdminMainEvent.kt @@ -1,8 +1,13 @@ package com.nextroom.nextroom.presentation.ui.adminmain +import com.nextroom.nextroom.domain.model.SubscribeStatus + sealed interface AdminMainEvent { data object NetworkError : AdminMainEvent data object UnknownError : AdminMainEvent data class ClientError(val message: String) : AdminMainEvent data object InAppReview : AdminMainEvent + data class ReadyToGameStart(val subscribeStatus: SubscribeStatus) : AdminMainEvent + data object NeedToSetPassword : AdminMainEvent + data class NeedToCheckPasswordForStartGame(val themeId: String) : AdminMainEvent } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/AdminMainFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/AdminMainFragment.kt index 3b21201d..0e9212be 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/AdminMainFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/AdminMainFragment.kt @@ -8,16 +8,22 @@ import android.view.View import androidx.activity.OnBackPressedCallback import androidx.core.os.bundleOf import androidx.core.view.isVisible +import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels +import androidx.navigation.NavOptions import androidx.navigation.fragment.findNavController import com.google.android.play.core.review.ReviewManagerFactory import com.google.firebase.analytics.FirebaseAnalytics import com.nextroom.nextroom.domain.model.SubscribeStatus import com.nextroom.nextroom.domain.repository.StatisticsRepository +import com.nextroom.nextroom.presentation.NavGraphDirections import com.nextroom.nextroom.presentation.R import com.nextroom.nextroom.presentation.base.BaseFragment +import com.nextroom.nextroom.presentation.common.NRTwoButtonDialog import com.nextroom.nextroom.presentation.databinding.FragmentAdminMainBinding import com.nextroom.nextroom.presentation.extension.addMargin +import com.nextroom.nextroom.presentation.extension.getResultData +import com.nextroom.nextroom.presentation.extension.hasResultData import com.nextroom.nextroom.presentation.extension.safeNavigate import com.nextroom.nextroom.presentation.extension.snackbar import com.nextroom.nextroom.presentation.extension.statusBarHeight @@ -40,7 +46,7 @@ class AdminMainFragment : private val viewModel: AdminMainViewModel by viewModels() private val adapter: ThemesAdapter by lazy { ThemesAdapter( - onStartGame = ::startGame, + onThemeClicked = { themeId -> viewModel.onThemeClicked(themeId.toString()) }, onClickUpdate = viewModel::updateTheme, ) } @@ -61,9 +67,34 @@ class AdminMainFragment : super.onViewCreated(view, savedInstanceState) initViews() + initSubscribe() viewModel.observe(viewLifecycleOwner, state = ::render, sideEffect = ::handleEvent) } + private fun initSubscribe() { + setFragmentResultListener(requestKeyCheckPassword, ::handleFragmentResults) + setFragmentResultListener(dialogKeyNeedToSetPassword, ::handleFragmentResults) + } + + private fun handleFragmentResults(requestKey: String, bundle: Bundle) { + when (requestKey) { + requestKeyCheckPassword -> { + try { + if (bundle.hasResultData()) { + bundle.getResultData()?.let { themeId -> + viewModel.tryGameStart(themeId.toInt()) + } + } + } catch (e: Exception) { + Timber.e(e) + snackbar(R.string.error_something) + } + } + + dialogKeyNeedToSetPassword -> moveToSetPassword() + } + } + override fun onResume() { super.onResume() viewModel.onResume() @@ -81,12 +112,6 @@ class AdminMainFragment : } }*/ - private fun startGame(themeId: Int) { - viewModel.start(themeId) { - goToMain(themeId) - } - } - private fun initViews() = with(binding) { updateSystemPadding(statusBar = false, navigationBar = true) @@ -146,6 +171,7 @@ class AdminMainFragment : tvShopName.text = state.shopName llEmptyThemeGuide.isVisible = state.themes.isEmpty() adapter.submitList(state.themes) + binding.layoutOpaqueLoading.root.isVisible = state.opaqueLoading } private fun handleEvent(event: AdminMainEvent) { @@ -154,6 +180,9 @@ class AdminMainFragment : is AdminMainEvent.UnknownError -> snackbar(R.string.error_something) is AdminMainEvent.ClientError -> snackbar(event.message) AdminMainEvent.InAppReview -> showInAppReview() + is AdminMainEvent.ReadyToGameStart -> moveToGameStart(event.subscribeStatus) + AdminMainEvent.NeedToSetPassword -> showNeedToSetPasswordDialog() + is AdminMainEvent.NeedToCheckPasswordForStartGame -> moveToCheckPasswordForGameStart(event.themeId) } } @@ -189,13 +218,42 @@ class AdminMainFragment : findNavController().safeNavigate(action) } - private fun goToMain(themeId: Int) { - val action = - AdminMainFragmentDirections.actionAdminMainFragmentToVerifyFragment( - themeId = themeId, - subscribeStatus = state.subscribeStatus - ) - findNavController().safeNavigate(action) + private fun moveToGameStart(subscribeStatus: SubscribeStatus) { + NavGraphDirections + .actionGlobalGameFragment(subscribeStatus) + .also { findNavController().safeNavigate(it) } + } + + private fun showNeedToSetPasswordDialog() { + NavGraphDirections + .actionGlobalNrTwoButtonDialog( + NRTwoButtonDialog.NRTwoButtonArgument( + title = getString(R.string.text_need_to_set_password_title), + message = getString(R.string.text_need_to_set_password_message), + posBtnText = getString(R.string.text_move_to_setting), + negBtnText = getString(R.string.dialog_close), + dialogKey = dialogKeyNeedToSetPassword, + ), + ).also { + findNavController().safeNavigate( + direction = it, + navOptions = NavOptions.Builder() + .setLaunchSingleTop(true) + .build() + ) + } + } + + private fun moveToSetPassword() { + NavGraphDirections + .moveToSetPassword() + .also { findNavController().safeNavigate(it) } + } + + private fun moveToCheckPasswordForGameStart(themeId: String) { + NavGraphDirections + .moveToCheckPassword(requestKey = requestKeyCheckPassword, resultData = themeId) + .also { findNavController().safeNavigate(it) } } override fun onDestroyView() { @@ -207,4 +265,9 @@ class AdminMainFragment : super.onDetach() backCallback.remove() } + + companion object { + private const val requestKeyCheckPassword = "requestKeyCheckPassword" + private const val dialogKeyNeedToSetPassword = "dialogKeyNeedToSetPassword" + } } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/AdminMainState.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/AdminMainState.kt index ea7c5aea..07fc0855 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/AdminMainState.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/AdminMainState.kt @@ -6,6 +6,7 @@ import com.nextroom.nextroom.presentation.model.ThemeInfoPresentation data class AdminMainState( val loading: Boolean = false, + val opaqueLoading: Boolean = false, val subscribeStatus: SubscribeStatus = SubscribeStatus.Default, val shopName: String = "", val themes: List = emptyList(), diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/AdminMainViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/AdminMainViewModel.kt index ad64d2b0..abc55812 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/AdminMainViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/AdminMainViewModel.kt @@ -14,10 +14,8 @@ import com.nextroom.nextroom.presentation.base.BaseViewModel import com.nextroom.nextroom.presentation.model.ThemeInfoPresentation import com.nextroom.nextroom.presentation.model.toPresentation import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.syntax.simple.intent import org.orbitmvi.orbit.syntax.simple.postSideEffect @@ -48,6 +46,7 @@ class AdminMainViewModel @Inject constructor( fun onResume() { loadData() + checkNeedToSetPassword() } fun incrementNetworkDisconnectedCount() { @@ -90,13 +89,6 @@ class AdminMainViewModel @Inject constructor( }.onFailure(::handleError) } - fun start(themeId: Int, readyToStart: () -> Unit) = intent { - themeRepository.updateLatestTheme(themeId) - withContext(Dispatchers.Main) { - readyToStart() - } - } - fun loadData() = intent { reduce { state.copy(loading = true) } adminRepository.getUserSubscribe().suspendOnSuccess { myPage -> @@ -128,6 +120,37 @@ class AdminMainViewModel @Inject constructor( reduce { state.copy(themes = themes) } } + fun tryGameStart(themeId: Int) = intent { + reduce { state.copy(opaqueLoading = true) } + themeRepository.updateLatestTheme(themeId) + adminRepository.getUserSubscribe().suspendOnSuccess { myPage -> + postSideEffect(AdminMainEvent.ReadyToGameStart(myPage.status)) + }.onFailure(::handleError) + reduce { state.copy(opaqueLoading = false) } + } + + private fun checkNeedToSetPassword() { + viewModelScope.launch { + if (adminRepository.getAppPassword().isEmpty()) { + intent { + postSideEffect(AdminMainEvent.NeedToSetPassword) + } + } + } + } + + fun onThemeClicked(themeId: String) { + viewModelScope.launch { + intent { + if (adminRepository.getAppPassword().isEmpty()) { + AdminMainEvent.NeedToSetPassword + } else { + AdminMainEvent.NeedToCheckPasswordForStartGame(themeId) + }.also { postSideEffect(it) } + } + } + } + private fun handleError(error: Result.Failure) = intent { when (error) { is Result.Failure.NetworkError -> postSideEffect(AdminMainEvent.NetworkError) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/ThemesAdapter.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/ThemesAdapter.kt index 8d1e308a..a79fadfd 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/ThemesAdapter.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/adminmain/ThemesAdapter.kt @@ -11,7 +11,7 @@ import com.nextroom.nextroom.presentation.databinding.ItemThemeBinding import com.nextroom.nextroom.presentation.model.ThemeInfoPresentation class ThemesAdapter( - private val onStartGame: (Int) -> Unit, + private val onThemeClicked: (Int) -> Unit, private val onClickUpdate: (Int) -> Unit, ) : ListAdapter(diffUtil) { @@ -44,7 +44,7 @@ class ThemesAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ThemeViewHolder { return ThemeViewHolder( ItemThemeBinding.inflate(LayoutInflater.from(parent.context), parent, false), - onStartGame, + onThemeClicked, onClickUpdate, ) } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageFragment.kt index f8bf04df..d6f8e7c5 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/mypage/MypageFragment.kt @@ -7,6 +7,7 @@ import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import com.nextroom.nextroom.domain.model.SubscribeStatus +import com.nextroom.nextroom.presentation.NavGraphDirections import com.nextroom.nextroom.presentation.R import com.nextroom.nextroom.presentation.base.BaseFragment import com.nextroom.nextroom.presentation.common.NRTwoButtonDialog @@ -48,10 +49,14 @@ class MypageFragment : BaseFragment(FragmentMypageBinding when (loaded.status) { SubscribeStatus.SUBSCRIPTION_EXPIRATION, SubscribeStatus.Default -> goToPurchase() + SubscribeStatus.Subscribed -> goToSubscriptionInfo() } } } + clChangeAppPassword.setOnClickListener { + moveToSetPassword() + } } private fun initObserve() { @@ -109,6 +114,12 @@ class MypageFragment : BaseFragment(FragmentMypageBinding ).also { findNavController().safeNavigate(it) } } + private fun moveToSetPassword() { + NavGraphDirections + .moveToSetPassword() + .also { findNavController().safeNavigate(it) } + } + companion object { const val REQUEST_KEY_RESIGN = "REQUEST_KEY_RESIGN" } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/password/CheckPasswordFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/password/CheckPasswordFragment.kt new file mode 100644 index 00000000..e63996f7 --- /dev/null +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/password/CheckPasswordFragment.kt @@ -0,0 +1,108 @@ +package com.nextroom.nextroom.presentation.ui.password + +import android.os.Build +import android.os.Bundle +import android.view.View +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf +import androidx.core.view.isVisible +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import com.nextroom.nextroom.presentation.R +import com.nextroom.nextroom.presentation.base.BaseFragment +import com.nextroom.nextroom.presentation.databinding.FragmentCheckPasswordBinding +import com.nextroom.nextroom.presentation.extension.BUNDLE_KEY_RESULT_DATA +import com.nextroom.nextroom.presentation.extension.repeatOnStarted +import com.nextroom.nextroom.presentation.extension.toast +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class CheckPasswordFragment : BaseFragment(FragmentCheckPasswordBinding::inflate) { + + private val viewModel: CheckPasswordViewModel by viewModels() + private val args: CheckPasswordFragmentArgs by navArgs() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initView() + initListener() + initSubscribe() + showBiometricPrompt() + } + + private fun initView() { + binding.keyBiometric.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P + } + + private fun initSubscribe() { + viewLifecycleOwner.repeatOnStarted { + launch { + viewModel.inputPassword.collect { password -> + updateUi(password) + } + } + launch { + viewModel.uiEvent.collect { event -> + when (event) { + CheckPasswordViewModel.UiEvent.PasswordCorrect -> onPasswordCorrected() + CheckPasswordViewModel.UiEvent.PasswordInCorrect -> { + binding.customCodeInput.setError() + toast(getString(R.string.text_incorrect_password_error_message)) + } + } + } + } + } + } + + private fun updateUi(password: String) { + binding.customCodeInput.setCode(password) + } + + private fun showBiometricPrompt() { + val executor = ContextCompat.getMainExecutor(requireContext()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + BiometricPrompt(this, executor, object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + onPasswordCorrected() + } + }).also { biometricPrompt -> + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(getString(R.string.text_finger_print_auth)) + .setNegativeButtonText(getString(R.string.text_cancel)) + .build() + + biometricPrompt.authenticate(promptInfo) + } + } + } + + private fun onPasswordCorrected() { + setFragmentResult( + requestKey = args.requestKey, + bundleOf(BUNDLE_KEY_RESULT_DATA to args.resultData) + ) + findNavController().popBackStack() + } + + private fun initListener() { + binding.tvKey1.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_1)) } + binding.tvKey2.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_2)) } + binding.tvKey3.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_3)) } + binding.tvKey4.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_4)) } + binding.tvKey5.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_5)) } + binding.tvKey6.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_6)) } + binding.tvKey7.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_7)) } + binding.tvKey8.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_8)) } + binding.tvKey9.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_9)) } + binding.tvKey0.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_0)) } + binding.keyBiometric.setOnClickListener { showBiometricPrompt() } + binding.keyBackspace.setOnClickListener { viewModel.onBackSpaceClicked() } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/password/CheckPasswordViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/password/CheckPasswordViewModel.kt new file mode 100644 index 00000000..0202e6f4 --- /dev/null +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/password/CheckPasswordViewModel.kt @@ -0,0 +1,73 @@ +package com.nextroom.nextroom.presentation.ui.password + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextroom.nextroom.domain.repository.AdminRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class CheckPasswordViewModel @Inject constructor( + val adminRepository: AdminRepository +) : ViewModel() { + val inputPassword = MutableStateFlow("") + private val _uiEvent = MutableSharedFlow() + val uiEvent = _uiEvent.asSharedFlow() + + init { + viewModelScope.launch { + inputPassword.collect { password -> + if (password.length == MAX_PASSWORD_LEN) { + checkPassword(inputPassword.value) + } + } + } + } + + fun onNumberClicked(number: String) { + if (inputPassword.value.length == MAX_PASSWORD_LEN) return + viewModelScope.launch { + inputPassword.emit(inputPassword.value + number) + } + } + + private fun checkPassword(inputPassword: String) { + viewModelScope.launch { + if (inputPassword == adminRepository.getAppPassword()) { + UiEvent.PasswordCorrect + } else { + clearPassword() + UiEvent.PasswordInCorrect + }.also { + _uiEvent.emit(it) + } + } + } + + private fun clearPassword() { + viewModelScope.launch { + inputPassword.emit("") + } + } + + fun onBackSpaceClicked() { + if (inputPassword.value.isNotEmpty()) { + viewModelScope.launch { + inputPassword.emit(inputPassword.value.dropLast(1)) + } + } + } + + sealed interface UiEvent { + data object PasswordCorrect : UiEvent + data object PasswordInCorrect : UiEvent + } + + companion object { + const val MAX_PASSWORD_LEN = 4 + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/password/SetPasswordFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/password/SetPasswordFragment.kt new file mode 100644 index 00000000..70441a94 --- /dev/null +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/password/SetPasswordFragment.kt @@ -0,0 +1,79 @@ +package com.nextroom.nextroom.presentation.ui.password + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import com.nextroom.nextroom.presentation.R +import com.nextroom.nextroom.presentation.base.BaseFragment +import com.nextroom.nextroom.presentation.databinding.FragmentSetPasswordBinding +import com.nextroom.nextroom.presentation.extension.repeatOnStarted +import com.nextroom.nextroom.presentation.extension.toast +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class SetPasswordFragment : BaseFragment(FragmentSetPasswordBinding::inflate) { + private val viewModel: SetPasswordViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initListener() + initSubscribe() + } + + private fun initSubscribe() { + viewLifecycleOwner.repeatOnStarted { + launch { + viewModel.uiState.collect { state -> + updateUi(state) + } + } + launch { + viewModel.uiEvent.collect { event -> + when (event) { + SetPasswordViewModel.UiEvent.SettingPasswordFinished -> { + toast(getString(R.string.text_set_password_succeed)) + findNavController().popBackStack() + } + + SetPasswordViewModel.UiEvent.PasswordNotMatched -> { + binding.customCodeInput.setError() + toast(getString(R.string.text_incorrect_password_error_message)) + } + } + } + } + } + } + + private fun updateUi(state: SetPasswordViewModel.UiState) { + binding.customCodeInput.setCode(state.displayPassword) + when (state.step) { + SetPasswordViewModel.UiState.Step.PasswordSetting -> { + binding.tvHeader.text = getString(R.string.text_set_password) + binding.tvDescription.text = getString(R.string.text_set_password_description) + } + + SetPasswordViewModel.UiState.Step.PasswordConfirm -> { + binding.tvHeader.text = getString(R.string.text_confirm_password) + binding.tvDescription.text = getString(R.string.text_set_password_description_for_confirm) + } + } + } + + private fun initListener() { + binding.tvKey1.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_1)) } + binding.tvKey2.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_2)) } + binding.tvKey3.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_3)) } + binding.tvKey4.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_4)) } + binding.tvKey5.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_5)) } + binding.tvKey6.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_6)) } + binding.tvKey7.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_7)) } + binding.tvKey8.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_8)) } + binding.tvKey9.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_9)) } + binding.tvKey0.setOnClickListener { viewModel.onNumberClicked(getString(R.string.text_0)) } + binding.keyBackspace.setOnClickListener { viewModel.onBackSpaceClicked() } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/password/SetPasswordViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/password/SetPasswordViewModel.kt new file mode 100644 index 00000000..7095ca5e --- /dev/null +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/password/SetPasswordViewModel.kt @@ -0,0 +1,118 @@ +package com.nextroom.nextroom.presentation.ui.password + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextroom.nextroom.domain.repository.AdminRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SetPasswordViewModel @Inject constructor( + val adminRepository: AdminRepository +) : ViewModel() { + private val firstPassword = MutableStateFlow("") + private val secondPassword = MutableStateFlow("") + private val step = MutableStateFlow(UiState.Step.PasswordSetting) + + val uiState = combine( + firstPassword, + secondPassword, + step, + ) { firstPassword, secondPassword, step -> + when (step) { + UiState.Step.PasswordSetting -> firstPassword + UiState.Step.PasswordConfirm -> secondPassword + }.let { password -> + UiState(displayPassword = password, step = step) + } + } + private val _uiEvent = MutableSharedFlow() + val uiEvent = _uiEvent.asSharedFlow() + + init { + viewModelScope.launch { + firstPassword.collect { password -> + if (password.length == MAX_PASSWORD_LEN) { + step.emit(UiState.Step.PasswordConfirm) + } + } + } + viewModelScope.launch { + secondPassword.collect { password -> + if (password.length == MAX_PASSWORD_LEN) { + if (isPasswordMatch()) { + savePassword(password) + _uiEvent.emit(UiEvent.SettingPasswordFinished) + } else { + secondPassword.emit("") + _uiEvent.emit(UiEvent.PasswordNotMatched) + } + } + } + } + } + + fun onNumberClicked(number: String) { + viewModelScope.launch { + when (step.value) { + UiState.Step.PasswordSetting -> { + if (firstPassword.value.length == MAX_PASSWORD_LEN) return@launch + firstPassword.emit(firstPassword.value + number) + } + + UiState.Step.PasswordConfirm -> { + if (secondPassword.value.length == MAX_PASSWORD_LEN) return@launch + secondPassword.emit(secondPassword.value + number) + } + } + } + } + + private fun isPasswordMatch() = firstPassword.value == secondPassword.value + + private suspend fun savePassword(password: String) { + adminRepository.saveAppPassword(password) + } + + fun onBackSpaceClicked() { + viewModelScope.launch { + when (step.value) { + UiState.Step.PasswordSetting -> { + if (firstPassword.value.isNotEmpty()) { + firstPassword.emit(firstPassword.value.dropLast(1)) + } + } + + UiState.Step.PasswordConfirm -> { + if (secondPassword.value.isNotEmpty()) { + secondPassword.emit(secondPassword.value.dropLast(1)) + } + } + } + } + } + + data class UiState( + val displayPassword: String, + val step: Step, + ) { + sealed interface Step { + data object PasswordSetting : Step + data object PasswordConfirm : Step + } + } + + sealed interface UiEvent { + data object PasswordNotMatched : UiEvent + data object SettingPasswordFinished : UiEvent + } + + companion object { + const val MAX_PASSWORD_LEN = 4 + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/verity/VerifyFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/verity/VerifyFragment.kt deleted file mode 100644 index c77b45e9..00000000 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/verity/VerifyFragment.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.nextroom.nextroom.presentation.ui.verity - -import android.os.Bundle -import android.view.View -import android.view.inputmethod.EditorInfo -import androidx.core.widget.doAfterTextChanged -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import com.nextroom.nextroom.presentation.base.BaseFragment -import com.nextroom.nextroom.presentation.databinding.FragmentAdminCodeBinding -import com.nextroom.nextroom.presentation.extension.safeNavigate -import com.nextroom.nextroom.presentation.extension.setStateListener -import com.nextroom.nextroom.presentation.extension.showKeyboard -import com.nextroom.nextroom.presentation.extension.snackbar -import com.nextroom.nextroom.presentation.model.InputState -import com.nextroom.nextroom.presentation.ui.login.LoginEvent -import dagger.hilt.android.AndroidEntryPoint -import org.orbitmvi.orbit.viewmodel.observe - -@AndroidEntryPoint -class VerifyFragment : BaseFragment(FragmentAdminCodeBinding::inflate) { - - private val viewModel: VerifyViewModel by viewModels() - - private val args by navArgs() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - initViews() - observe() - } - - private fun initViews() = with(binding) { - etAdminCode.apply { - setStateListener() - setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - viewModel.complete() - true - } - - else -> false - } - } - doAfterTextChanged { - viewModel.inputCode(it.toString()) - } - showKeyboard() - } - btnInput.setOnClickListener { viewModel.complete() } - } - - private fun observe() { - viewModel.observe(viewLifecycleOwner, state = ::render, sideEffect = ::handleEvent) - } - - private fun render(state: VerifyState) = with(binding) { - if (state.inputState is InputState.Ok) { - val action = VerifyFragmentDirections.actionVerifyFragmentToGameFragment(args.subscribeStatus) - findNavController().safeNavigate(action) - } - btnInput.isEnabled = state.currentInput.length == 5 - } - - private fun handleEvent(event: LoginEvent) { - when (event) { - is LoginEvent.LoginFailed -> snackbar(event.message) - is LoginEvent.ShowMessage -> snackbar(event.message.toString(requireContext())) - LoginEvent.GoToOnboardingScreen -> Unit - LoginEvent.GoToGameScreen -> { - val action = VerifyFragmentDirections.actionVerifyFragmentToGameFragment(args.subscribeStatus) - findNavController().safeNavigate(action) - } - } - } -} diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/verity/VerifyState.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/verity/VerifyState.kt deleted file mode 100644 index 9d21a6fb..00000000 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/verity/VerifyState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.nextroom.nextroom.presentation.ui.verity - -import com.nextroom.nextroom.presentation.model.InputState - -data class VerifyState( - val currentInput: String = "", - val inputState: InputState = InputState.Empty, -) diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/verity/VerifyViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/verity/VerifyViewModel.kt deleted file mode 100644 index 9053043e..00000000 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/verity/VerifyViewModel.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.nextroom.nextroom.presentation.ui.verity - -import androidx.annotation.StringRes -import com.nextroom.nextroom.domain.repository.AdminRepository -import com.nextroom.nextroom.presentation.R -import com.nextroom.nextroom.presentation.base.BaseViewModel -import com.nextroom.nextroom.presentation.model.InputState -import com.nextroom.nextroom.presentation.model.UiText -import com.nextroom.nextroom.presentation.ui.login.LoginEvent -import dagger.hilt.android.lifecycle.HiltViewModel -import org.orbitmvi.orbit.Container -import org.orbitmvi.orbit.syntax.simple.intent -import org.orbitmvi.orbit.syntax.simple.postSideEffect -import org.orbitmvi.orbit.syntax.simple.reduce -import org.orbitmvi.orbit.viewmodel.container -import javax.inject.Inject - -@HiltViewModel -class VerifyViewModel @Inject constructor( - private val adminRepository: AdminRepository, -) : BaseViewModel() { - override val container: Container = container(VerifyState()) - - fun inputCode(code: String) = intent { - reduce { - state.copy( - currentInput = code, - inputState = if (code.isEmpty()) InputState.Empty else InputState.Typing, - ) - } - } - - fun complete() = intent { - if (state.currentInput.isBlank()) { - showMessage(R.string.admin_code_required) - return@intent - } - - val success = adminRepository.verifyAdminCode(state.currentInput) - reduce { - state.copy(inputState = if (success) InputState.Ok else InputState.Error(R.string.blank)) - } - if (!success) { - showMessage(R.string.admin_code_invalid) - } - } - - private fun showMessage(message: String) = intent { - postSideEffect(LoginEvent.ShowMessage(UiText(message))) - } - - private fun showMessage(@StringRes messageId: Int) = intent { - postSideEffect(LoginEvent.ShowMessage(UiText(messageId))) - } -} diff --git a/presentation/src/main/res/drawable/ic_fingerprint.png b/presentation/src/main/res/drawable/ic_fingerprint.png new file mode 100644 index 00000000..821bf26f Binary files /dev/null and b/presentation/src/main/res/drawable/ic_fingerprint.png differ diff --git a/presentation/src/main/res/layout/fragment_admin_code.xml b/presentation/src/main/res/layout/fragment_admin_code.xml deleted file mode 100644 index 67ee4ac2..00000000 --- a/presentation/src/main/res/layout/fragment_admin_code.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_admin_main.xml b/presentation/src/main/res/layout/fragment_admin_main.xml index 262b4cf0..d1f03c89 100644 --- a/presentation/src/main/res/layout/fragment_admin_main.xml +++ b/presentation/src/main/res/layout/fragment_admin_main.xml @@ -174,6 +174,15 @@ android:gravity="center" android:text="@string/admin_main_empty_theme_guide" /> + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_check_password.xml b/presentation/src/main/res/layout/fragment_check_password.xml new file mode 100644 index 00000000..dd25a4d7 --- /dev/null +++ b/presentation/src/main/res/layout/fragment_check_password.xml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_mypage.xml b/presentation/src/main/res/layout/fragment_mypage.xml index bcaafa0f..5ac60333 100644 --- a/presentation/src/main/res/layout/fragment_mypage.xml +++ b/presentation/src/main/res/layout/fragment_mypage.xml @@ -91,6 +91,37 @@ app:layout_constraintTop_toTopOf="@id/tv_subscribe" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/layout_opaque_loading.xml b/presentation/src/main/res/layout/layout_opaque_loading.xml new file mode 100644 index 00000000..b2180331 --- /dev/null +++ b/presentation/src/main/res/layout/layout_opaque_loading.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/navigation/nav_graph.xml b/presentation/src/main/res/navigation/nav_graph.xml index aefaf0d4..510da960 100644 --- a/presentation/src/main/res/navigation/nav_graph.xml +++ b/presentation/src/main/res/navigation/nav_graph.xml @@ -22,18 +22,6 @@ android:name="com.nextroom.nextroom.presentation.ui.adminmain.AdminMainFragment" android:label="AdminMainFragment" tools:layout="@layout/fragment_admin_main"> - - - - - - @@ -84,34 +72,6 @@ android:name="nrTwoButtonArgument" app:argType="com.nextroom.nextroom.presentation.common.NRTwoButtonDialog$NRTwoButtonArgument" /> - - - - - - - - - - - - - - @@ -239,4 +199,30 @@ android:id="@+id/action_to_home" app:destination="@id/adminMainFragment" /> + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 686c39d1..42fed8b1 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -37,16 +37,7 @@ 그리기 지우개 - 관리자 코드 5자리를\n입력해주세요 계정이 없으신가요? - 관리자 웹 사이트에 있는 관리자 코드를 입력해주세요 - 관리자 코드 - 입력 - 5자리 숫자를 입력해주세요 - ID - PASSWORD - 관리자 코드가 일치하지 않습니다. - 관리자 코드를 입력해주세요 관리자 웹 사이트에서 힌트를 업데이트 했다면 반드시 업데이트 버튼을 눌러주세요! 우리 지점의 테마 목록 @@ -125,4 +116,32 @@ 게임을 시작하시겠습니까? 버튼을 클릭하면 타이머가 시작됩니다 이미지를 로드할 수 없습니다.\n직원에게 문의해 주세요. + + 비밀번호 입력 + 앱 비밀번호를 입력해주세요. + 비밀번호가 일치하지 않습니다. + 비밀번호 설정 + 비밀번호 확인 + 앱 비밀번호를 입력해주세요. + 확인을 위해 한번 더 입력해 주세요. + 비밀번호 설정이 완료되었습니다. + 앱 비밀번호 변경 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 + + 안내 + 관리자만 게임을 시작할 수 있도록\n비밀번호 설정이 필요합니다. + 설정하기 + 지문 인증 + 지문을 입력하세요. + 취소 + 지문 인증에 실패했습니다. \ No newline at end of file