From ca14ed9b2cc5c94bbf32400b25ee905d5bcb22f6 Mon Sep 17 00:00:00 2001 From: Nicholas Tinsley Date: Tue, 7 May 2024 18:10:10 -0400 Subject: [PATCH] Allow for captcha solving for reg v2. --- .../fragments/RegistrationConstants.java | 6 +- .../v2/data/RegistrationRepository.kt | 60 +++++++----- .../v2/data/network/SubmitCaptchaResult.kt | 29 ++++++ .../network/VerificationCodeRequestResult.kt | 3 +- .../v2/ui/RegistrationCheckpoint.kt | 2 +- .../registration/v2/ui/RegistrationV2State.kt | 1 + .../v2/ui/RegistrationV2ViewModel.kt | 95 +++++++++++++------ .../v2/ui/captcha/CaptchaFragment.kt | 49 ++++++++++ .../phonenumber/EnterPhoneNumberV2Fragment.kt | 11 ++- .../fragment_registration_captcha_v2.xml | 44 +++++++++ .../main/res/navigation/registration_v2.xml | 2 +- .../api/registration/RegistrationApi.kt | 11 ++- 12 files changed, 247 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/SubmitCaptchaResult.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/captcha/CaptchaFragment.kt create mode 100644 app/src/main/res/layout/fragment_registration_captcha_v2.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationConstants.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationConstants.java index 141b6227bb..8228bd6efa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationConstants.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationConstants.java @@ -1,11 +1,11 @@ package org.thoughtcrime.securesms.registration.fragments; -final class RegistrationConstants { +public final class RegistrationConstants { private RegistrationConstants() { } - static final String TERMS_AND_CONDITIONS_URL = "https://signal.org/legal"; - static final String SIGNAL_CAPTCHA_SCHEME = "signalcaptcha://"; + public static final String TERMS_AND_CONDITIONS_URL = "https://signal.org/legal"; + public static final String SIGNAL_CAPTCHA_SCHEME = "signalcaptcha://"; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt index feb781ad01..f016e5c3a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt @@ -265,33 +265,31 @@ object RegistrationRepository { val fcmToken: String? = FcmUtil.getToken(context).orElse(null) val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi - val result = ( - if (fcmToken == null) { - api.createRegistrationSession(null, mcc, mnc) - } else { - createSessionAndBlockForPushChallenge(api, fcmToken, mcc, mnc) - } - ).then { session -> - val sessionId = session.body.id - SignalStore.registrationValues().sessionId = sessionId - SignalStore.registrationValues().sessionE164 = e164 - if (!session.body.allowedToRequestCode) { - val challenges = session.body.requestedInformation.joinToString() - Log.w(TAG, "Not allowed to request code! Remaining challenges: $challenges") - // TODO [regv2]: actually handle challenges - } - // TODO [regv2]: support other verification code [Mode] options - if (mode == Mode.PHONE_CALL) { - // TODO [regv2] - val notImplementedError = NotImplementedError() - Log.w(TAG, "Not yet implemented!", notImplementedError) - NetworkResult.ApplicationError(notImplementedError) - } else { - api.requestSmsVerificationCode(sessionId, Locale.getDefault(), mode.isSmsRetrieverSupported) - } + val registrationSessionResult = if (fcmToken == null) { + api.createRegistrationSession(null, mcc, mnc) + } else { + createSessionAndBlockForPushChallenge(api, fcmToken, mcc, mnc) + } + val session = registrationSessionResult.successOrThrow() + val sessionId = session.body.id + SignalStore.registrationValues().sessionId = sessionId + SignalStore.registrationValues().sessionE164 = e164 + if (!session.body.allowedToRequestCode) { + val challenges = session.body.requestedInformation.joinToString() + Log.w(TAG, "Not allowed to request code! Remaining challenges: $challenges") + return@withContext VerificationCodeRequestResult.from(registrationSessionResult) + } + // TODO [regv2]: support other verification code [Mode] options + if (mode == Mode.PHONE_CALL) { + // TODO [regv2] + val notImplementedError = NotImplementedError() + Log.w(TAG, "Not yet implemented!", notImplementedError) + NetworkResult.ApplicationError(notImplementedError) + } else { + api.requestSmsVerificationCode(sessionId, Locale.getDefault(), mode.isSmsRetrieverSupported) } - return@withContext VerificationCodeRequestResult.from(result) + return@withContext VerificationCodeRequestResult.from(registrationSessionResult) } /** @@ -300,7 +298,17 @@ object RegistrationRepository { suspend fun submitVerificationCode(context: Context, e164: String, password: String, sessionId: String, registrationData: RegistrationData): NetworkResult = withContext(Dispatchers.IO) { val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi - api.verifyAccount(registrationData.code, sessionId) + api.verifyAccount(sessionId = sessionId, verificationCode = registrationData.code) + } + + /** + * Submits the solved captcha token to the service. + */ + suspend fun submitCaptchaToken(context: Context, e164: String, password: String, sessionId: String, captchaToken: String) = + withContext(Dispatchers.IO) { + val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi + val captchaSubmissionResult = api.submitCaptchaToken(sessionId = sessionId, captchaToken = captchaToken) + return@withContext VerificationCodeRequestResult.from(captchaSubmissionResult) } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/SubmitCaptchaResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/SubmitCaptchaResult.kt new file mode 100644 index 0000000000..f9e17bfd51 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/SubmitCaptchaResult.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.registration.v2.data.network + +import org.signal.core.util.logging.Log +import org.whispersystems.signalservice.api.NetworkResult +import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse + +sealed class SubmitCaptchaResult(cause: Throwable?) : RegistrationResult(cause) { + companion object { + private val TAG = Log.tag(SubmitCaptchaResult::class.java) + + fun from(networkResult: NetworkResult): SubmitCaptchaResult { + return when (networkResult) { + is NetworkResult.Success -> Success() + is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable) + is NetworkResult.NetworkError -> UnknownError(networkResult.exception) + is NetworkResult.StatusCodeError -> UnknownError(networkResult.exception) + } + } + } + + class Success : SubmitCaptchaResult(null) + class ChallengeRequired(val challenges: List) : SubmitCaptchaResult(null) + class UnknownError(cause: Throwable) : SubmitCaptchaResult(cause) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt index 46be63716e..703648bcad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt @@ -41,6 +41,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu } else { Success( sessionId = networkResult.result.body.id, + allowedToRequestCode = networkResult.result.body.allowedToRequestCode, nextSms = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.body.nextSms), nextCall = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.body.nextCall) ) @@ -91,7 +92,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu } } - class Success(val sessionId: String, val nextSms: Long, val nextCall: Long) : VerificationCodeRequestResult(null) + class Success(val sessionId: String, val allowedToRequestCode: Boolean, val nextSms: Long, val nextCall: Long) : VerificationCodeRequestResult(null) class ChallengeRequired(val challenges: List) : VerificationCodeRequestResult(null) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationCheckpoint.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationCheckpoint.kt index c1d03c2b50..06f682dd8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationCheckpoint.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationCheckpoint.kt @@ -16,9 +16,9 @@ enum class RegistrationCheckpoint { PUSH_NETWORK_AUDITED, PHONE_NUMBER_CONFIRMED, PIN_CONFIRMED, - VERIFICATION_CODE_REQUESTED, CHALLENGE_RECEIVED, CHALLENGE_COMPLETED, + VERIFICATION_CODE_REQUESTED, VERIFICATION_CODE_ENTERED, VERIFICATION_CODE_VALIDATED, SERVICE_REGISTRATION_COMPLETED, diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt index e39cb5697a..f44aff463b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt @@ -25,6 +25,7 @@ data class RegistrationV2State( val userSkippedReregistration: Boolean = false, val isFcmSupported: Boolean = false, val fcmToken: String? = null, + val captchaToken: String? = null, val nextSms: Long = 0L, val nextCall: Long = 0L, val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION, diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt index ba38744085..1cdd13c29b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt @@ -29,15 +29,16 @@ import org.thoughtcrime.securesms.registration.RegistrationData import org.thoughtcrime.securesms.registration.RegistrationUtil import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository import org.thoughtcrime.securesms.registration.v2.data.network.BackupAuthCheckResult +import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.AttemptsExhausted import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ChallengeRequired import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ExternalServiceFailure import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ImpossibleNumber import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.InvalidTransportModeFailure import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.MalformedRequest +import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.MustRetry import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.NonNormalizedNumber import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.RateLimited -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.MustRetry import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.Success import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.TokenNotAccepted import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.UnknownError @@ -99,6 +100,15 @@ class RegistrationV2ViewModel : ViewModel() { } } + fun setCaptchaResponse(token: String) { + store.update { + it.copy( + registrationCheckpoint = RegistrationCheckpoint.CHALLENGE_COMPLETED, + captchaToken = token + ) + } + } + fun fetchFcmToken(context: Context) { viewModelScope.launch(context = coroutineExceptionHandler) { val fcmToken = RegistrationRepository.getFcmToken(context) @@ -165,38 +175,63 @@ class RegistrationV2ViewModel : ViewModel() { val codeRequestResponse = RegistrationRepository.requestSmsCode(context, e164, password, mccMncProducer.mcc, mccMncProducer.mnc) - when (codeRequestResponse) { - is UnknownError -> { - handleGenericError(codeRequestResponse.getCause()) - return@launch - } - - is Success -> { - updateFcmToken(context) - store.update { - it.copy( - sessionId = codeRequestResponse.sessionId, - nextSms = codeRequestResponse.nextSms, - nextCall = codeRequestResponse.nextCall, - registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED - ) - } - } - - is AttemptsExhausted -> Log.w(TAG, "TODO") - is ChallengeRequired -> Log.w(TAG, "TODO") - is ImpossibleNumber -> Log.w(TAG, "TODO") - is NonNormalizedNumber -> Log.w(TAG, "TODO") - is RateLimited -> Log.w(TAG, "TODO") - is ExternalServiceFailure -> Log.w(TAG, "TODO") - is InvalidTransportModeFailure -> Log.w(TAG, "TODO") - is MalformedRequest -> Log.w(TAG, "TODO") - is MustRetry -> Log.w(TAG, "TODO") - is TokenNotAccepted -> Log.w(TAG, "TODO") - } + handleSessionStateResult(context, codeRequestResponse) } } + fun submitCaptchaToken(context: Context) { + val e164 = getCurrentE164() ?: throw IllegalStateException("TODO") + val sessionId = store.value.sessionId ?: throw IllegalStateException("TODO") + val captchaToken = store.value.captchaToken ?: throw IllegalStateException("TODO") + + viewModelScope.launch { + val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, sessionId, captchaToken) + + handleSessionStateResult(context, captchaSubmissionResult) + } + } + + /** + * @return whether the request was successful and execution should continue + */ + private suspend fun handleSessionStateResult(context: Context, sessionResult: VerificationCodeRequestResult): Boolean { + when (sessionResult) { + is UnknownError -> { + handleGenericError(sessionResult.getCause()) + return false + } + + is Success -> { + updateFcmToken(context) + store.update { + it.copy( + sessionId = sessionResult.sessionId, + nextSms = sessionResult.nextSms, + nextCall = sessionResult.nextCall, + registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED + ) + } + return true + } + + is AttemptsExhausted -> Log.w(TAG, "TODO") + is ChallengeRequired -> store.update { + it.copy( + registrationCheckpoint = RegistrationCheckpoint.CHALLENGE_RECEIVED + ) + } + is ImpossibleNumber -> Log.w(TAG, "TODO") + is NonNormalizedNumber -> Log.w(TAG, "TODO") + is RateLimited -> Log.w(TAG, "TODO") + is ExternalServiceFailure -> Log.w(TAG, "TODO") + is InvalidTransportModeFailure -> Log.w(TAG, "TODO") + is MalformedRequest -> Log.w(TAG, "TODO") + is MustRetry -> Log.w(TAG, "TODO") + is TokenNotAccepted -> Log.w(TAG, "TODO") + } + return false + } + private fun handleGenericError(cause: Throwable) { Log.w(TAG, "Encountered unknown error!", cause) store.update { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/captcha/CaptchaFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/captcha/CaptchaFragment.kt new file mode 100644 index 0000000000..c1204499f0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/captcha/CaptchaFragment.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.registration.v2.ui.captcha + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import org.thoughtcrime.securesms.BuildConfig +import org.thoughtcrime.securesms.LoggingFragment +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.ViewBinderDelegate +import org.thoughtcrime.securesms.databinding.FragmentRegistrationCaptchaV2Binding +import org.thoughtcrime.securesms.registration.fragments.RegistrationConstants +import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel + +class CaptchaFragment : LoggingFragment(R.layout.fragment_registration_captcha_v2) { + + private val sharedViewModel by activityViewModels() + private val binding: FragmentRegistrationCaptchaV2Binding by ViewBinderDelegate(FragmentRegistrationCaptchaV2Binding::bind) + + @SuppressLint("SetJavaScriptEnabled") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.registrationCaptchaWebView.settings.javaScriptEnabled = true + binding.registrationCaptchaWebView.clearCache(true) + + binding.registrationCaptchaWebView.webViewClient = object : WebViewClient() { + @Deprecated("Deprecated in Java") + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + if (url.startsWith(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME)) { + val token = url.substring(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME.length) + sharedViewModel.setCaptchaResponse(token) + findNavController().navigateUp() + return true + } + return false + } + } + + binding.registrationCaptchaWebView.loadUrl(BuildConfig.SIGNAL_CAPTCHA_URL) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt index f342ebb3ab..ac62477c1e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt @@ -100,10 +100,15 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio sharedState.networkError?.let { presentNetworkError(it) } + if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED && sharedState.canSkipSms) { moveToEnterPinScreen() } else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) { moveToVerificationEntryScreen() + } else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.CHALLENGE_COMPLETED) { + sharedViewModel.submitCaptchaToken(requireContext()) + } else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.CHALLENGE_RECEIVED) { + findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionRequestCaptcha()) } } @@ -370,18 +375,18 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio } private fun moveToEnterPinScreen() { - sharedViewModel.setInProgress(false) findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionReRegisterWithPinV2Fragment()) + sharedViewModel.setInProgress(false) } private fun moveToVerificationEntryScreen() { - NavHostFragment.findNavController(this).safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEnterVerificationCode()) + findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEnterVerificationCode()) sharedViewModel.setInProgress(false) } private fun popBackStack() { sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.INITIALIZATION) - NavHostFragment.findNavController(this).popBackStack() + findNavController().popBackStack() } private inner class FcmTokenRetrievedObserver : LiveDataObserverCallback(sharedViewModel.uiState) { diff --git a/app/src/main/res/layout/fragment_registration_captcha_v2.xml b/app/src/main/res/layout/fragment_registration_captcha_v2.xml new file mode 100644 index 0000000000..5087d02906 --- /dev/null +++ b/app/src/main/res/layout/fragment_registration_captcha_v2.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/registration_v2.xml b/app/src/main/res/navigation/registration_v2.xml index 2a75e969bd..4ea88cb33d 100644 --- a/app/src/main/res/navigation/registration_v2.xml +++ b/app/src/main/res/navigation/registration_v2.xml @@ -215,7 +215,7 @@ diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt index 1eea96734c..544c2ff4ae 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt @@ -54,12 +54,21 @@ class RegistrationApi( /** * Submit a verification code sent by the service via one of the supported channels (SMS, phone call) to prove the registrant's control of the phone number. */ - fun verifyAccount(verificationCode: String, sessionId: String): NetworkResult { + fun verifyAccount(sessionId: String, verificationCode: String): NetworkResult { return NetworkResult.fromFetch { pushServiceSocket.submitVerificationCode(sessionId, verificationCode) } } + /** + * Submits the solved captcha token to the service. + */ + fun submitCaptchaToken(sessionId: String, captchaToken: String): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.patchVerificationSession(sessionId, null, null, null, captchaToken, null) + } + } + /** * Submit the cryptographic assets required for an account to use the service. */