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

@@ -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) {