Initial error handling for registration v2.
This commit is contained in:
parent
49ba83dda8
commit
9c5bb4aa17
7 changed files with 328 additions and 54 deletions
|
@ -7,9 +7,9 @@ package org.thoughtcrime.securesms.registration.v2.data
|
|||
|
||||
import android.app.backup.BackupManager
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
|
@ -41,6 +41,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
|||
import org.thoughtcrime.securesms.registration.PushChallengeRequest
|
||||
import org.thoughtcrime.securesms.registration.RegistrationData
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.BackupAuthCheckResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener
|
||||
|
@ -59,6 +61,7 @@ import org.whispersystems.signalservice.api.registration.RegistrationApi
|
|||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataHeaders
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
@ -142,7 +145,6 @@ object RegistrationRepository {
|
|||
/**
|
||||
* Takes a server response from a successful registration and persists the relevant data.
|
||||
*/
|
||||
@WorkerThread
|
||||
@JvmStatic
|
||||
suspend fun registerAccountLocally(context: Context, registrationData: RegistrationData, response: AccountRegistrationResult, reglockEnabled: Boolean) =
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -258,20 +260,21 @@ object RegistrationRepository {
|
|||
* 2. (Optional) If the session has any proof requirements ("challenges"), the user must solve them and submit the proof.
|
||||
* 3. Once the service responds we are allowed to, we request the verification code.
|
||||
*/
|
||||
suspend fun requestSmsCode(context: Context, e164: String, password: String, mcc: String?, mnc: String?, mode: Mode = Mode.SMS_WITHOUT_LISTENER): NetworkResult<RegistrationSessionMetadataResponse> =
|
||||
suspend fun requestSmsCode(context: Context, e164: String, password: String, mcc: String?, mnc: String?, mode: Mode = Mode.SMS_WITHOUT_LISTENER): VerificationCodeRequestResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
val fcmToken: String? = FcmUtil.getToken(context).orElse(null)
|
||||
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
|
||||
val activeSession = if (fcmToken == null) {
|
||||
// TODO [regv2]
|
||||
val notImplementedError = NotImplementedError()
|
||||
Log.w(TAG, "Not yet implemented!", notImplementedError)
|
||||
NetworkResult.ApplicationError(notImplementedError)
|
||||
} else {
|
||||
createSessionAndBlockForPushChallenge(api, fcmToken, mcc, mnc)
|
||||
}
|
||||
|
||||
activeSession.then { session ->
|
||||
val result = (
|
||||
if (fcmToken == null) {
|
||||
// TODO [regv2]
|
||||
val notImplementedError = NotImplementedError()
|
||||
Log.w(TAG, "Not yet implemented!", notImplementedError)
|
||||
NetworkResult.ApplicationError(notImplementedError)
|
||||
} else {
|
||||
createSessionAndBlockForPushChallenge(api, fcmToken, mcc, mnc)
|
||||
}
|
||||
).then { session ->
|
||||
val sessionId = session.body.id
|
||||
SignalStore.registrationValues().sessionId = sessionId
|
||||
SignalStore.registrationValues().sessionE164 = e164
|
||||
|
@ -290,6 +293,8 @@ object RegistrationRepository {
|
|||
api.requestSmsVerificationCode(sessionId, Locale.getDefault(), mode.isSmsRetrieverSupported)
|
||||
}
|
||||
}
|
||||
|
||||
return@withContext VerificationCodeRequestResult.from(result)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -397,39 +402,45 @@ object RegistrationRepository {
|
|||
return timestamp + deltaSeconds.seconds.inWholeMilliseconds
|
||||
}
|
||||
|
||||
suspend fun hasValidSvrAuthCredentials(context: Context, e164: String, password: String): AuthCredentials? =
|
||||
suspend fun hasValidSvrAuthCredentials(context: Context, e164: String, password: String): BackupAuthCheckResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
val usernamePasswords = SignalStore.svr()
|
||||
.authTokenList
|
||||
.take(10)
|
||||
.map {
|
||||
it.replace("Basic ", "").trim()
|
||||
}
|
||||
.map {
|
||||
Base64.decode(it) // TODO [regv2]: figure out why Android Studio doesn't like mapCatching
|
||||
}
|
||||
.map {
|
||||
String(it, StandardCharsets.ISO_8859_1)
|
||||
}
|
||||
|
||||
if (usernamePasswords.isEmpty()) {
|
||||
return@withContext null
|
||||
}
|
||||
val usernamePasswords = async { retrieveLocalSvrCredentials() }
|
||||
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
|
||||
|
||||
val authCheck = api.getSvrAuthCredential(e164, usernamePasswords)
|
||||
if (authCheck !is NetworkResult.Success) {
|
||||
return@withContext null
|
||||
}
|
||||
val result = api.getSvrAuthCredential(e164, usernamePasswords.await())
|
||||
.runIfSuccessful {
|
||||
val removedInvalidTokens = SignalStore.svr().removeAuthTokens(it.invalid)
|
||||
if (removedInvalidTokens) {
|
||||
BackupManager(context).dataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
val removedInvalidTokens = SignalStore.svr().removeAuthTokens(authCheck.result.invalid)
|
||||
if (removedInvalidTokens) {
|
||||
BackupManager(context).dataChanged()
|
||||
}
|
||||
|
||||
return@withContext authCheck.result.match
|
||||
return@withContext BackupAuthCheckResult.from(result)
|
||||
}
|
||||
|
||||
private suspend fun retrieveLocalSvrCredentials(): List<String> = withContext(Dispatchers.IO) {
|
||||
return@withContext SignalStore.svr()
|
||||
.authTokenList
|
||||
.asSequence()
|
||||
.filterNotNull()
|
||||
.take<String>(10)
|
||||
.map<String, String> {
|
||||
it.replace("Basic ", "").trim()
|
||||
}
|
||||
.mapNotNull<String, ByteArray> {
|
||||
try {
|
||||
Base64.decode(it)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Encountered error trying to decode a token!", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
.map<ByteArray, String> {
|
||||
String(it, StandardCharsets.ISO_8859_1)
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
enum class Mode(val isSmsRetrieverSupported: Boolean) {
|
||||
SMS_WITH_LISTENER(true), SMS_WITHOUT_LISTENER(false), PHONE_CALL(false)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.data.network
|
||||
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.BackupAuthCheckResponse
|
||||
|
||||
/**
|
||||
* This is a processor to map a [BackupAuthCheckResponse] to all the known outcomes.
|
||||
*/
|
||||
sealed class BackupAuthCheckResult(cause: Throwable?) : RegistrationResult(cause) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun from(networkResult: NetworkResult<BackupAuthCheckResponse>): BackupAuthCheckResult {
|
||||
return when (networkResult) {
|
||||
is NetworkResult.Success -> {
|
||||
val match = networkResult.result.match
|
||||
if (match != null) {
|
||||
SuccessWithCredentials(match)
|
||||
} else {
|
||||
SuccessWithoutCredentials()
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable)
|
||||
is NetworkResult.NetworkError -> UnknownError(networkResult.exception)
|
||||
is NetworkResult.StatusCodeError -> UnknownError(networkResult.exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SuccessWithCredentials(val authCredentials: AuthCredentials) : BackupAuthCheckResult(null)
|
||||
|
||||
class SuccessWithoutCredentials : BackupAuthCheckResult(null)
|
||||
|
||||
class UnknownError(cause: Throwable) : BackupAuthCheckResult(cause)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.data.network
|
||||
|
||||
/**
|
||||
* This is a merging of the NetworkResult pattern and the Processor pattern of registration v1.
|
||||
* The goal is to enumerate all possible responses as sealed classes, which means the consumer will be able to handle them in an exhaustive when clause
|
||||
*
|
||||
* @property errorCause the [Throwable] that caused the Error. Null if the network request was successful.
|
||||
*
|
||||
*/
|
||||
abstract class RegistrationResult(private val errorCause: Throwable?) {
|
||||
fun isSuccess(): Boolean {
|
||||
return errorCause == null
|
||||
}
|
||||
|
||||
fun getCause(): Throwable {
|
||||
if (errorCause == null) {
|
||||
throw IllegalStateException("Cannot get cause from successful processor!")
|
||||
}
|
||||
|
||||
return errorCause
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.data.network
|
||||
|
||||
import okio.IOException
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ExternalServiceFailureException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ImpossiblePhoneNumberException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.InvalidTransportModeException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MalformedRequestException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonNormalizedPhoneNumberException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushChallengeRequiredException
|
||||
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.RegistrationSessionMetadataJson
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil
|
||||
|
||||
/**
|
||||
* This is a processor to map a [RegistrationSessionMetadataResponse] to all the known outcomes.
|
||||
*/
|
||||
sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResult(cause) {
|
||||
companion object {
|
||||
|
||||
private val TAG = Log.tag(VerificationCodeRequestResult::class.java)
|
||||
|
||||
@JvmStatic
|
||||
fun from(networkResult: NetworkResult<RegistrationSessionMetadataResponse>): VerificationCodeRequestResult {
|
||||
return when (networkResult) {
|
||||
is NetworkResult.Success -> {
|
||||
val challenges = networkResult.result.body.requestedInformation
|
||||
if (challenges.isNotEmpty()) {
|
||||
ChallengeRequired(challenges)
|
||||
} else {
|
||||
Success(
|
||||
sessionId = networkResult.result.body.id,
|
||||
nextSms = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.body.nextSms),
|
||||
nextCall = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.body.nextCall)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable)
|
||||
is NetworkResult.NetworkError -> UnknownError(networkResult.exception)
|
||||
is NetworkResult.StatusCodeError -> {
|
||||
when (val cause = networkResult.exception) {
|
||||
is PushChallengeRequiredException -> createChallengeRequiredProcessor(networkResult)
|
||||
is CaptchaRequiredException -> createChallengeRequiredProcessor(networkResult)
|
||||
is RateLimitException -> createRateLimitProcessor(cause)
|
||||
is ImpossiblePhoneNumberException -> ImpossibleNumber(cause)
|
||||
is NonNormalizedPhoneNumberException -> NonNormalizedNumber(cause)
|
||||
is TokenNotAcceptedException -> TokenNotAccepted(cause)
|
||||
is ExternalServiceFailureException -> ExternalServiceFailure(cause)
|
||||
is InvalidTransportModeException -> InvalidTransportModeFailure(cause)
|
||||
is MalformedRequestException -> MalformedRequest(cause)
|
||||
is RegistrationRetryException -> MustRetry(cause)
|
||||
else -> UnknownError(cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createChallengeRequiredProcessor(errorResult: NetworkResult.StatusCodeError<RegistrationSessionMetadataResponse>): VerificationCodeRequestResult {
|
||||
if (errorResult.body == null) {
|
||||
Log.w(TAG, "Attempted to parse error body with response code ${errorResult.code} for list of requested information, but body was null.")
|
||||
return UnknownError(errorResult.exception)
|
||||
}
|
||||
|
||||
try {
|
||||
val response = JsonUtil.fromJson(errorResult.body, RegistrationSessionMetadataJson::class.java)
|
||||
return ChallengeRequired(response.requestedInformation)
|
||||
} catch (parseException: IOException) {
|
||||
Log.w(TAG, "Attempted to parse error body for list of requested information, but encountered exception.", parseException)
|
||||
return UnknownError(parseException)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRateLimitProcessor(exception: RateLimitException): VerificationCodeRequestResult {
|
||||
return if (exception.retryAfterMilliseconds.isPresent) {
|
||||
RateLimited(exception, exception.retryAfterMilliseconds.get())
|
||||
} else {
|
||||
AttemptsExhausted(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Success(val sessionId: String, val nextSms: Long, val nextCall: Long) : VerificationCodeRequestResult(null)
|
||||
|
||||
class ChallengeRequired(val challenges: List<String>) : VerificationCodeRequestResult(null)
|
||||
|
||||
class RateLimited(cause: Throwable, val timeRemaining: Long) : VerificationCodeRequestResult(cause)
|
||||
|
||||
class AttemptsExhausted(cause: Throwable) : VerificationCodeRequestResult(cause)
|
||||
|
||||
class ImpossibleNumber(cause: Throwable) : VerificationCodeRequestResult(cause)
|
||||
|
||||
class NonNormalizedNumber(cause: Throwable) : VerificationCodeRequestResult(cause)
|
||||
|
||||
class TokenNotAccepted(cause: Throwable) : VerificationCodeRequestResult(cause)
|
||||
|
||||
class ExternalServiceFailure(cause: Throwable) : VerificationCodeRequestResult(cause)
|
||||
|
||||
class InvalidTransportModeFailure(cause: Throwable) : VerificationCodeRequestResult(cause)
|
||||
|
||||
class MalformedRequest(cause: Throwable) : VerificationCodeRequestResult(cause)
|
||||
|
||||
class MustRetry(cause: Throwable) : VerificationCodeRequestResult(cause)
|
||||
|
||||
class UnknownError(cause: Throwable) : VerificationCodeRequestResult(cause)
|
||||
}
|
|
@ -12,6 +12,7 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import com.google.i18n.phonenumbers.Phonenumber
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
@ -27,6 +28,19 @@ import org.thoughtcrime.securesms.pin.SvrWrongPinException
|
|||
import org.thoughtcrime.securesms.registration.RegistrationData
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.BackupAuthCheckResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.AttemptsExhausted
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ChallengeRequired
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ExternalServiceFailure
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ImpossibleNumber
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.InvalidTransportModeFailure
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.MalformedRequest
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.NonNormalizedNumber
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.RateLimited
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.MustRetry
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.Success
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.TokenNotAccepted
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.UnknownError
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
|
||||
|
@ -42,9 +56,15 @@ import java.io.IOException
|
|||
class RegistrationV2ViewModel : ViewModel() {
|
||||
|
||||
private val store = MutableStateFlow(RegistrationV2State())
|
||||
|
||||
private val password = Util.getSecret(18) // TODO [regv2]: persist this
|
||||
|
||||
private val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
|
||||
Log.w(TAG, "CoroutineExceptionHandler invoked.", exception)
|
||||
store.update {
|
||||
it.copy(networkError = exception)
|
||||
}
|
||||
}
|
||||
|
||||
val uiState = store.asLiveData()
|
||||
|
||||
init {
|
||||
|
@ -80,7 +100,7 @@ class RegistrationV2ViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
fun fetchFcmToken(context: Context) {
|
||||
viewModelScope.launch {
|
||||
viewModelScope.launch(context = coroutineExceptionHandler) {
|
||||
val fcmToken = RegistrationRepository.getFcmToken(context)
|
||||
store.update {
|
||||
it.copy(registrationCheckpoint = RegistrationCheckpoint.PUSH_NETWORK_AUDITED, isFcmSupported = true, fcmToken = fcmToken)
|
||||
|
@ -120,25 +140,70 @@ class RegistrationV2ViewModel : ViewModel() {
|
|||
store.update {
|
||||
it.copy(canSkipSms = true)
|
||||
}
|
||||
} else {
|
||||
viewModelScope.launch {
|
||||
val svrCredentials = RegistrationRepository.hasValidSvrAuthCredentials(context, e164, password)
|
||||
return
|
||||
}
|
||||
|
||||
if (svrCredentials != null) {
|
||||
// Re-registration when credentials stored in backup.
|
||||
viewModelScope.launch {
|
||||
val svrCredentialsResult = RegistrationRepository.hasValidSvrAuthCredentials(context, e164, password)
|
||||
|
||||
when (svrCredentialsResult) {
|
||||
is BackupAuthCheckResult.UnknownError -> {
|
||||
handleGenericError(svrCredentialsResult.getCause())
|
||||
return@launch
|
||||
}
|
||||
|
||||
is BackupAuthCheckResult.SuccessWithCredentials -> {
|
||||
Log.d(TAG, "Found local valid SVR auth credentials.")
|
||||
store.update {
|
||||
it.copy(canSkipSms = true, svrAuthCredentials = svrCredentials)
|
||||
it.copy(canSkipSms = true, svrAuthCredentials = svrCredentialsResult.authCredentials)
|
||||
}
|
||||
} else {
|
||||
val codeRequestResponse = RegistrationRepository.requestSmsCode(context, e164, password, mccMncProducer.mcc, mccMncProducer.mnc).successOrThrow()
|
||||
return@launch
|
||||
}
|
||||
|
||||
is BackupAuthCheckResult.SuccessWithoutCredentials -> Log.d(TAG, "No local SVR auth credentials could be found and/or validated.")
|
||||
}
|
||||
|
||||
val codeRequestResponse = RegistrationRepository.requestSmsCode(context, e164, password, mccMncProducer.mcc, mccMncProducer.mnc)
|
||||
|
||||
when (codeRequestResponse) {
|
||||
is UnknownError -> {
|
||||
handleGenericError(codeRequestResponse.getCause())
|
||||
return@launch
|
||||
}
|
||||
|
||||
is Success -> {
|
||||
updateFcmToken(context)
|
||||
store.update {
|
||||
it.copy(sessionId = codeRequestResponse.body.id, nextSms = RegistrationRepository.deriveTimestamp(codeRequestResponse.headers, codeRequestResponse.body.nextSms), nextCall = RegistrationRepository.deriveTimestamp(codeRequestResponse.headers, codeRequestResponse.body.nextCall), registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED)
|
||||
it.copy(
|
||||
sessionId = codeRequestResponse.sessionId,
|
||||
nextSms = codeRequestResponse.nextSms,
|
||||
nextCall = codeRequestResponse.nextCall,
|
||||
registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is AttemptsExhausted -> Log.w(TAG, "TODO")
|
||||
is ChallengeRequired -> Log.w(TAG, "TODO")
|
||||
is ImpossibleNumber -> Log.w(TAG, "TODO")
|
||||
is NonNormalizedNumber -> Log.w(TAG, "TODO")
|
||||
is RateLimited -> Log.w(TAG, "TODO")
|
||||
is ExternalServiceFailure -> Log.w(TAG, "TODO")
|
||||
is InvalidTransportModeFailure -> Log.w(TAG, "TODO")
|
||||
is MalformedRequest -> Log.w(TAG, "TODO")
|
||||
is MustRetry -> Log.w(TAG, "TODO")
|
||||
is TokenNotAccepted -> Log.w(TAG, "TODO")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleGenericError(cause: Throwable) {
|
||||
Log.w(TAG, "Encountered unknown error!", cause)
|
||||
store.update {
|
||||
it.copy(inProgress = false, networkError = cause)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setRecoveryPassword(recoveryPassword: String?) {
|
||||
store.update {
|
||||
it.copy(recoveryPassword = recoveryPassword)
|
||||
|
@ -164,7 +229,7 @@ class RegistrationV2ViewModel : ViewModel() {
|
|||
if (RegistrationRepository.canUseLocalRecoveryPassword()) {
|
||||
if (RegistrationRepository.doesPinMatchLocalHash(pin)) {
|
||||
Log.d(TAG, "Found recovery password, attempting to re-register.")
|
||||
viewModelScope.launch {
|
||||
viewModelScope.launch(context = coroutineExceptionHandler) {
|
||||
verifyReRegisterInternal(context, pin, SignalStore.svr().getOrCreateMasterKey())
|
||||
setInProgress(false)
|
||||
}
|
||||
|
@ -180,7 +245,7 @@ class RegistrationV2ViewModel : ViewModel() {
|
|||
val authCredentials = store.value.svrAuthCredentials
|
||||
if (authCredentials != null) {
|
||||
Log.d(TAG, "Found SVR auth credentials, fetching recovery password from SVR.")
|
||||
viewModelScope.launch {
|
||||
viewModelScope.launch(context = coroutineExceptionHandler) {
|
||||
try {
|
||||
val masterKey = RegistrationRepository.fetchMasterKeyFromSvrRemote(pin, authCredentials)
|
||||
setRecoveryPassword(masterKey.deriveRegistrationRecoveryPassword())
|
||||
|
@ -254,7 +319,7 @@ class RegistrationV2ViewModel : ViewModel() {
|
|||
}
|
||||
val e164: String = getCurrentE164() ?: throw IllegalStateException()
|
||||
|
||||
viewModelScope.launch {
|
||||
viewModelScope.launch(context = coroutineExceptionHandler) {
|
||||
val registrationData = getRegistrationData(code)
|
||||
val verificationResponse = RegistrationRepository.submitVerificationCode(context, e164, password, sessionId, registrationData).successOrThrow()
|
||||
|
||||
|
|
|
@ -96,6 +96,10 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
|||
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
|
||||
presentRegisterButton(sharedState)
|
||||
presentProgressBar(sharedState.inProgress, sharedState.isReRegister)
|
||||
|
||||
sharedState.networkError?.let {
|
||||
presentNetworkError(it)
|
||||
}
|
||||
if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED && sharedState.canSkipSms) {
|
||||
moveToEnterPinScreen()
|
||||
} else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) {
|
||||
|
@ -226,6 +230,15 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
|||
}
|
||||
}
|
||||
|
||||
private fun presentNetworkError(networkError: Throwable) {
|
||||
// TODO [regv2]: check specific errors with a when clause
|
||||
Log.i(TAG, "Unknown error during verification code request", networkError)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.RegistrationActivity_unable_to_connect_to_service)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun onRegistrationButtonClicked() {
|
||||
ViewUtil.hideKeyboard(requireContext(), phoneNumberInputLayout)
|
||||
sharedViewModel.setInProgress(true)
|
||||
|
|
|
@ -51,7 +51,7 @@ sealed class NetworkResult<T>(
|
|||
data class NetworkError<T>(val exception: IOException) : NetworkResult<T>()
|
||||
|
||||
/** Indicates we got a response, but it was a non-2xx response. */
|
||||
data class StatusCodeError<T>(val code: Int, val body: String?, val exception: IOException) : NetworkResult<T>()
|
||||
data class StatusCodeError<T>(val code: Int, val body: String?, val exception: NonSuccessfulResponseCodeException) : NetworkResult<T>()
|
||||
|
||||
/** Indicates that the application somehow failed in a way unrelated to network activity. Usually a runtime crash. */
|
||||
data class ApplicationError<T>(val throwable: Throwable) : NetworkResult<T>()
|
||||
|
|
Loading…
Add table
Reference in a new issue