Add screen lock protection to delete account flow.

This commit is contained in:
Greyson Parrelli
2026-09-18 12:46:36 -04:00
parent dcb6db177f
commit 8134c3b64f
6 changed files with 52 additions and 3 deletions
@@ -78,7 +78,13 @@ class AccountSettingsFragment : ComposeFragment() {
onAuthenticationFailed = { viewModel.onEvent(AccountSettingsEvent.AuthenticationFailed) }
)
CollectActions(viewModel.actions) { action -> handleAction(action, removalBiometrics, signalLoginBiometrics) }
val deleteAccountBiometrics = rememberBiometricsAuthentication(
promptTitle = stringResource(AppSettingsR.string.AccountSettingsFragment__unlock_to_confirm_its_you),
educationSheetMessage = stringResource(AppSettingsR.string.AccountSettingsFragment__to_delete_your_account_confirm_its_you),
onAuthenticationFailed = { viewModel.onEvent(AccountSettingsEvent.AuthenticationFailed) }
)
CollectActions(viewModel.actions) { action -> handleAction(action, removalBiometrics, signalLoginBiometrics, deleteAccountBiometrics) }
AccountSettingsScreen(
state = state,
@@ -86,7 +92,12 @@ class AccountSettingsFragment : ComposeFragment() {
)
}
private fun handleAction(action: AccountSettingsAction, removalBiometrics: BiometricsAuthentication, signalLoginBiometrics: BiometricsAuthentication) {
private fun handleAction(
action: AccountSettingsAction,
removalBiometrics: BiometricsAuthentication,
signalLoginBiometrics: BiometricsAuthentication,
deleteAccountBiometrics: BiometricsAuthentication
) {
when (action) {
AccountSettingsAction.NavigateBack -> requireActivity().onBackPressedDispatcher.onBackPressed()
AccountSettingsAction.LaunchCreatePinFlow -> pinFlowLauncher.launch(CreateSvrPinActivity.getIntentForPinCreate(requireContext()))
@@ -118,6 +129,11 @@ class AccountSettingsFragment : ComposeFragment() {
AccountSettingsAction.NavigateToChangePhoneNumber -> findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_changePhoneNumberFragment)
AccountSettingsAction.NavigateToDeviceTransfer -> findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_oldDeviceTransferActivity)
AccountSettingsAction.NavigateToExportAccountData -> findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_exportAccountFragment)
AccountSettingsAction.AuthenticateToDeleteAccount -> {
deleteAccountBiometrics.withBiometricsAuthentication {
viewModel.onEvent(AccountSettingsEvent.DeleteAccountAuthenticated)
}
}
AccountSettingsAction.NavigateToDeleteAccount -> findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_deleteAccountFragment)
AccountSettingsAction.OpenPlayStore -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext())
AccountSettingsAction.LaunchReRegistration -> startActivity(RegistrationActivity.newIntentForReRegistration(requireContext()))
@@ -138,6 +138,9 @@ class AccountSettingsViewModel(
_actions.send(AccountSettingsAction.ShowDataWipeFailed)
}
AccountSettingsEvent.DeleteAccountClicked -> {
_actions.send(AccountSettingsAction.AuthenticateToDeleteAccount)
}
AccountSettingsEvent.DeleteAccountAuthenticated -> {
_actions.send(AccountSettingsAction.NavigateToDeleteAccount)
}
AccountSettingsEvent.DialogDismissed -> {
@@ -591,6 +591,26 @@ class AccountSettingsViewModelTest {
assertThat(actions.last()).isEqualTo(AccountSettingsAction.NavigateToSignalLoginDetails)
}
@Test
fun `DeleteAccountClicked asks for the screen lock first`() = runTest(testDispatcher) {
val viewModel = createViewModel()
val actions = collectActions(viewModel.actions)
viewModel.onEvent(AccountSettingsEvent.DeleteAccountClicked)
assertThat(actions.last()).isEqualTo(AccountSettingsAction.AuthenticateToDeleteAccount)
}
@Test
fun `DeleteAccountAuthenticated opens the delete account screen`() = runTest(testDispatcher) {
val viewModel = createViewModel()
val actions = collectActions(viewModel.actions)
viewModel.onEvent(AccountSettingsEvent.DeleteAccountAuthenticated)
assertThat(actions.last()).isEqualTo(AccountSettingsAction.NavigateToDeleteAccount)
}
private fun methods(vararg methods: TwoFactorMethod) = AccountSettingsRepository.TwoFactorMethodsResult.Success(methods.toList())
private fun createViewModel(): AccountSettingsViewModel = AccountSettingsViewModel(repository)
@@ -72,6 +72,9 @@ sealed interface AccountSettingsAction {
/** Open registration so the user can re-register. */
data object LaunchReRegistration : AccountSettingsAction
/** Ask the user to get past their screen lock before we send them into the delete account flow. */
data object AuthenticateToDeleteAccount : AccountSettingsAction
/** Open the delete account flow. */
data object NavigateToDeleteAccount : AccountSettingsAction
@@ -96,9 +96,12 @@ sealed interface AccountSettingsEvent {
/** The fragment reported that clearing application data failed. */
data object DataWipeFailed : AccountSettingsEvent
/** The user tapped the delete account row. */
/** The user tapped the delete account row, which asks for the screen lock first. */
data object DeleteAccountClicked : AccountSettingsEvent
/** The user got past their screen lock, so we can send them into the delete account flow. */
data object DeleteAccountAuthenticated : AccountSettingsEvent
/** Dismisses whatever is in [AccountSettingsState.dialog]. */
data object DialogDismissed : AccountSettingsEvent
}
@@ -80,6 +80,10 @@
<string name="AccountSettingsFragment__unlock_to_view_signal_login">Unlock to view Signal Login</string>
<!-- Title of the sheet explaining that the device screen lock is needed before the Signal Login details can be viewed -->
<string name="AccountSettingsFragment__to_view_your_signal_login_confirm_its_you">To view your Signal Login, confirm it\'s you</string>
<!-- Title of the device screen lock prompt shown before the delete account flow can be opened -->
<string name="AccountSettingsFragment__unlock_to_confirm_its_you">Unlock to confirm it\'s you</string>
<!-- Title of the sheet explaining that the device screen lock is needed before the delete account flow can be opened -->
<string name="AccountSettingsFragment__to_delete_your_account_confirm_its_you">To delete your account, confirm it\'s you</string>
<!-- Toast shown when the device screen lock wasn\'t confirmed, so nothing happened -->
<string name="AccountSettingsFragment__authentication_required">Authentication required</string>
<!-- Title of the dialog shown when the account already has as many authenticator apps as it\'s allowed -->