mirror of
https://github.com/bitwarden/android.git
synced 2026-04-29 20:38:41 -05:00
BITAU-82 Show shared codes on the item listing screen (#241)
This commit is contained in:
@@ -51,6 +51,7 @@ 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
|
||||||
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.ItemListingExpandableFabAction
|
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.ItemListingExpandableFabAction
|
||||||
|
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.SharedCodesDisplayState
|
||||||
import com.bitwarden.authenticator.ui.platform.base.util.EventsEffect
|
import com.bitwarden.authenticator.ui.platform.base.util.EventsEffect
|
||||||
import com.bitwarden.authenticator.ui.platform.base.util.asText
|
import com.bitwarden.authenticator.ui.platform.base.util.asText
|
||||||
import com.bitwarden.authenticator.ui.platform.components.appbar.BitwardenMediumTopAppBar
|
import com.bitwarden.authenticator.ui.platform.components.appbar.BitwardenMediumTopAppBar
|
||||||
@@ -65,6 +66,7 @@ import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenTwoBut
|
|||||||
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
|
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
|
||||||
import com.bitwarden.authenticator.ui.platform.components.fab.ExpandableFabIcon
|
import com.bitwarden.authenticator.ui.platform.components.fab.ExpandableFabIcon
|
||||||
import com.bitwarden.authenticator.ui.platform.components.fab.ExpandableFloatingActionButton
|
import com.bitwarden.authenticator.ui.platform.components.fab.ExpandableFloatingActionButton
|
||||||
|
import com.bitwarden.authenticator.ui.platform.components.header.BitwardenListHeaderText
|
||||||
import com.bitwarden.authenticator.ui.platform.components.header.BitwardenListHeaderTextWithSupportLabel
|
import com.bitwarden.authenticator.ui.platform.components.header.BitwardenListHeaderTextWithSupportLabel
|
||||||
import com.bitwarden.authenticator.ui.platform.components.model.IconResource
|
import com.bitwarden.authenticator.ui.platform.components.model.IconResource
|
||||||
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||||
@@ -418,6 +420,7 @@ private fun ItemListingContent(
|
|||||||
onItemClick = { onItemClick(it.authCode) },
|
onItemClick = { onItemClick(it.authCode) },
|
||||||
onEditItemClick = { onEditItemClick(it.id) },
|
onEditItemClick = { onEditItemClick(it.id) },
|
||||||
onDeleteItemClick = { onDeleteItemClick(it.id) },
|
onDeleteItemClick = { onDeleteItemClick(it.id) },
|
||||||
|
allowLongPress = it.allowLongPressActions,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -445,10 +448,57 @@ private fun ItemListingContent(
|
|||||||
onItemClick = { onItemClick(it.authCode) },
|
onItemClick = { onItemClick(it.authCode) },
|
||||||
onEditItemClick = { onEditItemClick(it.id) },
|
onEditItemClick = { onEditItemClick(it.id) },
|
||||||
onDeleteItemClick = { onDeleteItemClick(it.id) },
|
onDeleteItemClick = { onDeleteItemClick(it.id) },
|
||||||
|
allowLongPress = it.allowLongPressActions,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there are any items in the local lists, add a spacer between
|
||||||
|
// local codes and shared codes:
|
||||||
|
if (state.itemList.isNotEmpty() || state.favoriteItems.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (state.sharedItems) {
|
||||||
|
is SharedCodesDisplayState.Codes -> {
|
||||||
|
items(state.sharedItems.sections) { section ->
|
||||||
|
BitwardenListHeaderText(
|
||||||
|
label = section.label(),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
section.codes.forEach {
|
||||||
|
VaultVerificationCodeItem(
|
||||||
|
authCode = it.authCode,
|
||||||
|
primaryLabel = it.issuer,
|
||||||
|
secondaryLabel = it.label,
|
||||||
|
periodSeconds = it.periodSeconds,
|
||||||
|
timeLeftSeconds = it.timeLeftSeconds,
|
||||||
|
alertThresholdSeconds = it.alertThresholdSeconds,
|
||||||
|
startIcon = it.startIcon,
|
||||||
|
onItemClick = { onItemClick(it.authCode) },
|
||||||
|
onEditItemClick = { },
|
||||||
|
onDeleteItemClick = { },
|
||||||
|
allowLongPress = it.allowLongPressActions,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedCodesDisplayState.Error -> item {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.shared_codes_error),
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add a spacer item to prevent the FAB from hiding verification codes at the
|
// Add a spacer item to prevent the FAB from hiding verification codes at the
|
||||||
// bottom of the list
|
// bottom of the list
|
||||||
item {
|
item {
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClip
|
|||||||
import com.bitwarden.authenticator.data.platform.manager.imports.model.GoogleAuthenticatorProtos
|
import com.bitwarden.authenticator.data.platform.manager.imports.model.GoogleAuthenticatorProtos
|
||||||
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
|
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
|
||||||
import com.bitwarden.authenticator.data.platform.repository.model.DataState
|
import com.bitwarden.authenticator.data.platform.repository.model.DataState
|
||||||
|
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.SharedCodesDisplayState
|
||||||
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VerificationCodeDisplayItem
|
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VerificationCodeDisplayItem
|
||||||
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util.toDisplayItem
|
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.BaseViewModel
|
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.Text
|
||||||
import com.bitwarden.authenticator.ui.platform.base.util.asText
|
import com.bitwarden.authenticator.ui.platform.base.util.asText
|
||||||
@@ -420,8 +422,21 @@ class ItemListingViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (localItems.isEmpty()) {
|
val sharedItemsState: SharedCodesDisplayState = when (action.sharedCodesState) {
|
||||||
// If there are no local items, show empty state:
|
SharedVerificationCodesState.Error -> SharedCodesDisplayState.Error
|
||||||
|
SharedVerificationCodesState.AppNotInstalled,
|
||||||
|
SharedVerificationCodesState.FeatureNotEnabled,
|
||||||
|
SharedVerificationCodesState.Loading,
|
||||||
|
SharedVerificationCodesState.OsVersionNotSupported,
|
||||||
|
SharedVerificationCodesState.SyncNotEnabled,
|
||||||
|
-> SharedCodesDisplayState.Codes(emptyList())
|
||||||
|
|
||||||
|
is SharedVerificationCodesState.Success ->
|
||||||
|
action.sharedCodesState.toSharedCodesDisplayState(state.alertThresholdSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localItems.isEmpty() && sharedItemsState.isEmpty()) {
|
||||||
|
// If there are no items, show empty state:
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
viewState = ItemListingState.ViewState.NoItems(
|
viewState = ItemListingState.ViewState.NoItems(
|
||||||
@@ -441,6 +456,7 @@ class ItemListingViewModel @Inject constructor(
|
|||||||
.map {
|
.map {
|
||||||
it.toDisplayItem(alertThresholdSeconds = state.alertThresholdSeconds)
|
it.toDisplayItem(alertThresholdSeconds = state.alertThresholdSeconds)
|
||||||
},
|
},
|
||||||
|
sharedItems = sharedItemsState,
|
||||||
actionCard = action.sharedCodesState.toActionCard(),
|
actionCard = action.sharedCodesState.toActionCard(),
|
||||||
)
|
)
|
||||||
mutableStateFlow.update { it.copy(viewState = viewState) }
|
mutableStateFlow.update { it.copy(viewState = viewState) }
|
||||||
@@ -590,6 +606,7 @@ data class ItemListingState(
|
|||||||
val actionCard: ActionCardState,
|
val actionCard: ActionCardState,
|
||||||
val favoriteItems: List<VerificationCodeDisplayItem>,
|
val favoriteItems: List<VerificationCodeDisplayItem>,
|
||||||
val itemList: List<VerificationCodeDisplayItem>,
|
val itemList: List<VerificationCodeDisplayItem>,
|
||||||
|
val sharedItems: SharedCodesDisplayState,
|
||||||
) : ViewState()
|
) : ViewState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting
|
package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -61,18 +62,27 @@ fun VaultVerificationCodeItem(
|
|||||||
onItemClick: () -> Unit,
|
onItemClick: () -> Unit,
|
||||||
onEditItemClick: () -> Unit,
|
onEditItemClick: () -> Unit,
|
||||||
onDeleteItemClick: () -> Unit,
|
onDeleteItemClick: () -> Unit,
|
||||||
|
allowLongPress: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var shouldShowDropdownMenu by remember { mutableStateOf(value = false) }
|
var shouldShowDropdownMenu by remember { mutableStateOf(value = false) }
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.combinedClickable(
|
.then(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
if (allowLongPress) {
|
||||||
indication = ripple(color = MaterialTheme.colorScheme.primary),
|
Modifier.combinedClickable(
|
||||||
onClick = onItemClick,
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
onLongClick = {
|
indication = ripple(color = MaterialTheme.colorScheme.primary),
|
||||||
shouldShowDropdownMenu = true
|
onClick = onItemClick,
|
||||||
|
onLongClick = { shouldShowDropdownMenu = true },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = ripple(color = MaterialTheme.colorScheme.primary),
|
||||||
|
onClick = onItemClick,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.defaultMinSize(minHeight = 72.dp)
|
.defaultMinSize(minHeight = 72.dp)
|
||||||
@@ -185,6 +195,7 @@ private fun VerificationCodeItem_preview() {
|
|||||||
onItemClick = {},
|
onItemClick = {},
|
||||||
onEditItemClick = {},
|
onEditItemClick = {},
|
||||||
onDeleteItemClick = {},
|
onDeleteItemClick = {},
|
||||||
|
allowLongPress = true,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.bitwarden.authenticator.ui.platform.base.util.Text
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models how shared codes should be displayed.
|
||||||
|
*/
|
||||||
|
sealed class SharedCodesDisplayState : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There was an error syncing codes.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data object Error : SharedCodesDisplayState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the given [sections] of verification codes.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Codes(val sections: List<SharedCodesAccountSection>) : SharedCodesDisplayState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models a section of shared authenticator codes to be displayed.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class SharedCodesAccountSection(
|
||||||
|
val label: Text,
|
||||||
|
val codes: List<VerificationCodeDisplayItem>,
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to determine if there are any codes synced.
|
||||||
|
*/
|
||||||
|
fun isEmpty() = when (this) {
|
||||||
|
is Codes -> this.sections.isEmpty()
|
||||||
|
Error -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,4 +19,5 @@ data class VerificationCodeDisplayItem(
|
|||||||
val authCode: String,
|
val authCode: String,
|
||||||
val startIcon: IconData = IconData.Local(R.drawable.ic_login_item),
|
val startIcon: IconData = IconData.Local(R.drawable.ic_login_item),
|
||||||
val favorite: Boolean,
|
val favorite: Boolean,
|
||||||
|
val allowLongPressActions: Boolean,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util
|
||||||
|
|
||||||
|
import com.bitwarden.authenticator.R
|
||||||
|
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.SharedCodesDisplayState
|
||||||
|
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VerificationCodeDisplayItem
|
||||||
|
import com.bitwarden.authenticator.ui.platform.base.util.asText
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert [SharedVerificationCodesState.Success] into [SharedCodesDisplayState.Codes].
|
||||||
|
*/
|
||||||
|
fun SharedVerificationCodesState.Success.toSharedCodesDisplayState(
|
||||||
|
alertThresholdSeconds: Int,
|
||||||
|
): SharedCodesDisplayState.Codes {
|
||||||
|
val codesMap =
|
||||||
|
mutableMapOf<AuthenticatorItem.Source.Shared, MutableList<VerificationCodeDisplayItem>>()
|
||||||
|
// Make a map where each key is a Bitwarden account and each value is a list of verification
|
||||||
|
// codes for that account:
|
||||||
|
this.items.forEach {
|
||||||
|
codesMap.putIfAbsent(it.source as AuthenticatorItem.Source.Shared, mutableListOf())
|
||||||
|
codesMap[it.source]?.add(it.toDisplayItem(alertThresholdSeconds))
|
||||||
|
}
|
||||||
|
// Flatten that map down to a list of accounts that each has a list of codes:
|
||||||
|
return codesMap
|
||||||
|
.map {
|
||||||
|
SharedCodesDisplayState.SharedCodesAccountSection(
|
||||||
|
label = R.string.shared_accounts_header.asText(
|
||||||
|
it.key.email,
|
||||||
|
it.key.environmentLabel,
|
||||||
|
),
|
||||||
|
codes = it.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.let { SharedCodesDisplayState.Codes(it) }
|
||||||
|
}
|
||||||
@@ -16,5 +16,9 @@ fun VerificationCodeItem.toDisplayItem(alertThresholdSeconds: Int) =
|
|||||||
periodSeconds = periodSeconds,
|
periodSeconds = periodSeconds,
|
||||||
alertThresholdSeconds = alertThresholdSeconds,
|
alertThresholdSeconds = alertThresholdSeconds,
|
||||||
authCode = code,
|
authCode = code,
|
||||||
|
allowLongPressActions = when (source) {
|
||||||
|
is AuthenticatorItem.Source.Local -> true
|
||||||
|
is AuthenticatorItem.Source.Shared -> false
|
||||||
|
},
|
||||||
favorite = (source as? AuthenticatorItem.Source.Local)?.isFavorite ?: false,
|
favorite = (source as? AuthenticatorItem.Source.Local)?.isFavorite ?: false,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -125,4 +125,6 @@
|
|||||||
<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">Download</string>
|
<string name="download">Download</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="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>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import com.bitwarden.authenticator.data.platform.manager.BitwardenEncodingManage
|
|||||||
import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager
|
import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||||
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
|
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
|
||||||
import com.bitwarden.authenticator.data.platform.repository.model.DataState
|
import com.bitwarden.authenticator.data.platform.repository.model.DataState
|
||||||
|
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.SharedCodesDisplayState
|
||||||
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util.toDisplayItem
|
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.BaseViewModelTest
|
||||||
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme
|
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
@@ -95,6 +97,7 @@ class ItemListViewModelTest : BaseViewModelTest() {
|
|||||||
actionCard = ItemListingState.ActionCardState.DownloadBitwardenApp,
|
actionCard = ItemListingState.ActionCardState.DownloadBitwardenApp,
|
||||||
favoriteItems = LOCAL_FAVORITE_ITEMS,
|
favoriteItems = LOCAL_FAVORITE_ITEMS,
|
||||||
itemList = LOCAL_NON_FAVORITE_ITEMS,
|
itemList = LOCAL_NON_FAVORITE_ITEMS,
|
||||||
|
sharedItems = SharedCodesDisplayState.Codes(emptyList()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
every { settingsRepository.hasUserDismissedDownloadBitwardenCard } returns false
|
every { settingsRepository.hasUserDismissedDownloadBitwardenCard } returns false
|
||||||
@@ -112,6 +115,7 @@ class ItemListViewModelTest : BaseViewModelTest() {
|
|||||||
actionCard = ItemListingState.ActionCardState.None,
|
actionCard = ItemListingState.ActionCardState.None,
|
||||||
favoriteItems = LOCAL_FAVORITE_ITEMS,
|
favoriteItems = LOCAL_FAVORITE_ITEMS,
|
||||||
itemList = LOCAL_NON_FAVORITE_ITEMS,
|
itemList = LOCAL_NON_FAVORITE_ITEMS,
|
||||||
|
sharedItems = SharedCodesDisplayState.Codes(emptyList()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
every { settingsRepository.hasUserDismissedDownloadBitwardenCard } returns true
|
every { settingsRepository.hasUserDismissedDownloadBitwardenCard } returns true
|
||||||
@@ -121,6 +125,74 @@ class ItemListViewModelTest : BaseViewModelTest() {
|
|||||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `stateFlow sharedItems value should be Error when shared state is Error `() {
|
||||||
|
val expectedState = DEFAULT_STATE.copy(
|
||||||
|
viewState = ItemListingState.ViewState.Content(
|
||||||
|
actionCard = ItemListingState.ActionCardState.None,
|
||||||
|
favoriteItems = LOCAL_FAVORITE_ITEMS,
|
||||||
|
itemList = LOCAL_NON_FAVORITE_ITEMS,
|
||||||
|
sharedItems = SharedCodesDisplayState.Error,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mutableVerificationCodesFlow.value = DataState.Loaded(LOCAL_VERIFICATION_ITEMS)
|
||||||
|
mutableSharedCodesFlow.value = SharedVerificationCodesState.Error
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `stateFlow sharedItems value should be Codes with empty list when shared state is Error `() {
|
||||||
|
val expectedState = DEFAULT_STATE.copy(
|
||||||
|
viewState = ItemListingState.ViewState.Content(
|
||||||
|
actionCard = ItemListingState.ActionCardState.None,
|
||||||
|
favoriteItems = LOCAL_FAVORITE_ITEMS,
|
||||||
|
itemList = LOCAL_NON_FAVORITE_ITEMS,
|
||||||
|
sharedItems = SHARED_DISPLAY_ITEMS,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mutableVerificationCodesFlow.value = DataState.Loaded(LOCAL_VERIFICATION_ITEMS)
|
||||||
|
mutableSharedCodesFlow.value =
|
||||||
|
SharedVerificationCodesState.Success(SHARED_VERIFICATION_ITEMS)
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `stateFlow sharedItems value should show items even when local items are empty`() {
|
||||||
|
val expectedState = DEFAULT_STATE.copy(
|
||||||
|
viewState = ItemListingState.ViewState.NoItems(
|
||||||
|
actionCard = ItemListingState.ActionCardState.None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mutableVerificationCodesFlow.value = DataState.Loaded(emptyList())
|
||||||
|
mutableSharedCodesFlow.value =
|
||||||
|
SharedVerificationCodesState.Success(emptyList())
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `stateFlow viewState value should be NoItems when both local and shared codes are empty`() {
|
||||||
|
val expectedState = DEFAULT_STATE.copy(
|
||||||
|
viewState = ItemListingState.ViewState.Content(
|
||||||
|
actionCard = ItemListingState.ActionCardState.None,
|
||||||
|
favoriteItems = emptyList(),
|
||||||
|
itemList = emptyList(),
|
||||||
|
sharedItems = SHARED_DISPLAY_ITEMS,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mutableVerificationCodesFlow.value = DataState.Loaded(emptyList())
|
||||||
|
mutableSharedCodesFlow.value =
|
||||||
|
SharedVerificationCodesState.Success(SHARED_VERIFICATION_ITEMS)
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on DownloadBitwardenClick receive should emit NavigateToBitwardenListing`() = runTest {
|
fun `on DownloadBitwardenClick receive should emit NavigateToBitwardenListing`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
@@ -139,6 +211,7 @@ class ItemListViewModelTest : BaseViewModelTest() {
|
|||||||
actionCard = ItemListingState.ActionCardState.None,
|
actionCard = ItemListingState.ActionCardState.None,
|
||||||
favoriteItems = LOCAL_FAVORITE_ITEMS,
|
favoriteItems = LOCAL_FAVORITE_ITEMS,
|
||||||
itemList = LOCAL_NON_FAVORITE_ITEMS,
|
itemList = LOCAL_NON_FAVORITE_ITEMS,
|
||||||
|
sharedItems = SharedCodesDisplayState.Codes(emptyList()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
every { settingsRepository.hasUserDismissedDownloadBitwardenCard = true } just runs
|
every { settingsRepository.hasUserDismissedDownloadBitwardenCard = true } just runs
|
||||||
@@ -208,9 +281,30 @@ private val LOCAL_VERIFICATION_ITEMS = listOf(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val SHARED_VERIFICATION_ITEMS = listOf(
|
||||||
|
VerificationCodeItem(
|
||||||
|
code = "987654",
|
||||||
|
periodSeconds = 60,
|
||||||
|
timeLeftSeconds = 430,
|
||||||
|
issueTime = 35L,
|
||||||
|
id = "1",
|
||||||
|
issuer = "sharedIssue",
|
||||||
|
accountName = "sharedAccountName",
|
||||||
|
source = AuthenticatorItem.Source.Shared(
|
||||||
|
userId = "1",
|
||||||
|
nameOfUser = null,
|
||||||
|
email = "email",
|
||||||
|
environmentLabel = "environmentLabel",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
private val LOCAL_DISPLAY_ITEMS = LOCAL_VERIFICATION_ITEMS.map {
|
private val LOCAL_DISPLAY_ITEMS = LOCAL_VERIFICATION_ITEMS.map {
|
||||||
it.toDisplayItem(AUTHENTICATOR_ALERT_SECONDS)
|
it.toDisplayItem(AUTHENTICATOR_ALERT_SECONDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val SHARED_DISPLAY_ITEMS = SharedVerificationCodesState.Success(SHARED_VERIFICATION_ITEMS)
|
||||||
|
.toSharedCodesDisplayState(AUTHENTICATOR_ALERT_SECONDS)
|
||||||
|
|
||||||
private val LOCAL_FAVORITE_ITEMS = LOCAL_DISPLAY_ITEMS.filter { it.favorite }
|
private val LOCAL_FAVORITE_ITEMS = LOCAL_DISPLAY_ITEMS.filter { it.favorite }
|
||||||
private val LOCAL_NON_FAVORITE_ITEMS = LOCAL_DISPLAY_ITEMS.filterNot { it.favorite }
|
private val LOCAL_NON_FAVORITE_ITEMS = LOCAL_DISPLAY_ITEMS.filterNot { it.favorite }
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.longClick
|
||||||
|
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 com.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
|
||||||
|
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.SharedCodesDisplayState
|
||||||
|
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VerificationCodeDisplayItem
|
||||||
|
import com.bitwarden.authenticator.ui.platform.base.BaseComposeTest
|
||||||
|
import com.bitwarden.authenticator.ui.platform.base.util.asText
|
||||||
|
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme
|
||||||
|
import com.bitwarden.authenticator.ui.platform.manager.intent.IntentManager
|
||||||
|
import com.bitwarden.authenticator.ui.platform.manager.permissions.FakePermissionManager
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.runs
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ItemListingScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
|
private var onNavigateBackCalled = false
|
||||||
|
private var onNavigateToSearchCalled = false
|
||||||
|
private var onNavigateToQrCodeScannerCalled = false
|
||||||
|
private var onNavigateToManualKeyEntryCalled = false
|
||||||
|
private var onNavigateToEditItemScreenCalled = false
|
||||||
|
|
||||||
|
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||||
|
private val mutableEventFlow = bufferedMutableSharedFlow<ItemListingEvent>()
|
||||||
|
|
||||||
|
private val viewModel: ItemListingViewModel = mockk {
|
||||||
|
every { stateFlow } returns mutableStateFlow
|
||||||
|
every { eventFlow } returns mutableEventFlow
|
||||||
|
every { trySendAction(any()) } just runs
|
||||||
|
}
|
||||||
|
|
||||||
|
private val intentManager: IntentManager = mockk()
|
||||||
|
private val permissionsManager = FakePermissionManager()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
ItemListingScreen(
|
||||||
|
viewModel = viewModel,
|
||||||
|
intentManager = intentManager,
|
||||||
|
permissionsManager = permissionsManager,
|
||||||
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
|
onNavigateToSearch = { onNavigateToSearchCalled = true },
|
||||||
|
onNavigateToQrCodeScanner = { onNavigateToQrCodeScannerCalled = true },
|
||||||
|
onNavigateToManualKeyEntry = { onNavigateToManualKeyEntryCalled = true },
|
||||||
|
onNavigateToEditItemScreen = { onNavigateToEditItemScreenCalled = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `shared accounts error message should show when view is Content with SharedCodesDisplayState Error`() {
|
||||||
|
mutableStateFlow.value = DEFAULT_STATE.copy(
|
||||||
|
viewState = ItemListingState.ViewState.Content(
|
||||||
|
actionCard = ItemListingState.ActionCardState.None,
|
||||||
|
favoriteItems = emptyList(),
|
||||||
|
itemList = emptyList(),
|
||||||
|
sharedItems = SharedCodesDisplayState.Error,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("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.")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.value = DEFAULT_STATE.copy(
|
||||||
|
viewState = ItemListingState.ViewState.Content(
|
||||||
|
actionCard = ItemListingState.ActionCardState.None,
|
||||||
|
favoriteItems = emptyList(),
|
||||||
|
itemList = emptyList(),
|
||||||
|
sharedItems = SharedCodesDisplayState.Codes(emptyList()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("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.")
|
||||||
|
.assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking shared accounts verification code item should send ItemClick action`() {
|
||||||
|
mutableStateFlow.value = DEFAULT_STATE.copy(
|
||||||
|
viewState = ItemListingState.ViewState.Content(
|
||||||
|
actionCard = ItemListingState.ActionCardState.None,
|
||||||
|
favoriteItems = emptyList(),
|
||||||
|
itemList = emptyList(),
|
||||||
|
sharedItems = SharedCodesDisplayState.Codes(
|
||||||
|
sections = listOf(
|
||||||
|
SHARED_ACCOUNTS_SECTION,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("joe+shared_code_1@test.com")
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
ItemListingAction.ItemClick(SHARED_ACCOUNTS_SECTION.codes[0].authCode),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure long press sends action as well, since local items have long press options
|
||||||
|
// but shared items do not:
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("joe+shared_code_1@test.com")
|
||||||
|
.performTouchInput { longClick() }
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
ItemListingAction.ItemClick(SHARED_ACCOUNTS_SECTION.codes[0].authCode),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val APP_THEME = AppTheme.DEFAULT
|
||||||
|
private const val ALERT_THRESHOLD = 7
|
||||||
|
|
||||||
|
private val SHARED_ACCOUNTS_SECTION = SharedCodesDisplayState.SharedCodesAccountSection(
|
||||||
|
label = "test@test.com".asText(),
|
||||||
|
codes = listOf(
|
||||||
|
VerificationCodeDisplayItem(
|
||||||
|
id = "1",
|
||||||
|
issuer = "bitwarden.com",
|
||||||
|
label = "joe+shared_code_1@test.com",
|
||||||
|
timeLeftSeconds = 10,
|
||||||
|
periodSeconds = 30,
|
||||||
|
alertThresholdSeconds = ALERT_THRESHOLD,
|
||||||
|
authCode = "123456",
|
||||||
|
favorite = false,
|
||||||
|
allowLongPressActions = false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val DEFAULT_STATE = ItemListingState(
|
||||||
|
appTheme = APP_THEME,
|
||||||
|
alertThresholdSeconds = ALERT_THRESHOLD,
|
||||||
|
viewState = ItemListingState.ViewState.NoItems(
|
||||||
|
actionCard = ItemListingState.ActionCardState.None,
|
||||||
|
),
|
||||||
|
dialog = null,
|
||||||
|
)
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util
|
||||||
|
|
||||||
|
import com.bitwarden.authenticator.R
|
||||||
|
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.SharedCodesDisplayState
|
||||||
|
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VerificationCodeDisplayItem
|
||||||
|
import com.bitwarden.authenticator.ui.platform.base.util.asText
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class SharedVerificationCodesStateTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toSharedCodesDisplayState on empty list should return empty list`() {
|
||||||
|
val state = SharedVerificationCodesState.Success(emptyList())
|
||||||
|
val expected = SharedCodesDisplayState.Codes(emptyList())
|
||||||
|
assertEquals(
|
||||||
|
expected,
|
||||||
|
state.toSharedCodesDisplayState(ALERT_THRESHOLD),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toSharedCodesDisplayState should return list of sections grouped by account`() {
|
||||||
|
val state = SharedVerificationCodesState.Success(
|
||||||
|
items = listOf(
|
||||||
|
VerificationCodeItem(
|
||||||
|
code = "123456",
|
||||||
|
periodSeconds = 30,
|
||||||
|
timeLeftSeconds = 10,
|
||||||
|
issueTime = 100L,
|
||||||
|
id = "123",
|
||||||
|
issuer = null,
|
||||||
|
accountName = null,
|
||||||
|
source = AuthenticatorItem.Source.Shared(
|
||||||
|
userId = "user1",
|
||||||
|
nameOfUser = "John Appleseed",
|
||||||
|
email = "John@test.com",
|
||||||
|
environmentLabel = "bitwarden.com",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
VerificationCodeItem(
|
||||||
|
code = "987654",
|
||||||
|
periodSeconds = 30,
|
||||||
|
timeLeftSeconds = 10,
|
||||||
|
issueTime = 100L,
|
||||||
|
id = "987",
|
||||||
|
issuer = "issuer",
|
||||||
|
accountName = "accountName",
|
||||||
|
source = AuthenticatorItem.Source.Shared(
|
||||||
|
userId = "user1",
|
||||||
|
nameOfUser = "Jane Doe",
|
||||||
|
email = "Jane@test.com",
|
||||||
|
environmentLabel = "bitwarden.eu",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val expected = SharedCodesDisplayState.Codes(
|
||||||
|
sections = listOf(
|
||||||
|
SharedCodesDisplayState.SharedCodesAccountSection(
|
||||||
|
label = R.string.shared_accounts_header.asText(
|
||||||
|
"John@test.com",
|
||||||
|
"bitwarden.com",
|
||||||
|
),
|
||||||
|
codes = listOf(
|
||||||
|
VerificationCodeDisplayItem(
|
||||||
|
authCode = "123456",
|
||||||
|
periodSeconds = 30,
|
||||||
|
timeLeftSeconds = 10,
|
||||||
|
id = "123",
|
||||||
|
issuer = null,
|
||||||
|
label = null,
|
||||||
|
favorite = false,
|
||||||
|
allowLongPressActions = false,
|
||||||
|
alertThresholdSeconds = ALERT_THRESHOLD,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SharedCodesDisplayState.SharedCodesAccountSection(
|
||||||
|
label = R.string.shared_accounts_header.asText(
|
||||||
|
"Jane@test.com",
|
||||||
|
"bitwarden.eu",
|
||||||
|
),
|
||||||
|
codes = listOf(
|
||||||
|
VerificationCodeDisplayItem(
|
||||||
|
authCode = "987654",
|
||||||
|
periodSeconds = 30,
|
||||||
|
timeLeftSeconds = 10,
|
||||||
|
id = "987",
|
||||||
|
issuer = "issuer",
|
||||||
|
label = "accountName",
|
||||||
|
favorite = false,
|
||||||
|
allowLongPressActions = false,
|
||||||
|
alertThresholdSeconds = ALERT_THRESHOLD,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
expected,
|
||||||
|
state.toSharedCodesDisplayState(ALERT_THRESHOLD),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val ALERT_THRESHOLD = 7
|
||||||
@@ -9,7 +9,7 @@ import org.junit.jupiter.api.Test
|
|||||||
class VerificationCodeItemExtensionsTest {
|
class VerificationCodeItemExtensionsTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `toDisplayItem should map items correctly`() {
|
fun `toDisplayItem should map Local items correctly`() {
|
||||||
val alertThresholdSeconds = 7
|
val alertThresholdSeconds = 7
|
||||||
val favoriteItem = createMockVerificationCodeItem(number = 1, favorite = true)
|
val favoriteItem = createMockVerificationCodeItem(number = 1, favorite = true)
|
||||||
val nonFavoriteItem = createMockVerificationCodeItem(number = 2)
|
val nonFavoriteItem = createMockVerificationCodeItem(number = 2)
|
||||||
@@ -23,6 +23,7 @@ class VerificationCodeItemExtensionsTest {
|
|||||||
alertThresholdSeconds = alertThresholdSeconds,
|
alertThresholdSeconds = alertThresholdSeconds,
|
||||||
authCode = favoriteItem.code,
|
authCode = favoriteItem.code,
|
||||||
favorite = (favoriteItem.source as AuthenticatorItem.Source.Local).isFavorite,
|
favorite = (favoriteItem.source as AuthenticatorItem.Source.Local).isFavorite,
|
||||||
|
allowLongPressActions = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
val expectedNonFavoriteItem = VerificationCodeDisplayItem(
|
val expectedNonFavoriteItem = VerificationCodeDisplayItem(
|
||||||
@@ -34,9 +35,38 @@ class VerificationCodeItemExtensionsTest {
|
|||||||
alertThresholdSeconds = alertThresholdSeconds,
|
alertThresholdSeconds = alertThresholdSeconds,
|
||||||
authCode = nonFavoriteItem.code,
|
authCode = nonFavoriteItem.code,
|
||||||
favorite = (nonFavoriteItem.source as AuthenticatorItem.Source.Local).isFavorite,
|
favorite = (nonFavoriteItem.source as AuthenticatorItem.Source.Local).isFavorite,
|
||||||
|
allowLongPressActions = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(expectedFavoriteItem, favoriteItem.toDisplayItem(alertThresholdSeconds))
|
assertEquals(expectedFavoriteItem, favoriteItem.toDisplayItem(alertThresholdSeconds))
|
||||||
assertEquals(expectedNonFavoriteItem, nonFavoriteItem.toDisplayItem(alertThresholdSeconds))
|
assertEquals(expectedNonFavoriteItem, nonFavoriteItem.toDisplayItem(alertThresholdSeconds))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toDisplayItem should map Shared items correctly`() {
|
||||||
|
val alertThresholdSeconds = 7
|
||||||
|
val favoriteItem = createMockVerificationCodeItem(number = 1, favorite = true)
|
||||||
|
.copy(
|
||||||
|
source = AuthenticatorItem.Source.Shared(
|
||||||
|
userId = "1",
|
||||||
|
nameOfUser = "John Doe",
|
||||||
|
email = "test@bitwarden.com",
|
||||||
|
environmentLabel = "bitwarden.com",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val expectedFavoriteItem = VerificationCodeDisplayItem(
|
||||||
|
id = favoriteItem.id,
|
||||||
|
issuer = favoriteItem.issuer,
|
||||||
|
label = favoriteItem.accountName,
|
||||||
|
timeLeftSeconds = favoriteItem.timeLeftSeconds,
|
||||||
|
periodSeconds = favoriteItem.periodSeconds,
|
||||||
|
alertThresholdSeconds = alertThresholdSeconds,
|
||||||
|
authCode = favoriteItem.code,
|
||||||
|
favorite = false,
|
||||||
|
allowLongPressActions = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(expectedFavoriteItem, favoriteItem.toDisplayItem(alertThresholdSeconds))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.bitwarden.authenticator.ui.platform.manager.permissions
|
||||||
|
|
||||||
|
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper class used to test permissions
|
||||||
|
*/
|
||||||
|
class FakePermissionManager : PermissionsManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value returned when we check if we have the permission.
|
||||||
|
*/
|
||||||
|
var checkPermissionResult: Boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value returned when the user is asked for permission.
|
||||||
|
*/
|
||||||
|
var getPermissionsResult: Boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value returned when the user is asked for permission.
|
||||||
|
*/
|
||||||
|
var getMultiplePermissionsResult: Map<String, Boolean> = emptyMap()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value for whether a rationale should be shown to the user.
|
||||||
|
*/
|
||||||
|
var shouldShowRequestRationale: Boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the [getLauncher] function has been called.
|
||||||
|
*/
|
||||||
|
var hasGetLauncherBeenCalled: Boolean = false
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun getLauncher(
|
||||||
|
onResult: (Boolean) -> Unit,
|
||||||
|
): ManagedActivityResultLauncher<String, Boolean> {
|
||||||
|
hasGetLauncherBeenCalled = true
|
||||||
|
return mockk {
|
||||||
|
every { launch(any()) } answers { onResult.invoke(getPermissionsResult) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun getPermissionsLauncher(
|
||||||
|
onResult: (Map<String, Boolean>) -> Unit,
|
||||||
|
): ManagedActivityResultLauncher<Array<String>, Map<String, Boolean>> {
|
||||||
|
return mockk {
|
||||||
|
every { launch(any()) } answers { onResult.invoke(getMultiplePermissionsResult) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkPermission(permission: String): Boolean {
|
||||||
|
return checkPermissionResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkPermissions(permissions: Array<String>): Boolean {
|
||||||
|
return checkPermissionResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldShouldRequestPermissionRationale(permission: String): Boolean {
|
||||||
|
return shouldShowRequestRationale
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user