Add additional error handling for registration v2.

This commit is contained in:
Nicholas Tinsley
2024-05-17 19:04:49 -04:00
committed by Cody Henthorne
parent 1ae2464df1
commit f37efd7e15
7 changed files with 281 additions and 58 deletions

View File

@@ -525,7 +525,8 @@ object RegistrationRepository {
enum class Mode(val isSmsRetrieverSupported: Boolean, val transport: PushServiceSocket.VerificationCodeTransport) {
SMS_WITH_LISTENER(true, PushServiceSocket.VerificationCodeTransport.SMS),
SMS_WITHOUT_LISTENER(false, PushServiceSocket.VerificationCodeTransport.SMS),
PHONE_CALL(false, PushServiceSocket.VerificationCodeTransport.VOICE)
PHONE_CALL(false, PushServiceSocket.VerificationCodeTransport.VOICE),
NONE(false, PushServiceSocket.VerificationCodeTransport.SMS)
}
private class PushTokenChallengeSubscriber {

View File

@@ -19,6 +19,7 @@ import org.whispersystems.signalservice.api.push.exceptions.PushChallengeRequire
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException
import org.whispersystems.signalservice.api.push.exceptions.RegistrationRetryException
import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException
import org.whispersystems.signalservice.internal.push.LockedException
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataJson
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import org.whispersystems.signalservice.internal.util.JsonUtil
@@ -56,12 +57,13 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
is CaptchaRequiredException -> createChallengeRequiredProcessor(networkResult)
is RateLimitException -> createRateLimitProcessor(cause)
is ImpossiblePhoneNumberException -> ImpossibleNumber(cause)
is NonNormalizedPhoneNumberException -> NonNormalizedNumber(cause)
is NonNormalizedPhoneNumberException -> NonNormalizedNumber(cause = cause, originalNumber = cause.originalNumber, normalizedNumber = cause.normalizedNumber)
is TokenNotAcceptedException -> TokenNotAccepted(cause)
is ExternalServiceFailureException -> ExternalServiceFailure(cause)
is InvalidTransportModeException -> InvalidTransportModeFailure(cause)
is MalformedRequestException -> MalformedRequest(cause)
is RegistrationRetryException -> MustRetry(cause)
is LockedException -> RegistrationLocked(cause = cause, timeRemaining = cause.timeRemaining)
else -> UnknownError(cause)
}
}
@@ -102,7 +104,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
class ImpossibleNumber(cause: Throwable) : VerificationCodeRequestResult(cause)
class NonNormalizedNumber(cause: Throwable) : VerificationCodeRequestResult(cause)
class NonNormalizedNumber(cause: Throwable, val originalNumber: String, val normalizedNumber: String) : VerificationCodeRequestResult(cause)
class TokenNotAccepted(cause: Throwable) : VerificationCodeRequestResult(cause)
@@ -114,5 +116,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
class MustRetry(cause: Throwable) : VerificationCodeRequestResult(cause)
class RegistrationLocked(cause: Throwable, val timeRemaining: Long) : VerificationCodeRequestResult(cause)
class UnknownError(cause: Throwable) : VerificationCodeRequestResult(cause)
}

View File

