mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Add additional error handling for registration v2.
This commit is contained in:
committed by
Cody Henthorne
parent
1ae2464df1
commit
f37efd7e15
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user