From f87ff5870192df0b1861a99920163e27b516b3aa Mon Sep 17 00:00:00 2001 From: Nicholas Tinsley Date: Thu, 25 Jul 2024 21:21:36 +0200 Subject: [PATCH] Convert registration error handling from callbacks to observers. --- .../registration/ui/RegistrationState.kt | 8 +- .../registration/ui/RegistrationViewModel.kt | 123 +++++++++------ .../ui/entercode/EnterCodeFragment.kt | 72 ++++----- .../phonenumber/EnterPhoneNumberFragment.kt | 145 ++++++++++-------- .../RegistrationLockFragment.kt | 23 ++- .../ReRegisterWithPinFragment.kt | 11 +- 6 files changed, 228 insertions(+), 154 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationState.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationState.kt index a6a4e5502b..40ec6df291 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationState.kt @@ -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 = challengesRequested.filterNot { it in challengesPresented } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt index 1ed3e31f3f..50ae77b505 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt @@ -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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt index a7df623a3f..2874f663b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt @@ -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() { - 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() { + 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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt index b6c5c8cb2d..007758f1a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt @@ -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() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/registrationlock/RegistrationLockFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/registrationlock/RegistrationLockFragment.kt index 5a674feb5c..3e9014ec3c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/registrationlock/RegistrationLockFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/registrationlock/RegistrationLockFragment.kt @@ -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()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinFragment.kt index 71a85ff660..509cfcabca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinFragment.kt @@ -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,