mirror of
https://github.com/bitwarden/android.git
synced 2025-12-11 04:39:19 -06:00
🍒PM-17660: Sync learn more cherry pick (#5249)
This commit is contained in:
parent
e738b458ef
commit
04e06904f2
@ -8,16 +8,21 @@ import android.widget.Toast
|
|||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@ -56,7 +61,9 @@ import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.Ve
|
|||||||
import com.bitwarden.authenticator.ui.platform.components.appbar.BitwardenMediumTopAppBar
|
import com.bitwarden.authenticator.ui.platform.components.appbar.BitwardenMediumTopAppBar
|
||||||
import com.bitwarden.authenticator.ui.platform.components.appbar.BitwardenTopAppBar
|
import com.bitwarden.authenticator.ui.platform.components.appbar.BitwardenTopAppBar
|
||||||
import com.bitwarden.authenticator.ui.platform.components.appbar.action.BitwardenSearchActionItem
|
import com.bitwarden.authenticator.ui.platform.components.appbar.action.BitwardenSearchActionItem
|
||||||
|
import com.bitwarden.authenticator.ui.platform.components.button.BitwardenFilledButton
|
||||||
import com.bitwarden.authenticator.ui.platform.components.button.BitwardenFilledTonalButton
|
import com.bitwarden.authenticator.ui.platform.components.button.BitwardenFilledTonalButton
|
||||||
|
import com.bitwarden.authenticator.ui.platform.components.button.BitwardenTextButton
|
||||||
import com.bitwarden.authenticator.ui.platform.components.card.BitwardenActionCard
|
import com.bitwarden.authenticator.ui.platform.components.card.BitwardenActionCard
|
||||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
|
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
|
||||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
|
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
|
||||||
@ -139,6 +146,10 @@ fun ItemListingScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ItemListingEvent.NavigateToSyncInformation -> {
|
||||||
|
intentManager.launchUri("https://bitwarden.com/help/totp-sync".toUri())
|
||||||
|
}
|
||||||
|
|
||||||
ItemListingEvent.NavigateToBitwardenSettings -> {
|
ItemListingEvent.NavigateToBitwardenSettings -> {
|
||||||
intentManager.startMainBitwardenAppAccountSettings()
|
intentManager.startMainBitwardenAppAccountSettings()
|
||||||
}
|
}
|
||||||
@ -230,6 +241,9 @@ fun ItemListingScreen(
|
|||||||
viewModel.trySendAction(ItemListingAction.SyncWithBitwardenDismiss)
|
viewModel.trySendAction(ItemListingAction.SyncWithBitwardenDismiss)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onSyncLearnMoreClick = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(ItemListingAction.SyncLearnMoreClick) }
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,6 +284,9 @@ fun ItemListingScreen(
|
|||||||
viewModel.trySendAction(ItemListingAction.SyncWithBitwardenClick)
|
viewModel.trySendAction(ItemListingAction.SyncWithBitwardenClick)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onSyncLearnMoreClick = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(ItemListingAction.SyncLearnMoreClick) }
|
||||||
|
},
|
||||||
onDismissSyncWithBitwardenClick = remember(viewModel) {
|
onDismissSyncWithBitwardenClick = remember(viewModel) {
|
||||||
{
|
{
|
||||||
viewModel.trySendAction(ItemListingAction.SyncWithBitwardenDismiss)
|
viewModel.trySendAction(ItemListingAction.SyncWithBitwardenDismiss)
|
||||||
@ -339,6 +356,7 @@ private fun ItemListingContent(
|
|||||||
onDismissDownloadBitwardenClick: () -> Unit,
|
onDismissDownloadBitwardenClick: () -> Unit,
|
||||||
onSyncWithBitwardenClick: () -> Unit,
|
onSyncWithBitwardenClick: () -> Unit,
|
||||||
onDismissSyncWithBitwardenClick: () -> Unit,
|
onDismissSyncWithBitwardenClick: () -> Unit,
|
||||||
|
onSyncLearnMoreClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
BitwardenScaffold(
|
BitwardenScaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -402,23 +420,15 @@ private fun ItemListingContent(
|
|||||||
) {
|
) {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
item {
|
item {
|
||||||
when (state.actionCard) {
|
ActionCard(
|
||||||
ItemListingState.ActionCardState.DownloadBitwardenApp ->
|
actionCardState = state.actionCard,
|
||||||
DownloadBitwardenActionCard(
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
|
||||||
onDownloadBitwardenClick = onDownloadBitwardenClick,
|
onDownloadBitwardenClick = onDownloadBitwardenClick,
|
||||||
onDismissClick = onDismissDownloadBitwardenClick,
|
onDownloadBitwardenDismissClick = onDismissDownloadBitwardenClick,
|
||||||
)
|
|
||||||
|
|
||||||
ItemListingState.ActionCardState.SyncWithBitwarden ->
|
|
||||||
SyncWithBitwardenActionCard(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
onSyncWithBitwardenClick = onSyncWithBitwardenClick,
|
onSyncWithBitwardenClick = onSyncWithBitwardenClick,
|
||||||
onDismissClick = onDismissSyncWithBitwardenClick,
|
onSyncWithBitwardenDismissClick = onDismissSyncWithBitwardenClick,
|
||||||
|
onSyncLearnMoreClick = onSyncLearnMoreClick,
|
||||||
|
modifier = Modifier.padding(all = 16.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
ItemListingState.ActionCardState.None -> Unit
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (state.favoriteItems.isNotEmpty()) {
|
if (state.favoriteItems.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
@ -572,6 +582,7 @@ fun EmptyItemListingContent(
|
|||||||
onDownloadBitwardenClick: () -> Unit,
|
onDownloadBitwardenClick: () -> Unit,
|
||||||
onDismissDownloadBitwardenClick: () -> Unit,
|
onDismissDownloadBitwardenClick: () -> Unit,
|
||||||
onSyncWithBitwardenClick: () -> Unit,
|
onSyncWithBitwardenClick: () -> Unit,
|
||||||
|
onSyncLearnMoreClick: () -> Unit,
|
||||||
onDismissSyncWithBitwardenClick: () -> Unit,
|
onDismissSyncWithBitwardenClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
BitwardenScaffold(
|
BitwardenScaffold(
|
||||||
@ -635,24 +646,15 @@ fun EmptyItemListingContent(
|
|||||||
ItemListingState.ActionCardState.SyncWithBitwarden -> Arrangement.Top
|
ItemListingState.ActionCardState.SyncWithBitwarden -> Arrangement.Top
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
when (actionCardState) {
|
ActionCard(
|
||||||
ItemListingState.ActionCardState.DownloadBitwardenApp ->
|
actionCardState = actionCardState,
|
||||||
DownloadBitwardenActionCard(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
onDismissClick = onDismissDownloadBitwardenClick,
|
|
||||||
onDownloadBitwardenClick = onDownloadBitwardenClick,
|
onDownloadBitwardenClick = onDownloadBitwardenClick,
|
||||||
)
|
onDownloadBitwardenDismissClick = onDismissDownloadBitwardenClick,
|
||||||
|
|
||||||
ItemListingState.ActionCardState.SyncWithBitwarden ->
|
|
||||||
SyncWithBitwardenActionCard(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
onDismissClick = onDismissSyncWithBitwardenClick,
|
|
||||||
onSyncWithBitwardenClick = onSyncWithBitwardenClick,
|
onSyncWithBitwardenClick = onSyncWithBitwardenClick,
|
||||||
|
onSyncWithBitwardenDismissClick = onDismissSyncWithBitwardenClick,
|
||||||
|
onSyncLearnMoreClick = onSyncLearnMoreClick,
|
||||||
)
|
)
|
||||||
|
|
||||||
ItemListingState.ActionCardState.None -> Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a spacer if an action card is showing:
|
// Add a spacer if an action card is showing:
|
||||||
when (actionCardState) {
|
when (actionCardState) {
|
||||||
ItemListingState.ActionCardState.None -> Unit
|
ItemListingState.ActionCardState.None -> Unit
|
||||||
@ -735,32 +737,114 @@ private fun DownloadBitwardenActionCard(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Suppress("LongMethod")
|
||||||
@Composable
|
@Composable
|
||||||
private fun SyncWithBitwardenActionCard(
|
private fun SyncWithBitwardenActionCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onDismissClick: () -> Unit,
|
onDismissClick: () -> Unit,
|
||||||
onSyncWithBitwardenClick: () -> Unit,
|
onAppSettingsClick: () -> Unit,
|
||||||
) = BitwardenActionCard(
|
onLearnMoreClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
actionIcon = rememberVectorPainter(R.drawable.ic_refresh),
|
shape = RoundedCornerShape(size = 16.dp),
|
||||||
actionText = stringResource(R.string.sync_with_bitwarden_action_card_message),
|
colors = CardDefaults.cardColors(
|
||||||
callToActionText = stringResource(R.string.go_to_settings),
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
titleText = stringResource(R.string.sync_with_the_bitwarden_app),
|
disabledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
onCardClicked = onSyncWithBitwardenClick,
|
),
|
||||||
trailingContent = {
|
elevation = CardDefaults.elevatedCardElevation(),
|
||||||
IconButton(
|
|
||||||
onClick = onDismissClick,
|
|
||||||
) {
|
) {
|
||||||
|
Spacer(Modifier.height(height = 4.dp))
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Spacer(Modifier.width(width = 16.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(top = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = rememberVectorPainter(id = R.drawable.ic_bitwarden),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(size = 20.dp),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(width = 16.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.sync_with_the_bitwarden_app),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.weight(weight = 1f))
|
||||||
|
Spacer(Modifier.width(width = 16.dp))
|
||||||
|
IconButton(onClick = onDismissClick) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.ic_close),
|
painter = painterResource(id = R.drawable.ic_close),
|
||||||
contentDescription = stringResource(id = R.string.close),
|
contentDescription = stringResource(id = R.string.close),
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier
|
modifier = Modifier.size(size = 24.dp),
|
||||||
.size(24.dp),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
Spacer(Modifier.width(width = 4.dp))
|
||||||
)
|
}
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.sync_with_bitwarden_action_card_message),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(start = 36.dp, end = 48.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(height = 16.dp))
|
||||||
|
BitwardenFilledButton(
|
||||||
|
label = stringResource(id = R.string.take_me_to_app_settings),
|
||||||
|
onClick = onAppSettingsClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
BitwardenTextButton(
|
||||||
|
label = stringResource(id = R.string.learn_more),
|
||||||
|
onClick = onLearnMoreClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(height = 4.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ActionCard(
|
||||||
|
actionCardState: ItemListingState.ActionCardState,
|
||||||
|
onDownloadBitwardenClick: () -> Unit,
|
||||||
|
onDownloadBitwardenDismissClick: () -> Unit,
|
||||||
|
onSyncWithBitwardenClick: () -> Unit,
|
||||||
|
onSyncWithBitwardenDismissClick: () -> Unit,
|
||||||
|
onSyncLearnMoreClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
when (actionCardState) {
|
||||||
|
ItemListingState.ActionCardState.DownloadBitwardenApp -> {
|
||||||
|
DownloadBitwardenActionCard(
|
||||||
|
modifier = modifier,
|
||||||
|
onDownloadBitwardenClick = onDownloadBitwardenClick,
|
||||||
|
onDismissClick = onDownloadBitwardenDismissClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemListingState.ActionCardState.SyncWithBitwarden -> {
|
||||||
|
SyncWithBitwardenActionCard(
|
||||||
|
modifier = modifier,
|
||||||
|
onAppSettingsClick = onSyncWithBitwardenClick,
|
||||||
|
onDismissClick = onSyncWithBitwardenDismissClick,
|
||||||
|
onLearnMoreClick = onSyncLearnMoreClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemListingState.ActionCardState.None -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -776,6 +860,7 @@ private fun EmptyListingContentPreview() {
|
|||||||
onDownloadBitwardenClick = { },
|
onDownloadBitwardenClick = { },
|
||||||
onDismissDownloadBitwardenClick = { },
|
onDismissDownloadBitwardenClick = { },
|
||||||
onSyncWithBitwardenClick = { },
|
onSyncWithBitwardenClick = { },
|
||||||
|
onSyncLearnMoreClick = { },
|
||||||
onDismissSyncWithBitwardenClick = { },
|
onDismissSyncWithBitwardenClick = { },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -153,6 +153,10 @@ class ItemListingViewModel @Inject constructor(
|
|||||||
ItemListingAction.SyncWithBitwardenDismiss -> {
|
ItemListingAction.SyncWithBitwardenDismiss -> {
|
||||||
handleSyncWithBitwardenDismiss()
|
handleSyncWithBitwardenDismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ItemListingAction.SyncLearnMoreClick -> {
|
||||||
|
handleSyncLearnMoreClick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -564,6 +568,10 @@ class ItemListingViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleSyncLearnMoreClick() {
|
||||||
|
sendEvent(ItemListingEvent.NavigateToSyncInformation)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a [SharedVerificationCodesState] into an action card for display.
|
* Converts a [SharedVerificationCodesState] into an action card for display.
|
||||||
*/
|
*/
|
||||||
@ -794,6 +802,11 @@ sealed class ItemListingEvent {
|
|||||||
*/
|
*/
|
||||||
data object NavigateToAppSettings : ItemListingEvent()
|
data object NavigateToAppSettings : ItemListingEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the sync information web page.
|
||||||
|
*/
|
||||||
|
data object NavigateToSyncInformation : ItemListingEvent()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to Bitwarden play store listing.
|
* Navigate to Bitwarden play store listing.
|
||||||
*/
|
*/
|
||||||
@ -872,6 +885,11 @@ sealed class ItemListingAction {
|
|||||||
*/
|
*/
|
||||||
data object SyncWithBitwardenClick : ItemListingAction()
|
data object SyncWithBitwardenClick : ItemListingAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user tapped the learn more button on the sync action card.
|
||||||
|
*/
|
||||||
|
data object SyncLearnMoreClick : ItemListingAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user dismissed sync Bitwarden action card.
|
* The user dismissed sync Bitwarden action card.
|
||||||
*/
|
*/
|
||||||
@ -886,7 +904,7 @@ sealed class ItemListingAction {
|
|||||||
* Represents an action triggered when the user clicks an item in the dropdown menu.
|
* Represents an action triggered when the user clicks an item in the dropdown menu.
|
||||||
*
|
*
|
||||||
* @param menuAction The action selected from the dropdown menu.
|
* @param menuAction The action selected from the dropdown menu.
|
||||||
* @param id The identifier of the item on which the action is being performed.
|
* @param item The item on which the action is being performed.
|
||||||
*/
|
*/
|
||||||
data class DropdownMenuClick(
|
data class DropdownMenuClick(
|
||||||
val menuAction: VaultDropdownMenuAction,
|
val menuAction: VaultDropdownMenuAction,
|
||||||
|
|||||||
@ -44,8 +44,8 @@ fun BitwardenActionCard(
|
|||||||
onClick = onCardClicked,
|
onClick = onCardClicked,
|
||||||
shape = RoundedCornerShape(size = 16.dp),
|
shape = RoundedCornerShape(size = 16.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
disabledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
elevation = CardDefaults.elevatedCardElevation(),
|
elevation = CardDefaults.elevatedCardElevation(),
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,7 +38,7 @@ fun BitwardenTextRow(
|
|||||||
text: String,
|
text: String,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
description: String? = null,
|
description: AnnotatedString? = null,
|
||||||
withDivider: Boolean = false,
|
withDivider: Boolean = false,
|
||||||
content: (@Composable () -> Unit)? = null,
|
content: (@Composable () -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -3,6 +3,10 @@
|
|||||||
package com.bitwarden.authenticator.ui.platform.feature.settings
|
package com.bitwarden.authenticator.ui.platform.feature.settings
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.text.Annotation
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.SpannedString
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@ -34,6 +38,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
@ -42,9 +47,15 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.semantics.testTag
|
import androidx.compose.ui.semantics.testTag
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.LinkAnnotation
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextLinkStyles
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.text.getSpans
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitwarden.authenticator.R
|
import com.bitwarden.authenticator.R
|
||||||
@ -66,6 +77,7 @@ import com.bitwarden.authenticator.ui.platform.manager.intent.IntentManager
|
|||||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||||
import com.bitwarden.authenticator.ui.platform.util.displayLabel
|
import com.bitwarden.authenticator.ui.platform.util.displayLabel
|
||||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||||
|
import com.bitwarden.ui.platform.base.util.toAnnotatedString
|
||||||
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||||
import com.bitwarden.ui.util.Text
|
import com.bitwarden.ui.util.Text
|
||||||
import com.bitwarden.ui.util.asText
|
import com.bitwarden.ui.util.asText
|
||||||
@ -107,6 +119,10 @@ fun SettingsScreen(
|
|||||||
intentManager.launchUri("https://bitwarden.com/privacy".toUri())
|
intentManager.launchUri("https://bitwarden.com/privacy".toUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsEvent.NavigateToSyncInformation -> {
|
||||||
|
intentManager.launchUri("https://bitwarden.com/help/totp-sync".toUri())
|
||||||
|
}
|
||||||
|
|
||||||
SettingsEvent.NavigateToBitwardenApp -> {
|
SettingsEvent.NavigateToBitwardenApp -> {
|
||||||
|
|
||||||
intentManager.startActivity(
|
intentManager.startActivity(
|
||||||
@ -177,6 +193,9 @@ fun SettingsScreen(
|
|||||||
viewModel.trySendAction(SettingsAction.DataClick.SyncWithBitwardenClick)
|
viewModel.trySendAction(SettingsAction.DataClick.SyncWithBitwardenClick)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onSyncLearnMoreClick = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(SettingsAction.DataClick.SyncLearnMoreClick) }
|
||||||
|
},
|
||||||
onDefaultSaveOptionUpdated = remember(viewModel) {
|
onDefaultSaveOptionUpdated = remember(viewModel) {
|
||||||
{
|
{
|
||||||
viewModel.trySendAction(
|
viewModel.trySendAction(
|
||||||
@ -280,6 +299,7 @@ private fun VaultSettings(
|
|||||||
onImportClick: () -> Unit,
|
onImportClick: () -> Unit,
|
||||||
onBackupClick: () -> Unit,
|
onBackupClick: () -> Unit,
|
||||||
onSyncWithBitwardenClick: () -> Unit,
|
onSyncWithBitwardenClick: () -> Unit,
|
||||||
|
onSyncLearnMoreClick: () -> Unit,
|
||||||
onDefaultSaveOptionUpdated: (DefaultSaveOption) -> Unit,
|
onDefaultSaveOptionUpdated: (DefaultSaveOption) -> Unit,
|
||||||
shouldShowSyncWithBitwardenApp: Boolean,
|
shouldShowSyncWithBitwardenApp: Boolean,
|
||||||
shouldShowDefaultSaveOptions: Boolean,
|
shouldShowDefaultSaveOptions: Boolean,
|
||||||
@ -339,6 +359,22 @@ private fun VaultSettings(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
BitwardenTextRow(
|
BitwardenTextRow(
|
||||||
text = stringResource(id = R.string.sync_with_bitwarden_app),
|
text = stringResource(id = R.string.sync_with_bitwarden_app),
|
||||||
|
description = R.string
|
||||||
|
.this_feature_is_not_not_yet_available_for_self_hosted_users
|
||||||
|
.toAnnotatedString(
|
||||||
|
style = spanStyleOf(
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium,
|
||||||
|
),
|
||||||
|
linkHighlightStyle = spanStyleOf(
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
textStyle = MaterialTheme.typography.labelLarge,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
when (it) {
|
||||||
|
"learnMore" -> onSyncLearnMoreClick()
|
||||||
|
}
|
||||||
|
},
|
||||||
onClick = onSyncWithBitwardenClick,
|
onClick = onSyncWithBitwardenClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
withDivider = true,
|
withDivider = true,
|
||||||
@ -634,6 +670,151 @@ private fun CopyRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new [SpanStyle] from the specified [color] and [textStyle].
|
||||||
|
*/
|
||||||
|
private fun spanStyleOf(
|
||||||
|
color: Color,
|
||||||
|
textStyle: TextStyle,
|
||||||
|
): SpanStyle =
|
||||||
|
SpanStyle(
|
||||||
|
color = color,
|
||||||
|
fontSize = textStyle.fontSize,
|
||||||
|
fontWeight = textStyle.fontWeight,
|
||||||
|
fontStyle = textStyle.fontStyle,
|
||||||
|
fontSynthesis = textStyle.fontSynthesis,
|
||||||
|
fontFamily = textStyle.fontFamily,
|
||||||
|
fontFeatureSettings = textStyle.fontFeatureSettings,
|
||||||
|
letterSpacing = textStyle.letterSpacing,
|
||||||
|
baselineShift = textStyle.baselineShift,
|
||||||
|
textGeometricTransform = textStyle.textGeometricTransform,
|
||||||
|
localeList = textStyle.localeList,
|
||||||
|
background = textStyle.background,
|
||||||
|
textDecoration = textStyle.textDecoration,
|
||||||
|
shadow = textStyle.shadow,
|
||||||
|
platformStyle = textStyle.platformStyle?.spanStyle,
|
||||||
|
drawStyle = textStyle.drawStyle,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an [AnnotatedString] from a string resource allowing for optional arguments
|
||||||
|
* to be applied.
|
||||||
|
* @param args Optional arguments to be applied to the string resource, must already be a
|
||||||
|
* [String]
|
||||||
|
* @param style Style to apply to the entire string
|
||||||
|
* @param linkHighlightStyle Style to apply to part of the resource that has been annotated with
|
||||||
|
* the "link" annotation.
|
||||||
|
* @param onAnnotationClick Callback to invoke when a link annotation is clicked. Will pass back
|
||||||
|
* the value of the annotation as a string to allow for delineation if there are multiple callbacks
|
||||||
|
* to be applied.
|
||||||
|
*
|
||||||
|
* In order for the styles to be applied the resource must contain custom annotations for example:
|
||||||
|
*
|
||||||
|
* If a word or phrase is to have the [linkHighlightStyle] applied then it must be annotated
|
||||||
|
* with the custom XML tag: <annotation link="callBackKey"> where the value will be passed back
|
||||||
|
* in [onAnnotationClick] and used to delineate which annotation was clicked.
|
||||||
|
* <string foo>Foo <annotation link="onBarClick">bar</annotation> baz</string>
|
||||||
|
*
|
||||||
|
* If the <string> contains a format argument (%1$s) then that argument should be wrapped in the
|
||||||
|
* following custom XML tag: <annotation arg="0"> where the value is the index of the argument,
|
||||||
|
* starting at 0.
|
||||||
|
*/
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
@Composable
|
||||||
|
private fun @receiver:StringRes Int.toAnnotatedString(
|
||||||
|
vararg args: String,
|
||||||
|
style: SpanStyle,
|
||||||
|
linkHighlightStyle: SpanStyle,
|
||||||
|
onAnnotationClick: ((annotationKey: String) -> Unit)? = null,
|
||||||
|
): AnnotatedString {
|
||||||
|
val resources = LocalContext.current.resources
|
||||||
|
// The spannableBuilder is used to help parse through the annotations in the string resource.
|
||||||
|
val spannableBuilder = try {
|
||||||
|
SpannableStringBuilder(resources.getText(this) as SpannedString)
|
||||||
|
} catch (_: ClassCastException) {
|
||||||
|
// the resource did not contain and valid spans so we just return the raw string.
|
||||||
|
return stringResource(id = this, *args).toAnnotatedString()
|
||||||
|
}
|
||||||
|
// Replace any format arguments with the provided arguments.
|
||||||
|
spannableBuilder.applyArgAnnotations(args = args)
|
||||||
|
|
||||||
|
// The annotatedStringBuilder is used to apply the styles to the string resource.
|
||||||
|
val annotatedStringBuilder = AnnotatedString.Builder()
|
||||||
|
|
||||||
|
// Add the entire string to the annotated string builder and apply the style.
|
||||||
|
annotatedStringBuilder.append(spannableBuilder)
|
||||||
|
annotatedStringBuilder.addStyle(
|
||||||
|
style = style,
|
||||||
|
start = 0,
|
||||||
|
end = spannableBuilder.length,
|
||||||
|
)
|
||||||
|
val annotations = spannableBuilder.getSpans<Annotation>()
|
||||||
|
// Iterate through the annotations and apply the appropriate style. If the [Annotation.key]
|
||||||
|
// does not match a [ValidAnnotationType] an exception will be thrown.
|
||||||
|
for (annotation in annotations) {
|
||||||
|
// Skip the annotation if it does not have a valid start in the spanned string.
|
||||||
|
val start = spannableBuilder.getSpanStart(annotation).takeIf { it >= 0 } ?: continue
|
||||||
|
val end = spannableBuilder.getSpanEnd(annotation)
|
||||||
|
when (ValidAnnotationType.valueOf(annotation.key.uppercase())) {
|
||||||
|
ValidAnnotationType.EMPHASIS -> Unit
|
||||||
|
ValidAnnotationType.LINK -> {
|
||||||
|
val link = LinkAnnotation.Clickable(
|
||||||
|
tag = annotation.value.orEmpty(),
|
||||||
|
styles = TextLinkStyles(
|
||||||
|
style = linkHighlightStyle,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
onAnnotationClick?.invoke(annotation.value.orEmpty())
|
||||||
|
}
|
||||||
|
annotatedStringBuilder.addLink(
|
||||||
|
link,
|
||||||
|
start = start,
|
||||||
|
end = end,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Handled prior to this point, not styling to be applied.
|
||||||
|
ValidAnnotationType.ARG -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return remember { annotatedStringBuilder.toAnnotatedString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The span between the <annotation arg="0"> and </annotation> tags in the string resource is
|
||||||
|
* replaced with the index value in the provided [args].
|
||||||
|
*/
|
||||||
|
private fun SpannableStringBuilder.applyArgAnnotations(
|
||||||
|
vararg args: String,
|
||||||
|
) {
|
||||||
|
val argAnnotations = getSpans<Annotation>()
|
||||||
|
.filter { it.isArgAnnotation() }
|
||||||
|
for (annotation in argAnnotations) {
|
||||||
|
// Skip the annotation if it does not have a valid start in the spanned string.
|
||||||
|
val spanStart = getSpanStart(annotation).takeIf { it >= 0 } ?: continue
|
||||||
|
val argIndex = Integer.parseInt(annotation.value)
|
||||||
|
// if no string is available just replace it with an empty string.
|
||||||
|
val replacementString = args.getOrNull(argIndex).orEmpty()
|
||||||
|
this.replace(
|
||||||
|
spanStart,
|
||||||
|
this.getSpanEnd(annotation),
|
||||||
|
replacementString,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Annotation.isArgAnnotation(): Boolean =
|
||||||
|
this.key.uppercase() == ValidAnnotationType.ARG.name
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumerated values representing the valid <annotation> keys that can be processed
|
||||||
|
* by [Int.toAnnotatedString]
|
||||||
|
*/
|
||||||
|
private enum class ValidAnnotationType {
|
||||||
|
ARG,
|
||||||
|
LINK,
|
||||||
|
EMPHASIS,
|
||||||
|
}
|
||||||
|
|
||||||
//endregion About settings
|
//endregion About settings
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
|||||||
@ -184,6 +184,7 @@ class SettingsViewModel @Inject constructor(
|
|||||||
SettingsAction.DataClick.ImportClick -> handleImportClick()
|
SettingsAction.DataClick.ImportClick -> handleImportClick()
|
||||||
SettingsAction.DataClick.BackupClick -> handleBackupClick()
|
SettingsAction.DataClick.BackupClick -> handleBackupClick()
|
||||||
SettingsAction.DataClick.SyncWithBitwardenClick -> handleSyncWithBitwardenClick()
|
SettingsAction.DataClick.SyncWithBitwardenClick -> handleSyncWithBitwardenClick()
|
||||||
|
SettingsAction.DataClick.SyncLearnMoreClick -> handleSyncLearnMoreClick()
|
||||||
is SettingsAction.DataClick.DefaultSaveOptionUpdated ->
|
is SettingsAction.DataClick.DefaultSaveOptionUpdated ->
|
||||||
handleDefaultSaveOptionChosen(action)
|
handleDefaultSaveOptionChosen(action)
|
||||||
}
|
}
|
||||||
@ -215,6 +216,10 @@ class SettingsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleSyncLearnMoreClick() {
|
||||||
|
sendEvent(SettingsEvent.NavigateToSyncInformation)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleExportClick() {
|
private fun handleExportClick() {
|
||||||
sendEvent(SettingsEvent.NavigateToExport)
|
sendEvent(SettingsEvent.NavigateToExport)
|
||||||
}
|
}
|
||||||
@ -424,6 +429,11 @@ sealed class SettingsEvent {
|
|||||||
*/
|
*/
|
||||||
data object NavigateToPrivacyPolicy : SettingsEvent()
|
data object NavigateToPrivacyPolicy : SettingsEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the sync learn more web page.
|
||||||
|
*/
|
||||||
|
data object NavigateToSyncInformation : SettingsEvent()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to the Bitwarden account settings.
|
* Navigate to the Bitwarden account settings.
|
||||||
*/
|
*/
|
||||||
@ -491,7 +501,12 @@ sealed class SettingsAction(
|
|||||||
data object SyncWithBitwardenClick : DataClick()
|
data object SyncWithBitwardenClick : DataClick()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User confirmed a new [DeafultSaveOption].
|
* Indicates the user clicked sync learn more button.
|
||||||
|
*/
|
||||||
|
data object SyncLearnMoreClick : DataClick()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User confirmed a new [DefaultSaveOption].
|
||||||
*/
|
*/
|
||||||
data class DefaultSaveOptionUpdated(val option: DefaultSaveOption) : DataClick()
|
data class DefaultSaveOptionUpdated(val option: DefaultSaveOption) : DataClick()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,11 +125,13 @@
|
|||||||
<string name="download_bitwarden_card_message">Store all of your logins and sync verification codes directly with the Authenticator app.</string>
|
<string name="download_bitwarden_card_message">Store all of your logins and sync verification codes directly with the Authenticator app.</string>
|
||||||
<string name="download_now">Download now</string>
|
<string name="download_now">Download now</string>
|
||||||
<string name="sync_with_bitwarden_app">Sync with Bitwarden app</string>
|
<string name="sync_with_bitwarden_app">Sync with Bitwarden app</string>
|
||||||
|
<string name="this_feature_is_not_not_yet_available_for_self_hosted_users">This feature is not yet available for self-hosted users. <annotation link="learnMore">Learn more</annotation></string>
|
||||||
<string name="shared_codes_error">Unable to sync codes from the Bitwarden app. Make sure both apps are up-to-date. You can still access your existing codes in the Bitwarden app.</string>
|
<string name="shared_codes_error">Unable to sync codes from the Bitwarden app. Make sure both apps are up-to-date. You can still access your existing codes in the Bitwarden app.</string>
|
||||||
<string name="shared_accounts_header">%1$s | %2$s</string>
|
<string name="shared_accounts_header">%1$s | %2$s</string>
|
||||||
<string name="sync_with_the_bitwarden_app">Sync with the Bitwarden app</string>
|
<string name="sync_with_the_bitwarden_app">Sync with the Bitwarden app</string>
|
||||||
<string name="go_to_settings">Go to settings</string>
|
<string name="go_to_settings">Go to settings</string>
|
||||||
<string name="sync_with_bitwarden_action_card_message">Allow Authenticator app syncing in settings to view all of your verification codes here.</string>
|
<string name="sync_with_bitwarden_action_card_message">In order to view all of your verification codes, you’ll need to allow for syncing on all of your accounts.</string>
|
||||||
|
<string name="take_me_to_app_settings">Take me to the app settings</string>
|
||||||
<string name="something_went_wrong">Something went wrong</string>
|
<string name="something_went_wrong">Something went wrong</string>
|
||||||
<string name="please_try_again">Please try again</string>
|
<string name="please_try_again">Please try again</string>
|
||||||
<string name="move_to_bitwarden">Move to Bitwarden</string>
|
<string name="move_to_bitwarden">Move to Bitwarden</string>
|
||||||
|
|||||||
@ -1,13 +1,20 @@
|
|||||||
package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting
|
package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.SemanticsNodeInteraction
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||||
|
import androidx.compose.ui.test.hasContentDescription
|
||||||
|
import androidx.compose.ui.test.hasScrollToNodeAction
|
||||||
|
import androidx.compose.ui.test.hasText
|
||||||
|
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||||
import androidx.compose.ui.test.longClick
|
import androidx.compose.ui.test.longClick
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.compose.ui.test.performScrollTo
|
import androidx.compose.ui.test.performScrollTo
|
||||||
|
import androidx.compose.ui.test.performScrollToNode
|
||||||
import androidx.compose.ui.test.performTouchInput
|
import androidx.compose.ui.test.performTouchInput
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.SharedCodesDisplayState
|
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.SharedCodesDisplayState
|
||||||
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VaultDropdownMenuAction
|
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VaultDropdownMenuAction
|
||||||
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VerificationCodeDisplayItem
|
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VerificationCodeDisplayItem
|
||||||
@ -44,7 +51,9 @@ class ItemListingScreenTest : BaseComposeTest() {
|
|||||||
every { trySendAction(any()) } just runs
|
every { trySendAction(any()) } just runs
|
||||||
}
|
}
|
||||||
|
|
||||||
private val intentManager: IntentManager = mockk()
|
private val intentManager: IntentManager = mockk {
|
||||||
|
every { launchUri(uri = any()) } just runs
|
||||||
|
}
|
||||||
private val permissionsManager = FakePermissionManager()
|
private val permissionsManager = FakePermissionManager()
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@ -63,6 +72,14 @@ class ItemListingScreenTest : BaseComposeTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on NavigateToSyncInformation should launch sync uri`() {
|
||||||
|
mutableEventFlow.tryEmit(ItemListingEvent.NavigateToSyncInformation)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
intentManager.launchUri(uri = "https://bitwarden.com/help/totp-sync".toUri())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
fun `when denying camera permissions and attempting to add a code we should be shown the manual entry screen`() {
|
fun `when denying camera permissions and attempting to add a code we should be shown the manual entry screen`() {
|
||||||
@ -174,22 +191,42 @@ class ItemListingScreenTest : BaseComposeTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
fun `on sync with bitwarden action card click in empty state should send SyncWithBitwardenClick`() {
|
fun `on sync with bitwarden app settings click in empty state should send SyncWithBitwardenClick`() {
|
||||||
mutableStateFlow.value = DEFAULT_STATE.copy(
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
viewState = ItemListingState.ViewState.NoItems(
|
viewState = ItemListingState.ViewState.NoItems(
|
||||||
actionCard = ItemListingState.ActionCardState.SyncWithBitwarden,
|
actionCard = ItemListingState.ActionCardState.SyncWithBitwarden,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Sync with the Bitwarden app")
|
.onNodeWithText(text = "Take me to the app settings")
|
||||||
.performClick()
|
.performClick()
|
||||||
verify { viewModel.trySendAction(ItemListingAction.SyncWithBitwardenClick) }
|
verify { viewModel.trySendAction(ItemListingAction.SyncWithBitwardenClick) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on sync with bitwarden learn more click in empty state should send SyncLearnMoreClick`() {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = ItemListingState.ViewState.NoItems(
|
||||||
|
actionCard = ItemListingState.ActionCardState.SyncWithBitwarden,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "Learn more")
|
||||||
|
.performClick()
|
||||||
|
verify { viewModel.trySendAction(ItemListingAction.SyncLearnMoreClick) }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
fun `on sync with bitwarden action card click in full state should send SyncWithBitwardenClick`() {
|
fun `on sync with bitwarden app settings click in full state should send SyncWithBitwardenClick`() {
|
||||||
mutableStateFlow.value = DEFAULT_STATE.copy(
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
viewState = ItemListingState.ViewState.Content(
|
viewState = ItemListingState.ViewState.Content(
|
||||||
favoriteItems = emptyList(),
|
favoriteItems = emptyList(),
|
||||||
itemList = emptyList(),
|
itemList = emptyList(),
|
||||||
@ -197,12 +234,31 @@ class ItemListingScreenTest : BaseComposeTest() {
|
|||||||
actionCard = ItemListingState.ActionCardState.SyncWithBitwarden,
|
actionCard = ItemListingState.ActionCardState.SyncWithBitwarden,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Sync with the Bitwarden app")
|
.onNodeWithTextAfterScroll(text = "Take me to the app settings")
|
||||||
.performClick()
|
.performClick()
|
||||||
verify { viewModel.trySendAction(ItemListingAction.SyncWithBitwardenClick) }
|
verify { viewModel.trySendAction(ItemListingAction.SyncWithBitwardenClick) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on sync with bitwarden learn more click in full state should send SyncLearnMoreClick`() {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = ItemListingState.ViewState.Content(
|
||||||
|
favoriteItems = emptyList(),
|
||||||
|
itemList = emptyList(),
|
||||||
|
sharedItems = SharedCodesDisplayState.Codes(emptyList()),
|
||||||
|
actionCard = ItemListingState.ActionCardState.SyncWithBitwarden,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithTextAfterScroll(text = "Learn more")
|
||||||
|
.performClick()
|
||||||
|
verify { viewModel.trySendAction(ItemListingAction.SyncLearnMoreClick) }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
fun `on sync with bitwarden action card dismiss in empty state should send SyncWithBitwardenDismiss`() {
|
fun `on sync with bitwarden action card dismiss in empty state should send SyncWithBitwardenDismiss`() {
|
||||||
@ -234,6 +290,64 @@ class ItemListingScreenTest : BaseComposeTest() {
|
|||||||
verify { viewModel.trySendAction(ItemListingAction.SyncWithBitwardenDismiss) }
|
verify { viewModel.trySendAction(ItemListingAction.SyncWithBitwardenDismiss) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on download bitwarden click in empty state should send DownloadBitwardenClick`() {
|
||||||
|
mutableStateFlow.value = DEFAULT_STATE.copy(
|
||||||
|
viewState = ItemListingState.ViewState.NoItems(
|
||||||
|
actionCard = ItemListingState.ActionCardState.DownloadBitwardenApp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "Download now")
|
||||||
|
.performClick()
|
||||||
|
verify { viewModel.trySendAction(ItemListingAction.DownloadBitwardenClick) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on download bitwarden click in full state should send DownloadBitwardenClick`() {
|
||||||
|
mutableStateFlow.value = DEFAULT_STATE.copy(
|
||||||
|
viewState = ItemListingState.ViewState.Content(
|
||||||
|
favoriteItems = emptyList(),
|
||||||
|
itemList = emptyList(),
|
||||||
|
sharedItems = SharedCodesDisplayState.Codes(emptyList()),
|
||||||
|
actionCard = ItemListingState.ActionCardState.DownloadBitwardenApp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithTextAfterScroll(text = "Download now")
|
||||||
|
.performClick()
|
||||||
|
verify { viewModel.trySendAction(ItemListingAction.DownloadBitwardenClick) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on download bitwarden dismiss in empty state should send DownloadBitwardenDismiss`() {
|
||||||
|
mutableStateFlow.value = DEFAULT_STATE.copy(
|
||||||
|
viewState = ItemListingState.ViewState.NoItems(
|
||||||
|
actionCard = ItemListingState.ActionCardState.DownloadBitwardenApp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription(label = "Close")
|
||||||
|
.performClick()
|
||||||
|
verify { viewModel.trySendAction(ItemListingAction.DownloadBitwardenDismiss) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on download bitwarden dismiss in full state should send DownloadBitwardenDismiss`() {
|
||||||
|
mutableStateFlow.value = DEFAULT_STATE.copy(
|
||||||
|
viewState = ItemListingState.ViewState.Content(
|
||||||
|
favoriteItems = emptyList(),
|
||||||
|
itemList = emptyList(),
|
||||||
|
sharedItems = SharedCodesDisplayState.Codes(emptyList()),
|
||||||
|
actionCard = ItemListingState.ActionCardState.DownloadBitwardenApp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescriptionAfterScroll(label = "Close")
|
||||||
|
.performClick()
|
||||||
|
verify { viewModel.trySendAction(ItemListingAction.DownloadBitwardenDismiss) }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking Move to Bitwarden should send MoveToBitwardenClick`() {
|
fun `clicking Move to Bitwarden should send MoveToBitwardenClick`() {
|
||||||
mutableStateFlow.value = DEFAULT_STATE.copy(
|
mutableStateFlow.value = DEFAULT_STATE.copy(
|
||||||
@ -350,3 +464,26 @@ private val DEFAULT_STATE = ItemListingState(
|
|||||||
),
|
),
|
||||||
dialog = null,
|
dialog = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper used to scroll to and get the matching node in a scrollable list. This is intended to
|
||||||
|
* be used with lazy lists that would otherwise fail when calling [performScrollToNode].
|
||||||
|
*/
|
||||||
|
fun ComposeContentTestRule.onNodeWithContentDescriptionAfterScroll(
|
||||||
|
label: String,
|
||||||
|
): SemanticsNodeInteraction {
|
||||||
|
onNode(hasScrollToNodeAction()).performScrollToNode(hasContentDescription(label))
|
||||||
|
return onNodeWithContentDescription(label)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper used to scroll to and get the matching node in a scrollable list. This is intended to
|
||||||
|
* be used with lazy lists that would otherwise fail when calling [performScrollToNode].
|
||||||
|
*/
|
||||||
|
fun ComposeContentTestRule.onNodeWithTextAfterScroll(
|
||||||
|
text: String,
|
||||||
|
substring: Boolean = false,
|
||||||
|
): SemanticsNodeInteraction {
|
||||||
|
onNode(hasScrollToNodeAction()).performScrollToNode(hasText(text, substring))
|
||||||
|
return onNodeWithText(text, substring)
|
||||||
|
}
|
||||||
|
|||||||
@ -368,6 +368,15 @@ class ItemListingViewModelTest : BaseViewModelTest() {
|
|||||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on SyncLearnMoreClick should send NavigateToSyncInformation`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(ItemListingAction.SyncLearnMoreClick)
|
||||||
|
assertEquals(ItemListingEvent.NavigateToSyncInformation, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on MoveToBitwardenClick receive should call startAddTotpLoginItemFlow`() {
|
fun `on MoveToBitwardenClick receive should call startAddTotpLoginItemFlow`() {
|
||||||
val expectedUriString = "expectedUriString"
|
val expectedUriString = "expectedUriString"
|
||||||
|
|||||||
@ -118,6 +118,15 @@ class SettingsScreenTest : BaseComposeTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on NavigateToSyncInformation receive launch sync totp uri`() {
|
||||||
|
every { intentManager.launchUri(uri = any()) } just runs
|
||||||
|
mutableEventFlow.tryEmit(SettingsEvent.NavigateToSyncInformation)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
intentManager.launchUri("https://bitwarden.com/help/totp-sync".toUri())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Default Save Option row should be hidden when showDefaultSaveOptionRow is false`() {
|
fun `Default Save Option row should be hidden when showDefaultSaveOptionRow is false`() {
|
||||||
mutableStateFlow.value = DEFAULT_STATE
|
mutableStateFlow.value = DEFAULT_STATE
|
||||||
|
|||||||
@ -157,6 +157,15 @@ class SettingsViewModelTest : BaseViewModelTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on SyncLearnMoreClick should emit NavigateToSyncInformation`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(SettingsAction.DataClick.SyncLearnMoreClick)
|
||||||
|
assertEquals(SettingsEvent.NavigateToSyncInformation, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
fun `Default save option row should only show when shared codes state shows syncing as enabled`() =
|
fun `Default save option row should only show when shared codes state shows syncing as enabled`() =
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user