diff --git a/app/libs/authenticatorbridge-0.1.0-SNAPSHOT-release.aar b/app/libs/authenticatorbridge-0.1.0-SNAPSHOT-release.aar index aa6f2e076a..defcb5273d 100644 Binary files a/app/libs/authenticatorbridge-0.1.0-SNAPSHOT-release.aar and b/app/libs/authenticatorbridge-0.1.0-SNAPSHOT-release.aar differ diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/di/AuthenticatorBridgeModule.kt b/app/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/di/AuthenticatorBridgeModule.kt index 924a7bfc21..30e7b7386a 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/di/AuthenticatorBridgeModule.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/di/AuthenticatorBridgeModule.kt @@ -51,6 +51,8 @@ object AuthenticatorBridgeModule { object : AuthenticatorBridgeManager { override val accountSyncStateFlow: StateFlow get() = MutableStateFlow(AccountSyncState.Loading) + + override fun startAddTotpLoginItemFlow(totpUri: String): Boolean = false } } diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt b/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt index 9604123db4..7c9373c9ee 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt @@ -237,6 +237,11 @@ fun ItemListingScreen( viewModel.trySendAction(ItemListingAction.SyncWithBitwardenDismiss) } }, + onMoveToBitwardenClick = remember(viewModel) { + { + viewModel.trySendAction(ItemListingAction.MoveToBitwardenClick(it)) + } + }, ) } @@ -342,6 +347,7 @@ private fun ItemListingContent( onItemClick: (String) -> Unit, onEditItemClick: (String) -> Unit, onDeleteItemClick: (String) -> Unit, + onMoveToBitwardenClick: (String) -> Unit, onDownloadBitwardenClick: () -> Unit, onDismissDownloadBitwardenClick: () -> Unit, onSyncWithBitwardenClick: () -> Unit, @@ -453,6 +459,8 @@ private fun ItemListingContent( onItemClick = { onItemClick(it.authCode) }, onEditItemClick = { onEditItemClick(it.id) }, onDeleteItemClick = { onDeleteItemClick(it.id) }, + onMoveToBitwardenClick = { onMoveToBitwardenClick(it.id) }, + showMoveToBitwarden = it.showMoveToBitwarden, allowLongPress = it.allowLongPressActions, modifier = Modifier.fillMaxWidth(), ) @@ -481,6 +489,8 @@ private fun ItemListingContent( onItemClick = { onItemClick(it.authCode) }, onEditItemClick = { onEditItemClick(it.id) }, onDeleteItemClick = { onDeleteItemClick(it.id) }, + onMoveToBitwardenClick = { onMoveToBitwardenClick(it.id) }, + showMoveToBitwarden = it.showMoveToBitwarden, allowLongPress = it.allowLongPressActions, modifier = Modifier.fillMaxWidth(), ) @@ -515,6 +525,8 @@ private fun ItemListingContent( onItemClick = { onItemClick(it.authCode) }, onEditItemClick = { }, onDeleteItemClick = { }, + onMoveToBitwardenClick = { }, + showMoveToBitwarden = it.showMoveToBitwarden, allowLongPress = it.allowLongPressActions, modifier = Modifier.fillMaxWidth(), ) diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt b/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt index 74ac31897e..c92e670775 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt @@ -28,8 +28,10 @@ import com.bitwarden.authenticator.ui.platform.base.BaseViewModel import com.bitwarden.authenticator.ui.platform.base.util.Text import com.bitwarden.authenticator.ui.platform.base.util.asText import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme +import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -46,6 +48,7 @@ import javax.inject.Inject @HiltViewModel class ItemListingViewModel @Inject constructor( private val authenticatorRepository: AuthenticatorRepository, + private val authenticatorBridgeManager: AuthenticatorBridgeManager, private val clipboardManager: BitwardenClipboardManager, private val encodingManager: BitwardenEncodingManager, private val settingsRepository: SettingsRepository, @@ -144,9 +147,14 @@ class ItemListingViewModel @Inject constructor( ItemListingAction.SyncWithBitwardenClick -> { handleSyncWithBitwardenClick() } + ItemListingAction.SyncWithBitwardenDismiss -> { handleSyncWithBitwardenDismiss() } + + is ItemListingAction.MoveToBitwardenClick -> { + handleMoveToBitwardenClick(action) + } } } @@ -167,6 +175,28 @@ class ItemListingViewModel @Inject constructor( sendEvent(ItemListingEvent.NavigateToEditItem(action.itemId)) } + private fun handleMoveToBitwardenClick(action: ItemListingAction.MoveToBitwardenClick) { + viewModelScope.launch { + val item = authenticatorRepository + .getItemStateFlow(action.entityId) + .first { it.data != null } + + val didLaunchAddTotpFlow = authenticatorBridgeManager.startAddTotpLoginItemFlow( + totpUri = item.data!!.toOtpAuthUriString(), + ) + if (!didLaunchAddTotpFlow) { + mutableStateFlow.update { + it.copy( + dialog = ItemListingState.DialogState.Error( + title = R.string.something_went_wrong.asText(), + message = R.string.please_try_again.asText(), + ), + ) + } + } + } + } + private fun handleDeleteItemClick(action: ItemListingAction.DeleteItemClick) { mutableStateFlow.update { it.copy( @@ -439,7 +469,9 @@ class ItemListingViewModel @Inject constructor( -> SharedCodesDisplayState.Codes(emptyList()) is SharedVerificationCodesState.Success -> - action.sharedCodesState.toSharedCodesDisplayState(state.alertThresholdSeconds) + action.sharedCodesState.toSharedCodesDisplayState( + alertThresholdSeconds = state.alertThresholdSeconds, + ) } if (localItems.isEmpty() && sharedItemsState.isEmpty()) { @@ -456,12 +488,20 @@ class ItemListingViewModel @Inject constructor( favoriteItems = localItems .filter { it.source is AuthenticatorItem.Source.Local && it.source.isFavorite } .map { - it.toDisplayItem(alertThresholdSeconds = state.alertThresholdSeconds) + it.toDisplayItem( + alertThresholdSeconds = state.alertThresholdSeconds, + sharedVerificationCodesState = + authenticatorRepository.sharedCodesStateFlow.value, + ) }, itemList = localItems .filter { it.source is AuthenticatorItem.Source.Local && !it.source.isFavorite } .map { - it.toDisplayItem(alertThresholdSeconds = state.alertThresholdSeconds) + it.toDisplayItem( + alertThresholdSeconds = state.alertThresholdSeconds, + sharedVerificationCodesState = + authenticatorRepository.sharedCodesStateFlow.value, + ) }, sharedItems = sharedItemsState, actionCard = action.sharedCodesState.toActionCard(), @@ -538,7 +578,7 @@ class ItemListingViewModel @Inject constructor( SharedVerificationCodesState.Loading, SharedVerificationCodesState.OsVersionNotSupported, is SharedVerificationCodesState.Success, - -> ItemListingState.ActionCardState.None + -> ItemListingState.ActionCardState.None } private fun String.toAuthenticatorEntityOrNull(): AuthenticatorItemEntity? { @@ -818,6 +858,11 @@ sealed class ItemListingAction { */ data object SyncWithBitwardenDismiss : ItemListingAction() + /** + * The user clicked the "Move to Bitwarden" action on a local verification item. + */ + data class MoveToBitwardenClick(val entityId: String) : ItemListingAction() + /** * Models actions that [ItemListingScreen] itself may send. */ diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/VaultVerificationCodeItem.kt b/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/VaultVerificationCodeItem.kt index 0d4f021b3b..d3413331e6 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/VaultVerificationCodeItem.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/VaultVerificationCodeItem.kt @@ -64,7 +64,9 @@ fun VaultVerificationCodeItem( onItemClick: () -> Unit, onEditItemClick: () -> Unit, onDeleteItemClick: () -> Unit, + onMoveToBitwardenClick: () -> Unit, allowLongPress: Boolean, + showMoveToBitwarden: Boolean, modifier: Modifier = Modifier, ) { var shouldShowDropdownMenu by remember { mutableStateOf(value = false) } @@ -168,6 +170,24 @@ fun VaultVerificationCodeItem( ) }, ) + if (showMoveToBitwarden) { + HorizontalDivider() + DropdownMenuItem( + text = { + Text(text = stringResource(id = R.string.move_to_bitwarden)) + }, + onClick = { + shouldShowDropdownMenu = false + onMoveToBitwardenClick() + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_right), + contentDescription = stringResource(id = R.string.move_to_bitwarden), + ) + }, + ) + } HorizontalDivider() DropdownMenuItem( text = { @@ -204,8 +224,10 @@ private fun VerificationCodeItem_preview() { onItemClick = {}, onEditItemClick = {}, onDeleteItemClick = {}, + onMoveToBitwardenClick = {}, allowLongPress = true, modifier = Modifier.padding(horizontal = 16.dp), + showMoveToBitwarden = true, ) } } diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/model/VerificationCodeDisplayItem.kt b/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/model/VerificationCodeDisplayItem.kt index 3b312c8b54..52485b8ed6 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/model/VerificationCodeDisplayItem.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/model/VerificationCodeDisplayItem.kt @@ -20,4 +20,5 @@ data class VerificationCodeDisplayItem( val startIcon: IconData = IconData.Local(R.drawable.ic_login_item), val favorite: Boolean, val allowLongPressActions: Boolean, + val showMoveToBitwarden: Boolean, ) : Parcelable diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/SharedVerificationCodesStateExtensions.kt b/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/SharedVerificationCodesStateExtensions.kt index d27478149e..0420d318fc 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/SharedVerificationCodesStateExtensions.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/SharedVerificationCodesStateExtensions.kt @@ -19,7 +19,14 @@ fun SharedVerificationCodesState.Success.toSharedCodesDisplayState( // codes for that account: this.items.forEach { codesMap.putIfAbsent(it.source as AuthenticatorItem.Source.Shared, mutableListOf()) - codesMap[it.source]?.add(it.toDisplayItem(alertThresholdSeconds)) + codesMap[it.source]?.add( + it.toDisplayItem( + alertThresholdSeconds = alertThresholdSeconds, + // Always map based on Error state, because shared codes will never + // show "Move to Bitwarden" action. + sharedVerificationCodesState = SharedVerificationCodesState.Error, + ), + ) } // Flatten that map down to a list of accounts that each has a list of codes: return codesMap diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/VerificationCodeItemExtensions.kt b/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/VerificationCodeItemExtensions.kt index e4896cd877..083eede277 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/VerificationCodeItemExtensions.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/VerificationCodeItemExtensions.kt @@ -2,23 +2,43 @@ package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util import com.bitwarden.authenticator.data.authenticator.manager.model.VerificationCodeItem import com.bitwarden.authenticator.data.authenticator.repository.model.AuthenticatorItem +import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVerificationCodesState import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VerificationCodeDisplayItem /** * Converts [VerificationCodeItem] to a [VerificationCodeDisplayItem]. */ -fun VerificationCodeItem.toDisplayItem(alertThresholdSeconds: Int) = - VerificationCodeDisplayItem( - id = id, - issuer = issuer, - label = accountName, - timeLeftSeconds = timeLeftSeconds, - periodSeconds = periodSeconds, - alertThresholdSeconds = alertThresholdSeconds, - authCode = code, - allowLongPressActions = when (source) { - is AuthenticatorItem.Source.Local -> true - is AuthenticatorItem.Source.Shared -> false - }, - favorite = (source as? AuthenticatorItem.Source.Local)?.isFavorite ?: false, - ) +fun VerificationCodeItem.toDisplayItem( + alertThresholdSeconds: Int, + sharedVerificationCodesState: SharedVerificationCodesState, +) = VerificationCodeDisplayItem( + id = id, + issuer = issuer, + label = accountName, + timeLeftSeconds = timeLeftSeconds, + periodSeconds = periodSeconds, + alertThresholdSeconds = alertThresholdSeconds, + authCode = code, + allowLongPressActions = when (source) { + is AuthenticatorItem.Source.Local -> true + is AuthenticatorItem.Source.Shared -> false + }, + favorite = (source as? AuthenticatorItem.Source.Local)?.isFavorite ?: false, + showMoveToBitwarden = when (source) { + // Shared items should never show Move to Bitwarden action: + is AuthenticatorItem.Source.Shared -> false + + // Local items should only show Move to Bitwarden if we are successfully syncing: = + is AuthenticatorItem.Source.Local -> when (sharedVerificationCodesState) { + SharedVerificationCodesState.AppNotInstalled, + SharedVerificationCodesState.Error, + SharedVerificationCodesState.FeatureNotEnabled, + SharedVerificationCodesState.Loading, + SharedVerificationCodesState.OsVersionNotSupported, + SharedVerificationCodesState.SyncNotEnabled, + -> false + + is SharedVerificationCodesState.Success -> true + } + }, +) diff --git a/app/src/main/res/drawable/ic_arrow_right.xml b/app/src/main/res/drawable/ic_arrow_right.xml new file mode 100644 index 0000000000..1df294a97a --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 06dbf5d82f..290a74c091 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -130,4 +130,7 @@ Sync with the Bitwarden app Go to settings Allow Authenticator app syncing in settings to view all of your verification codes here. + Something went wrong + Please try again + Move to Bitwarden diff --git a/app/src/test/java/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListViewModelTest.kt b/app/src/test/java/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListViewModelTest.kt index 2f88b3a574..16033a11f0 100644 --- a/app/src/test/java/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListViewModelTest.kt +++ b/app/src/test/java/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListViewModelTest.kt @@ -1,6 +1,8 @@ package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting import app.cash.turbine.test +import com.bitwarden.authenticator.R +import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity import com.bitwarden.authenticator.data.authenticator.manager.model.VerificationCodeItem import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository import com.bitwarden.authenticator.data.authenticator.repository.model.AuthenticatorItem @@ -13,7 +15,9 @@ import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.Sh import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util.toDisplayItem import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util.toSharedCodesDisplayState import com.bitwarden.authenticator.ui.platform.base.BaseViewModelTest +import com.bitwarden.authenticator.ui.platform.base.util.asText import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme +import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager import io.mockk.every import io.mockk.just import io.mockk.mockk @@ -40,6 +44,7 @@ class ItemListViewModelTest : BaseViewModelTest() { every { getLocalVerificationCodesFlow() } returns mutableVerificationCodesFlow every { sharedCodesStateFlow } returns mutableSharedCodesFlow } + private val authenticatorBridgeManager: AuthenticatorBridgeManager = mockk() private val clipboardManager: BitwardenClipboardManager = mockk() private val encodingManager: BitwardenEncodingManager = mockk() private val settingsRepository: SettingsRepository = mockk { @@ -144,12 +149,12 @@ class ItemListViewModelTest : BaseViewModelTest() { @Test @Suppress("MaxLineLength") - fun `stateFlow sharedItems value should be Codes with empty list when shared state is Error `() { + fun `stateFlow sharedItems value should be Codes with empty list when shared state is Success `() { val expectedState = DEFAULT_STATE.copy( viewState = ItemListingState.ViewState.Content( actionCard = ItemListingState.ActionCardState.None, - favoriteItems = LOCAL_FAVORITE_ITEMS, - itemList = LOCAL_NON_FAVORITE_ITEMS, + favoriteItems = LOCAL_FAVORITE_ITEMS.map { it.copy(showMoveToBitwarden = true) }, + itemList = LOCAL_NON_FAVORITE_ITEMS.map { it.copy(showMoveToBitwarden = true) }, sharedItems = SHARED_DISPLAY_ITEMS, ), ) @@ -356,8 +361,57 @@ class ItemListViewModelTest : BaseViewModelTest() { assertEquals(expectedState, viewModel.stateFlow.value) } + @Test + fun `on MoveToBitwardenClick receive should call startAddTotpLoginItemFlow`() { + val expectedUriString = "expectedUriString" + val entity: AuthenticatorItemEntity = mockk { + every { toOtpAuthUriString() } returns expectedUriString + } + every { + authenticatorRepository.getItemStateFlow("1") + } returns MutableStateFlow(DataState.Loaded(data = entity)) + every { + authenticatorBridgeManager.startAddTotpLoginItemFlow(expectedUriString) + } returns true + + val viewModel = createViewModel() + + viewModel.trySendAction(ItemListingAction.MoveToBitwardenClick(entityId = "1")) + verify { authenticatorBridgeManager.startAddTotpLoginItemFlow(expectedUriString) } + } + + @Test + @Suppress("MaxLineLength") + fun `on MoveToBitwardenClick should show error dialog when startAddTotpLoginItemFlow returns false`() { + val expectedState = DEFAULT_STATE.copy( + dialog = ItemListingState.DialogState.Error( + title = R.string.something_went_wrong.asText(), + message = R.string.please_try_again.asText(), + ), + ) + val expectedUriString = "expectedUriString" + val entity: AuthenticatorItemEntity = mockk { + every { toOtpAuthUriString() } returns expectedUriString + } + every { + authenticatorRepository.getItemStateFlow("1") + } returns MutableStateFlow(DataState.Loaded(data = entity)) + every { + authenticatorBridgeManager.startAddTotpLoginItemFlow(expectedUriString) + } returns false + + val viewModel = createViewModel() + viewModel.trySendAction(ItemListingAction.MoveToBitwardenClick(entityId = "1")) + assertEquals( + expectedState, + viewModel.stateFlow.value, + ) + verify { authenticatorBridgeManager.startAddTotpLoginItemFlow(expectedUriString) } + } + private fun createViewModel() = ItemListingViewModel( authenticatorRepository = authenticatorRepository, + authenticatorBridgeManager = authenticatorBridgeManager, clipboardManager = clipboardManager, encodingManager = encodingManager, settingsRepository = settingsRepository, @@ -415,7 +469,10 @@ private val SHARED_VERIFICATION_ITEMS = listOf( ) private val LOCAL_DISPLAY_ITEMS = LOCAL_VERIFICATION_ITEMS.map { - it.toDisplayItem(AUTHENTICATOR_ALERT_SECONDS) + it.toDisplayItem( + AUTHENTICATOR_ALERT_SECONDS, + SharedVerificationCodesState.AppNotInstalled, + ) } private val SHARED_DISPLAY_ITEMS = SharedVerificationCodesState.Success(SHARED_VERIFICATION_ITEMS) diff --git a/app/src/test/java/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreenTest.kt b/app/src/test/java/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreenTest.kt index 93e1f82c02..0e6136006c 100644 --- a/app/src/test/java/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreenTest.kt +++ b/app/src/test/java/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreenTest.kt @@ -199,11 +199,64 @@ class ItemListingScreenTest : BaseComposeTest() { .performClick() verify { viewModel.trySendAction(ItemListingAction.SyncWithBitwardenDismiss) } } + + @Test + fun `clicking Move to Bitwarden should send MoveToBitwardenClick`() { + mutableStateFlow.value = DEFAULT_STATE.copy( + viewState = ItemListingState.ViewState.Content( + actionCard = ItemListingState.ActionCardState.None, + favoriteItems = emptyList(), + itemList = listOf(LOCAL_CODE), + sharedItems = SharedCodesDisplayState.Error, + ), + ) + composeTestRule + .onNodeWithText("issuer") + .performTouchInput { longClick() } + + composeTestRule + .onNodeWithText("Move to Bitwarden") + .performClick() + + verify { viewModel.trySendAction(ItemListingAction.MoveToBitwardenClick("1")) } + } + + @Test + fun `Move to Bitwarden long press action should not show when showMoveToBitwarden is false`() { + mutableStateFlow.value = DEFAULT_STATE.copy( + viewState = ItemListingState.ViewState.Content( + actionCard = ItemListingState.ActionCardState.None, + favoriteItems = emptyList(), + itemList = listOf(LOCAL_CODE.copy(showMoveToBitwarden = false)), + sharedItems = SharedCodesDisplayState.Error, + ), + ) + composeTestRule + .onNodeWithText("issuer") + .performTouchInput { longClick() } + + composeTestRule + .onNodeWithText("Move to Bitwarden") + .assertDoesNotExist() + } } private val APP_THEME = AppTheme.DEFAULT private const val ALERT_THRESHOLD = 7 +private val LOCAL_CODE = VerificationCodeDisplayItem( + id = "1", + issuer = "issuer", + label = null, + timeLeftSeconds = 10, + periodSeconds = 30, + alertThresholdSeconds = 7, + authCode = "123456", + favorite = false, + allowLongPressActions = true, + showMoveToBitwarden = true, +) + private val SHARED_ACCOUNTS_SECTION = SharedCodesDisplayState.SharedCodesAccountSection( label = "test@test.com".asText(), codes = listOf( @@ -217,6 +270,7 @@ private val SHARED_ACCOUNTS_SECTION = SharedCodesDisplayState.SharedCodesAccount authCode = "123456", favorite = false, allowLongPressActions = false, + showMoveToBitwarden = false, ), ), ) diff --git a/app/src/test/java/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/SharedVerificationCodesStateTest.kt b/app/src/test/java/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/SharedVerificationCodesStateTest.kt index 0223ef1563..f0d9b8a11d 100644 --- a/app/src/test/java/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/SharedVerificationCodesStateTest.kt +++ b/app/src/test/java/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/SharedVerificationCodesStateTest.kt @@ -76,6 +76,7 @@ class SharedVerificationCodesStateTest { favorite = false, allowLongPressActions = false, alertThresholdSeconds = ALERT_THRESHOLD, + showMoveToBitwarden = false, ), ), ), @@ -95,6 +96,7 @@ class SharedVerificationCodesStateTest { favorite = false, allowLongPressActions = false, alertThresholdSeconds = ALERT_THRESHOLD, + showMoveToBitwarden = false, ), ), ), diff --git a/app/src/test/java/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/VerificationCodeItemExtensionsTest.kt b/app/src/test/java/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/VerificationCodeItemExtensionsTest.kt index 51e4896c71..8e2d4409a7 100644 --- a/app/src/test/java/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/VerificationCodeItemExtensionsTest.kt +++ b/app/src/test/java/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/util/VerificationCodeItemExtensionsTest.kt @@ -2,6 +2,7 @@ package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util import com.bitwarden.authenticator.data.authenticator.manager.util.createMockVerificationCodeItem import com.bitwarden.authenticator.data.authenticator.repository.model.AuthenticatorItem +import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVerificationCodesState import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VerificationCodeDisplayItem import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -24,6 +25,7 @@ class VerificationCodeItemExtensionsTest { authCode = favoriteItem.code, favorite = (favoriteItem.source as AuthenticatorItem.Source.Local).isFavorite, allowLongPressActions = true, + showMoveToBitwarden = false, ) val expectedNonFavoriteItem = VerificationCodeDisplayItem( @@ -36,10 +38,97 @@ class VerificationCodeItemExtensionsTest { authCode = nonFavoriteItem.code, favorite = (nonFavoriteItem.source as AuthenticatorItem.Source.Local).isFavorite, allowLongPressActions = true, + showMoveToBitwarden = false, ) - assertEquals(expectedFavoriteItem, favoriteItem.toDisplayItem(alertThresholdSeconds)) - assertEquals(expectedNonFavoriteItem, nonFavoriteItem.toDisplayItem(alertThresholdSeconds)) + assertEquals( + expectedFavoriteItem, + favoriteItem.toDisplayItem( + alertThresholdSeconds = alertThresholdSeconds, + sharedVerificationCodesState = SharedVerificationCodesState.Error, + ), + ) + assertEquals( + expectedNonFavoriteItem, + nonFavoriteItem.toDisplayItem( + alertThresholdSeconds = alertThresholdSeconds, + sharedVerificationCodesState = SharedVerificationCodesState.Error, + ), + ) + } + + @Test + @Suppress("MaxLineLength") + fun `toDisplayItem should only showMoveToBitwarden when SharedVerificationCodesState is Success`() { + val alertThresholdSeconds = 7 + val item = createMockVerificationCodeItem(1) + val expectedDontShowMoveToBitwardenItem = + VerificationCodeDisplayItem( + id = item.id, + issuer = item.issuer, + label = item.accountName, + timeLeftSeconds = item.timeLeftSeconds, + periodSeconds = item.periodSeconds, + alertThresholdSeconds = alertThresholdSeconds, + authCode = item.code, + favorite = false, + allowLongPressActions = true, + showMoveToBitwarden = false, + ) + + assertEquals( + expectedDontShowMoveToBitwardenItem, + item.toDisplayItem( + alertThresholdSeconds = alertThresholdSeconds, + sharedVerificationCodesState = SharedVerificationCodesState.AppNotInstalled, + ), + ) + assertEquals( + expectedDontShowMoveToBitwardenItem, + item.toDisplayItem( + alertThresholdSeconds = alertThresholdSeconds, + sharedVerificationCodesState = SharedVerificationCodesState.Error, + ), + ) + assertEquals( + expectedDontShowMoveToBitwardenItem, + item.toDisplayItem( + alertThresholdSeconds = alertThresholdSeconds, + sharedVerificationCodesState = SharedVerificationCodesState.FeatureNotEnabled, + ), + ) + assertEquals( + expectedDontShowMoveToBitwardenItem, + item.toDisplayItem( + alertThresholdSeconds = alertThresholdSeconds, + sharedVerificationCodesState = SharedVerificationCodesState.Loading, + ), + ) + assertEquals( + expectedDontShowMoveToBitwardenItem, + item.toDisplayItem( + alertThresholdSeconds = alertThresholdSeconds, + sharedVerificationCodesState = SharedVerificationCodesState.OsVersionNotSupported, + ), + ) + assertEquals( + expectedDontShowMoveToBitwardenItem, + item.toDisplayItem( + alertThresholdSeconds = alertThresholdSeconds, + sharedVerificationCodesState = SharedVerificationCodesState.SyncNotEnabled, + ), + ) + + val expectedShouldShowMoveToBitwardenItem = expectedDontShowMoveToBitwardenItem.copy( + showMoveToBitwarden = true, + ) + assertEquals( + expectedShouldShowMoveToBitwardenItem, + item.toDisplayItem( + alertThresholdSeconds = alertThresholdSeconds, + sharedVerificationCodesState = SharedVerificationCodesState.Success(emptyList()), + ), + ) } @Test @@ -65,8 +154,15 @@ class VerificationCodeItemExtensionsTest { authCode = favoriteItem.code, favorite = false, allowLongPressActions = false, + showMoveToBitwarden = false, ) - assertEquals(expectedFavoriteItem, favoriteItem.toDisplayItem(alertThresholdSeconds)) + assertEquals( + expectedFavoriteItem, + favoriteItem.toDisplayItem( + alertThresholdSeconds = alertThresholdSeconds, + sharedVerificationCodesState = SharedVerificationCodesState.Error, + ), + ) } }