Show disabled state if no password manager is available.

This commit is contained in:
Greyson Parrelli
2026-09-21 16:31:28 -04:00
parent f66a131a9c
commit f577d1a869
18 changed files with 195 additions and 38 deletions
@@ -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)
}
@@ -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)
@@ -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(
@@ -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)
@@ -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
@@ -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)
@@ -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()
}
}
}
@@ -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() {
@@ -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
}
@@ -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. -->
@@ -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
),
@@ -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
),
@@ -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
@@ -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() {
@@ -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. -->