Allow for multiple captchas to be solved during registration.

This commit is contained in:
Cody Henthorne
2025-09-08 16:23:19 -04:00
committed by Greyson Parrelli
parent 23b5a3dcb0
commit bdf2ef5a05
19 changed files with 60 additions and 168 deletions

View File

@@ -5,10 +5,7 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import android.os.Bundle
import android.view.View
import androidx.fragment.app.activityViewModels
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.ui.captcha.CaptchaFragment
/**
@@ -16,16 +13,8 @@ import org.thoughtcrime.securesms.registration.ui.captcha.CaptchaFragment
*/
class ChangeNumberCaptchaFragment : CaptchaFragment() {
private val viewModel by activityViewModels<ChangeNumberViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.addPresentedChallenge(Challenge.CAPTCHA)
}
override fun handleCaptchaToken(token: String) {
viewModel.setCaptchaResponse(token)
}
override fun handleUserExit() {
viewModel.removePresentedChallenge(Challenge.CAPTCHA)
}
}

View File

@@ -35,10 +35,9 @@ data class ChangeNumberState(
val challengesPresented: Set<Challenge> = emptySet(),
val allowedToRequestCode: Boolean = false,
val oldCountry: Country? = null,
val newCountry: Country? = null
) {
val challengesRemaining: List<Challenge> = challengesRequested.filterNot { it in challengesPresented }
}
val newCountry: Country? = null,
val challengeInProgress: Boolean = false
)
sealed interface ChangeNumberOutcome {
data object RecoveryPasswordWorked : ChangeNumberOutcome

View File

@@ -54,8 +54,10 @@ class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phon
private fun onStateUpdate(state: ChangeNumberState) {
if (state.challengesRequested.contains(Challenge.CAPTCHA) && state.captchaToken.isNotNullOrBlank()) {
viewModel.submitCaptchaToken(requireContext())
} else if (state.challengesRemaining.isNotEmpty()) {
handleChallenges(state.challengesRemaining)
} else if (state.challengesRequested.isNotEmpty()) {
if (!state.challengeInProgress) {
handleChallenges(state.challengesRequested)
}
} else if (state.changeNumberOutcome != null) {
handleRequestCodeResult(state.changeNumberOutcome)
} else if (!state.inProgress) {

View File

@@ -156,20 +156,6 @@ class ChangeNumberViewModel : ViewModel() {
}
}
fun addPresentedChallenge(challenge: Challenge) {
Log.v(TAG, "addPresentedChallenge()")
store.update {
it.copy(challengesPresented = it.challengesPresented.plus(challenge))
}
}
fun removePresentedChallenge(challenge: Challenge) {
Log.v(TAG, "addPresentedChallenge()")
store.update {
it.copy(challengesPresented = it.challengesPresented.minus(challenge))
}
}
fun resetLocalSessionState() {
Log.v(TAG, "resetLocalSessionState()")
store.update {
@@ -292,7 +278,8 @@ class ChangeNumberViewModel : ViewModel() {
it.copy(
captchaToken = null,
inProgress = true,
changeNumberOutcome = null
changeNumberOutcome = null,
challengeInProgress = true
)
}
@@ -304,7 +291,8 @@ class ChangeNumberViewModel : ViewModel() {
store.update {
it.copy(
inProgress = false,
changeNumberOutcome = null
changeNumberOutcome = null,
challengeInProgress = false
)
}
return@launch
@@ -313,7 +301,7 @@ class ChangeNumberViewModel : ViewModel() {
val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, sessionData.sessionId, captchaToken)
Log.d(TAG, "Captcha token submitted.")
store.update {
it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(captchaSubmissionResult))
it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(captchaSubmissionResult), challengeInProgress = false)
}
}
}
@@ -321,8 +309,6 @@ class ChangeNumberViewModel : ViewModel() {
fun requestAndSubmitPushToken(context: Context) {
Log.v(TAG, "validatePushToken()")
addPresentedChallenge(Challenge.PUSH)
val e164 = number.e164Number
viewModelScope.launch {

View File

@@ -27,8 +27,4 @@ enum class Challenge(val key: String) {
}
}
}
fun stringify(challenges: List<Challenge>): String {
return challenges.joinToString { it.key }
}
}

View File

@@ -16,8 +16,6 @@ enum class RegistrationCheckpoint {
PUSH_NETWORK_AUDITED,
PHONE_NUMBER_CONFIRMED,
PIN_CONFIRMED,
CHALLENGE_RECEIVED,
CHALLENGE_COMPLETED,
VERIFICATION_CODE_REQUESTED,
VERIFICATION_CODE_ENTERED,
PIN_ENTERED,

View File

@@ -41,7 +41,6 @@ data class RegistrationState(
val isAllowedToRequestCode: Boolean = false,
val fcmToken: String? = null,
val challengesRequested: List<Challenge> = emptyList(),
val challengesPresented: Set<Challenge> = emptySet(),
val captchaToken: String? = null,
val allowedToRequestCode: Boolean = false,
val nextSmsTimestamp: Duration = 0.seconds,
@@ -53,10 +52,9 @@ data class RegistrationState(
val networkError: Throwable? = null,
val sessionCreationError: RegistrationSessionResult? = null,
val sessionStateError: VerificationCodeRequestResult? = null,
val registerAccountError: RegisterAccountResult? = null
val registerAccountError: RegisterAccountResult? = null,
val challengeInProgress: Boolean = false
) {
val challengesRemaining: List<Challenge> = challengesRequested.filterNot { it in challengesPresented }
companion object {
private val TAG = Log.tag(RegistrationState::class)

View File

@@ -74,7 +74,6 @@ import java.io.IOException
import java.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit
import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
/**
@@ -164,7 +163,6 @@ class RegistrationViewModel : ViewModel() {
fun setCaptchaResponse(token: String) {
store.update {
it.copy(
registrationCheckpoint = RegistrationCheckpoint.CHALLENGE_COMPLETED,
captchaToken = token
)
}
@@ -194,18 +192,6 @@ class RegistrationViewModel : ViewModel() {
}
}
fun addPresentedChallenge(challenge: Challenge) {
store.update {
it.copy(challengesPresented = it.challengesPresented.plus(challenge))
}
}
fun removePresentedChallenge(challenge: Challenge) {
store.update {
it.copy(challengesPresented = it.challengesPresented.minus(challenge))
}
}
fun fetchFcmToken(context: Context) {
viewModelScope.launch(context = coroutineExceptionHandler) {
val fcmToken = RegistrationRepository.getFcmToken(context)
@@ -290,14 +276,8 @@ class RegistrationViewModel : ViewModel() {
}
if (!validSession.allowedToRequestCode) {
if (System.currentTimeMillis().milliseconds > validSession.nextVerificationAttempt) {
store.update {
it.copy(registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED)
}
} else {
Log.i(TAG, "Not allowed to request code! Remaining challenges: ${validSession.challengesRequested.joinToString()}")
handleSessionStateResult(context, ChallengeRequired(validSession.challengesRequested))
}
Log.i(TAG, "Not allowed to request code! Remaining challenges: ${validSession.challengesRequested.joinToString()}")
handleSessionStateResult(context, ChallengeRequired(validSession.challengesRequested))
return@launch
}
@@ -383,6 +363,8 @@ class RegistrationViewModel : ViewModel() {
registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED
)
}
} else {
Log.i(TAG, "SMS code request failed: ${codeRequestResponse::class.simpleName}")
}
}
@@ -400,6 +382,7 @@ class RegistrationViewModel : ViewModel() {
mcc = mccMncProducer.mcc,
mnc = mccMncProducer.mnc,
successListener = { sessionData ->
Log.i(TAG, "[getOrCreateValidSession] Challenges requested: ${sessionData.challengesRequested}", true)
store.update {
it.copy(
sessionId = sessionData.sessionId,
@@ -429,7 +412,7 @@ class RegistrationViewModel : ViewModel() {
val captchaToken = store.value.captchaToken ?: throw IllegalStateException("Can't submit captcha token if no captcha token is set!")
store.update {
it.copy(captchaToken = null)
it.copy(captchaToken = null, challengeInProgress = true, inProgress = true)
}
viewModelScope.launch {
@@ -439,14 +422,20 @@ class RegistrationViewModel : ViewModel() {
Log.d(TAG, "Captcha token submitted.")
handleSessionStateResult(context, captchaSubmissionResult)
store.update { it.copy(challengeInProgress = false) }
if (captchaSubmissionResult is Success) {
requestSmsCode(context)
} else {
setInProgress(false)
}
}
}
fun requestAndSubmitPushToken(context: Context) {
Log.v(TAG, "validatePushToken()")
addPresentedChallenge(Challenge.PUSH)
val e164 = getCurrentE164() ?: throw IllegalStateException("Can't submit captcha token if no phone number is set!")
viewModelScope.launch {
@@ -493,7 +482,6 @@ class RegistrationViewModel : ViewModel() {
Log.d(TAG, "[${sessionResult.challenges.joinToString()}] registration challenges received.")
store.update {
it.copy(
registrationCheckpoint = RegistrationCheckpoint.CHALLENGE_RECEIVED,
challengesRequested = sessionResult.challenges
)
}

View File

@@ -10,8 +10,6 @@ import android.os.Bundle
import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.OnBackPressedCallback
import androidx.activity.addCallback
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.LoggingFragment
@@ -24,12 +22,6 @@ abstract class CaptchaFragment : LoggingFragment(R.layout.fragment_registration_
private val binding: FragmentRegistrationCaptchaBinding by ViewBinderDelegate(FragmentRegistrationCaptchaBinding::bind)
private val backListener = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
handleUserExit()
}
}
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -42,20 +34,14 @@ abstract class CaptchaFragment : LoggingFragment(R.layout.fragment_registration_
if (url.startsWith(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME)) {
val token = url.substring(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME.length)
handleCaptchaToken(token)
backListener.isEnabled = false
findNavController().navigateUp()
return true
}
return false
}
}
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
handleUserExit()
}
binding.registrationCaptchaWebView.loadUrl(BuildConfig.SIGNAL_CAPTCHA_URL)
}
abstract fun handleCaptchaToken(token: String)
abstract fun handleUserExit()
}

View File

@@ -5,10 +5,7 @@
package org.thoughtcrime.securesms.registration.ui.captcha
import android.os.Bundle
import android.view.View
import androidx.fragment.app.activityViewModels
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
/**
@@ -19,16 +16,8 @@ import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
*/
class RegistrationCaptchaFragment : CaptchaFragment() {
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedViewModel.addPresentedChallenge(Challenge.CAPTCHA)
}
override fun handleCaptchaToken(token: String) {
sharedViewModel.setCaptchaResponse(token)
}
override fun handleUserExit() {
sharedViewModel.removePresentedChallenge(Challenge.CAPTCHA)
}
}

View File

@@ -140,8 +140,8 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
if (sharedState.challengesRequested.contains(Challenge.CAPTCHA) && sharedState.captchaToken.isNotNullOrBlank()) {
sharedViewModel.submitCaptchaToken(requireContext())
} else if (sharedState.challengesRemaining.isNotEmpty()) {
handleChallenges(sharedState.challengesRemaining)
} else if (sharedState.challengesRequested.isNotEmpty() && !sharedState.challengeInProgress) {
handleChallenges(sharedState.challengesRequested)
}
binding.resendSmsCountDown.startCountDownTo(sharedState.nextSmsTimestamp)

View File

@@ -159,8 +159,10 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
if (sharedState.challengesRequested.contains(Challenge.CAPTCHA) && sharedState.captchaToken.isNotNullOrBlank()) {
sharedViewModel.submitCaptchaToken(requireContext())
} else if (sharedState.challengesRemaining.isNotEmpty()) {
handleChallenges(sharedState.challengesRemaining)
} else if (sharedState.challengesRequested.isNotEmpty()) {
if (!sharedState.challengeInProgress) {
handleChallenges(sharedState.challengesRequested)
}
} else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED && sharedState.canSkipSms) {
moveToEnterPinScreen()
} else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) {

View File

@@ -16,8 +16,6 @@ enum class RegistrationCheckpoint {
PUSH_NETWORK_AUDITED,
PHONE_NUMBER_CONFIRMED,
PIN_CONFIRMED,
CHALLENGE_RECEIVED,
CHALLENGE_COMPLETED,
VERIFICATION_CODE_REQUESTED,
VERIFICATION_CODE_ENTERED,
PIN_ENTERED,

View File

@@ -42,7 +42,6 @@ data class RegistrationState(
val isAllowedToRequestCode: Boolean = false,
val fcmToken: String? = null,
val challengesRequested: List<Challenge> = emptyList(),
val challengesPresented: Set<Challenge> = emptySet(),
val captchaToken: String? = null,
val allowedToRequestCode: Boolean = false,
val nextSmsTimestamp: Duration = 0.seconds,
@@ -54,10 +53,9 @@ data class RegistrationState(
val networkError: Throwable? = null,
val sessionCreationError: RegistrationSessionResult? = null,
val sessionStateError: VerificationCodeRequestResult? = null,
val registerAccountError: RegisterAccountResult? = null
val registerAccountError: RegisterAccountResult? = null,
val challengeInProgress: Boolean = false
) {
val challengesRemaining: List<Challenge> = challengesRequested.filterNot { it in challengesPresented }
companion object {
private val TAG = Log.tag(RegistrationState::class)
@@ -77,7 +75,7 @@ data class RegistrationState(
}
fun toNavigationStateOnly(): NavigationState {
return NavigationState(challengesRequested, challengesPresented, captchaToken, registrationCheckpoint, canSkipSms)
return NavigationState(challengesRequested, captchaToken, registrationCheckpoint, canSkipSms, challengeInProgress)
}
/**
@@ -86,11 +84,9 @@ data class RegistrationState(
*/
data class NavigationState(
val challengesRequested: List<Challenge>,
val challengesPresented: Set<Challenge>,
val captchaToken: String? = null,
val registrationCheckpoint: RegistrationCheckpoint,
val canSkipSms: Boolean
) {
val challengesRemaining: List<Challenge> = challengesRequested.filterNot { it in challengesPresented }
}
val canSkipSms: Boolean,
val challengeInProgress: Boolean
)
}

View File

@@ -98,7 +98,6 @@ import java.io.IOException
import java.nio.charset.StandardCharsets
import kotlin.jvm.optionals.getOrNull
import kotlin.math.max
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
@@ -191,7 +190,6 @@ class RegistrationViewModel : ViewModel() {
fun setCaptchaResponse(token: String) {
store.update {
it.copy(
registrationCheckpoint = RegistrationCheckpoint.CHALLENGE_COMPLETED,
captchaToken = token
)
}
@@ -221,18 +219,6 @@ class RegistrationViewModel : ViewModel() {
}
}
fun addPresentedChallenge(challenge: Challenge) {
store.update {
it.copy(challengesPresented = it.challengesPresented.plus(challenge))
}
}
fun removePresentedChallenge(challenge: Challenge) {
store.update {
it.copy(challengesPresented = it.challengesPresented.minus(challenge))
}
}
fun fetchFcmToken(context: Context) {
viewModelScope.launch(context = coroutineExceptionHandler) {
val fcmToken = RegistrationRepository.getFcmToken(context)
@@ -317,14 +303,8 @@ class RegistrationViewModel : ViewModel() {
}
if (!validSession.allowedToRequestCode) {
if (System.currentTimeMillis().milliseconds > validSession.nextVerificationAttempt) {
store.update {
it.copy(registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED)
}
} else {
Log.i(TAG, "Not allowed to request code! Remaining challenges: ${validSession.challengesRequested.joinToString()}")
handleSessionStateResult(context, ChallengeRequired(validSession.challengesRequested))
}
Log.i(TAG, "Not allowed to request code! Remaining challenges: ${validSession.challengesRequested.joinToString()}")
handleSessionStateResult(context, ChallengeRequired(validSession.challengesRequested))
return@launch
}
@@ -410,6 +390,8 @@ class RegistrationViewModel : ViewModel() {
registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED
)
}
} else {
Log.i(TAG, "SMS code request failed: ${codeRequestResponse::class.simpleName}")
}
}
@@ -427,6 +409,7 @@ class RegistrationViewModel : ViewModel() {
mcc = mccMncProducer.mcc,
mnc = mccMncProducer.mnc,
successListener = { sessionData ->
Log.i(TAG, "[getOrCreateValidSession] Challenges requested: ${sessionData.challengesRequested}", true)
store.update {
it.copy(
sessionId = sessionData.sessionId,
@@ -456,7 +439,7 @@ class RegistrationViewModel : ViewModel() {
val captchaToken = store.value.captchaToken ?: throw IllegalStateException("Can't submit captcha token if no captcha token is set!")
store.update {
it.copy(captchaToken = null)
it.copy(captchaToken = null, challengeInProgress = true, inProgress = true)
}
viewModelScope.launch {
@@ -466,14 +449,20 @@ class RegistrationViewModel : ViewModel() {
Log.d(TAG, "Captcha token submitted.", true)
handleSessionStateResult(context, captchaSubmissionResult)
store.update { it.copy(challengeInProgress = false) }
if (captchaSubmissionResult is Success) {
requestSmsCode(context)
} else {
setInProgress(false)
}
}
}
fun requestAndSubmitPushToken(context: Context) {
Log.v(TAG, "validatePushToken()")
addPresentedChallenge(Challenge.PUSH)
val e164 = getCurrentE164() ?: throw IllegalStateException("Can't submit captcha token if no phone number is set!")
viewModelScope.launch {
@@ -520,7 +509,6 @@ class RegistrationViewModel : ViewModel() {
Log.d(TAG, "[${sessionResult.challenges.joinToString()}] registration challenges received.", true)
store.update {
it.copy(
registrationCheckpoint = RegistrationCheckpoint.CHALLENGE_RECEIVED,
challengesRequested = sessionResult.challenges
)
}

View File

@@ -10,8 +10,6 @@ import android.os.Bundle
import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.OnBackPressedCallback
import androidx.activity.addCallback
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.LoggingFragment
@@ -24,12 +22,6 @@ abstract class CaptchaFragment : LoggingFragment(R.layout.fragment_registration_
private val binding: FragmentRegistrationCaptchaBinding by ViewBinderDelegate(FragmentRegistrationCaptchaBinding::bind)
private val backListener = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
handleUserExit()
}
}
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -42,20 +34,14 @@ abstract class CaptchaFragment : LoggingFragment(R.layout.fragment_registration_
if (url.startsWith(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME)) {
val token = url.substring(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME.length)
handleCaptchaToken(token)
backListener.isEnabled = false
findNavController().navigateUp()
return true
}
return false
}
}
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
handleUserExit()
}
binding.registrationCaptchaWebView.loadUrl(BuildConfig.SIGNAL_CAPTCHA_URL)
}
abstract fun handleCaptchaToken(token: String)
abstract fun handleUserExit()
}

View File

@@ -5,10 +5,7 @@
package org.thoughtcrime.securesms.registrationv3.ui.captcha
import android.os.Bundle
import android.view.View
import androidx.fragment.app.activityViewModels
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationViewModel
/**
@@ -19,16 +16,8 @@ import org.thoughtcrime.securesms.registrationv3.ui.RegistrationViewModel
*/
class RegistrationCaptchaFragment : CaptchaFragment() {
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedViewModel.addPresentedChallenge(Challenge.CAPTCHA)
}
override fun handleCaptchaToken(token: String) {
sharedViewModel.setCaptchaResponse(token)
}
override fun handleUserExit() {
sharedViewModel.removePresentedChallenge(Challenge.CAPTCHA)
}
}

View File

@@ -140,8 +140,8 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
if (sharedState.challengesRequested.contains(Challenge.CAPTCHA) && sharedState.captchaToken.isNotNullOrBlank()) {
sharedViewModel.submitCaptchaToken(requireContext())
} else if (sharedState.challengesRemaining.isNotEmpty()) {
handleChallenges(sharedState.challengesRemaining)
} else if (sharedState.challengesRequested.isNotEmpty() && !sharedState.challengeInProgress) {
handleChallenges(sharedState.challengesRequested)
}
binding.resendSmsCountDown.startCountDownTo(sharedState.nextSmsTimestamp)

View File

@@ -170,8 +170,10 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
.observe(viewLifecycleOwner) { sharedState ->
if (sharedState.challengesRequested.contains(Challenge.CAPTCHA) && sharedState.captchaToken.isNotNullOrBlank()) {
sharedViewModel.submitCaptchaToken(requireContext())
} else if (sharedState.challengesRemaining.isNotEmpty()) {
handleChallenges(sharedState.challengesRemaining)
} else if (sharedState.challengesRequested.isNotEmpty()) {
if (!sharedState.challengeInProgress) {
handleChallenges(sharedState.challengesRequested)
}
} else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED && sharedState.canSkipSms) {
moveToEnterPinScreen()
} else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) {