From f5e48a3ff9288da811cd638ff0b01acd7721249e Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 7 Jul 2026 18:04:26 +0000 Subject: [PATCH] Reset regV5 flow state when re-registering. --- .../registration/ui/RegistrationActivity.kt | 2 +- .../registration/RegistrationActivity.kt | 8 +++- .../registration/RegistrationNavigation.kt | 5 +- .../registration/RegistrationViewModel.kt | 27 +++++++++-- .../registration/RegistrationViewModelTest.kt | 46 +++++++++++++++++++ 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationActivity.kt index b8cebdf283..ac88d85b7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationActivity.kt @@ -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) diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationActivity.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationActivity.kt index 7ccea399da..b0edaf2048 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationActivity.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationActivity.kt @@ -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(), 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 c860e5a81c..0acf236c02 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt @@ -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() 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 e0b85ea056..7eab6f4e10 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt @@ -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(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 = savedStateHandle.getMutableStateFlow( @@ -57,6 +65,17 @@ class RegistrationViewModel( } else { _state.value = _state.value.copy(isRestoringNavigationState = true) viewModelScope.launch { + if (startFresh && savedStateHandle.get(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 create(modelClass: KClass, extras: CreationExtras): T { - return RegistrationViewModel(repository, extras.createSavedStateHandle(), startDestination) as T + return RegistrationViewModel(repository, extras.createSavedStateHandle(), startDestination, startFresh) as T } } } 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 3112c45a93..c550fb55cc 100644 --- a/feature/registration/src/test/java/org/signal/registration/RegistrationViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/RegistrationViewModelTest.kt @@ -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(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