From 7e26badb4c163bf64bdf1d88be61cb732ca1cbb0 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 2 Jul 2026 12:04:10 -0400 Subject: [PATCH] Support automatic SMS fill in regV5. --- feature/registration/build.gradle.kts | 3 +- .../registration/RegistrationNavigation.kt | 2 + .../registration/RegistrationRepository.kt | 32 ++++++++ .../phonenumber/PhoneNumberEntryViewModel.kt | 4 +- .../VerificationCodeParser.kt | 29 +++++++ .../VerificationCodeScreen.kt | 12 +++ .../VerificationCodeScreenEvents.kt | 9 +++ .../verificationcode/VerificationCodeState.kt | 3 +- .../VerificationCodeViewModel.kt | 79 +++++++++++++++++- .../VerificationCodeParserTest.kt | 34 ++++++++ .../VerificationCodeScreenTest.kt | 30 +++++++ .../VerificationCodeViewModelTest.kt | 81 +++++++++++++++++++ 12 files changed, 311 insertions(+), 7 deletions(-) create mode 100644 feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeParser.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeParserTest.kt diff --git a/feature/registration/build.gradle.kts b/feature/registration/build.gradle.kts index c7e07cb85d..43316b9d27 100644 --- a/feature/registration/build.gradle.kts +++ b/feature/registration/build.gradle.kts @@ -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) diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt index a1792f3d17..32cfe59078 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt @@ -554,8 +554,10 @@ private fun EntryProviderScope.navigationEntries( // -- Verification Code Entry Screen entry { + val context = LocalContext.current.applicationContext val viewModel: VerificationCodeViewModel = viewModel( factory = VerificationCodeViewModel.Factory( + context = context, repository = registrationRepository, parentState = registrationViewModel.state, parentEventEmitter = registrationViewModel::onEvent diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt index 0688a03e01..0764f2aff1 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt @@ -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( diff --git a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt index ed54b1fc53..c686e62227 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt @@ -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 ) diff --git a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeParser.kt b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeParser.kt new file mode 100644 index 0000000000..0caab35b50 --- /dev/null +++ b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeParser.kt @@ -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()) + } +} diff --git a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeScreen.kt index c915ad0176..2ab3dd40f2 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeScreen.kt @@ -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 diff --git a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeScreenEvents.kt b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeScreenEvents.kt index 864b96673f..4316ed9625 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeScreenEvents.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeScreenEvents.kt @@ -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() diff --git a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeState.kt b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeState.kt index 9a3e7fb342..3a17afeb71 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeState.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeState.kt @@ -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 diff --git a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt index b3cc9a0fdd..11751545dd 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt @@ -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, private val parentEventEmitter: (RegistrationFlowEvent) -> Unit, + smsCodeEvents: Flow = emptyFlow(), private val clock: () -> Long = { System.currentTimeMillis() } ) : EventDrivenViewModel(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 = 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, - private val parentEventEmitter: (RegistrationFlowEvent) -> Unit + private val parentEventEmitter: (RegistrationFlowEvent) -> Unit, + private val smsCodeEvents: Flow ) : ViewModelProvider.Factory { + + /** + * Builds a real SMS retriever flow from [context]. Prefer the application context. + */ + constructor( + context: Context, + repository: RegistrationRepository, + parentState: StateFlow, + parentEventEmitter: (RegistrationFlowEvent) -> Unit + ) : this(repository, parentState, parentEventEmitter, smsCodeFlow(context)) + override fun create(modelClass: Class): T { - return VerificationCodeViewModel(repository, parentState, parentEventEmitter) as T + return VerificationCodeViewModel(repository, parentState, parentEventEmitter, smsCodeEvents) as T } } } diff --git a/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeParserTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeParserTest.kt new file mode 100644 index 0000000000..e759f74ded --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeParserTest.kt @@ -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() + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeScreenTest.kt index 6ab4d7cd8e..e36795043b 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeScreenTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeScreenTest.kt @@ -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 diff --git a/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModelTest.kt index 2d6951f754..0bbba8db8d 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModelTest.kt @@ -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 @@ -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(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()