mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-04 20:34:14 +01:00
Support automatic SMS fill in regV5.
This commit is contained in:
committed by
Alex Hart
parent
da8af40e4f
commit
7e26badb4c
@@ -84,8 +84,9 @@ dependencies {
|
||||
// Phone number formatting
|
||||
implementation(libs.google.libphonenumber)
|
||||
|
||||
// Phone number hint
|
||||
// Phone number hint + SMS verification code retriever
|
||||
implementation(libs.google.play.services.auth)
|
||||
implementation(libs.kotlinx.coroutines.play.services)
|
||||
|
||||
// Credential Manager (password manager retrieval)
|
||||
implementation(libs.androidx.credentials)
|
||||
|
||||
@@ -554,8 +554,10 @@ private fun EntryProviderScope<NavKey>.navigationEntries(
|
||||
|
||||
// -- Verification Code Entry Screen
|
||||
entry<RegistrationRoute.VerificationCodeEntry> {
|
||||
val context = LocalContext.current.applicationContext
|
||||
val viewModel: VerificationCodeViewModel = viewModel(
|
||||
factory = VerificationCodeViewModel.Factory(
|
||||
context = context,
|
||||
repository = registrationRepository,
|
||||
parentState = registrationViewModel.state,
|
||||
parentEventEmitter = registrationViewModel::onEvent
|
||||
|
||||
@@ -8,12 +8,15 @@ package org.signal.registration
|
||||
import android.app.backup.BackupManager
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.google.android.gms.auth.api.phone.SmsRetriever
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.json.Json
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.archive.LocalBackupRestoreProgress
|
||||
@@ -93,6 +96,35 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the Play Services SMS retriever so an incoming verification code can be automatically entered.
|
||||
*
|
||||
* The listener [lives for 5 minutes](https://developers.google.com/android/reference/com/google/android/gms/auth/api/phone/SmsRetrieverApi).
|
||||
* Callers should pass the result as `smsAutoRetrieveCodeSupported` when requesting a code so the server formats the
|
||||
* SMS for retrieval.
|
||||
*
|
||||
* @return whether the Play Services SMS retriever was successfully started.
|
||||
*/
|
||||
suspend fun registerSmsListener(): Boolean {
|
||||
Log.d(TAG, "Attempting to start verification code SMS retriever.")
|
||||
val started = withTimeoutOrNull(5.seconds.inWholeMilliseconds) {
|
||||
try {
|
||||
SmsRetriever.getClient(context).startSmsRetriever().await()
|
||||
Log.d(TAG, "Successfully started verification code SMS retriever.")
|
||||
true
|
||||
} catch (ex: Exception) {
|
||||
Log.w(TAG, "Could not start verification code SMS retriever due to exception.", ex)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
if (started == null) {
|
||||
Log.w(TAG, "Could not start verification code SMS retriever due to timeout.")
|
||||
}
|
||||
|
||||
return started == true
|
||||
}
|
||||
|
||||
fun getCaptchaUrl(): String = networkController.getCaptchaUrl()
|
||||
|
||||
suspend fun submitCaptchaToken(
|
||||
|
||||
+2
-2
@@ -540,7 +540,7 @@ class PhoneNumberEntryViewModel(
|
||||
|
||||
val verificationCodeResponse = this@PhoneNumberEntryViewModel.repository.requestVerificationCode(
|
||||
sessionMetadata.id,
|
||||
smsAutoRetrieveCodeSupported = false,
|
||||
smsAutoRetrieveCodeSupported = repository.registerSmsListener(),
|
||||
transport = NetworkController.VerificationCodeTransport.SMS
|
||||
)
|
||||
|
||||
@@ -650,7 +650,7 @@ class PhoneNumberEntryViewModel(
|
||||
|
||||
val verificationCodeResponse = this@PhoneNumberEntryViewModel.repository.requestVerificationCode(
|
||||
sessionId = sessionMetadata.id,
|
||||
smsAutoRetrieveCodeSupported = false, // TODO eventually support this
|
||||
smsAutoRetrieveCodeSupported = repository.registerSmsListener(),
|
||||
transport = NetworkController.VerificationCodeTransport.SMS
|
||||
)
|
||||
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.registration.screens.verificationcode
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* Extracts a verification code from the body of an SMS delivered by the Play Services SMS retriever.
|
||||
*/
|
||||
object VerificationCodeParser {
|
||||
|
||||
private val CHALLENGE_PATTERN = Pattern.compile("(.*\\D|^)([0-9]{3,4})-?([0-9]{3,4}).*", Pattern.DOTALL)
|
||||
|
||||
fun parse(messageBody: String?): String? {
|
||||
if (messageBody == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val matcher = CHALLENGE_PATTERN.matcher(messageBody)
|
||||
if (!matcher.matches()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return matcher.group(matcher.groupCount() - 1) + matcher.group(matcher.groupCount())
|
||||
}
|
||||
}
|
||||
+12
@@ -93,6 +93,18 @@ fun VerificationCodeScreen(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state.autoFillCode) {
|
||||
val code = state.autoFillCode ?: return@LaunchedEffect
|
||||
|
||||
if (code.length == 6 && code.all { it.isDigit() } && !state.isSubmittingCode) {
|
||||
code.forEachIndexed { index, digit ->
|
||||
digits = digits.toMutableList().also { it[index] = digit.toString() }
|
||||
delay(200)
|
||||
}
|
||||
}
|
||||
onEvent(VerificationCodeScreenEvents.ConsumeAutoFillCode)
|
||||
}
|
||||
|
||||
LaunchedEffect(state.oneTimeEvent) {
|
||||
val event = state.oneTimeEvent ?: return@LaunchedEffect
|
||||
|
||||
|
||||
+9
@@ -12,6 +12,15 @@ sealed class VerificationCodeScreenEvents {
|
||||
override fun toString(): String = "CodeEntered(code=${code.censor()})"
|
||||
}
|
||||
|
||||
/**
|
||||
* A verification code was automatically retrieved from an incoming SMS via the Play Services SMS retriever.
|
||||
*/
|
||||
data class CodeAutoFilled(val code: String) : VerificationCodeScreenEvents() {
|
||||
override fun toString(): String = "CodeAutoFilled(code=${code.censor()})"
|
||||
}
|
||||
|
||||
data object ConsumeAutoFillCode : VerificationCodeScreenEvents()
|
||||
|
||||
data object WrongNumber : VerificationCodeScreenEvents()
|
||||
|
||||
data object ResendSms : VerificationCodeScreenEvents()
|
||||
|
||||
+2
-1
@@ -15,9 +15,10 @@ data class VerificationCodeState(
|
||||
val isSubmittingCode: Boolean = false,
|
||||
val rateLimits: SmsAndCallRateLimits = SmsAndCallRateLimits(),
|
||||
val incorrectCodeAttempts: Int = 0,
|
||||
val autoFillCode: String? = null,
|
||||
val oneTimeEvent: OneTimeEvent? = null
|
||||
) {
|
||||
override fun toString(): String = "VerificationCodeState(sessionMetadata=${sessionMetadata?.let { "present" }}, e164=$e164, isSubmittingCode=$isSubmittingCode, rateLimits=$rateLimits, incorrectCodeAttempts=$incorrectCodeAttempts, oneTimeEvent=$oneTimeEvent)"
|
||||
override fun toString(): String = "VerificationCodeState(sessionMetadata=${sessionMetadata?.let { "present" }}, e164=$e164, isSubmittingCode=$isSubmittingCode, rateLimits=$rateLimits, incorrectCodeAttempts=$incorrectCodeAttempts, autoFillCode=${autoFillCode?.let { "present" }}, oneTimeEvent=$oneTimeEvent)"
|
||||
|
||||
sealed interface OneTimeEvent {
|
||||
data object NetworkError : OneTimeEvent
|
||||
|
||||
+76
-3
@@ -5,16 +5,29 @@
|
||||
|
||||
package org.signal.registration.screens.verificationcode
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.android.gms.auth.api.phone.SmsRetriever
|
||||
import com.google.android.gms.common.api.CommonStatusCodes
|
||||
import com.google.android.gms.common.api.Status
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.net.RequestResult
|
||||
import org.signal.registration.NetworkController
|
||||
@@ -34,11 +47,45 @@ class VerificationCodeViewModel(
|
||||
private val repository: RegistrationRepository,
|
||||
private val parentState: StateFlow<RegistrationFlowState>,
|
||||
private val parentEventEmitter: (RegistrationFlowEvent) -> Unit,
|
||||
smsCodeEvents: Flow<String> = emptyFlow(),
|
||||
private val clock: () -> Long = { System.currentTimeMillis() }
|
||||
) : EventDrivenViewModel<VerificationCodeScreenEvents>(TAG) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(VerificationCodeViewModel::class)
|
||||
|
||||
/**
|
||||
* Cold [Flow] of verification codes automatically retrieved from incoming SMS messages via the Play Services SMS
|
||||
* retriever. Registers a [BroadcastReceiver] for [SmsRetriever.SMS_RETRIEVED_ACTION] while collected, and
|
||||
* unregisters it when collection stops.
|
||||
*/
|
||||
fun smsCodeFlow(context: Context): Flow<String> = callbackFlow {
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != SmsRetriever.SMS_RETRIEVED_ACTION) {
|
||||
return
|
||||
}
|
||||
|
||||
val status = intent.extras?.get(SmsRetriever.EXTRA_STATUS) as? Status
|
||||
when (status?.statusCode) {
|
||||
CommonStatusCodes.SUCCESS -> {
|
||||
val code = VerificationCodeParser.parse(intent.extras?.getString(SmsRetriever.EXTRA_SMS_MESSAGE))
|
||||
if (code != null) {
|
||||
Log.i(TAG, "Received verification code via SMS retriever.")
|
||||
trySend(code)
|
||||
} else {
|
||||
Log.w(TAG, "Could not parse verification code from retrieved SMS.")
|
||||
}
|
||||
}
|
||||
CommonStatusCodes.TIMEOUT -> Log.w(TAG, "Timed out waiting for the verification SMS to arrive.")
|
||||
else -> Log.w(TAG, "SMS retriever broadcast had an unexpected status code: ${status?.statusCode}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContextCompat.registerReceiver(context, receiver, IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION), SmsRetriever.SEND_PERMISSION, null, ContextCompat.RECEIVER_EXPORTED)
|
||||
awaitClose { context.unregisterReceiver(receiver) }
|
||||
}
|
||||
}
|
||||
|
||||
private val _localState = MutableStateFlow(VerificationCodeState())
|
||||
@@ -49,6 +96,14 @@ class VerificationCodeViewModel(
|
||||
private var nextSmsAvailableAt: Duration = 0.seconds
|
||||
private var nextCallAvailableAt: Duration = 0.seconds
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
smsCodeEvents.collect { code ->
|
||||
onEvent(VerificationCodeScreenEvents.CodeAutoFilled(code))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun processEvent(event: VerificationCodeScreenEvents) {
|
||||
applyEvent(state.value, event) { _localState.value = it }
|
||||
}
|
||||
@@ -60,6 +115,8 @@ class VerificationCodeViewModel(
|
||||
stateEmitter(state.copy(isSubmittingCode = true))
|
||||
applyCodeEntered(state, event.code).copy(isSubmittingCode = false)
|
||||
}
|
||||
is VerificationCodeScreenEvents.CodeAutoFilled -> state.copy(autoFillCode = event.code)
|
||||
is VerificationCodeScreenEvents.ConsumeAutoFillCode -> state.copy(autoFillCode = null)
|
||||
is VerificationCodeScreenEvents.WrongNumber -> state.also { parentEventEmitter.navigateTo(RegistrationRoute.PhoneNumberEntry) }
|
||||
is VerificationCodeScreenEvents.ResendSms -> applyResendCode(state, NetworkController.VerificationCodeTransport.SMS)
|
||||
is VerificationCodeScreenEvents.CallMe -> applyResendCode(state, NetworkController.VerificationCodeTransport.VOICE)
|
||||
@@ -236,7 +293,7 @@ class VerificationCodeViewModel(
|
||||
|
||||
val result = repository.requestVerificationCode(
|
||||
sessionId = state.sessionMetadata.id,
|
||||
smsAutoRetrieveCodeSupported = false,
|
||||
smsAutoRetrieveCodeSupported = repository.registerSmsListener(),
|
||||
transport = transport
|
||||
)
|
||||
|
||||
@@ -322,13 +379,29 @@ class VerificationCodeViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param smsCodeEvents The stream of auto-retrieved verification codes. Tests can inject codes directly; production
|
||||
* should use the [Context]-based constructor, which builds a real SMS retriever flow.
|
||||
*/
|
||||
class Factory(
|
||||
private val repository: RegistrationRepository,
|
||||
private val parentState: StateFlow<RegistrationFlowState>,
|
||||
private val parentEventEmitter: (RegistrationFlowEvent) -> Unit
|
||||
private val parentEventEmitter: (RegistrationFlowEvent) -> Unit,
|
||||
private val smsCodeEvents: Flow<String>
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
/**
|
||||
* Builds a real SMS retriever flow from [context]. Prefer the application context.
|
||||
*/
|
||||
constructor(
|
||||
context: Context,
|
||||
repository: RegistrationRepository,
|
||||
parentState: StateFlow<RegistrationFlowState>,
|
||||
parentEventEmitter: (RegistrationFlowEvent) -> Unit
|
||||
) : this(repository, parentState, parentEventEmitter, smsCodeFlow(context))
|
||||
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return VerificationCodeViewModel(repository, parentState, parentEventEmitter) as T
|
||||
return VerificationCodeViewModel(repository, parentState, parentEventEmitter, smsCodeEvents) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.registration.screens.verificationcode
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNull
|
||||
import org.junit.Test
|
||||
|
||||
class VerificationCodeParserTest {
|
||||
|
||||
@Test
|
||||
fun `parses hyphenated code`() {
|
||||
assertThat(VerificationCodeParser.parse("<#> Your Signal code: 123-456 abcd1234efg")).isEqualTo("123456")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parses non-hyphenated code`() {
|
||||
assertThat(VerificationCodeParser.parse("Your Signal code: 123456")).isEqualTo("123456")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns null for message without a code`() {
|
||||
assertThat(VerificationCodeParser.parse("Your Signal code is on its way")).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns null for null message`() {
|
||||
assertThat(VerificationCodeParser.parse(null)).isNull()
|
||||
}
|
||||
}
|
||||
+30
@@ -177,6 +177,36 @@ class VerificationCodeScreenTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `autoFillCode populates the fields and emits CodeEntered`() {
|
||||
// Given
|
||||
var emittedEvent: VerificationCodeScreenEvents? = null
|
||||
|
||||
composeTestRule.setContent {
|
||||
SignalTheme {
|
||||
VerificationCodeScreen(
|
||||
state = VerificationCodeState(autoFillCode = "123456"),
|
||||
onEvent = { event ->
|
||||
if (event is VerificationCodeScreenEvents.CodeEntered) {
|
||||
emittedEvent = event
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// When - the auto-fill effect staggers digits into the fields
|
||||
composeTestRule.waitUntil(timeoutMillis = 5_000) { emittedEvent != null }
|
||||
|
||||
// Then
|
||||
assert(emittedEvent is VerificationCodeScreenEvents.CodeEntered) {
|
||||
"Expected CodeEntered event but got $emittedEvent"
|
||||
}
|
||||
assert((emittedEvent as VerificationCodeScreenEvents.CodeEntered).code == "123456") {
|
||||
"Expected code '123456' but got ${(emittedEvent as VerificationCodeScreenEvents.CodeEntered).code}"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `screen displays all action buttons`() {
|
||||
// Given
|
||||
|
||||
+81
@@ -14,9 +14,19 @@ import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import assertk.assertions.prop
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
@@ -29,8 +39,11 @@ import org.signal.registration.RegistrationRepository
|
||||
import org.signal.registration.RegistrationRoute
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class VerificationCodeViewModelTest {
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
|
||||
private lateinit var viewModel: VerificationCodeViewModel
|
||||
private lateinit var mockRepository: RegistrationRepository
|
||||
private lateinit var parentState: MutableStateFlow<RegistrationFlowState>
|
||||
@@ -41,6 +54,7 @@ class VerificationCodeViewModelTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
mockRepository = mockk(relaxed = true)
|
||||
// Initialize with valid session data to prevent ResetState emission during ViewModel initialization
|
||||
parentState = MutableStateFlow(
|
||||
@@ -56,6 +70,11 @@ class VerificationCodeViewModelTest {
|
||||
viewModel = VerificationCodeViewModel(mockRepository, parentState, parentEventEmitter)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
// ==================== applyParentState Tests ====================
|
||||
|
||||
@Test
|
||||
@@ -161,6 +180,48 @@ class VerificationCodeViewModelTest {
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNull()
|
||||
}
|
||||
|
||||
// ==================== applyEvent: SMS Auto-Fill Tests ====================
|
||||
|
||||
@Test
|
||||
fun `CodeAutoFilled stores the code in autoFillCode`() = runTest {
|
||||
val initialState = VerificationCodeState()
|
||||
|
||||
viewModel.applyEvent(
|
||||
initialState,
|
||||
VerificationCodeScreenEvents.CodeAutoFilled("123456"),
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().autoFillCode).isEqualTo("123456")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConsumeAutoFillCode clears autoFillCode`() = runTest {
|
||||
val initialState = VerificationCodeState(autoFillCode = "123456")
|
||||
|
||||
viewModel.applyEvent(
|
||||
initialState,
|
||||
VerificationCodeScreenEvents.ConsumeAutoFillCode,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().autoFillCode).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `codes from the SMS retriever flow are pushed into the state`() = runTest(testDispatcher) {
|
||||
val smsCodes = MutableSharedFlow<String>(extraBufferCapacity = 1)
|
||||
val vm = VerificationCodeViewModel(mockRepository, parentState, parentEventEmitter, smsCodes)
|
||||
|
||||
backgroundScope.launch { vm.state.collect {} }
|
||||
advanceUntilIdle()
|
||||
|
||||
smsCodes.emit("123456")
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(vm.state.value.autoFillCode).isEqualTo("123456")
|
||||
}
|
||||
|
||||
// ==================== applyEvent: WrongNumber Tests ====================
|
||||
|
||||
@Test
|
||||
@@ -573,6 +634,26 @@ class VerificationCodeViewModelTest {
|
||||
assertThat(emittedStates.last().sessionMetadata).isEqualTo(updatedSession)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ResendSms passes registerSmsListener result as smsAutoRetrieveCodeSupported`() = runTest {
|
||||
val sessionMetadata = createSessionMetadata()
|
||||
val initialState = VerificationCodeState(sessionMetadata = sessionMetadata)
|
||||
|
||||
coEvery { mockRepository.registerSmsListener() } returns true
|
||||
coEvery { mockRepository.requestVerificationCode(any(), any(), any()) } returns
|
||||
RequestResult.Success(sessionMetadata)
|
||||
|
||||
viewModel.applyEvent(initialState, VerificationCodeScreenEvents.ResendSms, stateEmitter)
|
||||
|
||||
coVerify {
|
||||
mockRepository.requestVerificationCode(
|
||||
sessionId = sessionMetadata.id,
|
||||
smsAutoRetrieveCodeSupported = true,
|
||||
transport = NetworkController.VerificationCodeTransport.SMS
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ResendSms with rate limit returns RateLimited event`() = runTest {
|
||||
val sessionMetadata = createSessionMetadata()
|
||||
|
||||
Reference in New Issue
Block a user