diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt index fd25957f01..032c26a7f1 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt @@ -8,16 +8,21 @@ import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FabPosition 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.BitwardenTopAppBar 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.BitwardenTextButton 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.BitwardenBasicDialog @@ -139,6 +146,10 @@ fun ItemListingScreen( ) } + ItemListingEvent.NavigateToSyncInformation -> { + intentManager.launchUri("https://bitwarden.com/help/totp-sync".toUri()) + } + ItemListingEvent.NavigateToBitwardenSettings -> { intentManager.startMainBitwardenAppAccountSettings() } @@ -230,6 +241,9 @@ fun ItemListingScreen( viewModel.trySendAction(ItemListingAction.SyncWithBitwardenDismiss) } }, + onSyncLearnMoreClick = remember(viewModel) { + { viewModel.trySendAction(ItemListingAction.SyncLearnMoreClick) } + }, ) } @@ -270,6 +284,9 @@ fun ItemListingScreen( viewModel.trySendAction(ItemListingAction.SyncWithBitwardenClick) } }, + onSyncLearnMoreClick = remember(viewModel) { + { viewModel.trySendAction(ItemListingAction.SyncLearnMoreClick) } + }, onDismissSyncWithBitwardenClick = remember(viewModel) { { viewModel.trySendAction(ItemListingAction.SyncWithBitwardenDismiss) @@ -339,6 +356,7 @@ private fun ItemListingContent( onDismissDownloadBitwardenClick: () -> Unit, onSyncWithBitwardenClick: () -> Unit, onDismissSyncWithBitwardenClick: () -> Unit, + onSyncLearnMoreClick: () -> Unit, ) { BitwardenScaffold( modifier = Modifier @@ -402,23 +420,15 @@ private fun ItemListingContent( ) { LazyColumn { item { - when (state.actionCard) { - ItemListingState.ActionCardState.DownloadBitwardenApp -> - DownloadBitwardenActionCard( - modifier = Modifier.padding(horizontal = 16.dp), - onDownloadBitwardenClick = onDownloadBitwardenClick, - onDismissClick = onDismissDownloadBitwardenClick, - ) - - ItemListingState.ActionCardState.SyncWithBitwarden -> - SyncWithBitwardenActionCard( - modifier = Modifier.padding(16.dp), - onSyncWithBitwardenClick = onSyncWithBitwardenClick, - onDismissClick = onDismissSyncWithBitwardenClick, - ) - - ItemListingState.ActionCardState.None -> Unit - } + ActionCard( + actionCardState = state.actionCard, + onDownloadBitwardenClick = onDownloadBitwardenClick, + onDownloadBitwardenDismissClick = onDismissDownloadBitwardenClick, + onSyncWithBitwardenClick = onSyncWithBitwardenClick, + onSyncWithBitwardenDismissClick = onDismissSyncWithBitwardenClick, + onSyncLearnMoreClick = onSyncLearnMoreClick, + modifier = Modifier.padding(all = 16.dp), + ) } if (state.favoriteItems.isNotEmpty()) { item { @@ -572,6 +582,7 @@ fun EmptyItemListingContent( onDownloadBitwardenClick: () -> Unit, onDismissDownloadBitwardenClick: () -> Unit, onSyncWithBitwardenClick: () -> Unit, + onSyncLearnMoreClick: () -> Unit, onDismissSyncWithBitwardenClick: () -> Unit, ) { BitwardenScaffold( @@ -635,23 +646,14 @@ fun EmptyItemListingContent( ItemListingState.ActionCardState.SyncWithBitwarden -> Arrangement.Top }, ) { - when (actionCardState) { - ItemListingState.ActionCardState.DownloadBitwardenApp -> - DownloadBitwardenActionCard( - modifier = Modifier.padding(16.dp), - onDismissClick = onDismissDownloadBitwardenClick, - onDownloadBitwardenClick = onDownloadBitwardenClick, - ) - - ItemListingState.ActionCardState.SyncWithBitwarden -> - SyncWithBitwardenActionCard( - modifier = Modifier.padding(16.dp), - onDismissClick = onDismissSyncWithBitwardenClick, - onSyncWithBitwardenClick = onSyncWithBitwardenClick, - ) - - ItemListingState.ActionCardState.None -> Unit - } + ActionCard( + actionCardState = actionCardState, + onDownloadBitwardenClick = onDownloadBitwardenClick, + onDownloadBitwardenDismissClick = onDismissDownloadBitwardenClick, + onSyncWithBitwardenClick = onSyncWithBitwardenClick, + onSyncWithBitwardenDismissClick = onDismissSyncWithBitwardenClick, + onSyncLearnMoreClick = onSyncLearnMoreClick, + ) // Add a spacer if an action card is showing: when (actionCardState) { @@ -735,32 +737,114 @@ private fun DownloadBitwardenActionCard( }, ) +@Suppress("LongMethod") @Composable private fun SyncWithBitwardenActionCard( modifier: Modifier = Modifier, onDismissClick: () -> Unit, + onAppSettingsClick: () -> Unit, + onLearnMoreClick: () -> Unit, +) { + Card( + modifier = modifier, + shape = RoundedCornerShape(size = 16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + disabledContainerColor = MaterialTheme.colorScheme.surfaceContainer, + ), + elevation = CardDefaults.elevatedCardElevation(), + ) { + 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( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = stringResource(id = R.string.close), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(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, -) = BitwardenActionCard( - modifier = modifier, - actionIcon = rememberVectorPainter(R.drawable.ic_refresh), - actionText = stringResource(R.string.sync_with_bitwarden_action_card_message), - callToActionText = stringResource(R.string.go_to_settings), - titleText = stringResource(R.string.sync_with_the_bitwarden_app), - onCardClicked = onSyncWithBitwardenClick, - trailingContent = { - IconButton( - onClick = onDismissClick, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_close), - contentDescription = stringResource(id = R.string.close), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .size(24.dp), + 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) @Composable @@ -776,6 +860,7 @@ private fun EmptyListingContentPreview() { onDownloadBitwardenClick = { }, onDismissDownloadBitwardenClick = { }, onSyncWithBitwardenClick = { }, + onSyncLearnMoreClick = { }, onDismissSyncWithBitwardenClick = { }, ) } diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt index 92d81865d4..484c3a5a08 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt @@ -153,6 +153,10 @@ class ItemListingViewModel @Inject constructor( ItemListingAction.SyncWithBitwardenDismiss -> { 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. */ @@ -794,6 +802,11 @@ sealed class ItemListingEvent { */ data object NavigateToAppSettings : ItemListingEvent() + /** + * Navigate to the sync information web page. + */ + data object NavigateToSyncInformation : ItemListingEvent() + /** * Navigate to Bitwarden play store listing. */ @@ -872,6 +885,11 @@ sealed class 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. */ @@ -886,7 +904,7 @@ sealed class ItemListingAction { * Represents an action triggered when the user clicks an item in 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( val menuAction: VaultDropdownMenuAction, diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/card/BitwardenActionCard.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/card/BitwardenActionCard.kt index 8afc4a6466..b327fad440 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/card/BitwardenActionCard.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/card/BitwardenActionCard.kt @@ -44,8 +44,8 @@ fun BitwardenActionCard( onClick = onCardClicked, shape = RoundedCornerShape(size = 16.dp), colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, - disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + containerColor = MaterialTheme.colorScheme.surfaceContainer, + disabledContainerColor = MaterialTheme.colorScheme.surfaceContainer, ), modifier = modifier, elevation = CardDefaults.elevatedCardElevation(), diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/row/BitwardenTextRow.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/row/BitwardenTextRow.kt index 637458df8d..0a33a63f9f 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/row/BitwardenTextRow.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/row/BitwardenTextRow.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp /** @@ -37,7 +38,7 @@ fun BitwardenTextRow( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, - description: String? = null, + description: AnnotatedString? = null, withDivider: Boolean = false, content: (@Composable () -> Unit)? = null, ) { diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreen.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreen.kt index 2ee4d7f8bb..f254c18c3a 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreen.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreen.kt @@ -65,6 +65,8 @@ import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme import com.bitwarden.authenticator.ui.platform.util.displayLabel import com.bitwarden.ui.platform.base.util.EventsEffect import com.bitwarden.ui.platform.base.util.mirrorIfRtl +import com.bitwarden.ui.platform.base.util.spanStyleOf +import com.bitwarden.ui.platform.base.util.toAnnotatedString import com.bitwarden.ui.platform.components.util.rememberVectorPainter import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme import com.bitwarden.ui.util.Text @@ -107,6 +109,10 @@ fun SettingsScreen( intentManager.launchUri("https://bitwarden.com/privacy".toUri()) } + SettingsEvent.NavigateToSyncInformation -> { + intentManager.launchUri("https://bitwarden.com/help/totp-sync".toUri()) + } + SettingsEvent.NavigateToBitwardenApp -> { intentManager.startActivity( @@ -177,6 +183,9 @@ fun SettingsScreen( viewModel.trySendAction(SettingsAction.DataClick.SyncWithBitwardenClick) } }, + onSyncLearnMoreClick = remember(viewModel) { + { viewModel.trySendAction(SettingsAction.DataClick.SyncLearnMoreClick) } + }, onDefaultSaveOptionUpdated = remember(viewModel) { { viewModel.trySendAction( @@ -280,6 +289,7 @@ private fun VaultSettings( onImportClick: () -> Unit, onBackupClick: () -> Unit, onSyncWithBitwardenClick: () -> Unit, + onSyncLearnMoreClick: () -> Unit, onDefaultSaveOptionUpdated: (DefaultSaveOption) -> Unit, shouldShowSyncWithBitwardenApp: Boolean, shouldShowDefaultSaveOptions: Boolean, @@ -339,6 +349,22 @@ private fun VaultSettings( Spacer(modifier = Modifier.height(8.dp)) BitwardenTextRow( 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, modifier = modifier, withDivider = true, diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModel.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModel.kt index 62b9afaf0d..e42e7c9d0f 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModel.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModel.kt @@ -184,6 +184,7 @@ class SettingsViewModel @Inject constructor( SettingsAction.DataClick.ImportClick -> handleImportClick() SettingsAction.DataClick.BackupClick -> handleBackupClick() SettingsAction.DataClick.SyncWithBitwardenClick -> handleSyncWithBitwardenClick() + SettingsAction.DataClick.SyncLearnMoreClick -> handleSyncLearnMoreClick() is SettingsAction.DataClick.DefaultSaveOptionUpdated -> handleDefaultSaveOptionChosen(action) } @@ -215,6 +216,10 @@ class SettingsViewModel @Inject constructor( } } + private fun handleSyncLearnMoreClick() { + sendEvent(SettingsEvent.NavigateToSyncInformation) + } + private fun handleExportClick() { sendEvent(SettingsEvent.NavigateToExport) } @@ -424,6 +429,11 @@ sealed class SettingsEvent { */ data object NavigateToPrivacyPolicy : SettingsEvent() + /** + * Navigate to the sync learn more web page. + */ + data object NavigateToSyncInformation : SettingsEvent() + /** * Navigate to the Bitwarden account settings. */ @@ -491,7 +501,12 @@ sealed class SettingsAction( 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() } diff --git a/authenticator/src/main/res/values/strings.xml b/authenticator/src/main/res/values/strings.xml index 148f630599..6bf18030ea 100644 --- a/authenticator/src/main/res/values/strings.xml +++ b/authenticator/src/main/res/values/strings.xml @@ -125,11 +125,13 @@ Store all of your logins and sync verification codes directly with the Authenticator app. Download now Sync with Bitwarden app + This feature is not yet available for self-hosted users. Learn more 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. %1$s | %2$s Sync with the Bitwarden app Go to settings - Allow Authenticator app syncing in settings to view all of your verification codes here. + In order to view all of your verification codes, you’ll need to allow for syncing on all of your accounts. + Take me to the app settings Something went wrong Please try again Move to Bitwarden diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreenTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreenTest.kt index e82188df41..0c83ff28c5 100644 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreenTest.kt +++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreenTest.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo 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.VaultDropdownMenuAction import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VerificationCodeDisplayItem @@ -17,6 +18,8 @@ import com.bitwarden.authenticator.ui.platform.manager.permissions.FakePermissio import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme import com.bitwarden.ui.util.asText +import com.bitwarden.ui.util.onNodeWithContentDescriptionAfterScroll +import com.bitwarden.ui.util.onNodeWithTextAfterScroll import io.mockk.every import io.mockk.just import io.mockk.mockk @@ -44,7 +47,9 @@ class ItemListingScreenTest : AuthenticatorComposeTest() { 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() @Before @@ -64,6 +69,14 @@ class ItemListingScreenTest : AuthenticatorComposeTest() { } } + @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 @Suppress("MaxLineLength") fun `when denying camera permissions and attempting to add a code we should be shown the manual entry screen`() { @@ -175,35 +188,74 @@ class ItemListingScreenTest : AuthenticatorComposeTest() { @Test @Suppress("MaxLineLength") - fun `on sync with bitwarden action card click in empty state should send SyncWithBitwardenClick`() { - mutableStateFlow.value = DEFAULT_STATE.copy( - viewState = ItemListingState.ViewState.NoItems( - actionCard = ItemListingState.ActionCardState.SyncWithBitwarden, - ), - ) + fun `on sync with bitwarden app settings click in empty state should send SyncWithBitwardenClick`() { + mutableStateFlow.update { + it.copy( + viewState = ItemListingState.ViewState.NoItems( + actionCard = ItemListingState.ActionCardState.SyncWithBitwarden, + ), + ) + } + composeTestRule - .onNodeWithText("Sync with the Bitwarden app") + .onNodeWithText(text = "Take me to the app settings") .performClick() verify { viewModel.trySendAction(ItemListingAction.SyncWithBitwardenClick) } } @Test - @Suppress("MaxLineLength") - fun `on sync with bitwarden action card click in full state should send SyncWithBitwardenClick`() { - mutableStateFlow.value = DEFAULT_STATE.copy( - viewState = ItemListingState.ViewState.Content( - favoriteItems = emptyList(), - itemList = emptyList(), - sharedItems = SharedCodesDisplayState.Codes(emptyList()), - actionCard = ItemListingState.ActionCardState.SyncWithBitwarden, - ), - ) + 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("Sync with the Bitwarden app") + .onNodeWithText(text = "Learn more") + .performClick() + verify { viewModel.trySendAction(ItemListingAction.SyncLearnMoreClick) } + } + + @Test + @Suppress("MaxLineLength") + fun `on sync with bitwarden app settings click in full state should send SyncWithBitwardenClick`() { + mutableStateFlow.update { + it.copy( + viewState = ItemListingState.ViewState.Content( + favoriteItems = emptyList(), + itemList = emptyList(), + sharedItems = SharedCodesDisplayState.Codes(emptyList()), + actionCard = ItemListingState.ActionCardState.SyncWithBitwarden, + ), + ) + } + composeTestRule + .onNodeWithTextAfterScroll(text = "Take me to the app settings") .performClick() 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 @Suppress("MaxLineLength") fun `on sync with bitwarden action card dismiss in empty state should send SyncWithBitwardenDismiss`() { @@ -235,6 +287,64 @@ class ItemListingScreenTest : AuthenticatorComposeTest() { 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 fun `clicking Move to Bitwarden should send MoveToBitwardenClick`() { mutableStateFlow.value = DEFAULT_STATE.copy( diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModelTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModelTest.kt index 07b9f08b6c..12f7207950 100644 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModelTest.kt +++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModelTest.kt @@ -368,6 +368,15 @@ class ItemListingViewModelTest : BaseViewModelTest() { 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 fun `on MoveToBitwardenClick receive should call startAddTotpLoginItemFlow`() { val expectedUriString = "expectedUriString" diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreenTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreenTest.kt index b0ee4d4dd5..05ad4dd5c3 100644 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreenTest.kt +++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreenTest.kt @@ -119,6 +119,15 @@ class SettingsScreenTest : AuthenticatorComposeTest() { } } + @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 fun `Default Save Option row should be hidden when showDefaultSaveOptionRow is false`() { mutableStateFlow.value = DEFAULT_STATE diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModelTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModelTest.kt index 8b05482254..0fa129d8be 100644 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModelTest.kt +++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModelTest.kt @@ -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 @Suppress("MaxLineLength") fun `Default save option row should only show when shared codes state shows syncing as enabled`() =