Convert registration error handling from callbacks to observers.

This commit is contained in:
Nicholas Tinsley
2024-07-25 21:21:36 +02:00
parent 4fb335de28
commit f87ff58701
6 changed files with 228 additions and 154 deletions

View File

@@ -11,6 +11,9 @@ import com.google.i18n.phonenumbers.Phonenumber
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
@@ -45,7 +48,10 @@ data class RegistrationState(
val verified: Boolean = false,
val smsListenerTimeout: Long = 0L,
val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION,
val networkError: Throwable? = null
val networkError: Throwable? = null,
val sessionCreationError: RegistrationSessionResult? = null,
val sessionStateError: VerificationCodeRequestResult? = null,
val registerAccountError: RegisterAccountResult? = null
) {
val challengesRemaining: List<Challenge> = challengesRequested.filterNot { it in challengesPresented }

View File

@@ -39,10 +39,10 @@ import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.data.network.BackupAuthCheckResult
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AlreadyVerified
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AttemptsExhausted
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ChallengeRequired
@@ -96,8 +96,6 @@ class RegistrationViewModel : ViewModel() {
val incorrectCodeAttempts = store.map { it.incorrectCodeAttempts }.asLiveData()
val inProgress = store.map { it.inProgress }.asLiveData()
val svrTriesRemaining: Int
get() = store.value.svrTriesRemaining
@@ -158,6 +156,24 @@ class RegistrationViewModel : ViewModel() {
}
}
fun sessionCreationErrorShown() {
store.update {
it.copy(sessionCreationError = null)
}
}
fun sessionStateErrorShown() {
store.update {
it.copy(sessionStateError = null)
}
}
fun registerAccountErrorShown() {
store.update {
it.copy(registerAccountError = null)
}
}
fun incrementIncorrectCodeAttempts() {
store.update {
it.copy(incorrectCodeAttempts = it.incorrectCodeAttempts + 1)
@@ -202,7 +218,7 @@ class RegistrationViewModel : ViewModel() {
}
}
fun onUserConfirmedPhoneNumber(context: Context, errorHandler: (RegistrationResult) -> Unit) {
fun onUserConfirmedPhoneNumber(context: Context) {
setRegistrationCheckpoint(RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED)
val state = store.value
@@ -253,11 +269,11 @@ class RegistrationViewModel : ViewModel() {
}
}
val validSession = getOrCreateValidSession(context, errorHandler) ?: return@launch bail { Log.i(TAG, "Could not create valid session for confirming the entered E164.") }
val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for confirming the entered E164.") }
if (validSession.body.verified) {
Log.i(TAG, "Session is already verified, registering account.")
registerVerifiedSession(context, validSession.body.id, errorHandler)
registerVerifiedSession(context, validSession.body.id)
return@launch
}
@@ -269,25 +285,25 @@ class RegistrationViewModel : ViewModel() {
} else {
val challenges = validSession.body.requestedInformation
Log.i(TAG, "Not allowed to request code! Remaining challenges: ${challenges.joinToString()}")
handleSessionStateResult(context, ChallengeRequired(Challenge.parse(validSession.body.requestedInformation)), errorHandler)
handleSessionStateResult(context, ChallengeRequired(Challenge.parse(validSession.body.requestedInformation)))
}
return@launch
}
requestSmsCodeInternal(context, validSession.body.id, e164, errorHandler)
requestSmsCodeInternal(context, validSession.body.id, e164)
}
}
fun requestSmsCode(context: Context, errorHandler: (RegistrationResult) -> Unit) {
fun requestSmsCode(context: Context) {
val e164 = getCurrentE164() ?: return bail { Log.i(TAG, "Phone number was null after confirmation.") }
viewModelScope.launch {
val validSession = getOrCreateValidSession(context, errorHandler) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting an SMS code.") }
requestSmsCodeInternal(context, validSession.body.id, e164, errorHandler)
val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting an SMS code.") }
requestSmsCodeInternal(context, validSession.body.id, e164)
}
}
fun requestVerificationCall(context: Context, errorHandler: (RegistrationResult) -> Unit) {
fun requestVerificationCall(context: Context) {
val e164 = getCurrentE164()
if (e164 == null) {
@@ -297,7 +313,7 @@ class RegistrationViewModel : ViewModel() {
}
viewModelScope.launch {
val validSession = getOrCreateValidSession(context, errorHandler) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting a verification call.") }
val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting a verification call.") }
Log.d(TAG, "Requesting voice call code…")
val codeRequestResponse = RegistrationRepository.requestSmsCode(
context = context,
@@ -308,14 +324,14 @@ class RegistrationViewModel : ViewModel() {
)
Log.d(TAG, "Voice code request network call completed.")
handleSessionStateResult(context, codeRequestResponse, errorHandler)
handleSessionStateResult(context, codeRequestResponse)
if (codeRequestResponse is Success) {
Log.d(TAG, "Voice code request was successful.")
}
}
}
private suspend fun requestSmsCodeInternal(context: Context, sessionId: String, e164: String, errorHandler: (RegistrationResult) -> Unit) {
private suspend fun requestSmsCodeInternal(context: Context, sessionId: String, e164: String) {
var smsListenerReady = false
Log.d(TAG, "Initializing SMS listener.")
if (store.value.smsListenerTimeout < System.currentTimeMillis()) {
@@ -341,7 +357,7 @@ class RegistrationViewModel : ViewModel() {
)
Log.d(TAG, "SMS code request network call completed.")
handleSessionStateResult(context, codeRequestResponse, errorHandler)
handleSessionStateResult(context, codeRequestResponse)
if (codeRequestResponse is Success) {
Log.d(TAG, "SMS code request was successful.")
@@ -353,7 +369,7 @@ class RegistrationViewModel : ViewModel() {
}
}
private suspend fun getOrCreateValidSession(context: Context, errorHandler: (RegistrationResult) -> Unit): RegistrationSessionMetadataResponse? {
private suspend fun getOrCreateValidSession(context: Context): RegistrationSessionMetadataResponse? {
Log.v(TAG, "getOrCreateValidSession()")
val e164 = getCurrentE164() ?: throw IllegalStateException("E164 required to create session!")
val mccMncProducer = MccMncProducer(context)
@@ -379,11 +395,17 @@ class RegistrationViewModel : ViewModel() {
)
}
},
errorHandler = errorHandler
errorHandler = { error ->
store.update {
it.copy(
sessionCreationError = error
)
}
}
)
}
fun submitCaptchaToken(context: Context, errorHandler: (RegistrationResult) -> Unit) {
fun submitCaptchaToken(context: Context) {
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!")
@@ -392,16 +414,16 @@ class RegistrationViewModel : ViewModel() {
}
viewModelScope.launch {
val session = getOrCreateValidSession(context, errorHandler) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a captcha token.") }
val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a captcha token.") }
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, errorHandler)
handleSessionStateResult(context, captchaSubmissionResult)
}
}
fun requestAndSubmitPushToken(context: Context, errorHandler: (RegistrationResult) -> Unit) {
fun requestAndSubmitPushToken(context: Context) {
Log.v(TAG, "validatePushToken()")
addPresentedChallenge(Challenge.PUSH)
@@ -410,7 +432,7 @@ class RegistrationViewModel : ViewModel() {
viewModelScope.launch {
Log.d(TAG, "Getting session in order to perform push token verification…")
val session = getOrCreateValidSession(context, errorHandler) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a push challenge token.") }
val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a push challenge token.") }
if (!Challenge.parse(session.body.requestedInformation).contains(Challenge.PUSH)) {
Log.d(TAG, "Push submission no longer necessary, bailing.")
@@ -425,14 +447,14 @@ class RegistrationViewModel : ViewModel() {
Log.d(TAG, "Requesting push challenge token…")
val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.body.id, e164, password)
Log.d(TAG, "Push challenge token submitted.")
handleSessionStateResult(context, pushSubmissionResult, errorHandler)
handleSessionStateResult(context, pushSubmissionResult)
}
}
/**
* @return whether the request was successful and execution should continue
*/
private suspend fun handleSessionStateResult(context: Context, sessionResult: RegistrationResult, errorHandler: (RegistrationResult) -> Unit): Boolean {
private suspend fun handleSessionStateResult(context: Context, sessionResult: VerificationCodeRequestResult): Boolean {
Log.v(TAG, "handleSessionStateResult()")
when (sessionResult) {
is UnknownError -> {
@@ -492,14 +514,18 @@ class RegistrationViewModel : ViewModel() {
is AlreadyVerified -> Log.i(TAG, "Received AlreadyVerified", sessionResult.getCause())
}
setInProgress(false)
errorHandler(sessionResult)
store.update {
it.copy(
sessionStateError = sessionResult
)
}
return false
}
/**
* @return whether the request was successful and execution should continue
*/
private suspend fun handleRegistrationResult(context: Context, registrationData: RegistrationData, registrationResult: RegisterAccountResult, reglockEnabled: Boolean, errorHandler: (RegisterAccountResult) -> Unit): Boolean {
private suspend fun handleRegistrationResult(context: Context, registrationData: RegistrationData, registrationResult: RegisterAccountResult, reglockEnabled: Boolean): Boolean {
Log.v(TAG, "handleRegistrationResult()")
when (registrationResult) {
is RegisterAccountResult.Success -> {
@@ -521,6 +547,7 @@ class RegistrationViewModel : ViewModel() {
is RegisterAccountResult.RegistrationLocked -> {
Log.i(TAG, "Account is registration locked!", registrationResult.getCause())
}
is RegisterAccountResult.SvrWrongPin -> {
Log.i(TAG, "Received wrong SVR PIN response! ${registrationResult.triesRemaining} tries remaining.")
updateSvrTriesRemaining(registrationResult.triesRemaining)
@@ -535,7 +562,11 @@ class RegistrationViewModel : ViewModel() {
is RegisterAccountResult.UnknownError -> Log.i(TAG, "Received error when trying to register!", registrationResult.getCause())
}
setInProgress(false)
errorHandler(registrationResult)
store.update {
it.copy(
registerAccountError = registrationResult
)
}
return false
}
@@ -564,7 +595,7 @@ class RegistrationViewModel : ViewModel() {
}
}
fun verifyReRegisterWithPin(context: Context, pin: String, wrongPinHandler: () -> Unit, registrationErrorHandler: (RegisterAccountResult) -> Unit) {
fun verifyReRegisterWithPin(context: Context, pin: String, wrongPinHandler: () -> Unit) {
setInProgress(true)
// Local recovery password
@@ -572,7 +603,7 @@ class RegistrationViewModel : ViewModel() {
if (RegistrationRepository.doesPinMatchLocalHash(pin)) {
Log.d(TAG, "Found recovery password, attempting to re-register.")
viewModelScope.launch(context = coroutineExceptionHandler) {
verifyReRegisterInternal(context, pin, SignalStore.svr.getOrCreateMasterKey(), registrationErrorHandler)
verifyReRegisterInternal(context, pin, SignalStore.svr.getOrCreateMasterKey())
setInProgress(false)
}
} else {
@@ -594,7 +625,7 @@ class RegistrationViewModel : ViewModel() {
val masterKey = RegistrationRepository.fetchMasterKeyFromSvrRemote(pin, svr2Credentials, svr3Credentials)
setRecoveryPassword(masterKey.deriveRegistrationRecoveryPassword())
updateSvrTriesRemaining(10)
verifyReRegisterInternal(context, pin, masterKey, registrationErrorHandler)
verifyReRegisterInternal(context, pin, masterKey)
} catch (rejectedPin: SvrWrongPinException) {
Log.w(TAG, "Submitted PIN was rejected by SVR.", rejectedPin)
updateSvrTriesRemaining(rejectedPin.triesRemaining)
@@ -615,7 +646,7 @@ class RegistrationViewModel : ViewModel() {
}
}
private suspend fun verifyReRegisterInternal(context: Context, pin: String, masterKey: MasterKey, registrationErrorHandler: (RegisterAccountResult) -> Unit) {
private suspend fun verifyReRegisterInternal(context: Context, pin: String, masterKey: MasterKey) {
Log.v(TAG, "verifyReRegisterInternal()")
updateFcmToken(context)
@@ -625,7 +656,7 @@ class RegistrationViewModel : ViewModel() {
val result = resultAndRegLockStatus.first
val reglockEnabled = resultAndRegLockStatus.second
handleRegistrationResult(context, registrationData, result, reglockEnabled, registrationErrorHandler)
handleRegistrationResult(context, registrationData, result, reglockEnabled)
}
/**
@@ -652,7 +683,7 @@ class RegistrationViewModel : ViewModel() {
return Pair(RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, pin = pin) { masterKey }, true)
}
fun verifyCodeWithoutRegistrationLock(context: Context, code: String, submissionErrorHandler: (RegistrationResult) -> Unit, registrationErrorHandler: (RegisterAccountResult) -> Unit) {
fun verifyCodeWithoutRegistrationLock(context: Context, code: String) {
Log.v(TAG, "verifyCodeWithoutRegistrationLock()")
store.update {
it.copy(
@@ -665,15 +696,13 @@ class RegistrationViewModel : ViewModel() {
viewModelScope.launch(context = coroutineExceptionHandler) {
verifyCodeInternal(
context = context,
pin = null,
registrationLocked = false,
submissionErrorHandler = submissionErrorHandler,
registrationErrorHandler = registrationErrorHandler
pin = null
)
}
}
fun verifyCodeAndRegisterAccountWithRegistrationLock(context: Context, pin: String, submissionErrorHandler: (RegistrationResult) -> Unit, registrationErrorHandler: (RegisterAccountResult) -> Unit) {
fun verifyCodeAndRegisterAccountWithRegistrationLock(context: Context, pin: String) {
Log.v(TAG, "verifyCodeAndRegisterAccountWithRegistrationLock()")
store.update {
it.copy(
@@ -684,15 +713,13 @@ class RegistrationViewModel : ViewModel() {
viewModelScope.launch {
verifyCodeInternal(
context = context,
pin = pin,
registrationLocked = true,
submissionErrorHandler = submissionErrorHandler,
registrationErrorHandler = registrationErrorHandler
pin = pin
)
}
}
private suspend fun verifyCodeInternal(context: Context, registrationLocked: Boolean, pin: String?, submissionErrorHandler: (RegistrationResult) -> Unit, registrationErrorHandler: (RegisterAccountResult) -> Unit) {
private suspend fun verifyCodeInternal(context: Context, registrationLocked: Boolean, pin: String?) {
Log.d(TAG, "Getting valid session in order to submit verification code.")
if (registrationLocked && pin.isNullOrBlank()) {
@@ -701,7 +728,7 @@ class RegistrationViewModel : ViewModel() {
var reglock = registrationLocked
val sessionId = getOrCreateValidSession(context, submissionErrorHandler)?.body?.id ?: return
val sessionId = getOrCreateValidSession(context)?.body?.id ?: return
val registrationData = getRegistrationData()
Log.d(TAG, "Submitting verification code…")
@@ -714,7 +741,7 @@ class RegistrationViewModel : ViewModel() {
Log.d(TAG, "Verification code submission network call completed. Submission successful? $submissionSuccessful Account already verified? $alreadyVerified")
if (!submissionSuccessful && !alreadyVerified) {
handleSessionStateResult(context, verificationResponse, submissionErrorHandler)
handleSessionStateResult(context, verificationResponse)
return
}
@@ -758,16 +785,16 @@ class RegistrationViewModel : ViewModel() {
}
if (result != null) {
handleRegistrationResult(context, registrationData, result, reglock, registrationErrorHandler)
handleRegistrationResult(context, registrationData, result, reglock)
} else {
Log.w(TAG, "No registration response received!")
}
}
private suspend fun registerVerifiedSession(context: Context, sessionId: String, registrationErrorHandler: (RegisterAccountResult) -> Unit) {
private suspend fun registerVerifiedSession(context: Context, sessionId: String) {
val registrationData = getRegistrationData()
val registrationResponse: RegisterAccountResult = RegistrationRepository.registerAccount(context, sessionId, registrationData)
handleRegistrationResult(context, registrationData, registrationResponse, false, registrationErrorHandler)
handleRegistrationResult(context, registrationData, registrationResponse, false)
}
private suspend fun onSuccessfulRegistration(context: Context, registrationData: RegistrationData, remoteResult: RegistrationRepository.AccountRegistrationResult, reglockEnabled: Boolean) {
@@ -813,7 +840,7 @@ class RegistrationViewModel : ViewModel() {
RegistrationUtil.maybeMarkRegistrationComplete()
}
fun clearNetworkError() {
fun networkErrorShown() {
store.update {
it.copy(networkError = null)
}

View File

@@ -11,12 +11,10 @@ import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.i18n.phonenumbers.PhoneNumberUtil
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.signal.core.util.logging.Log
@@ -76,7 +74,7 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
}
binding.code.setOnCompleteListener {
sharedViewModel.verifyCodeWithoutRegistrationLock(requireContext(), it, ::handleSessionErrorResponse, ::handleRegistrationErrorResponse)
sharedViewModel.verifyCodeWithoutRegistrationLock(requireContext(), it)
}
binding.havingTroubleButton.setOnClickListener {
@@ -86,14 +84,14 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
binding.callMeCountDown.apply {
setTextResources(R.string.RegistrationActivity_call, R.string.RegistrationActivity_call_me_instead_available_in)
setOnClickListener {
sharedViewModel.requestVerificationCall(requireContext(), ::handleSessionErrorResponse)
sharedViewModel.requestVerificationCall(requireContext())
}
}
binding.resendSmsCountDown.apply {
setTextResources(R.string.RegistrationActivity_resend_code, R.string.RegistrationActivity_resend_sms_available_in)
setOnClickListener {
sharedViewModel.requestSmsCode(requireContext(), ::handleSessionErrorResponse)
sharedViewModel.requestSmsCode(requireContext())
}
}
@@ -114,6 +112,16 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
}
sharedViewModel.uiState.observe(viewLifecycleOwner) {
it.sessionStateError?.let { error ->
handleSessionErrorResponse(error)
sharedViewModel.sessionStateErrorShown()
}
it.registerAccountError?.let { error ->
handleRegistrationErrorResponse(error)
sharedViewModel.registerAccountErrorShown()
}
binding.resendSmsCountDown.startCountDownTo(it.nextSmsTimestamp)
binding.callMeCountDown.startCountDownTo(it.nextCallTimestamp)
if (it.inProgress) {
@@ -132,29 +140,25 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
}
}
private fun handleSessionErrorResponse(result: RegistrationResult) {
viewLifecycleOwner.lifecycleScope.launch {
when (result) {
is VerificationCodeRequestResult.Success -> binding.keyboard.displaySuccess()
is VerificationCodeRequestResult.RateLimited -> presentRateLimitedDialog()
is VerificationCodeRequestResult.AttemptsExhausted -> presentAccountLocked()
is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
else -> presentGenericError(result)
}
private fun handleSessionErrorResponse(result: VerificationCodeRequestResult) {
when (result) {
is VerificationCodeRequestResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
is VerificationCodeRequestResult.RateLimited -> presentRateLimitedDialog()
is VerificationCodeRequestResult.AttemptsExhausted -> presentAccountLocked()
is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
else -> presentGenericError(result)
}
}
private fun handleRegistrationErrorResponse(result: RegisterAccountResult) {
viewLifecycleOwner.lifecycleScope.launch {
when (result) {
is RegisterAccountResult.Success -> binding.keyboard.displaySuccess()
is RegisterAccountResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
is RegisterAccountResult.AuthorizationFailed -> presentIncorrectCodeDialog()
is RegisterAccountResult.AttemptsExhausted -> presentAccountLocked()
is RegisterAccountResult.RateLimited -> presentRateLimitedDialog()
when (result) {
is RegisterAccountResult.Success -> throw IllegalStateException("Register account error handler called on successful response!")
is RegisterAccountResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
is RegisterAccountResult.AuthorizationFailed -> presentIncorrectCodeDialog()
is RegisterAccountResult.AttemptsExhausted -> presentAccountLocked()
is RegisterAccountResult.RateLimited -> presentRateLimitedDialog()
else -> presentGenericError(result)
}
else -> presentGenericError(result)
}
}
@@ -202,19 +206,17 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
private fun presentIncorrectCodeDialog() {
sharedViewModel.incrementIncorrectCodeAttempts()
viewLifecycleOwner.lifecycleScope.launch {
Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_code, Toast.LENGTH_LONG).show()
Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_code, Toast.LENGTH_LONG).show()
binding.keyboard.displayFailure().addListener(object : AssertedSuccessListener<Boolean?>() {
override fun onSuccess(result: Boolean?) {
binding.callMeCountDown.visibility = View.VISIBLE
binding.resendSmsCountDown.visibility = View.VISIBLE
binding.wrongNumber.visibility = View.VISIBLE
binding.code.clear()
binding.keyboard.displayKeyboard()
}
})
}
binding.keyboard.displayFailure().addListener(object : AssertedSuccessListener<Boolean?>() {
override fun onSuccess(result: Boolean?) {
binding.callMeCountDown.visibility = View.VISIBLE
binding.resendSmsCountDown.visibility = View.VISIBLE
binding.wrongNumber.visibility = View.VISIBLE
binding.code.clear()
binding.keyboard.displayKeyboard()
}
})
}
private fun presentGenericError(requestResult: RegistrationResult) {

View File

@@ -25,7 +25,6 @@ import androidx.core.view.MenuProvider
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import com.google.android.gms.common.ConnectionResult
@@ -36,7 +35,6 @@ 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 kotlinx.coroutines.launch
import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
@@ -46,8 +44,9 @@ import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterPhoneNumb
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
@@ -116,10 +115,21 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
sharedState.networkError?.let {
presentNetworkError(it)
sharedViewModel.networkErrorShown()
}
sharedState.sessionCreationError?.let {
handleSessionCreationError(it)
sharedViewModel.sessionCreationErrorShown()
}
sharedState.sessionStateError?.let {
handleSessionStateError(it)
sharedViewModel.sessionStateErrorShown()
}
if (sharedState.challengesRequested.contains(Challenge.CAPTCHA) && sharedState.captchaToken.isNotNullOrBlank()) {
sharedViewModel.submitCaptchaToken(requireContext(), ::handleErrorResponse)
sharedViewModel.submitCaptchaToken(requireContext())
} else if (sharedState.challengesRemaining.isNotEmpty()) {
handleChallenges(sharedState.challengesRemaining)
} else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED && sharedState.canSkipSms) {
@@ -184,7 +194,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
}
private fun performPushChallenge() {
sharedViewModel.requestAndSubmitPushToken(requireContext(), ::handleErrorResponse)
sharedViewModel.requestAndSubmitPushToken(requireContext())
}
private fun initializeInputFields() {
@@ -294,67 +304,82 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
Log.i(TAG, "Unknown error during verification code request", networkError)
MaterialAlertDialogBuilder(requireContext()).apply {
setMessage(R.string.RegistrationActivity_unable_to_connect_to_service)
setPositiveButton(android.R.string.ok) { _, _ -> sharedViewModel.clearNetworkError() }
setOnCancelListener { sharedViewModel.clearNetworkError() }
setOnDismissListener { sharedViewModel.clearNetworkError() }
setPositiveButton(android.R.string.ok, null)
show()
}
}
private fun handleErrorResponse(result: RegistrationResult) {
viewLifecycleOwner.lifecycleScope.launch {
if (!result.isSuccess()) {
Log.i(TAG, "Handling error response.", result.getCause())
private fun handleSessionCreationError(result: RegistrationSessionResult) {
if (!result.isSuccess()) {
Log.i(TAG, "Handling error response.", result.getCause())
}
when (result) {
is RegistrationSessionCheckResult.Success,
is RegistrationSessionCreationResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
is RegistrationSessionCreationResult.AttemptsExhausted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_service))
is RegistrationSessionCreationResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen)
is RegistrationSessionCreationResult.RateLimited -> {
Log.i(TAG, "Session creation rate limited! Next attempt: ${result.timeRemaining.milliseconds}")
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, result.timeRemaining.milliseconds.toString()))
}
when (result) {
is RegistrationSessionCreationResult.Success,
is VerificationCodeRequestResult.Success -> Unit
is RegistrationSessionCreationResult.AttemptsExhausted,
is VerificationCodeRequestResult.AttemptsExhausted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_service))
is RegistrationSessionCreationResult.ServerUnableToParse -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service))
is RegistrationSessionCheckResult.SessionNotFound -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service))
is RegistrationSessionCheckResult.UnknownError,
is RegistrationSessionCreationResult.UnknownError -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service))
}
}
is VerificationCodeRequestResult.ChallengeRequired -> {
handleChallenges(result.challenges)
}
private fun handleSessionStateError(result: VerificationCodeRequestResult) {
if (!result.isSuccess()) {
Log.i(TAG, "Handling error response.", result.getCause())
}
when (result) {
is VerificationCodeRequestResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
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.AttemptsExhausted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_service))
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 RegistrationSessionCreationResult.MalformedRequest,
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, fragmentViewModel.mode)
is RegistrationSessionCreationResult.RateLimited -> {
Log.i(TAG, "Session creation rate limited! Next attempt: ${result.timeRemaining.milliseconds}")
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, result.timeRemaining.milliseconds.toString()))
}
is VerificationCodeRequestResult.RateLimited -> {
Log.i(TAG, "Code request rate limited! Next attempt: ${result.timeRemaining.milliseconds}")
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)) { _, _ -> moveToCaptcha() }
else -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service))
is VerificationCodeRequestResult.ChallengeRequired -> {
handleChallenges(result.challenges)
}
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())
}
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, fragmentViewModel.mode)
is VerificationCodeRequestResult.RateLimited -> {
Log.i(TAG, "Code request rate limited! Next attempt: ${result.timeRemaining.milliseconds}")
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)) { _, _ -> moveToCaptcha() }
is VerificationCodeRequestResult.RegistrationLocked -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service))
is VerificationCodeRequestResult.AlreadyVerified -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service))
is VerificationCodeRequestResult.NoSuchSession -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service))
is VerificationCodeRequestResult.UnknownError -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service))
}
}
@@ -390,8 +415,8 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
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.SMS_WITHOUT_LISTENER -> sharedViewModel.requestSmsCode(requireContext())
RegistrationRepository.Mode.PHONE_CALL -> sharedViewModel.requestVerificationCall(requireContext())
}
dialogInterface.dismiss()
}
@@ -510,7 +535,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
if (missingFcmConsentRequired) {
handlePromptForNoPlayServices()
} else {
sharedViewModel.onUserConfirmedPhoneNumber(requireContext(), ::handleErrorResponse)
sharedViewModel.onUserConfirmedPhoneNumber(requireContext())
}
}
setNegativeButton(R.string.RegistrationActivity_edit_number) { _, _ -> handleConfirmNumberDialogCanceled() }
@@ -525,7 +550,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
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(), ::handleErrorResponse)
sharedViewModel.onUserConfirmedPhoneNumber(requireContext())
}
setNegativeButton(android.R.string.cancel, null)
setOnCancelListener { fragmentViewModel.clearError() }

View File

@@ -24,7 +24,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.lock.v2.SvrConstants
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
@@ -101,12 +100,22 @@ class RegistrationLockFragment : LoggingFragment(R.layout.fragment_registration_
binding.kbsLockPinInputLabel.text = requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__d_attempts_remaining, triesRemaining, triesRemaining)
}
viewModel.inProgress.observe(viewLifecycleOwner) {
if (it) {
viewModel.uiState.observe(viewLifecycleOwner) {
if (it.inProgress) {
binding.kbsLockPinConfirm.setSpinning()
} else {
binding.kbsLockPinConfirm.cancelSpinning()
}
it.sessionStateError?.let { error ->
handleSessionErrorResponse(error)
viewModel.sessionStateErrorShown()
}
it.registerAccountError?.let { error ->
handleRegistrationErrorResponse(error)
viewModel.registerAccountErrorShown()
}
}
}
@@ -132,12 +141,12 @@ class RegistrationLockFragment : LoggingFragment(R.layout.fragment_registration_
binding.kbsLockPinConfirm.setSpinning()
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(requireContext(), pin, ::handleSessionErrorResponse, ::handleRegistrationErrorResponse)
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(requireContext(), pin)
}
private fun handleSessionErrorResponse(requestResult: RegistrationResult) {
private fun handleSessionErrorResponse(requestResult: VerificationCodeRequestResult) {
when (requestResult) {
is VerificationCodeRequestResult.Success -> Unit
is VerificationCodeRequestResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
is VerificationCodeRequestResult.RateLimited -> onRateLimited()
is VerificationCodeRequestResult.AttemptsExhausted -> {
findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked())
@@ -159,7 +168,7 @@ class RegistrationLockFragment : LoggingFragment(R.layout.fragment_registration_
private fun handleRegistrationErrorResponse(result: RegisterAccountResult) {
when (result) {
is RegisterAccountResult.Success -> Unit
is RegisterAccountResult.Success -> throw IllegalStateException("Register account error handler called on successful response!")
is RegisterAccountResult.RateLimited -> onRateLimited()
is RegisterAccountResult.AttemptsExhausted -> {
findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked())

View File

@@ -82,6 +82,7 @@ class ReRegisterWithPinFragment : LoggingFragment(R.layout.fragment_registration
private fun updateViewState(state: RegistrationState) {
if (state.networkError != null) {
genericErrorDialog()
registrationViewModel.networkErrorShown()
} else if (!state.canSkipSms) {
findNavController().safeNavigate(ReRegisterWithPinFragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberFragment())
} else if (state.isRegistrationLockEnabled && state.svrTriesRemaining == 0) {
@@ -91,6 +92,11 @@ class ReRegisterWithPinFragment : LoggingFragment(R.layout.fragment_registration
presentProgress(state.inProgress)
presentTriesRemaining(state.svrTriesRemaining)
}
state.registerAccountError?.let { error ->
registrationErrorHandler(error)
registrationViewModel.registerAccountErrorShown()
}
}
private fun presentProgress(inProgress: Boolean) {
@@ -126,8 +132,7 @@ class ReRegisterWithPinFragment : LoggingFragment(R.layout.fragment_registration
pin = pin,
wrongPinHandler = {
reRegisterViewModel.markIncorrectGuess()
},
registrationErrorHandler = ::registrationErrorHandler
}
)
}
@@ -251,7 +256,7 @@ class ReRegisterWithPinFragment : LoggingFragment(R.layout.fragment_registration
private fun registrationErrorHandler(result: RegisterAccountResult) {
when (result) {
is RegisterAccountResult.Success -> Log.d(TAG, "Register account was successful.")
is RegisterAccountResult.Success -> throw IllegalStateException("Register account error handler called on successful response!")
is RegisterAccountResult.AuthorizationFailed,
is RegisterAccountResult.MalformedRequest,
is RegisterAccountResult.UnknownError,