@@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeR
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.RegistrationLocked
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
@@ -68,7 +69,10 @@ class RegistrationV2ViewModel : ViewModel() {
private val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
Log.w(TAG, "CoroutineExceptionHandler invoked.", exception)
store.update {
it.copy(networkError = exception)
it.copy(
networkError = exception,
inProgress = false
)
}
}
@@ -141,7 +145,7 @@ class RegistrationV2ViewModel : ViewModel() {
}
}
fun onUserConfirmedPhoneNumber(context: Context) {
fun onUserConfirmedPhoneNumber(context: Context, errorHandler: (VerificationCodeRequestResult, RegistrationRepository.Mode) -> Unit) {
setRegistrationCheckpoint(RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED)
val state = store.value
if (state.phoneNumber == null) {
@@ -153,8 +157,12 @@ class RegistrationV2ViewModel : ViewModel() {
val e164 = state.phoneNumber.toE164()
if (hasRecoveryPassword() && matchesSavedE164(e164)) {
// Re-registration when the local database is intact.
Log.d(TAG, "Has recovery password, and therefore can skip SMS verification.")
store.update {
it.copy(canSkipSms = true)
it.copy(
canSkipSms = true,
inProgress = false
)
}
return
}
@@ -171,7 +179,7 @@ class RegistrationV2ViewModel : ViewModel() {
is BackupAuthCheckResult.SuccessWithCredentials -> {
Log.d(TAG, "Found local valid SVR auth credentials.")
store.update {
it.copy(canSkipSms = true, svrAuthCredentials = svrCredentialsResult.authCredentials)
it.copy(canSkipSms = true, svrAuthCredentials = svrCredentialsResult.authCredentials, inProgress = false)
}
return@launch
}
@@ -184,15 +192,15 @@ class RegistrationV2ViewModel : ViewModel() {
if (!validSession.body.allowedToRequestCode) {
val challenges = validSession.body.requestedInformation.joinToString()
Log.i(TAG, "Not allowed to request code! Remaining challenges: $challenges")
handleSessionStateResult(context, ChallengeRequired(validSession.body.requestedInformation))
handleSessionStateResult(context, ChallengeRequired(validSession.body.requestedInformation), RegistrationRepository.Mode.NONE, errorHandler)
return@launch
}
requestSmsCodeInternal(context, validSession.body.id, e164)
requestSmsCodeInternal(context, validSession.body.id, e164, errorHandler)
}
}
fun requestSmsCode(context: Context) {
fun requestSmsCode(context: Context, errorHandler: (VerificationCodeRequestResult, RegistrationRepository.Mode) -> Unit) {
val e164 = getCurrentE164()
if (e164 == null) {
@@ -203,11 +211,11 @@ class RegistrationV2ViewModel : ViewModel() {
viewModelScope.launch {
val validSession = getOrCreateValidSession(context) ?: return@launch
requestSmsCodeInternal(context, validSession.body.id, e164)
requestSmsCodeInternal(context, validSession.body.id, e164, errorHandler)
}
}
fun requestVerificationCall(context: Context) {
fun requestVerificationCall(context: Context, errorHandler: (VerificationCodeRequestResult, RegistrationRepository.Mode) -> Unit) {
val e164 = getCurrentE164()
if (e164 == null) {
@@ -228,11 +236,11 @@ class RegistrationV2ViewModel : ViewModel() {
)
Log.d(TAG, "Voice call code request submitted.")
handleSessionStateResult(context, codeRequestResponse)
handleSessionStateResult(context, codeRequestResponse, RegistrationRepository.Mode.PHONE_CALL, errorHandler)
}
}
private suspend fun requestSmsCodeInternal(context: Context, sessionId: String, e164: String) {
private suspend fun requestSmsCodeInternal(context: Context, sessionId: String, e164: String, errorHandler: (VerificationCodeRequestResult, RegistrationRepository.Mode) -> Unit) {
var smsListenerReady = false
Log.d(TAG, "Captcha token submitted.")
if (store.value.smsListenerTimeout < System.currentTimeMillis()) {
@@ -248,16 +256,17 @@ class RegistrationV2ViewModel : ViewModel() {
}
Log.d(TAG, "Requesting SMS code…")
val transportMode = if (smsListenerReady) RegistrationRepository.Mode.SMS_WITH_LISTENER else RegistrationRepository.Mode.SMS_WITHOUT_LISTENER
val codeRequestResponse = RegistrationRepository.requestSmsCode(
context = context,
sessionId = sessionId,
e164 = e164,
password = password,
mode = if (smsListenerReady) RegistrationRepository.Mode.SMS_WITH_LISTENER else RegistrationRepository.Mode.SMS_WITHOUT_LISTENER
mode = transportMode
)
Log.d(TAG, "SMS code request submitted.")
handleSessionStateResult(context, codeRequestResponse)
handleSessionStateResult(context, codeRequestResponse, transportMode, errorHandler)
}
private suspend fun getOrCreateValidSession(context: Context): RegistrationSessionMetadataResponse? {
@@ -281,6 +290,7 @@ class RegistrationV2ViewModel : ViewModel() {
Log.d(TAG, "Registration session validated.")
return metadata
}
is RegistrationSessionCreationResult.Success -> {
val metadata = sessionResult.getMetadata()
val newSessionId = metadata.body.id
@@ -294,26 +304,32 @@ class RegistrationV2ViewModel : ViewModel() {
Log.d(TAG, "Registration session created.")
return metadata
}
is RegistrationSessionCheckResult.SessionNotFound -> {
Log.w(TAG, "This should be impossible to reach at this stage; it should have been handled in RegistrationRepository.", sessionResult.getCause())
handleGenericError(sessionResult.getCause())
}
is RegistrationSessionCheckResult.UnknownError -> {
Log.i(TAG, "Unknown error occurred while checking registration session.", sessionResult.getCause())
handleGenericError(sessionResult.getCause())
}
is RegistrationSessionCreationResult.MalformedRequest -> {
Log.i(TAG, "Malformed request error occurred while creating registration session.", sessionResult.getCause())
handleGenericError(sessionResult.getCause())
}
is RegistrationSessionCreationResult.RateLimited -> {
Log.i(TAG, "Rate limit occurred while creating registration session.", sessionResult.getCause())
handleGenericError(sessionResult.getCause())
}
is RegistrationSessionCreationResult.ServerUnableToParse -> {
Log.i(TAG, "Server unable to parse request for creating registration session.", sessionResult.getCause())
handleGenericError(sessionResult.getCause())
}
is RegistrationSessionCreationResult.UnknownError -> {
Log.i(TAG, "Unknown error occurred while checking registration session.", sessionResult.getCause())
handleGenericError(sessionResult.getCause())
@@ -322,23 +338,23 @@ class RegistrationV2ViewModel : ViewModel() {
return null
}
fun submitCaptchaToken(context: Context) {
val e164 = getCurrentE164() ?: throw IllegalStateException("TODO")
val captchaToken = store.value.captchaToken ?: throw IllegalStateException("TODO")
fun submitCaptchaToken(context: Context, errorHandler: (VerificationCodeRequestResult, RegistrationRepository.Mode) -> Unit) {
val e164 = getCurrentE164() ?: throw IllegalStateException("Can't submit captcha token if no phone number is set!")
val captchaToken = store.value.captchaToken ?: throw IllegalStateException("Can't submit captcha token if no captcha token is set!")
viewModelScope.launch {
val session = getOrCreateValidSession(context) ?: return@launch
Log.d(TAG, "Submitting captcha token…")
val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.body.id, captchaToken)
Log.d(TAG, "Captcha token submitted.")
handleSessionStateResult(context, captchaSubmissionResult)
handleSessionStateResult(context, captchaSubmissionResult, RegistrationRepository.Mode.NONE, errorHandler)
}
}
/**
* @return whether the request was successful and execution should continue
*/
private suspend fun handleSessionStateResult(context: Context, sessionResult: VerificationCodeRequestResult): Boolean {
private suspend fun handleSessionStateResult(context: Context, sessionResult: VerificationCodeRequestResult, requestedTransport: RegistrationRepository.Mode, errorHandler: (VerificationCodeRequestResult, RegistrationRepository.Mode) -> Unit): Boolean {
when (sessionResult) {
is UnknownError -> {
handleGenericError(sessionResult.getCause())
@@ -353,30 +369,46 @@ class RegistrationV2ViewModel : ViewModel() {
sessionId = sessionResult.sessionId,
nextSms = sessionResult.nextSmsTimestamp,
nextCall = sessionResult.nextCallTimestamp,
registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED
registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED,
inProgress = false
)
}
return true
}
is AttemptsExhausted -> Log.w(TAG, "TODO")
is ChallengeRequired -> store.update {
// TODO [regv2] handle push challenge required
is ChallengeRequired -> {
Log.d(TAG, "[${sessionResult.challenges.joinToString()}] registration challenges received.")
it.copy(
registrationCheckpoint = RegistrationCheckpoint.CHALLENGE_RECEIVED
)
store.update {
it.copy(
registrationCheckpoint = RegistrationCheckpoint.CHALLENGE_RECEIVED,
inProgress = false
)
}
return false
}
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")
is AttemptsExhausted -> Log.i(TAG, "Received AttemptsExhausted.", sessionResult.getCause())
is ImpossibleNumber -> Log.i(TAG, "Received ImpossibleNumber.", sessionResult.getCause())
is NonNormalizedNumber -> Log.i(TAG, "Received NonNormalizedNumber.", sessionResult.getCause())
is RateLimited -> Log.i(TAG, "Received RateLimited.", sessionResult.getCause())
is ExternalServiceFailure -> Log.i(TAG, "Received ExternalServiceFailure.", sessionResult.getCause())
is InvalidTransportModeFailure -> Log.i(TAG, "Received InvalidTransportModeFailure.", sessionResult.getCause())
is MalformedRequest -> Log.i(TAG, "Received MalformedRequest.", sessionResult.getCause())
is MustRetry -> Log.i(TAG, "Received MustRetry.", sessionResult.getCause())
is TokenNotAccepted -> Log.i(TAG, "Received TokenNotAccepted.", sessionResult.getCause())
is RegistrationLocked -> Log.i(TAG, "Received RegistrationLocked.", sessionResult.getCause())
}
setInProgress(false)
errorHandler(sessionResult, requestedTransport)
return false
}
@@ -515,6 +547,7 @@ class RegistrationV2ViewModel : ViewModel() {
setRegistrationCheckpoint(RegistrationCheckpoint.VERIFICATION_CODE_VALIDATED)
val registrationResponse = RegistrationRepository.registerAccount(context, sessionId, registrationData).successOrThrow()
// TODO [regv2]: error handling
onSuccessfulRegistration(context, registrationData, registrationResponse, false)
}
}
@@ -525,7 +558,10 @@ class RegistrationV2ViewModel : ViewModel() {
refreshFeatureFlags()
store.update {
it.copy(registrationCheckpoint = RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED)
it.copy(
registrationCheckpoint = RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED,
inProgress = false
)
}
}

View File

@@ -5,26 +5,38 @@
package org.thoughtcrime.securesms.registration.v2.ui.entercode
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeV2Binding
import org.thoughtcrime.securesms.registration.fragments.ContactSupportBottomSheetFragment
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* The final screen of account registration, where the user enters their verification code.
*/
class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter_code_v2) {
companion object {
private const val BOTTOM_SHEET_TAG = "support_bottom_sheet"
}
private val TAG = Log.tag(EnterCodeV2Fragment::class.java)
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
@@ -34,6 +46,8 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
private var autopilotCodeEntryActive = false
private val bottomSheet = ContactSupportBottomSheetFragment()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -58,17 +72,21 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
sharedViewModel.verifyCodeWithoutRegistrationLock(requireContext(), it)
}
binding.havingTroubleButton.setOnClickListener {
bottomSheet.show(childFragmentManager, BOTTOM_SHEET_TAG)
}
binding.callMeCountDown.apply {
setTextResources(R.string.RegistrationActivity_call, R.string.RegistrationActivity_call_me_instead_available_in)
setOnClickListener {
sharedViewModel.requestVerificationCall(requireContext())
sharedViewModel.requestVerificationCall(requireContext(), ::handleErrorResponse)
}
}
binding.resendSmsCountDown.apply {
setTextResources(R.string.RegistrationActivity_resend_code, R.string.RegistrationActivity_resend_sms_available_in)
setOnClickListener {
sharedViewModel.requestSmsCode(requireContext())
sharedViewModel.requestSmsCode(requireContext(), ::handleErrorResponse)
}
}
@@ -85,6 +103,60 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
sharedViewModel.uiState.observe(viewLifecycleOwner) {
binding.resendSmsCountDown.startCountDownTo(it.nextSms)
binding.callMeCountDown.startCountDownTo(it.nextCall)
if (it.inProgress) {
binding.keyboard.displayProgress()
} else {
binding.keyboard.displayKeyboard()
}
}
}
private fun handleErrorResponse(requestResult: VerificationCodeRequestResult, mode: RegistrationRepository.Mode) {
when (requestResult) {
is VerificationCodeRequestResult.Success -> binding.keyboard.displaySuccess()
is VerificationCodeRequestResult.RateLimited -> {
binding.keyboard.displayFailure().addListener(
object : AssertedSuccessListener<Boolean>() {
override fun onSuccess(result: Boolean?) {
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)) { _, _ ->
binding.code.clear()
}
}
}
)
}
is VerificationCodeRequestResult.RegistrationLocked -> {
binding.keyboard.displayLocked().addListener(
object : AssertedSuccessListener<Boolean>() {
override fun onSuccess(result: Boolean?) {
findNavController().safeNavigate(EnterCodeV2FragmentDirections.actionRequireKbsLockPin(requestResult.timeRemaining))
}
}
)
}
else -> {
binding.keyboard.displayFailure().addListener(
object : AssertedSuccessListener<Boolean>() {
override fun onSuccess(result: Boolean?) {
Log.w(TAG, "Encountered unexpected error!", requestResult.getCause())
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_error_connecting_to_service))
}
}
)
}
}
}
private fun presentRemoteErrorDialog(message: String, title: String? = null, positiveButtonListener: DialogInterface.OnClickListener? = null) {
MaterialAlertDialogBuilder(requireContext()).apply {
title?.let {
setTitle(it)
}
setMessage(message)
setPositiveButton(android.R.string.ok, positiveButtonListener)
show()
}
}
@@ -93,13 +165,15 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
NavHostFragment.findNavController(this).popBackStack()
}
private class PhoneStateCallback : SignalStrengthPhoneStateListener.Callback {
private inner class PhoneStateCallback : SignalStrengthPhoneStateListener.Callback {
override fun onNoCellSignalPresent() {
// TODO [regv2]: animate in bottom sheet
bottomSheet.show(childFragmentManager, BOTTOM_SHEET_TAG)
}
override fun onCellSignalPresent() {
// TODO [regv2]: animate in bottom sheet
if (bottomSheet.isResumed) {
bottomSheet.dismiss()
}
}
}
}

View File

@@ -6,6 +6,7 @@
package org.thoughtcrime.securesms.registration.v2.ui.phonenumber
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.text.TextWatcher
@@ -30,6 +31,7 @@ import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.MaterialAutoCompleteTextView
import com.google.android.material.textfield.TextInputEditText
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
import org.signal.core.util.logging.Log
@@ -40,16 +42,22 @@ import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterPhoneNumb
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.util.CountryPrefix
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2State
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
import org.thoughtcrime.securesms.registration.v2.ui.toE164
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.Dialogs
import org.thoughtcrime.securesms.util.PlayServicesUtil
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.SupportEmailUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.livedata.LiveDataObserverCallback
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.visible
import kotlin.time.Duration.Companion.milliseconds
/**
* Screen in registration where the user enters their phone number.
@@ -61,6 +69,8 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
private val fragmentViewModel by viewModels<EnterPhoneNumberV2ViewModel>()
private val binding: FragmentRegistrationEnterPhoneNumberV2Binding by ViewBinderDelegate(FragmentRegistrationEnterPhoneNumberV2Binding::bind)
private val skipToNextScreen: DialogInterface.OnClickListener = DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> moveToVerificationEntryScreen() }
private lateinit var spinnerAdapter: ArrayAdapter<CountryPrefix>
private lateinit var phoneNumberInputLayout: TextInputEditText
private lateinit var spinnerView: MaterialAutoCompleteTextView
@@ -106,9 +116,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
} 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())
sharedViewModel.submitCaptchaToken(requireContext(), ::handleErrorResponse)
}
}
@@ -133,7 +141,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
}
if (fragmentState.error != EnterPhoneNumberV2State.Error.NONE) {
presentError(fragmentState)
presentLocalError(fragmentState)
}
}
@@ -145,10 +153,10 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
fragmentViewModel.phoneNumber()?.let {
phoneNumberInputLayout.setText(it.nationalNumber.toString())
}
} else if (spinnerView.editableText.isBlank()) {
spinnerView.setText(fragmentViewModel.countryPrefix().toString())
}
spinnerView.setText(fragmentViewModel.countryPrefix().toString())
ViewUtil.focusAndShowKeyboard(phoneNumberInputLayout)
}
@@ -195,15 +203,17 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
}
private fun presentRegisterButton(sharedState: RegistrationV2State) {
binding.registerButton.isEnabled = sharedState.phoneNumber != null && PhoneNumberUtil.getInstance().isValidNumber(sharedState.phoneNumber) && !sharedState.inProgress
// TODO [regv2]: always enable the button but display error dialogs if the entered phone number is invalid
binding.registerButton.isEnabled = sharedState.phoneNumber != null && PhoneNumberUtil.getInstance().isValidNumber(sharedState.phoneNumber)
if (sharedState.inProgress) {
binding.registerButton.setSpinning()
} else {
binding.registerButton.cancelSpinning()
}
}
private fun presentError(state: EnterPhoneNumberV2State) {
private fun presentLocalError(state: EnterPhoneNumberV2State) {
when (state.error) {
EnterPhoneNumberV2State.Error.NONE -> {
Unit
}
EnterPhoneNumberV2State.Error.NONE -> Unit
EnterPhoneNumberV2State.Error.INVALID_PHONE_NUMBER -> {
MaterialAlertDialogBuilder(requireContext()).apply {
@@ -222,7 +232,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
}
EnterPhoneNumberV2State.Error.PLAY_SERVICES_MISSING -> {
Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
handlePromptForNoPlayServices()
}
EnterPhoneNumberV2State.Error.PLAY_SERVICES_NEEDS_UPDATE -> {
@@ -253,6 +263,85 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
}
}
private fun handleErrorResponse(result: VerificationCodeRequestResult, mode: RegistrationRepository.Mode) {
when (result) {
is VerificationCodeRequestResult.Success -> Unit
is VerificationCodeRequestResult.AttemptsExhausted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_service))
is VerificationCodeRequestResult.ChallengeRequired -> findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionRequestCaptcha())
is VerificationCodeRequestResult.ExternalServiceFailure -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen)
is VerificationCodeRequestResult.ImpossibleNumber -> {
MaterialAlertDialogBuilder(requireContext()).apply {
setMessage(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid, fragmentViewModel.phoneNumber()?.toE164()))
setPositiveButton(android.R.string.ok, null)
show()
}
}
is VerificationCodeRequestResult.InvalidTransportModeFailure -> {
MaterialAlertDialogBuilder(requireContext()).apply {
setMessage(R.string.RegistrationActivity_we_couldnt_send_you_a_verification_code)
setPositiveButton(R.string.RegistrationActivity_voice_call) { _, _ ->
sharedViewModel.requestVerificationCall(requireContext(), ::handleErrorResponse)
}
setNegativeButton(R.string.RegistrationActivity_cancel, null)
show()
}
}
is VerificationCodeRequestResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen)
is VerificationCodeRequestResult.MustRetry -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen)
is VerificationCodeRequestResult.NonNormalizedNumber -> handleNonNormalizedNumberError(result.originalNumber, result.normalizedNumber, mode)
is VerificationCodeRequestResult.RateLimited -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, result.timeRemaining.milliseconds.toString()))
is VerificationCodeRequestResult.TokenNotAccepted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human))
else -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service))
}
}
private fun presentRemoteErrorDialog(message: String, positiveButtonListener: DialogInterface.OnClickListener? = null) {
MaterialAlertDialogBuilder(requireContext()).apply {
setMessage(message)
setPositiveButton(android.R.string.ok, positiveButtonListener)
show()
}
}
private fun handleNonNormalizedNumberError(originalNumber: String, normalizedNumber: String, mode: RegistrationRepository.Mode) {
try {
val phoneNumber = PhoneNumberUtil.getInstance().parse(normalizedNumber, null)
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(R.string.RegistrationActivity_non_standard_number_format)
setMessage(getString(R.string.RegistrationActivity_the_number_you_entered_appears_to_be_a_non_standard, originalNumber, normalizedNumber))
setNegativeButton(android.R.string.no) { d: DialogInterface, i: Int -> d.dismiss() }
setNeutralButton(R.string.RegistrationActivity_contact_signal_support) { dialogInterface, _ ->
val subject = getString(R.string.RegistrationActivity_signal_android_phone_number_format)
val body = SupportEmailUtil.generateSupportEmailBody(requireContext(), R.string.RegistrationActivity_signal_android_phone_number_format, null, null)
CommunicationActions.openEmail(requireContext(), SupportEmailUtil.getSupportEmailAddress(requireContext()), subject, body)
dialogInterface.dismiss()
}
setPositiveButton(R.string.yes) { dialogInterface, _ ->
spinnerView.setText(phoneNumber.countryCode.toString())
phoneNumberInputLayout.setText(phoneNumber.nationalNumber.toString())
when (mode) {
RegistrationRepository.Mode.SMS_WITH_LISTENER,
RegistrationRepository.Mode.SMS_WITHOUT_LISTENER -> sharedViewModel.requestSmsCode(requireContext(), ::handleErrorResponse)
RegistrationRepository.Mode.PHONE_CALL -> sharedViewModel.requestVerificationCall(requireContext(), ::handleErrorResponse)
RegistrationRepository.Mode.NONE -> Log.w(TAG, "Somehow got a non normalized number exception even though we didn't request a code.")
}
dialogInterface.dismiss()
}
show()
}
} catch (e: NumberParseException) {
Log.w(TAG, "Failed to parse number!", e)
Dialogs.showAlertDialog(
requireContext(),
getString(R.string.RegistrationActivity_invalid_number),
getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid, fragmentViewModel.phoneNumber()?.toE164())
)
}
}
private fun onRegistrationButtonClicked() {
ViewUtil.hideKeyboard(requireContext(), phoneNumberInputLayout)
sharedViewModel.setInProgress(true)
@@ -352,7 +441,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
if (missingFcmConsentRequired) {
handlePromptForNoPlayServices()
} else {
sharedViewModel.onUserConfirmedPhoneNumber(requireContext())
sharedViewModel.onUserConfirmedPhoneNumber(requireContext(), ::handleErrorResponse)
}
}
setNegativeButton(R.string.RegistrationActivity_edit_number) { _, _ -> onConfirmNumberDialogCanceled() }
@@ -367,7 +456,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
setMessage(R.string.RegistrationActivity_this_device_is_missing_google_play_services)
setPositiveButton(R.string.RegistrationActivity_i_understand) { _, _ ->
Log.d(TAG, "User confirmed number.")
sharedViewModel.onUserConfirmedPhoneNumber(requireContext())
sharedViewModel.onUserConfirmedPhoneNumber(requireContext(), ::handleErrorResponse)
}
setNegativeButton(android.R.string.cancel, null)
setOnCancelListener { fragmentViewModel.clearError() }

View File

@@ -14,7 +14,7 @@ data class EnterPhoneNumberV2State(val countryPrefixIndex: Int, val phoneNumber:
companion object {
@JvmStatic
val INIT = EnterPhoneNumberV2State(0, "")
val INIT = EnterPhoneNumberV2State(1, "")
}
enum class Error {

View File

@@ -23,6 +23,8 @@ class RegistrationApi(
/**
* Request that the service initialize a new registration session.
*
* `POST /v1/verification/session`
*/
fun createRegistrationSession(fcmToken: String?, mcc: String?, mnc: String?): NetworkResult<RegistrationSessionMetadataResponse> {
return NetworkResult.fromFetch {
@@ -32,6 +34,8 @@ class RegistrationApi(
/**
* Retrieve current status of a registration session.
*
* `GET /v1/verification/session/{session-id}`
*/
fun getRegistrationSessionStatus(sessionId: String): NetworkResult<RegistrationSessionMetadataResponse> {
return NetworkResult.fromFetch {
@@ -41,6 +45,8 @@ class RegistrationApi(
/**
* Submit an FCM token to the service as proof that this is an honest user attempting to register.
*
* `PATCH /v1/verification/session/{session-id}`
*/
fun submitPushChallengeToken(sessionId: String?, pushChallengeToken: String?): NetworkResult<RegistrationSessionMetadataResponse> {
return NetworkResult.fromFetch {
@@ -52,6 +58,8 @@ class RegistrationApi(
* Request an SMS verification code. On success, the server will send
* an SMS verification code to this Signal user.
*
* `POST /v1/verification/session/{session-id}/code`
*
* @param androidSmsRetrieverSupported whether the system framework will automatically parse the incoming verification message.
*/
fun requestSmsVerificationCode(sessionId: String?, locale: Locale?, androidSmsRetrieverSupported: Boolean, transport: PushServiceSocket.VerificationCodeTransport): NetworkResult<RegistrationSessionMetadataResponse> {
@@ -62,6 +70,8 @@ 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.
*
* `PUT /v1/verification/session/{session-id}/code`
*/
fun verifyAccount(sessionId: String, verificationCode: String): NetworkResult<RegistrationSessionMetadataResponse> {
return NetworkResult.fromFetch {
@@ -71,6 +81,8 @@ class RegistrationApi(
/**
* Submits the solved captcha token to the service.
*
* `PATCH /v1/verification/session/{session-id}`
*/
fun submitCaptchaToken(sessionId: String, captchaToken: String): NetworkResult<RegistrationSessionMetadataResponse> {
return NetworkResult.fromFetch {
@@ -80,6 +92,8 @@ class RegistrationApi(
/**
* Submit the cryptographic assets required for an account to use the service.
*
* `POST /v1/registration`
*/
fun registerAccount(sessionId: String?, recoveryPassword: String?, attributes: AccountAttributes?, aciPreKeys: PreKeyCollection?, pniPreKeys: PreKeyCollection?, fcmToken: String?, skipDeviceTransfer: Boolean): NetworkResult<VerifyAccountResponse> {
return NetworkResult.fromFetch {
@@ -87,6 +101,11 @@ class RegistrationApi(
}
}
/**
* Retrieves an SVR auth credential that corresponds with the supplied username and password.
*
* `POST /v2/backup/auth/check`
*/
fun getSvrAuthCredential(e164: String, usernamePasswords: List<String>): NetworkResult<BackupAuthCheckResponse> {
return NetworkResult.fromFetch {
pushServiceSocket.checkBackupAuthCredentials(e164, usernamePasswords)