Reset regV5 flow state when re-registering.

This commit is contained in:
Greyson Parrelli
2026-07-07 18:04:26 +00:00
parent 46006cbfde
commit f5e48a3ff9
5 changed files with 81 additions and 7 deletions
@@ -97,7 +97,7 @@ class RegistrationActivity : BaseActivity() {
@JvmStatic
fun newIntentForReRegistration(context: Context): Intent {
return if (Environment.USE_NEW_REGISTRATION) {
org.signal.registration.RegistrationActivity.createIntent(context, nextIntent = MainActivity.clearTop(context))
org.signal.registration.RegistrationActivity.createIntent(context, nextIntent = MainActivity.clearTop(context), startFresh = true)
} else {
Intent(context, RegistrationActivity::class.java).apply {
putExtra(RE_REGISTRATION_EXTRA, true)
@@ -27,6 +27,7 @@ class RegistrationActivity : ComponentActivity() {
companion object {
private const val NEXT_INTENT_EXTRA = "next_intent"
private const val START_DESTINATION_EXTRA = "start_destination"
private const val START_FRESH_EXTRA = "start_fresh"
/**
* @param nextIntent An optional intent to launch once registration completes successfully. This is how the caller
@@ -34,10 +35,12 @@ class RegistrationActivity : ComponentActivity() {
* typically have finished itself.
* @param startDestination An optional route to open directly instead of resuming a previous flow. Used, for example,
* to send a deregistered linked device straight to the link-device screen.
* @param startFresh When true, any persisted registration data is not restored and the user starts the flow fresh
* from the beginning.
*/
@JvmStatic
@JvmOverloads
fun createIntent(context: Context, nextIntent: Intent? = null, startDestination: RegistrationRoute? = null): Intent {
fun createIntent(context: Context, nextIntent: Intent? = null, startDestination: RegistrationRoute? = null, startFresh: Boolean = false): Intent {
return Intent(context, RegistrationActivity::class.java).apply {
if (nextIntent != null) {
putExtra(NEXT_INTENT_EXTRA, nextIntent)
@@ -45,6 +48,7 @@ class RegistrationActivity : ComponentActivity() {
if (startDestination != null) {
putExtra(START_DESTINATION_EXTRA, startDestination)
}
putExtra(START_FRESH_EXTRA, startFresh)
}
}
}
@@ -64,6 +68,7 @@ class RegistrationActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
val startDestination = IntentCompat.getParcelableExtra(intent, START_DESTINATION_EXTRA, RegistrationRoute::class.java)
val startFresh = intent.getBooleanExtra(START_FRESH_EXTRA, false)
setContent {
SignalTheme(incognitoKeyboardEnabled = false) {
@@ -71,6 +76,7 @@ class RegistrationActivity : ComponentActivity() {
RegistrationNavHost(
registrationRepository = repository,
startDestination = startDestination,
startFresh = startFresh,
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding(),
@@ -261,6 +261,8 @@ private const val PIN_LEARN_MORE_URL = "https://support.signal.org/hc/articles/3
*
* @param registrationRepository The repository for registration data.
* @param registrationViewModel Optional ViewModel for testing. If null, creates one internally.
* @param startFresh When true, any persisted registration data is not restored and the user starts the flow fresh from
* the beginning.
* @param permissionsState Optional permissions state for testing. If null, creates one internally.
* @param startDestination Optional route to open directly as the sole start destination, instead of showing [RegistrationRoute.Welcome] or restoring a previous flow.
* @param modifier Modifier to be applied to the NavDisplay.
@@ -271,13 +273,14 @@ private const val PIN_LEARN_MORE_URL = "https://support.signal.org/hc/articles/3
fun RegistrationNavHost(
registrationRepository: RegistrationRepository,
registrationViewModel: RegistrationViewModel? = null,
startFresh: Boolean = false,
permissionsState: MultiplePermissionsState? = null,
startDestination: RegistrationRoute? = null,
modifier: Modifier = Modifier,
onRegistrationComplete: () -> Unit = {}
) {
val viewModel: RegistrationViewModel = registrationViewModel ?: viewModel(
factory = RegistrationViewModel.Factory(registrationRepository, startDestination)
factory = RegistrationViewModel.Factory(registrationRepository, startDestination, startFresh)
)
val registrationState by viewModel.state.collectAsStateWithLifecycle()
@@ -32,12 +32,20 @@ import kotlin.reflect.KClass
*/
class RegistrationViewModel(
private val repository: RegistrationRepository,
savedStateHandle: SavedStateHandle,
startDestination: RegistrationRoute? = null
private val savedStateHandle: SavedStateHandle,
startDestination: RegistrationRoute? = null,
private val startFresh: Boolean = false
) : EventDrivenViewModel<RegistrationFlowEvent>(TAG) {
companion object {
private val TAG = Log.tag(RegistrationViewModel::class)
/**
* Marks that a start-fresh reset has already been performed for this ViewModel. Persisted in the
* [SavedStateHandle] so that a process death mid-flow (which re-delivers the start-fresh intent) does not
* wipe the fresh in-progress data the user has since built up.
*/
private const val RESET_PERFORMED_KEY = "start_fresh_reset_performed"
}
private var _state: MutableStateFlow<RegistrationFlowState> = savedStateHandle.getMutableStateFlow(
@@ -57,6 +65,17 @@ class RegistrationViewModel(
} else {
_state.value = _state.value.copy(isRestoringNavigationState = true)
viewModelScope.launch {
if (startFresh && savedStateHandle.get<Boolean>(RESET_PERFORMED_KEY) != true) {
Log.i(TAG, "[init] Start-fresh requested. Clearing any persisted in-progress registration data so the user starts fresh.")
repository.clearInProgressRegistrationData()
savedStateHandle[RESET_PERFORMED_KEY] = true
_state.value = RegistrationFlowState(
preExistingRegistrationData = repository.getPreExistingRegistrationData(),
isRestoringNavigationState = false
)
return@launch
}
val restored = repository.restoreFlowState()
if (restored != null) {
Log.i(TAG, "[init] Restored flow state from disk. Backstack size: ${restored.backStack.size}, hasSession: ${restored.sessionMetadata != null}")
@@ -214,9 +233,9 @@ class RegistrationViewModel(
}
}
class Factory(private val repository: RegistrationRepository, private val startDestination: RegistrationRoute? = null) : ViewModelProvider.Factory {
class Factory(private val repository: RegistrationRepository, private val startDestination: RegistrationRoute? = null, private val startFresh: Boolean = false) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: KClass<T>, extras: CreationExtras): T {
return RegistrationViewModel(repository, extras.createSavedStateHandle(), startDestination) as T
return RegistrationViewModel(repository, extras.createSavedStateHandle(), startDestination, startFresh) as T
}
}
}
@@ -184,6 +184,52 @@ class RegistrationViewModelTest {
assertThat(viewModel.state.value.isRestoringNavigationState).isEqualTo(false)
}
@Test
fun `start fresh clears in-progress data and starts fresh from Welcome`() = runTest(testDispatcher) {
val preExisting = mockk<PreExistingRegistrationData>(relaxed = true)
val savedState = RegistrationFlowState(
backStack = listOf(
RegistrationRoute.Welcome,
RegistrationRoute.Permissions(nextRoute = RegistrationRoute.PhoneNumberEntry),
RegistrationRoute.PhoneNumberEntry,
RegistrationRoute.VerificationCodeEntry
),
sessionMetadata = createSessionMetadata("stale-session")
)
coEvery { mockRepository.restoreFlowState() } returns savedState
coEvery { mockRepository.getPreExistingRegistrationData() } returns preExisting
val viewModel = RegistrationViewModel(mockRepository, SavedStateHandle(), startFresh = true)
advanceUntilIdle()
val state = viewModel.state.value
assertThat(state.backStack).isEqualTo(listOf(RegistrationRoute.Welcome))
assertThat(state.sessionMetadata).isNull()
assertThat(state.preExistingRegistrationData).isEqualTo(preExisting)
assertThat(state.isRestoringNavigationState).isEqualTo(false)
coVerify { mockRepository.clearInProgressRegistrationData() }
coVerify(exactly = 0) { mockRepository.restoreFlowState() }
}
@Test
fun `start fresh does not clear again once reset has been performed`() = runTest(testDispatcher) {
val savedState = RegistrationFlowState(
backStack = listOf(RegistrationRoute.Welcome, RegistrationRoute.PhoneNumberEntry),
sessionMetadata = null
)
coEvery { mockRepository.restoreFlowState() } returns savedState
val savedStateHandle = SavedStateHandle(mapOf("start_fresh_reset_performed" to true))
val viewModel = RegistrationViewModel(mockRepository, savedStateHandle, startFresh = true)
advanceUntilIdle()
assertThat(viewModel.state.value.backStack).isEqualTo(savedState.backStack)
coVerify(exactly = 0) { mockRepository.clearInProgressRegistrationData() }
coVerify { mockRepository.restoreFlowState() }
}
// ==================== Persistence Side-Effect Tests ====================
@Test