mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-28 07:51:42 +01:00
Show disabled state if no password manager is available.
This commit is contained in:
+7
-2
@@ -20,7 +20,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -54,6 +53,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
import org.signal.signallogin.R as SignalLoginR
|
||||
|
||||
/**
|
||||
* Handles the selection, payment, and changing of a user's backup tier.
|
||||
@@ -83,7 +83,9 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
)
|
||||
}
|
||||
|
||||
private val signalLoginDetailsViewModel: MessageBackupsSignalLoginDetailsViewModel by viewModels()
|
||||
private val signalLoginDetailsViewModel: MessageBackupsSignalLoginDetailsViewModel by viewModel {
|
||||
MessageBackupsSignalLoginDetailsViewModel(isPasswordManagerAvailable = SignalCredentialManager.isSupported(requireContext()))
|
||||
}
|
||||
|
||||
private val savePdfLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument(PDF_MIME_TYPE)) { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
@@ -334,6 +336,9 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
)
|
||||
}
|
||||
}
|
||||
SignalLoginViewDetailsAction.ShowNoPasswordManagerAvailable -> {
|
||||
Toast.makeText(requireContext(), SignalLoginR.string.SignalLoginViewDetailsScreen__no_password_manager_available, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
SignalLoginViewDetailsAction.LaunchSaveAsPdf -> savePdfLauncher.launch(SignalLoginPdfRenderer.suggestedFileName(requireContext()))
|
||||
is SignalLoginViewDetailsAction.CopyTextToClipboard -> Util.copyToClipboardSensitive(requireContext(), action.text)
|
||||
}
|
||||
|
||||
+9
-3
@@ -24,7 +24,8 @@ import org.thoughtcrime.securesms.components.settings.app.account.signallogin.Si
|
||||
* saved. Resets belong to account settings, so this only ever emits [SignalLoginViewDetailsAction.Shared].
|
||||
*/
|
||||
class MessageBackupsSignalLoginDetailsViewModel(
|
||||
repository: SignalLoginViewDetailsRepository = SignalLoginViewDetailsRepository()
|
||||
repository: SignalLoginViewDetailsRepository = SignalLoginViewDetailsRepository(),
|
||||
isPasswordManagerAvailable: Boolean = true
|
||||
) : EventDrivenViewModel<SignalLoginViewDetailsScreenEvents>(TAG) {
|
||||
|
||||
companion object {
|
||||
@@ -34,7 +35,8 @@ class MessageBackupsSignalLoginDetailsViewModel(
|
||||
private val _state = MutableStateFlow(
|
||||
SignalLoginViewDetailsState(
|
||||
accountKey = repository.getAci()?.toString()?.uppercase().orEmpty(),
|
||||
recoveryKey = repository.getAccountEntropyPool()?.displayValue.orEmpty()
|
||||
recoveryKey = repository.getAccountEntropyPool()?.displayValue.orEmpty(),
|
||||
isPasswordManagerAvailable = isPasswordManagerAvailable
|
||||
)
|
||||
)
|
||||
private val _actions = Channel<SignalLoginViewDetailsAction.Shared>(Channel.BUFFERED)
|
||||
@@ -48,7 +50,11 @@ class MessageBackupsSignalLoginDetailsViewModel(
|
||||
_actions.send(SignalLoginViewDetailsAction.NavigateBack)
|
||||
}
|
||||
SignalLoginViewDetailsScreenEvents.SaveToPasswordManagerClicked -> {
|
||||
_actions.send(SignalLoginViewDetailsAction.LaunchSaveToPasswordManager)
|
||||
if (_state.value.isPasswordManagerAvailable) {
|
||||
_actions.send(SignalLoginViewDetailsAction.LaunchSaveToPasswordManager)
|
||||
} else {
|
||||
_actions.send(SignalLoginViewDetailsAction.ShowNoPasswordManagerAvailable)
|
||||
}
|
||||
}
|
||||
SignalLoginViewDetailsScreenEvents.SaveAsPdfClicked -> {
|
||||
_actions.send(SignalLoginViewDetailsAction.LaunchSaveAsPdf)
|
||||
|
||||
+10
-1
@@ -29,13 +29,19 @@ import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyDisplayFragment
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
import org.signal.signallogin.R as SignalLoginR
|
||||
|
||||
/**
|
||||
* Shows the account and recovery keys that make up the user's Signal Login, the same way registration does.
|
||||
*/
|
||||
class SettingsSignalLoginDetailsFragment : ComposeFragment() {
|
||||
|
||||
private val viewModel: SettingsSignalLoginDetailsViewModel by viewModel { SettingsSignalLoginDetailsViewModel(showResetRecoveryKeyButton = true) }
|
||||
private val viewModel: SettingsSignalLoginDetailsViewModel by viewModel {
|
||||
SettingsSignalLoginDetailsViewModel(
|
||||
showResetRecoveryKeyButton = true,
|
||||
isPasswordManagerAvailable = SignalCredentialManager.isSupported(requireContext())
|
||||
)
|
||||
}
|
||||
|
||||
private val savePdfLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("application/pdf")) { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
@@ -111,6 +117,9 @@ class SettingsSignalLoginDetailsFragment : ComposeFragment() {
|
||||
)
|
||||
}
|
||||
}
|
||||
SignalLoginViewDetailsAction.ShowNoPasswordManagerAvailable -> {
|
||||
Toast.makeText(requireContext(), SignalLoginR.string.SignalLoginViewDetailsScreen__no_password_manager_available, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
SignalLoginViewDetailsAction.LaunchSaveAsPdf -> savePdfLauncher.launch(SignalLoginPdfRenderer.suggestedFileName(requireContext()))
|
||||
SignalLoginViewDetailsAction.LaunchRecoveryKeyReset -> {
|
||||
findNavController().safeNavigate(
|
||||
|
||||
+10
-2
@@ -24,10 +24,13 @@ import org.signal.signallogin.viewdetails.SignalLoginViewDetailsState
|
||||
* account settings.
|
||||
*
|
||||
* @param showResetRecoveryKeyButton True if the option to reset the recovery key should be offered in the UI.
|
||||
* @param isPasswordManagerAvailable False if the device has no password manager, which leaves the save button showing
|
||||
* but styled as disabled.
|
||||
*/
|
||||
class SettingsSignalLoginDetailsViewModel(
|
||||
private val repository: SignalLoginViewDetailsRepository = SignalLoginViewDetailsRepository(),
|
||||
showResetRecoveryKeyButton: Boolean = false
|
||||
showResetRecoveryKeyButton: Boolean = false,
|
||||
isPasswordManagerAvailable: Boolean = true
|
||||
) : EventDrivenViewModel<SettingsSignalLoginDetailsEvent>(TAG) {
|
||||
|
||||
companion object {
|
||||
@@ -38,6 +41,7 @@ class SettingsSignalLoginDetailsViewModel(
|
||||
SignalLoginViewDetailsState(
|
||||
accountKey = repository.getAci()?.toString()?.uppercase().orEmpty(),
|
||||
recoveryKey = repository.getAccountEntropyPool()?.displayValue.orEmpty(),
|
||||
isPasswordManagerAvailable = isPasswordManagerAvailable,
|
||||
showResetRecoveryKeyButton = showResetRecoveryKeyButton,
|
||||
resetRecoveryKeyButtonLoading = showResetRecoveryKeyButton
|
||||
)
|
||||
@@ -100,7 +104,11 @@ class SettingsSignalLoginDetailsViewModel(
|
||||
_actions.send(SignalLoginViewDetailsAction.NavigateBack)
|
||||
}
|
||||
SignalLoginViewDetailsScreenEvents.SaveToPasswordManagerClicked -> {
|
||||
_actions.send(SignalLoginViewDetailsAction.LaunchSaveToPasswordManager)
|
||||
if (_state.value.isPasswordManagerAvailable) {
|
||||
_actions.send(SignalLoginViewDetailsAction.LaunchSaveToPasswordManager)
|
||||
} else {
|
||||
_actions.send(SignalLoginViewDetailsAction.ShowNoPasswordManagerAvailable)
|
||||
}
|
||||
}
|
||||
SignalLoginViewDetailsScreenEvents.SaveAsPdfClicked -> {
|
||||
_actions.send(SignalLoginViewDetailsAction.LaunchSaveAsPdf)
|
||||
|
||||
+3
@@ -27,6 +27,9 @@ sealed interface SignalLoginViewDetailsAction {
|
||||
/** Launch the system credential manager UI so the user can store the login in their password manager. */
|
||||
data object LaunchSaveToPasswordManager : Shared
|
||||
|
||||
/** Tell the user that there is no password manager on the device to save the login into. */
|
||||
data object ShowNoPasswordManagerAvailable : Shared
|
||||
|
||||
/** Launch the system document picker so the user can choose where to save the login PDF. */
|
||||
data object LaunchSaveAsPdf : Shared
|
||||
|
||||
|
||||
+10
@@ -98,6 +98,16 @@ class MessageBackupsSignalLoginDetailsViewModelTest {
|
||||
assertThat(actions).containsExactly(SignalLoginViewDetailsAction.LaunchSaveToPasswordManager)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SaveToPasswordManagerClicked without a password manager tells the user there isn't one`() = runTest(testDispatcher) {
|
||||
val viewModel = MessageBackupsSignalLoginDetailsViewModel(repository, isPasswordManagerAvailable = false)
|
||||
val actions = collectActions(viewModel)
|
||||
|
||||
viewModel.onEvent(SignalLoginViewDetailsScreenEvents.SaveToPasswordManagerClicked)
|
||||
|
||||
assertThat(actions).containsExactly(SignalLoginViewDetailsAction.ShowNoPasswordManagerAvailable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SaveAsPdfClicked launches the save as PDF flow`() = runTest(testDispatcher) {
|
||||
val viewModel = MessageBackupsSignalLoginDetailsViewModel(repository)
|
||||
|
||||
+11
@@ -98,6 +98,17 @@ class SettingsSignalLoginDetailsViewModelTest {
|
||||
assertThat(actions).containsExactly(SignalLoginViewDetailsAction.LaunchSaveToPasswordManager)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SaveToPasswordManagerClicked without a password manager tells the user there isn't one`() = runTest(testDispatcher) {
|
||||
val viewModel = SettingsSignalLoginDetailsViewModel(repository, isPasswordManagerAvailable = false)
|
||||
val actions = mutableListOf<SignalLoginViewDetailsAction>()
|
||||
backgroundScope.launch { viewModel.actions.toList(actions) }
|
||||
|
||||
viewModel.onEvent(SettingsSignalLoginDetailsEvent.Screen(SignalLoginViewDetailsScreenEvents.SaveToPasswordManagerClicked))
|
||||
|
||||
assertThat(actions).containsExactly(SignalLoginViewDetailsAction.ShowNoPasswordManagerAvailable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SaveAsPdfClicked launches the save as PDF flow`() = runTest(testDispatcher) {
|
||||
val viewModel = SettingsSignalLoginDetailsViewModel(repository)
|
||||
|
||||
@@ -807,6 +807,10 @@ private fun EntryProviderScope<NavKey>.navigationEntries(
|
||||
viewModel.onEvent(SignalLoginInfoScreenEvents.SavedCredentialRetrieved(credential))
|
||||
}
|
||||
}
|
||||
|
||||
SignalLoginInfoScreenActions.ShowNoPasswordManagerAvailable -> {
|
||||
Toast.makeText(context, R.string.SignalLoginInfoScreen__no_password_manager_available, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+36
-22
@@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ButtonColors
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -248,28 +249,23 @@ private fun Footer(
|
||||
.fillMaxWidth()
|
||||
.padding(params.footerPadding)
|
||||
) {
|
||||
if (state.isPasswordManagerAvailable) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = { onEvent(SignalLoginInfoScreenEvents.SaveToPasswordManagerClicked) },
|
||||
enabled = !state.showSpinner,
|
||||
colors = ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
),
|
||||
modifier = Modifier
|
||||
.widthIn(max = params.maxButtonWidth)
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.SIGNAL_LOGIN_INFO_SAVE_TO_PASSWORD_MANAGER_BUTTON)
|
||||
) {
|
||||
if (state.showSpinner) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
strokeWidth = 2.dp,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
} else {
|
||||
Text(stringResource(R.string.SignalLoginInfoScreen__save_to_password_manager))
|
||||
}
|
||||
Buttons.LargeTonal(
|
||||
onClick = { onEvent(SignalLoginInfoScreenEvents.SaveToPasswordManagerClicked) },
|
||||
enabled = !state.showSpinner,
|
||||
colors = primaryContainerButtonColors(enabledLook = state.isPasswordManagerAvailable),
|
||||
modifier = Modifier
|
||||
.widthIn(max = params.maxButtonWidth)
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.SIGNAL_LOGIN_INFO_SAVE_TO_PASSWORD_MANAGER_BUTTON)
|
||||
) {
|
||||
if (state.showSpinner) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
strokeWidth = 2.dp,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
} else {
|
||||
Text(stringResource(R.string.SignalLoginInfoScreen__save_to_password_manager))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,6 +287,24 @@ private fun Footer(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tonal button colors, optionally rendered with the disabled palette while the button stays clickable so that tapping
|
||||
* it can explain why it won't work.
|
||||
*/
|
||||
@Composable
|
||||
private fun primaryContainerButtonColors(enabledLook: Boolean): ButtonColors {
|
||||
val colors = ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
|
||||
return if (enabledLook) {
|
||||
colors
|
||||
} else {
|
||||
colors.copy(containerColor = colors.disabledContainerColor, contentColor = colors.disabledContentColor)
|
||||
}
|
||||
}
|
||||
|
||||
@AllDevicePreviews
|
||||
@Composable
|
||||
private fun SignalLoginInfoScreenPreview() {
|
||||
|
||||
+3
@@ -21,4 +21,7 @@ sealed interface SignalLoginInfoScreenActions {
|
||||
data class ReadBackFromPasswordManager(val accountId: String) : SignalLoginInfoScreenActions {
|
||||
override fun toString(): String = "ReadBackFromPasswordManager(accountId=${accountId.censor()})"
|
||||
}
|
||||
|
||||
/** Tell the user that there is no password manager on the device to save the login into. */
|
||||
data object ShowNoPasswordManagerAvailable : SignalLoginInfoScreenActions
|
||||
}
|
||||
|
||||
+6
@@ -131,6 +131,12 @@ class SignalLoginInfoViewModel(
|
||||
isRetry: Boolean,
|
||||
stateEmitter: (SignalLoginInfoState) -> Unit
|
||||
) {
|
||||
if (!state.isPasswordManagerAvailable) {
|
||||
Log.w(TAG, "[SaveToPasswordManager] There is no password manager on this device.")
|
||||
_actions.trySend(SignalLoginInfoScreenActions.ShowNoPasswordManagerAvailable)
|
||||
return
|
||||
}
|
||||
|
||||
val accountId = state.passwordManagerAccountId
|
||||
val recoveryKey = state.passwordManagerRecoveryKey
|
||||
|
||||
|
||||
@@ -683,6 +683,8 @@
|
||||
<string name="SignalLoginInfoScreen__thanks_for_your_purchase">Thanks for your purchase. Use your Signal Login to register and restore your account. If you forget your keys, you will not be able to recover your account.</string>
|
||||
<!-- Action button that stores the Signal Login in the device password manager. -->
|
||||
<string name="SignalLoginInfoScreen__save_to_password_manager">Save to password manager</string>
|
||||
<!-- Toast shown when the user taps the disabled save to password manager button and the device has no password manager. -->
|
||||
<string name="SignalLoginInfoScreen__no_password_manager_available">You don\'t have a password manager set up, or the password manager you\'re using isn\'t supported.</string>
|
||||
<!-- Action button for users who would rather write the Signal Login down themselves. -->
|
||||
<string name="SignalLoginInfoScreen__save_manually">Save manually</string>
|
||||
<!-- Error shown when the Signal Login could not be stored in the password manager. -->
|
||||
|
||||
+16
-1
@@ -96,6 +96,15 @@ class SignalLoginViewDetailsScreenTest {
|
||||
composeTestRule.onNodeWithTag(SignalLoginTestTags.VIEW_DETAILS_SAVE_AS_PDF_BUTTON).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when there is no password manager, the save to password manager button is still shown and clickable`() {
|
||||
setContent(isPasswordManagerAvailable = false)
|
||||
|
||||
composeTestRule.onNodeWithTag(SignalLoginTestTags.VIEW_DETAILS_SAVE_TO_PASSWORD_MANAGER_BUTTON).assertIsDisplayed().performClick()
|
||||
|
||||
assertThat(events).contains(SignalLoginViewDetailsScreenEvents.SaveToPasswordManagerClicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when the screen cannot reset the recovery key, the reset button is not shown`() {
|
||||
setContent()
|
||||
@@ -120,7 +129,12 @@ class SignalLoginViewDetailsScreenTest {
|
||||
composeTestRule.onNodeWithTag(SignalLoginTestTags.VIEW_DETAILS_RESET_RECOVERY_KEY_BUTTON).assertIsNotDisplayed()
|
||||
}
|
||||
|
||||
private fun setContent(showSaveToPasswordManagerButton: Boolean = true, showResetRecoveryKeyButton: Boolean = false, resetRecoveryKeyButtonLoading: Boolean = false) {
|
||||
private fun setContent(
|
||||
showSaveToPasswordManagerButton: Boolean = true,
|
||||
isPasswordManagerAvailable: Boolean = true,
|
||||
showResetRecoveryKeyButton: Boolean = false,
|
||||
resetRecoveryKeyButtonLoading: Boolean = false
|
||||
) {
|
||||
composeTestRule.setContent {
|
||||
SignalTheme {
|
||||
SignalLoginViewDetailsScreen(
|
||||
@@ -128,6 +142,7 @@ class SignalLoginViewDetailsScreenTest {
|
||||
accountKey = ACCOUNT_KEY,
|
||||
recoveryKey = RECOVERY_KEY,
|
||||
showSaveToPasswordManagerButton = showSaveToPasswordManagerButton,
|
||||
isPasswordManagerAvailable = isPasswordManagerAvailable,
|
||||
showResetRecoveryKeyButton = showResetRecoveryKeyButton,
|
||||
resetRecoveryKeyButtonLoading = resetRecoveryKeyButtonLoading
|
||||
),
|
||||
|
||||
+11
-1
@@ -60,6 +60,15 @@ class SignalLoginInfoScreenTest {
|
||||
assertThat(events).contains(SignalLoginInfoScreenEvents.SaveToPasswordManagerClicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when there is no password manager, the save to password manager button is still shown and clickable`() {
|
||||
setContent(isPasswordManagerAvailable = false)
|
||||
|
||||
composeTestRule.onNodeWithTag(TestTags.SIGNAL_LOGIN_INFO_SAVE_TO_PASSWORD_MANAGER_BUTTON).assertIsDisplayed().performClick()
|
||||
|
||||
assertThat(events).contains(SignalLoginInfoScreenEvents.SaveToPasswordManagerClicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when save manually is clicked, SaveManuallyClicked is emitted`() {
|
||||
setContent()
|
||||
@@ -117,6 +126,7 @@ class SignalLoginInfoScreenTest {
|
||||
|
||||
private fun setContent(
|
||||
showConfirmSavedSheet: Boolean = false,
|
||||
isPasswordManagerAvailable: Boolean = true,
|
||||
dialogs: SignalLoginInfoState.Dialogs = SignalLoginInfoState.Dialogs()
|
||||
) {
|
||||
composeTestRule.setContent {
|
||||
@@ -125,7 +135,7 @@ class SignalLoginInfoScreenTest {
|
||||
state = SignalLoginInfoState(
|
||||
aci = ACI_VALUE,
|
||||
aep = AEP,
|
||||
isPasswordManagerAvailable = true,
|
||||
isPasswordManagerAvailable = isPasswordManagerAvailable,
|
||||
showConfirmSavedSheet = showConfirmSavedSheet,
|
||||
dialogs = dialogs
|
||||
),
|
||||
|
||||
+13
-1
@@ -107,12 +107,24 @@ class SignalLoginInfoViewModelTest {
|
||||
fun `SaveToPasswordManagerClicked without a login in the flow state shows the unknown error dialog`() = runTest(testDispatcher) {
|
||||
var emittedState: SignalLoginInfoState? = null
|
||||
|
||||
viewModel.applyEvent(SignalLoginInfoState(), SignalLoginInfoScreenEvents.SaveToPasswordManagerClicked, {}) { emittedState = it }
|
||||
viewModel.applyEvent(SignalLoginInfoState(isPasswordManagerAvailable = true), SignalLoginInfoScreenEvents.SaveToPasswordManagerClicked, {}) { emittedState = it }
|
||||
|
||||
assertThat(emittedState?.dialogs?.unknownError).isEqualTo(true)
|
||||
assertThat(emittedState?.showSpinner).isEqualTo(false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SaveToPasswordManagerClicked without a password manager tells the user there isn't one`() = runTest(testDispatcher) {
|
||||
val actions = mutableListOf<SignalLoginInfoScreenActions>()
|
||||
backgroundScope.launch { viewModel.actions.toList(actions) }
|
||||
var emittedState: SignalLoginInfoState? = null
|
||||
|
||||
viewModel.applyEvent(SignalLoginInfoState(isPasswordManagerAvailable = false), SignalLoginInfoScreenEvents.SaveToPasswordManagerClicked, {}) { emittedState = it }
|
||||
|
||||
assertThat(actions).containsExactly(SignalLoginInfoScreenActions.ShowNoPasswordManagerAvailable)
|
||||
assertThat(emittedState).isEqualTo(null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a successful save raises the confirm-you-saved-it sheet`() = runTest(testDispatcher) {
|
||||
var emittedState: SignalLoginInfoState? = null
|
||||
|
||||
+37
-4
@@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ButtonColors
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -141,6 +142,7 @@ fun SignalLoginViewDetailsScreen(
|
||||
maxButtonWidth = layout.maxButtonWidth,
|
||||
isElevated = firstPaneScrollState.canScrollForward || secondPaneScrollState.canScrollForward,
|
||||
showSaveToPasswordManagerButton = state.showSaveToPasswordManagerButton,
|
||||
isPasswordManagerAvailable = state.isPasswordManagerAvailable,
|
||||
showResetRecoveryKeyButton = state.showResetRecoveryKeyButton,
|
||||
resetRecoveryKeyButtonLoading = state.resetRecoveryKeyButtonLoading,
|
||||
onEvent = onEvent
|
||||
@@ -296,6 +298,7 @@ private fun Footer(
|
||||
maxButtonWidth: Dp,
|
||||
isElevated: Boolean,
|
||||
showSaveToPasswordManagerButton: Boolean,
|
||||
isPasswordManagerAvailable: Boolean,
|
||||
showResetRecoveryKeyButton: Boolean,
|
||||
resetRecoveryKeyButtonLoading: Boolean,
|
||||
onEvent: (SignalLoginViewDetailsScreenEvents) -> Unit
|
||||
@@ -313,10 +316,7 @@ private fun Footer(
|
||||
if (showSaveToPasswordManagerButton) {
|
||||
Buttons.MediumTonal(
|
||||
onClick = { onEvent(SignalLoginViewDetailsScreenEvents.SaveToPasswordManagerClicked) },
|
||||
colors = ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
),
|
||||
colors = primaryContainerButtonColors(enabledLook = isPasswordManagerAvailable),
|
||||
modifier = Modifier
|
||||
.widthIn(max = maxButtonWidth)
|
||||
.fillMaxWidth()
|
||||
@@ -369,6 +369,24 @@ private fun Footer(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tonal button colors, optionally rendered with the disabled palette while the button stays clickable so that tapping
|
||||
* it can explain why it won't work.
|
||||
*/
|
||||
@Composable
|
||||
private fun primaryContainerButtonColors(enabledLook: Boolean): ButtonColors {
|
||||
val colors = ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
|
||||
return if (enabledLook) {
|
||||
colors
|
||||
} else {
|
||||
colors.copy(containerColor = colors.disabledContainerColor, contentColor = colors.disabledContentColor)
|
||||
}
|
||||
}
|
||||
|
||||
@BreakpointPreviews
|
||||
@Composable
|
||||
private fun SignalLoginViewDetailsScreenPreview() {
|
||||
@@ -383,6 +401,21 @@ private fun SignalLoginViewDetailsScreenPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@AllDevicePreviews
|
||||
@Composable
|
||||
private fun SignalLoginViewDetailsScreenNoPasswordManagerPreview() {
|
||||
Previews.Preview {
|
||||
SignalLoginViewDetailsScreen(
|
||||
state = SignalLoginViewDetailsState(
|
||||
accountKey = "A6B28482-2E32-83D0-7F23-91360A4C2B91",
|
||||
recoveryKey = "UY38JH2778HJJHJ8LK19GA61S672JSJ=89R=23S6A578=9BAP92J2YH5T326VV7T",
|
||||
isPasswordManagerAvailable = false
|
||||
),
|
||||
onEvent = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AllDevicePreviews
|
||||
@Composable
|
||||
private fun SignalLoginViewDetailsScreenWithResetPreview() {
|
||||
|
||||
+5
-1
@@ -16,11 +16,15 @@ import org.signal.signallogin.RecoveryKeyGroups
|
||||
* know whether the user has any resets left, so a spinner stands in for the button.
|
||||
*
|
||||
* [showSaveToPasswordManagerButton] is false during registration, where the only save option we offer is the PDF.
|
||||
*
|
||||
* [isPasswordManagerAvailable] is false when the device has no password manager at all. The save button is still shown
|
||||
* then, but styled as disabled, so the user can find out why it isn't an option rather than wonder where it went.
|
||||
*/
|
||||
data class SignalLoginViewDetailsState(
|
||||
val accountKey: String = "",
|
||||
val recoveryKey: String = "",
|
||||
val showSaveToPasswordManagerButton: Boolean = true,
|
||||
val isPasswordManagerAvailable: Boolean = true,
|
||||
val showResetRecoveryKeyButton: Boolean = false,
|
||||
val resetRecoveryKeyButtonLoading: Boolean = false
|
||||
) {
|
||||
@@ -28,5 +32,5 @@ data class SignalLoginViewDetailsState(
|
||||
val recoveryKeyGroups: RecoveryKeyGroups
|
||||
get() = RecoveryKeyGroups.from(recoveryKey)
|
||||
|
||||
override fun toString(): String = "SignalLoginViewDetailsState(accountKey=${accountKey.censor()}, recoveryKey=${recoveryKey.censor()}, showSaveToPasswordManagerButton=$showSaveToPasswordManagerButton, showResetRecoveryKeyButton=$showResetRecoveryKeyButton, resetRecoveryKeyButtonLoading=$resetRecoveryKeyButtonLoading)"
|
||||
override fun toString(): String = "SignalLoginViewDetailsState(accountKey=${accountKey.censor()}, recoveryKey=${recoveryKey.censor()}, showSaveToPasswordManagerButton=$showSaveToPasswordManagerButton, isPasswordManagerAvailable=$isPasswordManagerAvailable, showResetRecoveryKeyButton=$showResetRecoveryKeyButton, resetRecoveryKeyButtonLoading=$resetRecoveryKeyButtonLoading)"
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
<string name="SignalLoginViewDetailsScreen__reset_recovery_key">Reset recovery key</string>
|
||||
<!-- Suggested file name prefilled in the system save dialog when saving the Signal Login as a PDF. Keep the .pdf extension. -->
|
||||
<string name="SignalLoginViewDetailsScreen__signal_login_pdf">Signal Login.pdf</string>
|
||||
<!-- Toast shown when the user taps the disabled save to password manager button and the device has no password manager. -->
|
||||
<string name="SignalLoginViewDetailsScreen__no_password_manager_available">You don\'t have a password manager set up, or the password manager you\'re using isn\'t supported.</string>
|
||||
<!-- Toast shown when saving the Signal Login PDF fails. -->
|
||||
<string name="SignalLoginViewDetailsScreen__unable_to_save_pdf">Unable to save PDF. Please try again.</string>
|
||||
<!-- Toast shown when the location the user picked for the Signal Login PDF refuses the write. -->
|
||||
|
||||
Reference in New Issue
Block a user