Fix storageCapable bug in regV5.

This commit is contained in:
Greyson Parrelli 2026-06-23 11:05:10 -04:00 committed by jeffrey-signal
parent c55f281213
commit f375750cf8
No known key found for this signature in database
14 changed files with 73 additions and 24 deletions

View File

@ -24,7 +24,8 @@ data class PersistedFlowState(
val doNotAttemptRecoveryPassword: Boolean,
val pendingRestoreOption: PendingRestoreOption? = null,
val restoredAepValue: String? = null,
val restoreMethodToken: String? = null
val restoreMethodToken: String? = null,
val storageCapable: Boolean = false
)
/**
@ -38,7 +39,8 @@ fun RegistrationFlowState.toPersistedFlowState(): PersistedFlowState {
doNotAttemptRecoveryPassword = doNotAttemptRecoveryPassword,
pendingRestoreOption = pendingRestoreOption,
restoredAepValue = unverifiedRestoredAep?.value,
restoreMethodToken = restoreMethodToken
restoreMethodToken = restoreMethodToken,
storageCapable = storageCapable
)
}
@ -64,6 +66,7 @@ fun PersistedFlowState.toRegistrationFlowState(
doNotAttemptRecoveryPassword = doNotAttemptRecoveryPassword,
pendingRestoreOption = pendingRestoreOption,
unverifiedRestoredAep = restoredAepValue?.let { AccountEntropyPool(it) },
restoreMethodToken = restoreMethodToken
restoreMethodToken = restoreMethodToken,
storageCapable = storageCapable
)
}

View File

@ -25,9 +25,14 @@ sealed interface RegistrationFlowEvent {
/** The e164 associated with this registration attempt has been updated. */
data class E164Chosen(val e164: String) : RegistrationFlowEvent
/** The user has successfully registered. */
data class Registered(val accountEntropyPool: AccountEntropyPool) : RegistrationFlowEvent {
override fun toString(): String = "Registered(accountEntropyPool=${accountEntropyPool.displayValue.censor()})"
/**
* The user has successfully registered.
*
* @param storageCapable Whether the server reports that this account already has SVR/PIN data, as returned in the
* registration response. Used later (e.g. when skipping a restore) to decide between PIN entry and PIN creation.
*/
data class Registered(val accountEntropyPool: AccountEntropyPool, val storageCapable: Boolean) : RegistrationFlowEvent {
override fun toString(): String = "Registered(accountEntropyPool=${accountEntropyPool.displayValue.censor()}, storageCapable=$storageCapable)"
}
/** The master key has been restored from SVR. */

View File

@ -30,6 +30,9 @@ data class RegistrationFlowState(
/** The AEP we generated as part of this registration. */
val accountEntropyPool: AccountEntropyPool? = null,
/** Whether the server reported that this account already has SVR/PIN data, captured from the registration response. */
val storageCapable: Boolean = false,
/** The master key we restored from SVR. Needed for initial storage service restore, but afterwards we'll generate a new one. */
val temporaryMasterKey: MasterKey? = null,
@ -55,6 +58,6 @@ data class RegistrationFlowState(
val isRestoringNavigationState: Boolean = true
) : Parcelable {
override fun toString(): String {
return "RegistrationFlowState(backStack=${backStack.joinToString()}, sessionMetadata=${sessionMetadata.let { "present" }}, sessionE164=$sessionE164, accountEntropyPool=${accountEntropyPool?.displayValue?.censor()}, temporaryMasterKey=${temporaryMasterKey?.toString()?.censor()}, preExistingRegistrationData=${preExistingRegistrationData?.let { "present" }}, doNotAttemptRecoveryPassword=$doNotAttemptRecoveryPassword, pendingRestoreOption=$pendingRestoreOption, unverifiedRestoredAep=${unverifiedRestoredAep?.displayValue?.censor()}, restoreMethodToken=${restoreMethodToken?.censor()}, isRestoringNavigation=$isRestoringNavigationState)"
return "RegistrationFlowState(backStack=${backStack.joinToString()}, sessionMetadata=${sessionMetadata.let { "present" }}, sessionE164=$sessionE164, accountEntropyPool=${accountEntropyPool?.displayValue?.censor()}, storageCapable=$storageCapable, temporaryMasterKey=${temporaryMasterKey?.toString()?.censor()}, preExistingRegistrationData=${preExistingRegistrationData?.let { "present" }}, doNotAttemptRecoveryPassword=$doNotAttemptRecoveryPassword, pendingRestoreOption=$pendingRestoreOption, unverifiedRestoredAep=${unverifiedRestoredAep?.displayValue?.censor()}, restoreMethodToken=${restoreMethodToken?.censor()}, isRestoringNavigation=$isRestoringNavigationState)"
}
}

View File

@ -70,7 +70,7 @@ class RegistrationViewModel(private val repository: RegistrationRepository, save
is RegistrationFlowEvent.ResetState -> RegistrationFlowState(isRestoringNavigationState = false)
is RegistrationFlowEvent.SessionUpdated -> state.copy(sessionMetadata = event.session)
is RegistrationFlowEvent.E164Chosen -> state.copy(sessionE164 = event.e164)
is RegistrationFlowEvent.Registered -> state.copy(accountEntropyPool = event.accountEntropyPool)
is RegistrationFlowEvent.Registered -> state.copy(accountEntropyPool = event.accountEntropyPool, storageCapable = event.storageCapable)
is RegistrationFlowEvent.MasterKeyRestoredFromSvr -> state.copy(temporaryMasterKey = event.masterKey)
is RegistrationFlowEvent.NavigateToScreen -> applyNavigationToScreenEvent(state, event)
is RegistrationFlowEvent.NavigateBack -> {

View File

@ -71,10 +71,10 @@ class EnterAepForRemoteBackupPreRegistrationViewModel(
when (val result = repository.registerAccountWithRecoveryPassword(e164, recoveryPassword, existingAccountEntropyPool = aep)) {
is RequestResult.Success -> {
Log.i(TAG, "[Submit] Successfully registered using RRP from user-supplied AEP.")
val (_, keyMaterial) = result.result
val (response, keyMaterial) = result.result
stateEmitter(inputState.copy(isRegistering = false))
parentEventEmitter(RegistrationFlowEvent.Registered(keyMaterial.accountEntropyPool))
parentEventEmitter(RegistrationFlowEvent.Registered(keyMaterial.accountEntropyPool, response.storageCapable))
parentEventEmitter.navigateTo(RegistrationRoute.RemoteRestore(aep))
}
is RequestResult.NonSuccess -> {

View File

@ -234,7 +234,7 @@ class PhoneNumberEntryViewModel(
Log.i(TAG, "[Register] Successfully re-registered using RRP from pre-existing data.")
val (response, keyMaterial) = registerResult.result
parentEventEmitter(RegistrationFlowEvent.Registered(keyMaterial.accountEntropyPool))
parentEventEmitter(RegistrationFlowEvent.Registered(keyMaterial.accountEntropyPool, response.storageCapable))
if (response.storageCapable) {
parentEventEmitter.navigateTo(RegistrationRoute.PinEntryForSvrRestore)
@ -320,7 +320,7 @@ class PhoneNumberEntryViewModel(
Log.i(TAG, "[LocalRestore] Successfully registered using RRP from restored AEP.")
val (response, keyMaterial) = result.result
parentEventEmitter(RegistrationFlowEvent.Registered(keyMaterial.accountEntropyPool))
parentEventEmitter(RegistrationFlowEvent.Registered(keyMaterial.accountEntropyPool, response.storageCapable))
if (response.storageCapable) {
parentEventEmitter.navigateTo(RegistrationRoute.PinEntryForSvrRestore)

View File

@ -134,7 +134,7 @@ class PinEntryForRegistrationLockViewModel(
is RequestResult.Success -> {
Log.i(TAG, "[PinEntered] Successfully registered!")
val (response, keyMaterial) = registerResult.result
parentEventEmitter(RegistrationFlowEvent.Registered(keyMaterial.accountEntropyPool))
parentEventEmitter(RegistrationFlowEvent.Registered(keyMaterial.accountEntropyPool, response.storageCapable))
when {
response.reregistration -> parentEventEmitter.navigateTo(RegistrationRoute.ArchiveRestoreSelection.forPostRegister())
else -> repository.finishRegistrationOrCreateProfile(parentEventEmitter)

View File

@ -112,7 +112,7 @@ class QuickRestoreQrViewModel(
is RequestResult.Success -> {
val (response, keyMaterial) = registerResult.result
Log.i(TAG, "[Register] Success! reregistration: ${response.reregistration}")
parentEventEmitter(RegistrationFlowEvent.Registered(keyMaterial.accountEntropyPool))
parentEventEmitter(RegistrationFlowEvent.Registered(keyMaterial.accountEntropyPool, response.storageCapable))
parentEventEmitter.navigateTo(RegistrationRoute.ArchiveRestoreSelection.forQuickRestore(hasRemoteBackup = message.tier != null))
}
is RequestResult.NonSuccess -> {

View File

@ -11,9 +11,11 @@ data class ArchiveRestoreSelectionState(
val restoreOptions: List<ArchiveRestoreOption> = emptyList(),
val showSkipWarningDialog: Boolean = false,
/** Token that, if present, indicates that the user did a quick restore, and we should hit a network endpoint to indicate our restore selection. */
val restoreMethodToken: String? = null
val restoreMethodToken: String? = null,
/** Whether the account already has SVR/PIN data on the server. Determines whether skipping restore leads to PIN entry or PIN creation. */
val storageCapable: Boolean = false
) {
override fun toString(): String = "ArchiveRestoreSelectionState(restoreOptions=$restoreOptions, showSkipWarningDialog=$showSkipWarningDialog, restoreMethodToken=${restoreMethodToken?.censor()})"
override fun toString(): String = "ArchiveRestoreSelectionState(restoreOptions=$restoreOptions, showSkipWarningDialog=$showSkipWarningDialog, restoreMethodToken=${restoreMethodToken?.censor()}, storageCapable=$storageCapable)"
val showSkipButton: Boolean get() = ArchiveRestoreOption.None !in restoreOptions
}

View File

@ -64,7 +64,7 @@ class ArchiveRestoreSelectionViewModel(
@VisibleForTesting
fun applyParentState(state: ArchiveRestoreSelectionState, parentState: RegistrationFlowState): ArchiveRestoreSelectionState {
return state.copy(restoreMethodToken = parentState.restoreMethodToken)
return state.copy(restoreMethodToken = parentState.restoreMethodToken, storageCapable = parentState.storageCapable)
}
@VisibleForTesting
@ -108,7 +108,13 @@ class ArchiveRestoreSelectionViewModel(
is ArchiveRestoreSelectionScreenEvents.ConfirmSkip -> {
notifyOldDevice(state.restoreMethodToken, NetworkController.RestoreMethod.DECLINE)
repository.setRestoreDecision(RestoreDecision.SKIPPED)
parentEventEmitter.navigateTo(RegistrationRoute.PinCreate)
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)
}
is ArchiveRestoreSelectionScreenEvents.DismissSkipWarning -> {

View File

@ -173,7 +173,7 @@ class VerificationCodeViewModel(
is RequestResult.Success -> {
val (response, keyMaterial) = registerResult.result
parentEventEmitter(RegistrationFlowEvent.Registered(keyMaterial.accountEntropyPool))
parentEventEmitter(RegistrationFlowEvent.Registered(keyMaterial.accountEntropyPool, response.storageCapable))
when {
response.reregistration -> parentEventEmitter.navigateTo(RegistrationRoute.ArchiveRestoreSelection.forPostRegister())

View File

@ -174,6 +174,7 @@ class PersistedFlowStateTest {
sessionMetadata = session,
sessionE164 = "+15551234567",
accountEntropyPool = AccountEntropyPool.generate(),
storageCapable = true,
temporaryMasterKey = MasterKey(ByteArray(32)),
doNotAttemptRecoveryPassword = true
)
@ -184,6 +185,7 @@ class PersistedFlowStateTest {
assertThat(persisted.sessionMetadata).isEqualTo(session)
assertThat(persisted.sessionE164).isEqualTo("+15551234567")
assertThat(persisted.doNotAttemptRecoveryPassword).isEqualTo(true)
assertThat(persisted.storageCapable).isEqualTo(true)
}
@Test
@ -202,7 +204,8 @@ class PersistedFlowStateTest {
backStack = listOf(RegistrationRoute.Welcome, RegistrationRoute.PinCreate),
sessionMetadata = session,
sessionE164 = "+15551234567",
doNotAttemptRecoveryPassword = true
doNotAttemptRecoveryPassword = true,
storageCapable = true
)
val aep = AccountEntropyPool.generate()
@ -221,5 +224,6 @@ class PersistedFlowStateTest {
assertThat(flowState.temporaryMasterKey).isEqualTo(masterKey)
assertThat(flowState.preExistingRegistrationData).isNull()
assertThat(flowState.doNotAttemptRecoveryPassword).isEqualTo(true)
assertThat(flowState.storageCapable).isEqualTo(true)
}
}

View File

@ -236,7 +236,7 @@ class RegistrationViewModelTest {
val viewModel = RegistrationViewModel(mockRepository, SavedStateHandle())
advanceUntilIdle()
viewModel.onEvent(RegistrationFlowEvent.Registered(AccountEntropyPool.generate()))
viewModel.onEvent(RegistrationFlowEvent.Registered(AccountEntropyPool.generate(), storageCapable = false))
advanceUntilIdle()
coVerify(exactly = 0) { mockRepository.saveFlowState(any()) }
@ -380,10 +380,11 @@ class RegistrationViewModelTest {
val result = viewModel.applyEvent(
RegistrationFlowState(),
RegistrationFlowEvent.Registered(aep)
RegistrationFlowEvent.Registered(aep, storageCapable = true)
)
assertThat(result.accountEntropyPool).isEqualTo(aep)
assertThat(result.storageCapable).isTrue()
}
@Test

View File

@ -190,9 +190,9 @@ class ArchiveRestoreSelectionViewModelTest {
// ==================== ConfirmSkip Tests ====================
@Test
fun `ConfirmSkip navigates to PinCreate and clears dialog`() = runTest {
fun `ConfirmSkip when not storage capable navigates to PinCreate and clears dialog`() = runTest {
val viewModel = createViewModel(isPreRegistration = false)
val initialState = ArchiveRestoreSelectionState(showSkipWarningDialog = true)
val initialState = ArchiveRestoreSelectionState(showSkipWarningDialog = true, storageCapable = false)
viewModel.applyEvent(initialState, ArchiveRestoreSelectionScreenEvents.ConfirmSkip, stateEmitter)
@ -205,6 +205,22 @@ class ArchiveRestoreSelectionViewModelTest {
assertThat(emittedStates.last().showSkipWarningDialog).isFalse()
}
@Test
fun `ConfirmSkip when storage capable navigates to PinEntryForSvrRestore and clears dialog`() = runTest {
val viewModel = createViewModel(isPreRegistration = false)
val initialState = ArchiveRestoreSelectionState(showSkipWarningDialog = true, storageCapable = true)
viewModel.applyEvent(initialState, ArchiveRestoreSelectionScreenEvents.ConfirmSkip, stateEmitter)
coVerify { mockRepository.setRestoreDecision(RestoreDecision.SKIPPED) }
assertThat(emittedParentEvents).hasSize(1)
assertThat(emittedParentEvents.first())
.isInstanceOf<RegistrationFlowEvent.NavigateToScreen>()
.prop(RegistrationFlowEvent.NavigateToScreen::route)
.isEqualTo(RegistrationRoute.PinEntryForSvrRestore)
assertThat(emittedStates.last().showSkipWarningDialog).isFalse()
}
// ==================== DismissSkipWarning Tests ====================
@Test
@ -249,4 +265,13 @@ class ArchiveRestoreSelectionViewModelTest {
assertThat(viewModel.state.value.showSkipButton).isTrue()
}
@Test
fun `applyParentState copies storageCapable from parent`() = runTest {
val viewModel = createViewModel()
val result = viewModel.applyParentState(ArchiveRestoreSelectionState(), RegistrationFlowState(storageCapable = true))
assertThat(result.storageCapable).isTrue()
}
}