Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,71 @@ package com.nextroom.nextroom.presentation.ui.password

import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.biometric.BiometricPrompt
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
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.base.ComposeBaseViewModelFragment
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 com.nextroom.nextroom.presentation.ui.password.compose.CheckPasswordScreen
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch

@AndroidEntryPoint
class CheckPasswordFragment : BaseFragment<FragmentCheckPasswordBinding>(FragmentCheckPasswordBinding::inflate) {
class CheckPasswordFragment : ComposeBaseViewModelFragment<CheckPasswordViewModel>() {

private val viewModel: CheckPasswordViewModel by viewModels()
override val screenName: String = "check_password"
override val viewModel: CheckPasswordViewModel by viewModels()
private val args: CheckPasswordFragmentArgs by navArgs()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
private val showBiometric: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P

initView()
initListener()
initSubscribe()
showBiometricPrompt()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
val state by viewModel.uiState.collectAsState()
CheckPasswordScreen(
state = state,
onKeyClick = viewModel::onNumberClicked,
onBackspaceClick = viewModel::onBackSpaceClicked,
onBackClick = { findNavController().navigateUp() },
onBiometricClick = ::showBiometricPrompt,
showBiometric = showBiometric,
)
}
}
}

private fun initView() {
binding.keyBiometric.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
showBiometricPrompt()
}

private fun initSubscribe() {
override 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))
}
}
Expand All @@ -60,26 +75,20 @@ class CheckPasswordFragment : BaseFragment<FragmentCheckPasswordBinding>(Fragmen
}
}

private fun updateUi(password: String) {
binding.customCodeInput.setCode(password)
}

private fun showBiometricPrompt() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return
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)
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)
}
}

Expand All @@ -90,20 +99,4 @@ class CheckPasswordFragment : BaseFragment<FragmentCheckPasswordBinding>(Fragmen
)
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() }
binding.ivBack.setOnClickListener { findNavController().navigateUp() }
}
}
}
Original file line number Diff line number Diff line change
@@ -1,73 +1,82 @@
package com.nextroom.nextroom.presentation.ui.password

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nextroom.nextroom.domain.repository.AdminRepository
import com.nextroom.nextroom.presentation.R
import com.nextroom.nextroom.presentation.base.NewBaseViewModel
import com.nextroom.nextroom.presentation.model.InputState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class CheckPasswordViewModel @Inject constructor(
val adminRepository: AdminRepository
) : ViewModel() {
val inputPassword = MutableStateFlow("")
private val _uiEvent = MutableSharedFlow<UiEvent>()
private val adminRepository: AdminRepository
) : NewBaseViewModel() {

private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()

private val _uiEvent = MutableSharedFlow<UiEvent>(extraBufferCapacity = 1)
val uiEvent = _uiEvent.asSharedFlow()

init {
viewModelScope.launch {
inputPassword.collect { password ->
if (password.length == MAX_PASSWORD_LEN) {
checkPassword(inputPassword.value)
}
}
}
}
fun onNumberClicked(number: Int) {
val current = _uiState.value.input
if (current.length >= MAX_PASSWORD_LEN) return

fun onNumberClicked(number: String) {
if (inputPassword.value.length == MAX_PASSWORD_LEN) return
viewModelScope.launch {
inputPassword.emit(inputPassword.value + number)
}
}
val newInput = current + number.toString()
_uiState.update { it.copy(input = newInput, inputState = InputState.Typing) }

private fun checkPassword(inputPassword: String) {
viewModelScope.launch {
if (inputPassword == adminRepository.getAppPassword()) {
UiEvent.PasswordCorrect
} else {
clearPassword()
UiEvent.PasswordInCorrect
}.also {
_uiEvent.emit(it)
}
if (newInput.length == MAX_PASSWORD_LEN) {
checkPassword(newInput)
}
}

private fun clearPassword() {
viewModelScope.launch {
inputPassword.emit("")
fun onBackSpaceClicked() {
val current = _uiState.value.input
if (current.isEmpty()) return
_uiState.update {
it.copy(
input = current.dropLast(1),
inputState = if (current.length <= 1) InputState.Empty else InputState.Typing
)
}
}

fun onBackSpaceClicked() {
if (inputPassword.value.isNotEmpty()) {
viewModelScope.launch {
inputPassword.emit(inputPassword.value.dropLast(1))
private fun checkPassword(input: String) {
baseViewModelScope.launch {
if (input == adminRepository.getAppPassword()) {
_uiEvent.emit(UiEvent.PasswordCorrect)
} else {
_uiState.update {
it.copy(
inputState = InputState.Error(R.string.text_incorrect_password_error_message)
)
}
_uiEvent.emit(UiEvent.PasswordInCorrect)
delay(ERROR_DISPLAY_MILLIS)
_uiState.update { UiState() }
}
}
}

data class UiState(
val input: String = "",
val inputState: InputState = InputState.Empty,
)

sealed interface UiEvent {
data object PasswordCorrect : UiEvent
data object PasswordInCorrect : UiEvent
}

companion object {
const val MAX_PASSWORD_LEN = 4
private const val ERROR_DISPLAY_MILLIS = 500L
}
}
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,50 @@
package com.nextroom.nextroom.presentation.ui.password

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
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.base.ComposeBaseViewModelFragment
import com.nextroom.nextroom.presentation.extension.repeatOnStarted
import com.nextroom.nextroom.presentation.extension.toast
import com.nextroom.nextroom.presentation.ui.password.compose.SetPasswordScreen
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch

@AndroidEntryPoint
class SetPasswordFragment : BaseFragment<FragmentSetPasswordBinding>(FragmentSetPasswordBinding::inflate) {
private val viewModel: SetPasswordViewModel by viewModels()
class SetPasswordFragment : ComposeBaseViewModelFragment<SetPasswordViewModel>() {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
override val screenName: String = "set_password"
override val viewModel: SetPasswordViewModel by viewModels()

initListener()
initSubscribe()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
val state by viewModel.uiState.collectAsState()
SetPasswordScreen(
state = state,
onKeyClick = viewModel::onNumberClicked,
onBackspaceClick = viewModel::onBackSpaceClicked,
onBackClick = { findNavController().navigateUp() },
)
}
}
}

private fun initSubscribe() {
override fun initSubscribe() {
viewLifecycleOwner.repeatOnStarted {
launch {
viewModel.uiState.collect { state ->
updateUi(state)
}
}
launch {
viewModel.uiEvent.collect { event ->
when (event) {
Expand All @@ -39,42 +54,11 @@ class SetPasswordFragment : BaseFragment<FragmentSetPasswordBinding>(FragmentSet
}

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() }
binding.ivBack.setOnClickListener { findNavController().navigateUp() }
}
}
}
Loading
Loading