mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-14 20:57:26 +01:00
Fix regV5 SMS auto-fill and paste.
This commit is contained in:
+1
-3
@@ -89,9 +89,7 @@ fun VerificationCodeScreen(
|
||||
val code = state.autoFillCode ?: return@LaunchedEffect
|
||||
|
||||
if (code.length == VerificationCodeState.CODE_LENGTH && code.all { it.isDigit() } && !state.isSubmittingCode) {
|
||||
code.forEachIndexed { index, digit ->
|
||||
onEvent(VerificationCodeScreenEvents.DigitChanged(index, digit.toString()))
|
||||
}
|
||||
onEvent(VerificationCodeScreenEvents.DigitChanged(0, code))
|
||||
}
|
||||
onEvent(VerificationCodeScreenEvents.ConsumeAutoFillCode)
|
||||
}
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@ sealed class VerificationCodeScreenEvents {
|
||||
/**
|
||||
* The raw [value] of the digit field at [index] changed. The view model interprets it: a single digit is recorded
|
||||
* (submitting once the full code is present), an empty [value] is a backspace (deleting a digit and shifting the
|
||||
* following ones left), and multi-character input (e.g. a pasted "123-456") is treated as a pasted code.
|
||||
* following ones left), and multi-character input (e.g. a pasted "123-456" or an auto-filled SMS code) populates
|
||||
* every field at once and submits.
|
||||
*/
|
||||
data class DigitChanged(val index: Int, val value: String) : VerificationCodeScreenEvents() {
|
||||
override fun toString(): String = "DigitChanged(index=$index)"
|
||||
|
||||
+29
-16
@@ -191,7 +191,8 @@ class VerificationCodeViewModel(
|
||||
*
|
||||
* - an empty [value] is a backspace, deleting a digit and moving focus back
|
||||
* - a single digit is recorded and focus advances, submitting once the full code is present
|
||||
* - multi-character input (e.g. a pasted "123-456") is treated as a pasted code
|
||||
* - multi-character input (e.g. a pasted "123-456" or an auto-filled SMS code) populates every field at once and
|
||||
* submits, all in this single reducer pass
|
||||
*/
|
||||
private suspend fun applyDigitChanged(
|
||||
state: VerificationCodeState,
|
||||
@@ -225,7 +226,33 @@ class VerificationCodeViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
else -> applyPastedCode(state, remainder)
|
||||
else -> applyFullCode(state, addedDigits, stateEmitter)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates every digit field from a full pasted or auto-filled [code] in a single reducer pass and submits it.
|
||||
* Multi-character input that isn't a complete code is ignored.
|
||||
*/
|
||||
private suspend fun applyFullCode(
|
||||
state: VerificationCodeState,
|
||||
code: String,
|
||||
stateEmitter: (VerificationCodeState) -> Unit
|
||||
): VerificationCodeState {
|
||||
if (code.length != CODE_LENGTH) {
|
||||
Log.w(TAG, "[DigitChanged] Ignoring multi-character input containing ${code.length} digits.")
|
||||
return state
|
||||
}
|
||||
|
||||
val updated = state.copy(
|
||||
digits = code.map { it.toString() },
|
||||
focusedDigitIndex = CODE_LENGTH - 1
|
||||
)
|
||||
|
||||
return if (!updated.isSubmittingCode) {
|
||||
submitCode(updated, updated.code, stateEmitter)
|
||||
} else {
|
||||
updated
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,20 +284,6 @@ class VerificationCodeViewModel(
|
||||
return applyCodeEntered(state, code).copy(isSubmittingCode = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips any formatting (e.g. a hyphen) from pasted text and, if what remains is a full code, populates the fields
|
||||
* by reusing the [VerificationCodeState.autoFillCode] path. Pasted text that doesn't contain a full code is ignored.
|
||||
*/
|
||||
private fun applyPastedCode(state: VerificationCodeState, rawCode: String): VerificationCodeState {
|
||||
val digits = rawCode.filter { it.isDigit() }
|
||||
if (digits.length != CODE_LENGTH) {
|
||||
Log.w(TAG, "[DigitChanged] Ignoring pasted text containing ${digits.length} digits.")
|
||||
return state
|
||||
}
|
||||
|
||||
return state.copy(autoFillCode = digits)
|
||||
}
|
||||
|
||||
private suspend fun applyCodeEntered(inputState: VerificationCodeState, code: String): VerificationCodeState {
|
||||
var state = inputState
|
||||
var sessionMetadata = state.sessionMetadata ?: return state.also {
|
||||
|
||||
+5
-5
@@ -216,7 +216,7 @@ class VerificationCodeScreenTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `autoFillCode emits a DigitChanged for each digit`() {
|
||||
fun `autoFillCode emits a single DigitChanged with the full code`() {
|
||||
// Given
|
||||
val emittedEvents = mutableListOf<VerificationCodeScreenEvents>()
|
||||
|
||||
@@ -231,13 +231,13 @@ class VerificationCodeScreenTest {
|
||||
|
||||
// When - the auto-fill effect populates the fields
|
||||
composeTestRule.waitUntil(timeoutMillis = 5_000) {
|
||||
emittedEvents.filterIsInstance<VerificationCodeScreenEvents.DigitChanged>().size == 6
|
||||
emittedEvents.any { it is VerificationCodeScreenEvents.DigitChanged }
|
||||
}
|
||||
|
||||
// Then
|
||||
// Then - a single event carries the whole code, rather than a burst of per-digit events
|
||||
val digitChanges = emittedEvents.filterIsInstance<VerificationCodeScreenEvents.DigitChanged>()
|
||||
assert(digitChanges == (0 until 6).map { VerificationCodeScreenEvents.DigitChanged(it, "${it + 1}") }) {
|
||||
"Expected a DigitChanged per digit but got $digitChanges"
|
||||
assert(digitChanges == listOf(VerificationCodeScreenEvents.DigitChanged(0, "123456"))) {
|
||||
"Expected a single DigitChanged(0, 123456) but got $digitChanges"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+56
-8
@@ -197,8 +197,17 @@ class VerificationCodeViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DigitChanged with pasted hyphenated text stores the stripped code in autoFillCode`() = runTest {
|
||||
val initialState = VerificationCodeState()
|
||||
fun `DigitChanged with pasted hyphenated text populates all digits and submits`() = runTest {
|
||||
val sessionMetadata = createSessionMetadata()
|
||||
val initialState = VerificationCodeState(
|
||||
sessionMetadata = sessionMetadata,
|
||||
e164 = "+15551234567"
|
||||
)
|
||||
|
||||
coEvery { mockRepository.submitVerificationCode(any(), any()) } returns
|
||||
RequestResult.NonSuccess(
|
||||
NetworkController.SubmitVerificationCodeError.InvalidSessionIdOrVerificationCode("Wrong code")
|
||||
)
|
||||
|
||||
viewModel.applyEvent(
|
||||
initialState,
|
||||
@@ -206,12 +215,23 @@ class VerificationCodeViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().autoFillCode).isEqualTo("123456")
|
||||
coVerify { mockRepository.submitVerificationCode(sessionMetadata.id, "123456") }
|
||||
assertThat(emittedStates.first().digits).isEqualTo(listOf("1", "2", "3", "4", "5", "6"))
|
||||
assertThat(emittedStates.first().isSubmittingCode).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DigitChanged with a pasted plain code stores it in autoFillCode`() = runTest {
|
||||
val initialState = VerificationCodeState()
|
||||
fun `DigitChanged with a pasted plain code populates all digits and submits`() = runTest {
|
||||
val sessionMetadata = createSessionMetadata()
|
||||
val initialState = VerificationCodeState(
|
||||
sessionMetadata = sessionMetadata,
|
||||
e164 = "+15551234567"
|
||||
)
|
||||
|
||||
coEvery { mockRepository.submitVerificationCode(any(), any()) } returns
|
||||
RequestResult.NonSuccess(
|
||||
NetworkController.SubmitVerificationCodeError.InvalidSessionIdOrVerificationCode("Wrong code")
|
||||
)
|
||||
|
||||
viewModel.applyEvent(
|
||||
initialState,
|
||||
@@ -219,12 +239,17 @@ class VerificationCodeViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().autoFillCode).isEqualTo("123456")
|
||||
coVerify { mockRepository.submitVerificationCode(sessionMetadata.id, "123456") }
|
||||
assertThat(emittedStates.first().digits).isEqualTo(listOf("1", "2", "3", "4", "5", "6"))
|
||||
assertThat(emittedStates.first().isSubmittingCode).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DigitChanged with pasted text of the wrong length is ignored`() = runTest {
|
||||
val initialState = VerificationCodeState()
|
||||
val initialState = VerificationCodeState(
|
||||
sessionMetadata = createSessionMetadata(),
|
||||
e164 = "+15551234567"
|
||||
)
|
||||
|
||||
viewModel.applyEvent(
|
||||
initialState,
|
||||
@@ -232,7 +257,8 @@ class VerificationCodeViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().autoFillCode).isNull()
|
||||
coVerify(exactly = 0) { mockRepository.submitVerificationCode(any(), any()) }
|
||||
assertThat(emittedStates.last().digits).isEqualTo(listOf("", "", "", "", "", ""))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -262,6 +288,28 @@ class VerificationCodeViewModelTest {
|
||||
assertThat(vm.state.value.autoFillCode).isEqualTo("123456")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DigitChanged with a full code dispatched through the event channel submits it in a single pass`() = runTest(testDispatcher) {
|
||||
val sessionMetadata = createSessionMetadata()
|
||||
parentState.value = RegistrationFlowState(
|
||||
sessionMetadata = sessionMetadata,
|
||||
sessionE164 = "+15551234567"
|
||||
)
|
||||
|
||||
coEvery { mockRepository.submitVerificationCode(any(), any()) } returns
|
||||
RequestResult.NonSuccess(
|
||||
NetworkController.SubmitVerificationCodeError.InvalidSessionIdOrVerificationCode("Wrong code")
|
||||
)
|
||||
|
||||
backgroundScope.launch { viewModel.state.collect {} }
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.onEvent(VerificationCodeScreenEvents.DigitChanged(0, "123456"))
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify { mockRepository.submitVerificationCode(sessionMetadata.id, "123456") }
|
||||
}
|
||||
|
||||
// ==================== applyEvent: DigitChanged Tests ====================
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user