From cc72a2232ee51d45dec6a78a6efc6c114f8d796d Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 8 Jul 2026 11:25:21 -0400 Subject: [PATCH] Fix some reglock bugs in regV5. --- .../registration/RegistrationNavigation.kt | 24 +++++-- .../registration/RegistrationRepository.kt | 34 +--------- .../registration/RegistrationViewModel.kt | 3 +- .../DeviceTransferCompleteViewModel.kt | 3 +- .../LocalBackupRestoreViewModel.kt | 3 +- .../pincreation/PinCreationViewModel.kt | 3 +- .../PinEntryForRegistrationLockViewModel.kt | 14 ++-- .../pinentry/PinEntryForSmsBypassViewModel.kt | 3 +- .../PinEntryForSvrRestoreViewModel.kt | 5 +- .../RemoteBackupRestoreViewModel.kt | 3 +- .../ArchiveRestoreSelectionViewModel.kt | 67 ++++++++++++------- .../restoreselection/RegisteredState.kt | 13 ++++ .../VerificationCodeViewModel.kt | 2 +- .../registration/RegistrationViewModelTest.kt | 2 +- .../DeviceTransferCompleteViewModelTest.kt | 13 ++-- .../LocalBackupRestoreViewModelTest.kt | 4 +- .../pincreation/PinCreationViewModelTest.kt | 6 +- ...inEntryForRegistrationLockViewModelTest.kt | 35 ++++++++-- .../PinEntryForSmsBypassViewModelTest.kt | 12 ++-- .../PinEntryForSvrRestoreViewModelTest.kt | 7 +- .../RemoteBackupRestoreViewModelTest.kt | 9 +-- .../ArchiveRestoreSelectionViewModelTest.kt | 37 ++++++---- .../signalservice/api/svr/Svr2Socket.kt | 2 + 23 files changed, 184 insertions(+), 120 deletions(-) create mode 100644 feature/registration/src/main/java/org/signal/registration/screens/restoreselection/RegisteredState.kt diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt index 0acf236c02..520df3c2a0 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt @@ -100,6 +100,7 @@ import org.signal.registration.screens.remotebackuprestore.RemoteRestoreScreen import org.signal.registration.screens.restoreselection.ArchiveRestoreOption import org.signal.registration.screens.restoreselection.ArchiveRestoreSelectionScreen import org.signal.registration.screens.restoreselection.ArchiveRestoreSelectionViewModel +import org.signal.registration.screens.restoreselection.RegisteredState import org.signal.registration.screens.util.navigateBack import org.signal.registration.screens.util.navigateTo import org.signal.registration.screens.verificationcode.VerificationCodeScreen @@ -162,7 +163,7 @@ sealed interface RegistrationRoute : NavKey, Parcelable { data object PinCreate : RegistrationRoute @Serializable - data class ArchiveRestoreSelection(val restoreOptions: List, val isPreRegistration: Boolean) : RegistrationRoute { + data class ArchiveRestoreSelection(val restoreOptions: List, val registeredState: RegisteredState) : RegistrationRoute { companion object { fun forQuickRestore(hasRemoteBackup: Boolean): ArchiveRestoreSelection { return ArchiveRestoreSelection( @@ -174,7 +175,7 @@ sealed interface RegistrationRoute : NavKey, Parcelable { add(ArchiveRestoreOption.DeviceTransfer) add(ArchiveRestoreOption.None) }, - isPreRegistration = true + registeredState = RegisteredState.NotRegistered ) } @@ -185,18 +186,29 @@ sealed interface RegistrationRoute : NavKey, Parcelable { add(ArchiveRestoreOption.LocalBackup) add(ArchiveRestoreOption.None) }, - isPreRegistration = true + registeredState = RegisteredState.NotRegistered ) } - fun forPostRegister(): ArchiveRestoreSelection { + fun forPostRegisterWithPinUnknown(): ArchiveRestoreSelection { return ArchiveRestoreSelection( restoreOptions = buildList { add(ArchiveRestoreOption.SignalSecureBackup) add(ArchiveRestoreOption.LocalBackup) add(ArchiveRestoreOption.None) }, - isPreRegistration = false + registeredState = RegisteredState.RegisteredAndPinUnknown + ) + } + + fun forPostRegisterWithPinKnown(): ArchiveRestoreSelection { + return ArchiveRestoreSelection( + restoreOptions = buildList { + add(ArchiveRestoreOption.SignalSecureBackup) + add(ArchiveRestoreOption.LocalBackup) + add(ArchiveRestoreOption.None) + }, + registeredState = RegisteredState.RegisteredAndPinKnown ) } } @@ -694,7 +706,7 @@ private fun EntryProviderScope.navigationEntries( val viewModel: ArchiveRestoreSelectionViewModel = viewModel( factory = ArchiveRestoreSelectionViewModel.Factory( restoreOptions = key.restoreOptions, - isPreRegistration = key.isPreRegistration, + registeredState = key.registeredState, repository = registrationRepository, parentState = registrationViewModel.state, parentEventEmitter = registrationViewModel::onEvent diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt index 0c70f829fc..6073b75998 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt @@ -648,43 +648,11 @@ class RegistrationRepository(val context: Context, val networkController: Networ * The work continues in the background even if [timeout] elapses. See [NetworkController.restoreAccountRecord]. */ suspend fun restoreAccountRecord( - timeout: Duration + timeout: Duration = 10.seconds ): RequestResult = withContext(Dispatchers.IO) { networkController.restoreAccountRecord(timeout) } - /** - * Best-effort restore the AccountRecord (when local profile data is incomplete) and then signal - * registration completion on [parentEventEmitter]. The Profile screen is intentionally not - * routed to from here for now — even when the restore doesn't fully populate profile data, we - * emit [RegistrationFlowEvent.RegistrationComplete]. - * - * Intended for any screen that, in the legacy flow, would have signalled "we're done". Pre- - * existing-data callers (re-registration, device transfer, backup restore) won't pay the - * restore-record cost. - */ - suspend fun finishRegistrationOrCreateProfile( - parentEventEmitter: (RegistrationFlowEvent) -> Unit, - restoreTimeout: Duration = 10.seconds - ) { - if (hasProfileNameAndAvatar()) { - Log.i(TAG, "[finishRegistrationOrCreateProfile] Profile name + avatar already on disk; finishing.") - parentEventEmitter(RegistrationFlowEvent.RegistrationComplete) - return - } - - Log.i(TAG, "[finishRegistrationOrCreateProfile] Profile data incomplete; attempting best-effort account-record restore (timeout=${restoreTimeout.inWholeSeconds}s).") - restoreAccountRecord(restoreTimeout) - - Log.i(TAG, "[finishRegistrationOrCreateProfile] Account-record restore finished; finishing without routing to Profile screen.") - parentEventEmitter(RegistrationFlowEvent.RegistrationComplete) - } - - private suspend fun hasProfileNameAndAvatar(): Boolean { - val stored = getStoredProfileData() - return stored.givenName.isNotEmpty() && stored.avatar != null - } - /** * Persists the freshly-created profile to local storage and arranges for it to be uploaded. * See [NetworkController.setProfile]. diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt index 7eab6f4e10..043c641df4 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.launch import org.signal.core.ui.navigation.ResultEventBus import org.signal.core.util.logging.Log import org.signal.registration.screens.EventDrivenViewModel +import org.signal.registration.screens.restoreselection.RegisteredState import kotlin.reflect.KClass /** @@ -148,7 +149,7 @@ class RegistrationViewModel( is RegistrationRoute.PinCreate, is RegistrationRoute.PinEntryForSvrRestore, is RegistrationRoute.RemoteRestore -> true - is RegistrationRoute.ArchiveRestoreSelection -> !this.isPreRegistration + is RegistrationRoute.ArchiveRestoreSelection -> this.registeredState != RegisteredState.NotRegistered else -> false } } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteViewModel.kt index 99c8b2d1cd..979603ecef 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteViewModel.kt @@ -43,7 +43,8 @@ class DeviceTransferCompleteViewModel( when (event) { DeviceTransferCompleteScreenEvents.ContinueClicked -> { repository.setRestoreDecision(RestoreDecision.COMPLETED) - repository.finishRegistrationOrCreateProfile(parentEventEmitter) + repository.restoreAccountRecord() + parentEventEmitter(RegistrationFlowEvent.RegistrationComplete) } DeviceTransferCompleteScreenEvents.ConsumeOneTimeEvent -> { stateEmitter(state.copy(oneTimeEvent = null)) diff --git a/feature/registration/src/main/java/org/signal/registration/screens/localbackuprestore/LocalBackupRestoreViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/localbackuprestore/LocalBackupRestoreViewModel.kt index 5d587334c9..56c45ad5f6 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/localbackuprestore/LocalBackupRestoreViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/localbackuprestore/LocalBackupRestoreViewModel.kt @@ -120,7 +120,8 @@ class LocalBackupRestoreViewModel( parentEventEmitter.navigateBack() } else { repository.setRestoreDecision(RestoreDecision.COMPLETED) - repository.finishRegistrationOrCreateProfile(parentEventEmitter) + repository.restoreAccountRecord() + parentEventEmitter(RegistrationFlowEvent.RegistrationComplete) } } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt index fd95892bdd..1477bea163 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt @@ -125,7 +125,8 @@ class PinCreationViewModel( is RequestResult.Success -> { Log.i(TAG, "[PinSubmitted] Successfully backed up master key to SVR.") repository.setRestoreDecision(RestoreDecision.NEW_ACCOUNT) - repository.finishRegistrationOrCreateProfile(parentEventEmitter) + repository.restoreAccountRecord() + parentEventEmitter(RegistrationFlowEvent.RegistrationComplete) state } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt index 49e8077102..72933660d9 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt @@ -67,7 +67,7 @@ class PinEntryForRegistrationLockViewModel( stateEmitter(applyPinEntered(localState, event, parentEventEmitter)) } is PinEntryScreenEvents.Skip -> { - handleSkip() + throw NotImplementedError("Skip is not a valid action during registration lock PIN entry") } is PinEntryScreenEvents.CreateNewPin, is PinEntryScreenEvents.ContactSupport -> Unit @@ -142,9 +142,11 @@ class PinEntryForRegistrationLockViewModel( Log.i(TAG, "[PinEntered] Successfully registered!") val (response, keyMaterial) = registerResult.result parentEventEmitter(RegistrationFlowEvent.Registered(keyMaterial.accountEntropyPool, response.storageCapable)) + repository.enqueueSvrResetGuessCountJob() + repository.restoreAccountRecord() when { - response.reregistration -> parentEventEmitter.navigateTo(RegistrationRoute.ArchiveRestoreSelection.forPostRegister()) - else -> repository.finishRegistrationOrCreateProfile(parentEventEmitter) + response.reregistration -> parentEventEmitter.navigateTo(RegistrationRoute.ArchiveRestoreSelection.forPostRegisterWithPinKnown()) + else -> parentEventEmitter(RegistrationFlowEvent.RegistrationComplete) } state } @@ -191,12 +193,6 @@ class PinEntryForRegistrationLockViewModel( } } - private fun handleSkip() { - // Registration lock is enforced server-side, so there's no way to register without the PIN. The skip option is - // never shown in this mode, so reaching here indicates a bug. - throw NotImplementedError("Skip is not a valid action during registration lock PIN entry") - } - class Factory( private val repository: RegistrationRepository, private val parentState: StateFlow, diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModel.kt index c4a44d449e..dc2d43b7ad 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModel.kt @@ -151,7 +151,8 @@ class PinEntryForSmsBypassViewModel( return when (val result = repository.registerAccountWithRecoveryPassword(e164, recoveryPassword, registrationLock, skipDeviceTransfer = true)) { is RequestResult.Success -> { repository.enqueueSvrResetGuessCountJob() - repository.finishRegistrationOrCreateProfile(parentEventEmitter) + repository.restoreAccountRecord() + parentEventEmitter(RegistrationFlowEvent.RegistrationComplete) state } is RequestResult.RetryableNetworkError -> { diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt index 100fbcf590..9887ace976 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt @@ -98,7 +98,7 @@ class PinEntryForSvrRestoreViewModel( result.result } is RequestResult.NonSuccess -> { - when (val error = result.error) { + when (result.error) { NetworkController.GetSvrCredentialsError.NoServiceCredentialsAvailable -> { Log.w(TAG, "[PinEntered] No service credentials available when restoring from SVR. This should not happen. Resetting.") parentEventEmitter(RegistrationFlowEvent.ResetState) @@ -125,7 +125,8 @@ class PinEntryForSvrRestoreViewModel( repository.enqueueSvrResetGuessCountJob() repository.setRestoreDecision(RestoreDecision.COMPLETED) parentEventEmitter(RegistrationFlowEvent.MasterKeyRestoredFromSvr(result.result.masterKey)) - repository.finishRegistrationOrCreateProfile(parentEventEmitter) + repository.restoreAccountRecord() + parentEventEmitter(RegistrationFlowEvent.RegistrationComplete) state } is RequestResult.NonSuccess -> { diff --git a/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreViewModel.kt index c7550d242f..49ed8bc54b 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreViewModel.kt @@ -122,7 +122,8 @@ class RemoteBackupRestoreViewModel( parentEventEmitter(RegistrationFlowEvent.UserSuppliedAepVerified(aep)) repository.persistRemoteBackupRestoredState(progress.restoredSvrPin, progress.restoredProfileKey) repository.setRestoreDecision(RestoreDecision.COMPLETED) - repository.finishRegistrationOrCreateProfile(parentEventEmitter) + repository.restoreAccountRecord() + parentEventEmitter(RegistrationFlowEvent.RegistrationComplete) } is RemoteBackupRestoreProgress.NetworkError -> { Log.w(TAG, "[restoreBackup] Remote restore failed with network error.", progress.cause) diff --git a/feature/registration/src/main/java/org/signal/registration/screens/restoreselection/ArchiveRestoreSelectionViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/restoreselection/ArchiveRestoreSelectionViewModel.kt index 54c8432bb9..ef9db7b7cd 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/restoreselection/ArchiveRestoreSelectionViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/restoreselection/ArchiveRestoreSelectionViewModel.kt @@ -34,7 +34,7 @@ import org.signal.registration.screens.util.navigateTo */ class ArchiveRestoreSelectionViewModel( private val restoreOptions: List, - private val isPreRegistration: Boolean, + private val registeredState: RegisteredState, private val repository: RegistrationRepository, private val parentState: StateFlow, private val parentEventEmitter: (RegistrationFlowEvent) -> Unit @@ -74,21 +74,27 @@ class ArchiveRestoreSelectionViewModel( when (event.option) { ArchiveRestoreOption.SignalSecureBackup -> { notifyOldDevice(state.restoreMethodToken, NetworkController.RestoreMethod.REMOTE_BACKUP) - if (isPreRegistration) { - parentEventEmitter(RegistrationFlowEvent.PendingRestoreOptionSelected(PendingRestoreOption.RemoteBackup)) - parentEventEmitter.navigateTo(RegistrationRoute.PhoneNumberEntry) - } else { - parentEventEmitter.navigateTo(RegistrationRoute.EnterAepForRemoteBackupPostRegistration) + when (registeredState) { + RegisteredState.NotRegistered -> { + parentEventEmitter(RegistrationFlowEvent.PendingRestoreOptionSelected(PendingRestoreOption.RemoteBackup)) + parentEventEmitter.navigateTo(RegistrationRoute.PhoneNumberEntry) + } + else -> { + parentEventEmitter.navigateTo(RegistrationRoute.EnterAepForRemoteBackupPostRegistration) + } } state } ArchiveRestoreOption.LocalBackup -> { notifyOldDevice(state.restoreMethodToken, NetworkController.RestoreMethod.LOCAL_BACKUP) - if (isPreRegistration) { - parentEventEmitter(RegistrationFlowEvent.PendingRestoreOptionSelected(PendingRestoreOption.LocalBackup)) - parentEventEmitter.navigateTo(RegistrationRoute.PhoneNumberEntry) - } else { - parentEventEmitter.navigateTo(RegistrationRoute.LocalBackupRestore(isPreRegistration = false)) + when (registeredState) { + RegisteredState.NotRegistered -> { + parentEventEmitter(RegistrationFlowEvent.PendingRestoreOptionSelected(PendingRestoreOption.LocalBackup)) + parentEventEmitter.navigateTo(RegistrationRoute.PhoneNumberEntry) + } + else -> { + parentEventEmitter.navigateTo(RegistrationRoute.LocalBackupRestore(isPreRegistration = false)) + } } state } @@ -103,20 +109,29 @@ class ArchiveRestoreSelectionViewModel( } } is ArchiveRestoreSelectionScreenEvents.ConfirmSkip -> { - if (isPreRegistration) { - parentEventEmitter.navigateTo(RegistrationRoute.PhoneNumberEntry) - state.copy(showSkipWarningDialog = false) - } else { - notifyOldDevice(state.restoreMethodToken, NetworkController.RestoreMethod.DECLINE) - repository.setRestoreDecision(RestoreDecision.SKIPPED) - if (state.storageCapable) { - Log.i(TAG, "[ConfirmSkip] Account is storage capable. Navigating to PIN entry to restore the existing PIN.") - parentEventEmitter.navigateTo(RegistrationRoute.PinEntryForSvrRestore) - } else { - Log.i(TAG, "[ConfirmSkip] Account is not storage capable. Navigating to PIN creation.") - parentEventEmitter.navigateTo(RegistrationRoute.PinCreate) + when (registeredState) { + RegisteredState.NotRegistered -> { + parentEventEmitter.navigateTo(RegistrationRoute.PhoneNumberEntry) + state.copy(showSkipWarningDialog = false) + } + RegisteredState.RegisteredAndPinUnknown -> { + notifyOldDevice(state.restoreMethodToken, NetworkController.RestoreMethod.DECLINE) + repository.setRestoreDecision(RestoreDecision.SKIPPED) + if (state.storageCapable) { + Log.i(TAG, "[ConfirmSkip] Account is storage capable. Navigating to PIN entry to restore the existing PIN.") + parentEventEmitter.navigateTo(RegistrationRoute.PinEntryForSvrRestore) + } else { + Log.i(TAG, "[ConfirmSkip] Account is not storage capable. Navigating to PIN creation.") + parentEventEmitter.navigateTo(RegistrationRoute.PinCreate) + } + state.copy(showSkipWarningDialog = false) + } + RegisteredState.RegisteredAndPinKnown -> { + notifyOldDevice(state.restoreMethodToken, NetworkController.RestoreMethod.DECLINE) + repository.setRestoreDecision(RestoreDecision.SKIPPED) + parentEventEmitter(RegistrationFlowEvent.RegistrationComplete) + state.copy(showSkipWarningDialog = false) } - state.copy(showSkipWarningDialog = false) } } is ArchiveRestoreSelectionScreenEvents.DismissSkipWarning -> { @@ -145,13 +160,13 @@ class ArchiveRestoreSelectionViewModel( class Factory( private val restoreOptions: List, - private val isPreRegistration: Boolean, + private val registeredState: RegisteredState, private val repository: RegistrationRepository, private val parentState: StateFlow, private val parentEventEmitter: (RegistrationFlowEvent) -> Unit ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return ArchiveRestoreSelectionViewModel(restoreOptions, isPreRegistration, repository, parentState, parentEventEmitter) as T + return ArchiveRestoreSelectionViewModel(restoreOptions, registeredState, repository, parentState, parentEventEmitter) as T } } } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/restoreselection/RegisteredState.kt b/feature/registration/src/main/java/org/signal/registration/screens/restoreselection/RegisteredState.kt new file mode 100644 index 0000000000..581703b065 --- /dev/null +++ b/feature/registration/src/main/java/org/signal/registration/screens/restoreselection/RegisteredState.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.restoreselection + +/** + * Just an enum for letting the view model know what the registered status is so it can navigate appropriately. + */ +enum class RegisteredState { + NotRegistered, RegisteredAndPinUnknown, RegisteredAndPinKnown +} diff --git a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt index 1a60522f6f..704f3c6fc3 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt @@ -355,7 +355,7 @@ class VerificationCodeViewModel( parentEventEmitter(RegistrationFlowEvent.Registered(keyMaterial.accountEntropyPool, response.storageCapable)) when { - response.reregistration -> parentEventEmitter.navigateTo(RegistrationRoute.ArchiveRestoreSelection.forPostRegister()) + response.reregistration -> parentEventEmitter.navigateTo(RegistrationRoute.ArchiveRestoreSelection.forPostRegisterWithPinUnknown()) response.storageCapable -> parentEventEmitter.navigateTo(RegistrationRoute.PinEntryForSvrRestore) else -> parentEventEmitter.navigateTo(RegistrationRoute.PinCreate) } diff --git a/feature/registration/src/test/java/org/signal/registration/RegistrationViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/RegistrationViewModelTest.kt index c550fb55cc..b4a2cf201f 100644 --- a/feature/registration/src/test/java/org/signal/registration/RegistrationViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/RegistrationViewModelTest.kt @@ -475,7 +475,7 @@ class RegistrationViewModelTest { ) ) - val postRegisterSelection = RegistrationRoute.ArchiveRestoreSelection.forPostRegister() + val postRegisterSelection = RegistrationRoute.ArchiveRestoreSelection.forPostRegisterWithPinKnown() val result = viewModel.applyEvent( initialState, diff --git a/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteViewModelTest.kt index 0ca8a266c3..066c3f3a2f 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteViewModelTest.kt @@ -6,6 +6,7 @@ package org.signal.registration.screens.devicetransfer.complete import assertk.assertThat +import assertk.assertions.contains import assertk.assertions.hasSize import assertk.assertions.isEmpty import assertk.assertions.isNull @@ -54,7 +55,7 @@ class DeviceTransferCompleteViewModelTest { } @Test - fun `ContinueClicked hands off to finishRegistrationOrCreateProfile`() = runTest { + fun `ContinueClicked restores account record and completes registration`() = runTest { viewModel.applyEvent( DeviceTransferCompleteState(), DeviceTransferCompleteScreenEvents.ContinueClicked, @@ -64,7 +65,8 @@ class DeviceTransferCompleteViewModelTest { ) coVerify { mockRepository.setRestoreDecision(RestoreDecision.COMPLETED) } - coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) } + coVerify { mockRepository.restoreAccountRecord(any()) } + assertThat(emittedEvents).contains(RegistrationFlowEvent.RegistrationComplete) } @Test @@ -79,7 +81,7 @@ class DeviceTransferCompleteViewModelTest { coVerifyOrder { mockRepository.setRestoreDecision(RestoreDecision.COMPLETED) - mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) + mockRepository.restoreAccountRecord(any()) } } @@ -109,7 +111,7 @@ class DeviceTransferCompleteViewModelTest { assertThat(emittedStates).hasSize(1) assertThat(emittedStates.last().oneTimeEvent).isNull() coVerify(exactly = 0) { mockRepository.setRestoreDecision(any()) } - coVerify(exactly = 0) { mockRepository.finishRegistrationOrCreateProfile(any(), any()) } + coVerify(exactly = 0) { mockRepository.restoreAccountRecord(any()) } } @Test @@ -118,6 +120,7 @@ class DeviceTransferCompleteViewModelTest { testDispatcher.scheduler.advanceUntilIdle() coVerify { mockRepository.setRestoreDecision(RestoreDecision.COMPLETED) } - coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) } + coVerify { mockRepository.restoreAccountRecord(any()) } + assertThat(emittedEvents).contains(RegistrationFlowEvent.RegistrationComplete) } } diff --git a/feature/registration/src/test/java/org/signal/registration/screens/localbackuprestore/LocalBackupRestoreViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/localbackuprestore/LocalBackupRestoreViewModelTest.kt index 907bcaa336..bb440a3606 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/localbackuprestore/LocalBackupRestoreViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/localbackuprestore/LocalBackupRestoreViewModelTest.kt @@ -7,6 +7,7 @@ package org.signal.registration.screens.localbackuprestore import android.net.Uri import assertk.assertThat +import assertk.assertions.contains import assertk.assertions.hasSize import assertk.assertions.isEmpty import assertk.assertions.isEqualTo @@ -266,6 +267,7 @@ class LocalBackupRestoreViewModelTest { viewModel.applyEvent(initialState, LocalBackupRestoreEvents.PassphraseSubmitted("passphrase"), stateEmitter) coVerify { mockRepository.setRestoreDecision(RestoreDecision.COMPLETED) } - coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) } + coVerify { mockRepository.restoreAccountRecord(any()) } + assertThat(emittedParentEvents).contains(RegistrationFlowEvent.RegistrationComplete) } } diff --git a/feature/registration/src/test/java/org/signal/registration/screens/pincreation/PinCreationViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/pincreation/PinCreationViewModelTest.kt index 19bed9d14d..5cee481082 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/pincreation/PinCreationViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/pincreation/PinCreationViewModelTest.kt @@ -6,6 +6,7 @@ package org.signal.registration.screens.pincreation import assertk.assertThat +import assertk.assertions.contains import assertk.assertions.hasSize import assertk.assertions.isEqualTo import assertk.assertions.isFalse @@ -130,7 +131,7 @@ class PinCreationViewModelTest { // ==================== PinSubmitted Success Tests ==================== @Test - fun `matching confirmation PIN with valid AEP and successful SVR backup hands off to finishRegistrationOrCreateProfile`() = runTest(testDispatcher) { + fun `matching confirmation PIN with valid AEP and successful SVR backup completes registration`() = runTest(testDispatcher) { val states = collectStates() val aep = AccountEntropyPool.generate() val confirmState = PinCreationState(accountEntropyPool = aep, isConfirmEnabled = true, firstPin = "123456") @@ -141,7 +142,8 @@ class PinCreationViewModelTest { viewModel.applyEvent(confirmState, PinCreationScreenEvents.PinSubmitted("123456")) coVerify { mockRepository.setRestoreDecision(RestoreDecision.NEW_ACCOUNT) } - coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) } + coVerify { mockRepository.restoreAccountRecord(any()) } + assertThat(emittedParentEvents).contains(RegistrationFlowEvent.RegistrationComplete) assertThat(states.last().loading).isTrue() } diff --git a/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModelTest.kt index 9c86d0c1c7..07083bcbd9 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModelTest.kt @@ -82,10 +82,36 @@ class PinEntryForRegistrationLockViewModelTest { viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter) - assertThat(emittedParentEvents).hasSize(2) + assertThat(emittedParentEvents).hasSize(3) assertThat(emittedParentEvents[0]).isInstanceOf() assertThat(emittedParentEvents[1]).isInstanceOf() - coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) } + assertThat(emittedParentEvents[2]).isEqualTo(RegistrationFlowEvent.RegistrationComplete) + coVerify { mockRepository.restoreAccountRecord(any()) } + assertThat(emittedStates.last().loading).isEqualTo(true) + } + + @Test + fun `PinEntered with correct PIN on re-registration navigates to post-register restore selection`() = runTest { + val masterKey = mockk(relaxed = true) + val keyMaterial = mockk(relaxed = true) + val registerResponse = createRegisterAccountResponse(reregistration = true) + val initialState = PinEntryState(mode = PinEntryState.Mode.RegistrationLock) + + coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), forRegistrationLock = true) } returns + RequestResult.Success(NetworkController.MasterKeyResponse(masterKey)) + coEvery { mockRepository.registerAccountWithSession(any(), any(), any(), any()) } returns + RequestResult.Success(registerResponse to keyMaterial) + + viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter) + + assertThat(emittedParentEvents).hasSize(3) + assertThat(emittedParentEvents[0]).isInstanceOf() + assertThat(emittedParentEvents[1]).isInstanceOf() + assertThat(emittedParentEvents[2]) + .isInstanceOf() + .prop(RegistrationFlowEvent.NavigateToScreen::route) + .isEqualTo(RegistrationRoute.ArchiveRestoreSelection.forPostRegisterWithPinKnown()) + coVerify { mockRepository.restoreAccountRecord(any()) } assertThat(emittedStates.last().loading).isEqualTo(true) } @@ -427,7 +453,8 @@ class PinEntryForRegistrationLockViewModelTest { aci: String = "test-aci", pni: String = "test-pni", e164: String = "+15551234567", - storageCapable: Boolean = true + storageCapable: Boolean = true, + reregistration: Boolean = false ) = NetworkController.RegisterAccountResponse( aci = aci, pni = pni, @@ -436,6 +463,6 @@ class PinEntryForRegistrationLockViewModelTest { usernameLinkHandle = null, storageCapable = storageCapable, entitlements = null, - reregistration = false + reregistration = reregistration ) } diff --git a/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModelTest.kt index ea1b96601a..810a7eb497 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModelTest.kt @@ -65,7 +65,7 @@ class PinEntryForSmsBypassViewModelTest { // ==================== PinEntered - Restore Master Key Tests ==================== @Test - fun `PinEntered with correct PIN restores master key and hands off to finishRegistrationOrCreateProfile`() = runTest { + fun `PinEntered with correct PIN restores master key and completes registration`() = runTest { val masterKey = mockk(relaxed = true) val initialState = PinEntryState(mode = PinEntryState.Mode.SmsBypass, e164 = "+15551234567") @@ -76,9 +76,10 @@ class PinEntryForSmsBypassViewModelTest { viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter) - assertThat(emittedParentEvents).hasSize(1) + assertThat(emittedParentEvents).hasSize(2) assertThat(emittedParentEvents[0]).isInstanceOf() - coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) } + assertThat(emittedParentEvents[1]).isEqualTo(RegistrationFlowEvent.RegistrationComplete) + coVerify { mockRepository.restoreAccountRecord(any()) } assertThat(emittedStates.last().loading).isEqualTo(true) } @@ -290,9 +291,10 @@ class PinEntryForSmsBypassViewModelTest { viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter) - assertThat(emittedParentEvents).hasSize(1) + assertThat(emittedParentEvents).hasSize(2) assertThat(emittedParentEvents[0]).isInstanceOf() - coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) } + assertThat(emittedParentEvents[1]).isEqualTo(RegistrationFlowEvent.RegistrationComplete) + coVerify { mockRepository.restoreAccountRecord(any()) } assertThat(emittedStates.last().loading).isEqualTo(true) } diff --git a/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModelTest.kt index 4f4fcf1fe9..4ca1e4a3a0 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModelTest.kt @@ -59,7 +59,7 @@ class PinEntryForSvrRestoreViewModelTest { // ==================== PinEntered Success Tests ==================== @Test - fun `PinEntered with correct PIN restores master key and hands off to finishRegistrationOrCreateProfile`() = runTest { + fun `PinEntered with correct PIN restores master key and completes registration`() = runTest { val masterKey = mockk(relaxed = true) val svrCredentials = NetworkController.SvrCredentials( username = "test-username", @@ -74,10 +74,11 @@ class PinEntryForSvrRestoreViewModelTest { viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter) - assertThat(emittedParentEvents).hasSize(1) + assertThat(emittedParentEvents).hasSize(2) assertThat(emittedParentEvents[0]).isInstanceOf() + assertThat(emittedParentEvents[1]).isEqualTo(RegistrationFlowEvent.RegistrationComplete) coVerify { mockRepository.setRestoreDecision(RestoreDecision.COMPLETED) } - coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) } + coVerify { mockRepository.restoreAccountRecord(any()) } assertThat(emittedStates.last().loading).isEqualTo(true) } diff --git a/feature/registration/src/test/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreViewModelTest.kt index 14703d77d6..9778a7e4e0 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreViewModelTest.kt @@ -347,7 +347,7 @@ class RemoteBackupRestoreViewModelTest { } @Test - fun `Complete progress emits UserSuppliedAepVerified and hands off to finishRegistrationOrCreateProfile`() = runTest(testDispatcher) { + fun `Complete progress emits UserSuppliedAepVerified and completes registration`() = runTest(testDispatcher) { every { mockRepository.restoreRemoteBackup(any()) } returns flowOf( RemoteBackupRestoreProgress.Complete(restoredSvrPin = null, restoredProfileKey = null) ) @@ -361,10 +361,11 @@ class RemoteBackupRestoreViewModelTest { stateEmitter ) - assertThat(emittedParentEvents).hasSize(1) + assertThat(emittedParentEvents).hasSize(2) assertThat(emittedParentEvents[0]).isInstanceOf() + assertThat(emittedParentEvents[1]).isEqualTo(RegistrationFlowEvent.RegistrationComplete) coVerify { mockRepository.setRestoreDecision(RestoreDecision.COMPLETED) } - coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) } + coVerify { mockRepository.restoreAccountRecord(any()) } } @Test @@ -462,6 +463,6 @@ class RemoteBackupRestoreViewModelTest { viewModel.applyEvent(RemoteBackupRestoreState(aep = aep), RemoteBackupRestoreScreenEvents.BackupRestoreBackup, stateEmitter) assertThat(states.last().restoreState).isEqualTo(RemoteBackupRestoreState.RestoreState.Restored) - coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) } + coVerify { mockRepository.restoreAccountRecord(any()) } } } diff --git a/feature/registration/src/test/java/org/signal/registration/screens/restoreselection/ArchiveRestoreSelectionViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/restoreselection/ArchiveRestoreSelectionViewModelTest.kt index 16abbadecb..53ff6533b9 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/restoreselection/ArchiveRestoreSelectionViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/restoreselection/ArchiveRestoreSelectionViewModelTest.kt @@ -48,11 +48,11 @@ class ArchiveRestoreSelectionViewModelTest { ArchiveRestoreOption.LocalBackup, ArchiveRestoreOption.DeviceTransfer ), - isPreRegistration: Boolean = false + registeredState: RegisteredState = RegisteredState.RegisteredAndPinUnknown ): ArchiveRestoreSelectionViewModel { return ArchiveRestoreSelectionViewModel( restoreOptions = restoreOptions, - isPreRegistration = isPreRegistration, + registeredState = registeredState, repository = mockRepository, parentState = MutableStateFlow(RegistrationFlowState()), parentEventEmitter = parentEventEmitter @@ -63,7 +63,7 @@ class ArchiveRestoreSelectionViewModelTest { @Test fun `SignalSecureBackup pre-registration emits PendingRestoreOptionSelected and navigates to PhoneNumberEntry`() = runTest { - val viewModel = createViewModel(isPreRegistration = true) + val viewModel = createViewModel(registeredState = RegisteredState.NotRegistered) val initialState = ArchiveRestoreSelectionState() viewModel.applyEvent( @@ -85,7 +85,7 @@ class ArchiveRestoreSelectionViewModelTest { @Test fun `SignalSecureBackup post-registration navigates to EnterAepForRemoteBackupPostRegistration`() = runTest { - val viewModel = createViewModel(isPreRegistration = false) + val viewModel = createViewModel(registeredState = RegisteredState.RegisteredAndPinUnknown) val initialState = ArchiveRestoreSelectionState() viewModel.applyEvent( @@ -103,7 +103,7 @@ class ArchiveRestoreSelectionViewModelTest { @Test fun `LocalBackup pre-registration emits PendingRestoreOptionSelected and navigates to PhoneNumberEntry`() = runTest { - val viewModel = createViewModel(isPreRegistration = true) + val viewModel = createViewModel(registeredState = RegisteredState.NotRegistered) val initialState = ArchiveRestoreSelectionState() viewModel.applyEvent( @@ -125,7 +125,7 @@ class ArchiveRestoreSelectionViewModelTest { @Test fun `LocalBackup post-registration navigates to LocalBackupRestore`() = runTest { - val viewModel = createViewModel(isPreRegistration = false) + val viewModel = createViewModel(registeredState = RegisteredState.RegisteredAndPinUnknown) val initialState = ArchiveRestoreSelectionState() viewModel.applyEvent( @@ -143,7 +143,7 @@ class ArchiveRestoreSelectionViewModelTest { @Test fun `DeviceTransfer navigates to DeviceTransferInstructions`() = runTest { - val viewModel = createViewModel(isPreRegistration = false) + val viewModel = createViewModel(registeredState = RegisteredState.RegisteredAndPinUnknown) val initialState = ArchiveRestoreSelectionState() viewModel.applyEvent( @@ -161,7 +161,7 @@ class ArchiveRestoreSelectionViewModelTest { @Test fun `None option sets showSkipWarningDialog to true`() = runTest { - val viewModel = createViewModel(isPreRegistration = false) + val viewModel = createViewModel(registeredState = RegisteredState.RegisteredAndPinUnknown) val initialState = ArchiveRestoreSelectionState() viewModel.applyEvent( @@ -178,7 +178,7 @@ class ArchiveRestoreSelectionViewModelTest { @Test fun `ConfirmSkip pre-registration navigates to PhoneNumberEntry and clears dialog without recording a skip`() = runTest { - val viewModel = createViewModel(isPreRegistration = true) + val viewModel = createViewModel(registeredState = RegisteredState.NotRegistered) val initialState = ArchiveRestoreSelectionState(showSkipWarningDialog = true) viewModel.applyEvent(initialState, ArchiveRestoreSelectionScreenEvents.ConfirmSkip, stateEmitter) @@ -194,7 +194,7 @@ class ArchiveRestoreSelectionViewModelTest { @Test fun `ConfirmSkip post-registration when not storage capable navigates to PinCreate and clears dialog`() = runTest { - val viewModel = createViewModel(isPreRegistration = false) + val viewModel = createViewModel(registeredState = RegisteredState.RegisteredAndPinUnknown) val initialState = ArchiveRestoreSelectionState(showSkipWarningDialog = true, storageCapable = false) viewModel.applyEvent(initialState, ArchiveRestoreSelectionScreenEvents.ConfirmSkip, stateEmitter) @@ -210,7 +210,7 @@ class ArchiveRestoreSelectionViewModelTest { @Test fun `ConfirmSkip post-registration when storage capable navigates to PinEntryForSvrRestore and clears dialog`() = runTest { - val viewModel = createViewModel(isPreRegistration = false) + val viewModel = createViewModel(registeredState = RegisteredState.RegisteredAndPinUnknown) val initialState = ArchiveRestoreSelectionState(showSkipWarningDialog = true, storageCapable = true) viewModel.applyEvent(initialState, ArchiveRestoreSelectionScreenEvents.ConfirmSkip, stateEmitter) @@ -224,11 +224,24 @@ class ArchiveRestoreSelectionViewModelTest { assertThat(emittedStates.last().showSkipWarningDialog).isFalse() } + @Test + fun `ConfirmSkip post-registration when PIN is known records skip and completes registration`() = runTest { + val viewModel = createViewModel(registeredState = RegisteredState.RegisteredAndPinKnown) + val initialState = ArchiveRestoreSelectionState(showSkipWarningDialog = true) + + viewModel.applyEvent(initialState, ArchiveRestoreSelectionScreenEvents.ConfirmSkip, stateEmitter) + + coVerify { mockRepository.setRestoreDecision(RestoreDecision.SKIPPED) } + assertThat(emittedParentEvents).hasSize(1) + assertThat(emittedParentEvents.first()).isEqualTo(RegistrationFlowEvent.RegistrationComplete) + assertThat(emittedStates.last().showSkipWarningDialog).isFalse() + } + // ==================== DismissSkipWarning Tests ==================== @Test fun `DismissSkipWarning sets showSkipWarningDialog to false`() = runTest { - val viewModel = createViewModel(isPreRegistration = false) + val viewModel = createViewModel(registeredState = RegisteredState.RegisteredAndPinUnknown) val initialState = ArchiveRestoreSelectionState(showSkipWarningDialog = true) viewModel.applyEvent( diff --git a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/Svr2Socket.kt b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/Svr2Socket.kt index 21ed23485c..055805beef 100644 --- a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/Svr2Socket.kt +++ b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/Svr2Socket.kt @@ -151,6 +151,8 @@ internal class Svr2Socket( override fun onFailure(webSocket: WebSocket, t: Throwable, response: OkHttpResponse?) { val exception = if (t.message?.contains("404") == true) { NonSuccessfulResponseCodeException(404) + } else if (t.message?.contains("429") == true) { + NonSuccessfulResponseCodeException(429) } else { IOException(t) }