mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-20 00:35:47 +01:00
Use a shared text entry field for TOTP entry.
This commit is contained in:
+20
-4
@@ -5,11 +5,14 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.account.authenticator
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.signal.appsettings.totpcodeentry.TotpCodeEntryAction
|
||||
@@ -18,6 +21,7 @@ import org.signal.appsettings.totpcodeentry.TotpCodeEntryState
|
||||
import org.signal.appsettings.totpcodeentry.TotpCodeEntryState.Error
|
||||
import org.signal.core.ui.compose.EventDrivenViewModel
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.uicomponents.codeentryfield.CodeEntryFieldPresenter
|
||||
|
||||
/**
|
||||
* Drives the screen that collects a code from the user's authenticator app, which is how the service learns the user
|
||||
@@ -37,16 +41,28 @@ class TotpCodeEntryViewModel(
|
||||
val state: StateFlow<TotpCodeEntryState> = _state.asStateFlow()
|
||||
val actions: Flow<TotpCodeEntryAction> = _actions.receiveAsFlow()
|
||||
|
||||
private val codeEntryPresenter = CodeEntryFieldPresenter(viewModelScope)
|
||||
|
||||
init {
|
||||
codeEntryPresenter
|
||||
.state
|
||||
.onEach { onEvent(TotpCodeEntryEvent.CodeEntryStateChanged(it)) }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override suspend fun processEvent(event: TotpCodeEntryEvent) {
|
||||
when (event) {
|
||||
TotpCodeEntryEvent.NavigateBackClicked -> {
|
||||
_actions.send(TotpCodeEntryAction.NavigateBack)
|
||||
}
|
||||
is TotpCodeEntryEvent.CodeChanged -> {
|
||||
val digits = event.code.filter { it.isDigit() }.take(TotpCodeEntryState.CODE_LENGTH)
|
||||
_state.update { it.copy(code = digits, error = Error.None) }
|
||||
is TotpCodeEntryEvent.CodeEntryEvent -> {
|
||||
_state.update { it.copy(error = Error.None) }
|
||||
codeEntryPresenter.onEvent(event.event)
|
||||
}
|
||||
TotpCodeEntryEvent.DoneClicked -> {
|
||||
is TotpCodeEntryEvent.CodeEntryStateChanged -> {
|
||||
_state.update { it.copy(codeEntry = event.codeEntryState) }
|
||||
}
|
||||
TotpCodeEntryEvent.NextClicked -> {
|
||||
if (!_state.value.canSubmit) {
|
||||
return
|
||||
}
|
||||
|
||||
+12
-14
@@ -28,6 +28,7 @@ import org.junit.Test
|
||||
import org.signal.appsettings.totpcodeentry.TotpCodeEntryAction
|
||||
import org.signal.appsettings.totpcodeentry.TotpCodeEntryEvent
|
||||
import org.signal.appsettings.totpcodeentry.TotpCodeEntryState.Error
|
||||
import org.signal.uicomponents.codeentryfield.CodeEntryFieldEvents
|
||||
import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -57,25 +58,16 @@ class TotpCodeEntryViewModelTest {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non-digits are dropped and the code is capped at six digits`() = runTest(testDispatcher) {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.onEvent(TotpCodeEntryEvent.CodeChanged("12a34 5678"))
|
||||
|
||||
assertThat(viewModel.state.value.code).isEqualTo(FULL_CODE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a partial code can't be submitted`() = runTest(testDispatcher) {
|
||||
val viewModel = createViewModel()
|
||||
val actions = collectActions(viewModel.actions)
|
||||
|
||||
viewModel.onEvent(TotpCodeEntryEvent.CodeChanged("123"))
|
||||
enterCode(viewModel, "123")
|
||||
|
||||
assertThat(viewModel.state.value.canSubmit).isFalse()
|
||||
|
||||
viewModel.onEvent(TotpCodeEntryEvent.DoneClicked)
|
||||
viewModel.onEvent(TotpCodeEntryEvent.NextClicked)
|
||||
|
||||
assertThat(actions).isEmpty()
|
||||
}
|
||||
@@ -124,7 +116,7 @@ class TotpCodeEntryViewModelTest {
|
||||
val viewModel = createViewModel()
|
||||
submit(viewModel)
|
||||
|
||||
viewModel.onEvent(TotpCodeEntryEvent.CodeChanged("1"))
|
||||
enterCode(viewModel, "1")
|
||||
|
||||
assertThat(viewModel.state.value.error).isEqualTo(Error.None)
|
||||
}
|
||||
@@ -140,8 +132,14 @@ class TotpCodeEntryViewModelTest {
|
||||
}
|
||||
|
||||
private fun submit(viewModel: TotpCodeEntryViewModel) {
|
||||
viewModel.onEvent(TotpCodeEntryEvent.CodeChanged(FULL_CODE))
|
||||
viewModel.onEvent(TotpCodeEntryEvent.DoneClicked)
|
||||
enterCode(viewModel, FULL_CODE)
|
||||
viewModel.onEvent(TotpCodeEntryEvent.NextClicked)
|
||||
}
|
||||
|
||||
private fun enterCode(viewModel: TotpCodeEntryViewModel, code: String) {
|
||||
code.forEachIndexed { index, digit ->
|
||||
viewModel.onEvent(TotpCodeEntryEvent.CodeEntryEvent(CodeEntryFieldEvents.DigitChanged(index, digit.toString())))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel() = TotpCodeEntryViewModel(repository = repository)
|
||||
|
||||
Reference in New Issue
Block a user