diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt index da3699c2dd..5b4027c7df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt @@ -73,10 +73,16 @@ class AccountSettingsFragment : ComposeFragment() { val removalBiometrics = rememberBiometricsAuthentication( promptTitle = stringResource(AppSettingsR.string.AccountSettingsFragment__unlock_to_remove_two_factor_method), educationSheetMessage = stringResource(AppSettingsR.string.AccountSettingsFragment__to_remove_this_method_confirm_its_you), - onAuthenticationFailed = { viewModel.onEvent(AccountSettingsEvent.MethodRemovalAuthenticationFailed) } + onAuthenticationFailed = { viewModel.onEvent(AccountSettingsEvent.AuthenticationFailed) } ) - CollectActions(viewModel.actions) { action -> handleAction(action, removalBiometrics) } + val signalLoginBiometrics = rememberBiometricsAuthentication( + promptTitle = stringResource(AppSettingsR.string.AccountSettingsFragment__unlock_to_view_signal_login), + educationSheetMessage = stringResource(AppSettingsR.string.AccountSettingsFragment__to_view_your_signal_login_confirm_its_you), + onAuthenticationFailed = { viewModel.onEvent(AccountSettingsEvent.AuthenticationFailed) } + ) + + CollectActions(viewModel.actions) { action -> handleAction(action, removalBiometrics, signalLoginBiometrics) } AccountSettingsScreen( state = state, @@ -84,12 +90,17 @@ class AccountSettingsFragment : ComposeFragment() { ) } - private fun handleAction(action: AccountSettingsAction, removalBiometrics: BiometricsAuthentication) { + private fun handleAction(action: AccountSettingsAction, removalBiometrics: BiometricsAuthentication, signalLoginBiometrics: BiometricsAuthentication) { when (action) { AccountSettingsAction.NavigateBack -> requireActivity().onBackPressedDispatcher.onBackPressed() AccountSettingsAction.LaunchCreatePinFlow -> pinFlowLauncher.launch(CreateSvrPinActivity.getIntentForPinCreate(requireContext())) AccountSettingsAction.LaunchChangePinFlow -> pinFlowLauncher.launch(CreateSvrPinActivity.getIntentForPinChangeFromSettings(requireContext())) AccountSettingsAction.ShowPinCreatedConfirmation -> Snackbar.make(requireView(), R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).show() + AccountSettingsAction.AuthenticateToViewSignalLoginDetails -> { + signalLoginBiometrics.withBiometricsAuthentication { + viewModel.onEvent(AccountSettingsEvent.SignalLoginDetailsAuthenticated) + } + } AccountSettingsAction.NavigateToSignalLoginDetails -> findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_signalLoginViewDetailsFragment) AccountSettingsAction.NavigateToTotpSetup -> findNavController().safeNavigate(R.id.action_accountSettingsFragment_to_authenticatorSetupFragment) is AccountSettingsAction.NavigateToRenameTotpApp -> { @@ -103,7 +114,7 @@ class AccountSettingsFragment : ComposeFragment() { viewModel.onEvent(AccountSettingsEvent.MethodRemovalAuthenticated(action.method)) } } - AccountSettingsAction.ShowRemovalAuthenticationFailed -> toast(AppSettingsR.string.AccountSettingsFragment__authentication_required) + AccountSettingsAction.ShowAuthenticationFailed -> toast(AppSettingsR.string.AccountSettingsFragment__authentication_required) AccountSettingsAction.ShowTotpAppRemoved -> toast(AppSettingsR.string.AccountSettingsFragment__authenticator_app_removed) AccountSettingsAction.ShowTotpAppRemovalFailed -> toast(AppSettingsR.string.AccountSettingsFragment__couldnt_remove_authenticator_app) // TODO Open the two-factor authentication support article once one exists. diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModel.kt index 810b995b16..55e6a85181 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModel.kt @@ -83,6 +83,9 @@ class AccountSettingsViewModel( applyRegistrationLockConfirmed() } AccountSettingsEvent.AccountAndRecoveryClicked -> { + _actions.send(AccountSettingsAction.AuthenticateToViewSignalLoginDetails) + } + AccountSettingsEvent.SignalLoginDetailsAuthenticated -> { _actions.send(AccountSettingsAction.NavigateToSignalLoginDetails) } AccountSettingsEvent.AddTotpAppClicked -> { @@ -100,8 +103,8 @@ class AccountSettingsViewModel( is AccountSettingsEvent.MethodRemovalAuthenticated -> { applyMethodRemovalAuthenticated(event.method) } - AccountSettingsEvent.MethodRemovalAuthenticationFailed -> { - _actions.send(AccountSettingsAction.ShowRemovalAuthenticationFailed) + AccountSettingsEvent.AuthenticationFailed -> { + _actions.send(AccountSettingsAction.ShowAuthenticationFailed) } AccountSettingsEvent.RemoveTotpAppConfirmed -> { applyRemoveTotpAppConfirmed() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/signallogin/SignalLoginViewDetailsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/signallogin/SignalLoginViewDetailsFragment.kt index 4728c5aa3e..1dadc668b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/signallogin/SignalLoginViewDetailsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/signallogin/SignalLoginViewDetailsFragment.kt @@ -21,6 +21,7 @@ import org.signal.core.util.Util import org.signal.passwordmanager.SignalCredentialManager import org.signal.signallogin.pdf.SignalLoginPdfRenderer import org.signal.signallogin.viewdetails.SignalLoginViewDetailsScreen +import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity /** * Shows the account and recovery keys that make up the user's Signal Login, the same way registration does. @@ -49,6 +50,8 @@ class SignalLoginViewDetailsFragment : ComposeFragment() { override fun FragmentContent() { val state by viewModel.state.collectAsStateWithLifecycle() + TemporaryScreenshotSecurity.bind() + CollectActions(viewModel.actions) { action -> handleAction(action) } SignalLoginViewDetailsScreen( diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModelTest.kt index 1dc8751d25..2bb33be7ff 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsViewModelTest.kt @@ -444,7 +444,7 @@ class AccountSettingsViewModelTest { } @Test - fun `MethodRemovalAuthenticationFailed says so and removes nothing`() = runTest(testDispatcher) { + fun `AuthenticationFailed says so and removes nothing`() = runTest(testDispatcher) { every { repository.isPhoneNumberless() } returns true coEvery { repository.getTwoFactorMethods() } returns methods(TOTP_APP) @@ -452,9 +452,9 @@ class AccountSettingsViewModelTest { val actions = collectActions(viewModel.actions) viewModel.onEvent(AccountSettingsEvent.RemoveMethodClicked(TOTP_APP)) - viewModel.onEvent(AccountSettingsEvent.MethodRemovalAuthenticationFailed) + viewModel.onEvent(AccountSettingsEvent.AuthenticationFailed) - assertThat(actions.last()).isEqualTo(AccountSettingsAction.ShowRemovalAuthenticationFailed) + assertThat(actions.last()).isEqualTo(AccountSettingsAction.ShowAuthenticationFailed) assertThat(viewModel.state.value.dialog).isEqualTo(Dialog.None) coVerify(exactly = 0) { repository.removeTotpApp(any()) } } @@ -546,7 +546,7 @@ class AccountSettingsViewModelTest { } @Test - fun `AccountAndRecoveryClicked opens the Signal Login details screen`() = runTest(testDispatcher) { + fun `AccountAndRecoveryClicked asks for the screen lock first`() = runTest(testDispatcher) { every { repository.isPhoneNumberless() } returns true val viewModel = createViewModel() @@ -554,6 +554,18 @@ class AccountSettingsViewModelTest { viewModel.onEvent(AccountSettingsEvent.AccountAndRecoveryClicked) + assertThat(actions.last()).isEqualTo(AccountSettingsAction.AuthenticateToViewSignalLoginDetails) + } + + @Test + fun `SignalLoginDetailsAuthenticated opens the Signal Login details screen`() = runTest(testDispatcher) { + every { repository.isPhoneNumberless() } returns true + + val viewModel = createViewModel() + val actions = collectActions(viewModel.actions) + + viewModel.onEvent(AccountSettingsEvent.SignalLoginDetailsAuthenticated) + assertThat(actions.last()).isEqualTo(AccountSettingsAction.NavigateToSignalLoginDetails) } diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsAction.kt b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsAction.kt index 071870966b..30c02439ea 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsAction.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsAction.kt @@ -27,6 +27,9 @@ sealed interface AccountSettingsAction { /** Tell the user their PIN was created. */ data object ShowPinCreatedConfirmation : AccountSettingsAction + /** Ask the user to get past their screen lock before we show them the account and recovery keys. */ + data object AuthenticateToViewSignalLoginDetails : AccountSettingsAction + /** Open the screen that shows the account and recovery keys that make up the Signal Login. */ data object NavigateToSignalLoginDetails : AccountSettingsAction @@ -39,8 +42,8 @@ sealed interface AccountSettingsAction { /** Ask the user to get past their screen lock before we remove [method] from the account. */ data class AuthenticateToRemoveMethod(val method: TwoFactorMethod) : AccountSettingsAction - /** Tell the user we couldn't confirm it was them, so nothing was removed. */ - data object ShowRemovalAuthenticationFailed : AccountSettingsAction + /** Tell the user we couldn't confirm it was them, so whatever they asked for didn't happen. */ + data object ShowAuthenticationFailed : AccountSettingsAction /** Tell the user their authenticator app was removed. */ data object ShowTotpAppRemoved : AccountSettingsAction diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsEvent.kt b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsEvent.kt index 9c3ef68841..800551f950 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsEvent.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/account/AccountSettingsEvent.kt @@ -16,9 +16,12 @@ sealed interface AccountSettingsEvent { /** The user tapped the navigation (back) icon. */ data object NavigateBackClicked : AccountSettingsEvent - /** The user tapped the Signal Login card, which shows the account and recovery keys. */ + /** The user tapped the Signal Login card, which asks for the screen lock before showing the account and recovery keys. */ data object AccountAndRecoveryClicked : AccountSettingsEvent + /** The user got past their screen lock, so we can show them the account and recovery keys. */ + data object SignalLoginDetailsAuthenticated : AccountSettingsEvent + /** The user tapped the row that either creates or changes their PIN. */ data object ModifyPinClicked : AccountSettingsEvent @@ -60,8 +63,8 @@ sealed interface AccountSettingsEvent { /** The user got past their screen lock, so we can go on asking them to confirm removing [method]. */ data class MethodRemovalAuthenticated(val method: TwoFactorMethod) : AccountSettingsEvent - /** The screen lock turned the user away, so [RemoveMethodClicked] goes no further. */ - data object MethodRemovalAuthenticationFailed : AccountSettingsEvent + /** The screen lock turned the user away, so whatever asked for it goes no further. */ + data object AuthenticationFailed : AccountSettingsEvent /** The user confirmed removing the authenticator app named by the open dialog, which removes it. */ data object RemoveTotpAppConfirmed : AccountSettingsEvent diff --git a/feature/app-settings/src/main/res/values/strings.xml b/feature/app-settings/src/main/res/values/strings.xml index 6314e00dab..49a0f5ae33 100644 --- a/feature/app-settings/src/main/res/values/strings.xml +++ b/feature/app-settings/src/main/res/values/strings.xml @@ -76,7 +76,11 @@ Unlock to remove two-factor method To remove this method, confirm it\'s you - + + Unlock to view Signal Login + + To view your Signal Login, confirm it\'s you + Authentication required Can\'t add authenticator app