mirror of
https://github.com/bitwarden/android.git
synced 2025-02-17 12:30:00 +03:00
BIT-1518: Process shared sends (#698)
This commit is contained in:
parent
4510695f76
commit
c9d7a48598
14 changed files with 462 additions and 60 deletions
|
@ -0,0 +1,14 @@
|
|||
package com.x8bit.bitwarden.ui.platform.manager.exit
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
/**
|
||||
* A manager class for handling the various ways to exit the app.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
interface ExitManager {
|
||||
/**
|
||||
* Finishes the activity.
|
||||
*/
|
||||
fun exitApplication()
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.x8bit.bitwarden.ui.platform.manager.exit
|
||||
|
||||
import android.app.Activity
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
/**
|
||||
* The default implementation of the [ExitManager] for managing the various ways to exit the app.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
class ExitManagerImpl(
|
||||
val activity: Activity,
|
||||
) : ExitManager {
|
||||
override fun exitApplication() {
|
||||
activity.finish()
|
||||
}
|
||||
}
|
|
@ -11,12 +11,6 @@ import androidx.compose.runtime.Composable
|
|||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface IntentManager {
|
||||
|
||||
/**
|
||||
* Starts an intent to exit the application.
|
||||
*/
|
||||
fun exitApplication()
|
||||
|
||||
/**
|
||||
* Start an activity using the provided [Intent].
|
||||
*/
|
||||
|
@ -60,7 +54,12 @@ interface IntentManager {
|
|||
/**
|
||||
* Processes the [activityResult] and attempts to get the relevant file data from it.
|
||||
*/
|
||||
fun getFileDataFromIntent(activityResult: ActivityResult): FileData?
|
||||
fun getFileDataFromActivityResult(activityResult: ActivityResult): FileData?
|
||||
|
||||
/**
|
||||
* Processes the [intent] and attempts to get the relevant file data from it.
|
||||
*/
|
||||
fun getFileDataFromIntent(intent: Intent): FileData?
|
||||
|
||||
/**
|
||||
* Processes the [intent] and attempts to derive [ShareData] information from it.
|
||||
|
@ -97,7 +96,7 @@ interface IntentManager {
|
|||
* The data required to create a new File Send.
|
||||
*/
|
||||
data class FileSend(
|
||||
val fileData: IntentManager.FileData,
|
||||
val fileData: FileData,
|
||||
) : ShareData()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,16 +51,6 @@ class IntentManagerImpl(
|
|||
private val context: Context,
|
||||
private val clock: Clock = Clock.systemDefaultZone(),
|
||||
) : IntentManager {
|
||||
|
||||
override fun exitApplication() {
|
||||
// Note that we fire an explicit Intent rather than try to cast to an Activity and call
|
||||
// finish to avoid assumptions about what kind of context we have.
|
||||
val intent = Intent(Intent.ACTION_MAIN).apply {
|
||||
addCategory(Intent.CATEGORY_HOME)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun startActivity(intent: Intent) {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
@ -116,12 +106,17 @@ class IntentManagerImpl(
|
|||
startActivity(Intent.createChooser(sendIntent, null))
|
||||
}
|
||||
|
||||
override fun getFileDataFromIntent(activityResult: ActivityResult): IntentManager.FileData? {
|
||||
override fun getFileDataFromActivityResult(
|
||||
activityResult: ActivityResult,
|
||||
): IntentManager.FileData? {
|
||||
if (activityResult.resultCode != Activity.RESULT_OK) return null
|
||||
val uri = activityResult.data?.data
|
||||
return if (uri != null) getLocalFileData(uri) else getCameraFileData()
|
||||
}
|
||||
|
||||
override fun getFileDataFromIntent(intent: Intent): IntentManager.FileData? =
|
||||
intent.clipData?.getItemAt(0)?.uri?.let { getLocalFileData(it) }
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override fun getShareDataFromIntent(intent: Intent): IntentManager.ShareData? {
|
||||
if (intent.action != Intent.ACTION_SEND) return null
|
||||
|
@ -133,12 +128,7 @@ class IntentManagerImpl(
|
|||
text = title,
|
||||
)
|
||||
} else {
|
||||
getFileDataFromIntent(
|
||||
ActivityResult(
|
||||
Activity.RESULT_OK,
|
||||
intent,
|
||||
),
|
||||
)
|
||||
getFileDataFromIntent(intent = intent)
|
||||
?.let {
|
||||
IntentManager.ShareData.FileSend(
|
||||
fileData = it,
|
||||
|
|
|
@ -23,6 +23,8 @@ import androidx.compose.ui.platform.LocalView
|
|||
import androidx.core.view.WindowCompat
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManagerImpl
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManagerImpl
|
||||
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
|
||||
|
@ -45,6 +47,7 @@ fun BitwardenTheme(
|
|||
|
||||
// Get the current scheme
|
||||
val context = LocalContext.current
|
||||
val activity = context as Activity
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
|
@ -76,8 +79,9 @@ fun BitwardenTheme(
|
|||
CompositionLocalProvider(
|
||||
LocalNonMaterialColors provides nonMaterialColors,
|
||||
LocalNonMaterialTypography provides nonMaterialTypography,
|
||||
LocalPermissionsManager provides PermissionsManagerImpl(context as Activity),
|
||||
LocalPermissionsManager provides PermissionsManagerImpl(activity),
|
||||
LocalIntentManager provides IntentManagerImpl(context),
|
||||
LocalExitManager provides ExitManagerImpl(activity),
|
||||
) {
|
||||
// Set overall theme based on color scheme and typography settings
|
||||
MaterialTheme(
|
||||
|
@ -166,6 +170,13 @@ private fun lightColorScheme(context: Context): ColorScheme =
|
|||
private fun Int.toColor(context: Context): Color =
|
||||
Color(context.getColor(this))
|
||||
|
||||
/**
|
||||
* Provides access to the exit manager throughout the app.
|
||||
*/
|
||||
val LocalExitManager: ProvidableCompositionLocal<ExitManager> = compositionLocalOf {
|
||||
error("CompositionLocal ExitManager not present")
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides access to the intent manager throughout the app.
|
||||
*/
|
||||
|
|
|
@ -55,6 +55,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandler
|
|||
fun AddSendContent(
|
||||
state: AddSendState.ViewState.Content,
|
||||
isAddMode: Boolean,
|
||||
isShared: Boolean,
|
||||
addSendHandlers: AddSendHandlers,
|
||||
permissionsManager: PermissionsManager,
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -77,7 +78,7 @@ fun AddSendContent(
|
|||
onValueChange = addSendHandlers.onNamChange,
|
||||
)
|
||||
|
||||
if (isAddMode) {
|
||||
if (isAddMode && !isShared) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.type),
|
||||
|
@ -116,7 +117,25 @@ fun AddSendContent(
|
|||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
if (isAddMode) {
|
||||
if (isShared) {
|
||||
Text(
|
||||
text = type.name.orEmpty(),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.max_file_size),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
} else if (isAddMode) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
text = type.name ?: stringResource(id = R.string.no_file_chosen),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.ui.tools.feature.send.addsend
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
@ -33,9 +34,12 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
|||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.NavigationIcon
|
||||
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
|
||||
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
|
||||
import com.x8bit.bitwarden.ui.platform.theme.LocalExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.theme.LocalPermissionsManager
|
||||
import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
|
||||
|
@ -49,6 +53,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandler
|
|||
@Composable
|
||||
fun AddSendScreen(
|
||||
viewModel: AddSendViewModel = hiltViewModel(),
|
||||
exitManager: ExitManager = LocalExitManager.current,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
|
||||
onNavigateBack: () -> Unit,
|
||||
|
@ -59,13 +64,20 @@ fun AddSendScreen(
|
|||
val resources = context.resources
|
||||
|
||||
val fileChooserLauncher = intentManager.launchActivityForResult { activityResult ->
|
||||
intentManager.getFileDataFromIntent(activityResult)?.let {
|
||||
intentManager.getFileDataFromActivityResult(activityResult)?.let {
|
||||
viewModel.trySendAction(AddSendAction.FileChoose(it))
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(
|
||||
onBack = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AddSendAction.CloseClick) }
|
||||
},
|
||||
)
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
AddSendEvent.ExitApp -> exitManager.exitApplication()
|
||||
|
||||
is AddSendEvent.NavigateBack -> onNavigateBack()
|
||||
|
||||
is AddSendEvent.ShowChooserSheet -> {
|
||||
|
@ -115,11 +127,14 @@ fun AddSendScreen(
|
|||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = state.screenDisplayName(),
|
||||
navigationIcon = painterResource(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AddSendAction.CloseClick) }
|
||||
},
|
||||
navigationIcon = NavigationIcon(
|
||||
navigationIcon = painterResource(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AddSendAction.CloseClick) }
|
||||
},
|
||||
)
|
||||
.takeUnless { state.isShared },
|
||||
scrollBehavior = scrollBehavior,
|
||||
actions = {
|
||||
BitwardenTextButton(
|
||||
|
@ -174,6 +189,7 @@ fun AddSendScreen(
|
|||
is AddSendState.ViewState.Content -> AddSendContent(
|
||||
state = viewState,
|
||||
isAddMode = state.isAddMode,
|
||||
isShared = state.isShared,
|
||||
addSendHandlers = remember(viewModel) { AddSendHandlers.create(viewModel) },
|
||||
permissionsManager = permissionsManager,
|
||||
modifier = modifier,
|
||||
|
|
|
@ -24,6 +24,9 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText
|
|||
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.shouldFinishOnComplete
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toSendName
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toSendType
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toSendView
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendUrl
|
||||
|
@ -54,7 +57,7 @@ private const val MAX_FILE_SIZE_BYTES: Long = 100 * 1024 * 1024
|
|||
@HiltViewModel
|
||||
class AddSendViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
authRepo: AuthRepository,
|
||||
private val authRepo: AuthRepository,
|
||||
private val clock: Clock,
|
||||
private val clipboardManager: BitwardenClipboardManager,
|
||||
private val environmentRepo: EnvironmentRepository,
|
||||
|
@ -62,13 +65,18 @@ class AddSendViewModel @Inject constructor(
|
|||
) : BaseViewModel<AddSendState, AddSendEvent, AddSendAction>(
|
||||
// We load the state from the savedStateHandle for testing purposes.
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
val addSendType = AddSendArgs(savedStateHandle).sendAddType
|
||||
// Check to see if we are navigating here from an external source
|
||||
val specialCircumstance = authRepo.specialCircumstance
|
||||
val shareSendType = specialCircumstance.toSendType()
|
||||
val sendAddType = AddSendArgs(savedStateHandle).sendAddType
|
||||
AddSendState(
|
||||
addSendType = addSendType,
|
||||
viewState = when (addSendType) {
|
||||
shouldFinishOnComplete = specialCircumstance.shouldFinishOnComplete(),
|
||||
isShared = shareSendType != null,
|
||||
addSendType = sendAddType,
|
||||
viewState = when (sendAddType) {
|
||||
AddSendType.AddItem -> AddSendState.ViewState.Content(
|
||||
common = AddSendState.ViewState.Content.Common(
|
||||
name = "",
|
||||
name = specialCircumstance.toSendName().orEmpty(),
|
||||
currentAccessCount = null,
|
||||
maxAccessCount = null,
|
||||
passwordInput = "",
|
||||
|
@ -85,7 +93,7 @@ class AddSendViewModel @Inject constructor(
|
|||
sendUrl = null,
|
||||
hasPassword = false,
|
||||
),
|
||||
selectedType = AddSendState.ViewState.Content.SendType.Text(
|
||||
selectedType = shareSendType ?: AddSendState.ViewState.Content.SendType.Text(
|
||||
input = "",
|
||||
isHideByDefaultChecked = false,
|
||||
),
|
||||
|
@ -179,12 +187,17 @@ class AddSendViewModel @Inject constructor(
|
|||
|
||||
is CreateSendResult.Success -> {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
sendEvent(AddSendEvent.NavigateBack)
|
||||
sendEvent(
|
||||
AddSendEvent.ShowShareSheet(
|
||||
message = result.sendView.toSendUrl(state.baseWebSendUrl),
|
||||
),
|
||||
)
|
||||
if (state.isShared) {
|
||||
navigateBack()
|
||||
clipboardManager.setText(result.sendView.toSendUrl(state.baseWebSendUrl))
|
||||
} else {
|
||||
navigateBack()
|
||||
sendEvent(
|
||||
AddSendEvent.ShowShareSheet(
|
||||
message = result.sendView.toSendUrl(state.baseWebSendUrl),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -209,7 +222,7 @@ class AddSendViewModel @Inject constructor(
|
|||
|
||||
is UpdateSendResult.Success -> {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
sendEvent(AddSendEvent.NavigateBack)
|
||||
navigateBack()
|
||||
sendEvent(
|
||||
AddSendEvent.ShowShareSheet(
|
||||
message = result.sendView.toSendUrl(state.baseWebSendUrl),
|
||||
|
@ -236,7 +249,7 @@ class AddSendViewModel @Inject constructor(
|
|||
|
||||
is DeleteSendResult.Success -> {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
sendEvent(AddSendEvent.NavigateBack)
|
||||
navigateBack()
|
||||
sendEvent(AddSendEvent.ShowToast(message = R.string.send_deleted.asText()))
|
||||
}
|
||||
}
|
||||
|
@ -421,7 +434,7 @@ class AddSendViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleCloseClick() = sendEvent(AddSendEvent.NavigateBack)
|
||||
private fun handleCloseClick() = navigateBack()
|
||||
|
||||
private fun handleDeletionDateChange(action: AddSendAction.DeletionDateChange) {
|
||||
updateCommonContent {
|
||||
|
@ -582,6 +595,17 @@ class AddSendViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun navigateBack() {
|
||||
authRepo.specialCircumstance = null
|
||||
sendEvent(
|
||||
event = if (state.shouldFinishOnComplete) {
|
||||
AddSendEvent.ExitApp
|
||||
} else {
|
||||
AddSendEvent.NavigateBack
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun onContent(
|
||||
crossinline block: (AddSendState.ViewState.Content) -> Unit,
|
||||
) {
|
||||
|
@ -645,7 +669,9 @@ data class AddSendState(
|
|||
val addSendType: AddSendType,
|
||||
val dialogState: DialogState?,
|
||||
val viewState: ViewState,
|
||||
val shouldFinishOnComplete: Boolean,
|
||||
val isPremiumUser: Boolean,
|
||||
val isShared: Boolean,
|
||||
val baseWebSendUrl: String,
|
||||
) : Parcelable {
|
||||
|
||||
|
@ -783,6 +809,11 @@ data class AddSendState(
|
|||
* Models events for the new send screen.
|
||||
*/
|
||||
sealed class AddSendEvent {
|
||||
/**
|
||||
* Closes the app.
|
||||
*/
|
||||
data object ExitApp : AddSendEvent()
|
||||
|
||||
/**
|
||||
* Navigate back.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
package com.x8bit.bitwarden.ui.tools.feature.send.addsend.util
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendState
|
||||
|
||||
/**
|
||||
* Determines the initial [AddSendState.ViewState.Content.SendType] based on the data in the
|
||||
* [UserState.SpecialCircumstance].
|
||||
*/
|
||||
fun UserState.SpecialCircumstance?.toSendType(): AddSendState.ViewState.Content.SendType? =
|
||||
when (this) {
|
||||
is UserState.SpecialCircumstance.ShareNewSend -> {
|
||||
when (data) {
|
||||
is IntentManager.ShareData.FileSend -> AddSendState.ViewState.Content.SendType.File(
|
||||
uri = data.fileData.uri,
|
||||
name = data.fileData.fileName,
|
||||
sizeBytes = data.fileData.sizeBytes,
|
||||
displaySize = null,
|
||||
)
|
||||
|
||||
is IntentManager.ShareData.TextSend -> AddSendState.ViewState.Content.SendType.Text(
|
||||
input = data.text,
|
||||
isHideByDefaultChecked = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the initial send name based on the data in the [UserState.SpecialCircumstance].
|
||||
*/
|
||||
fun UserState.SpecialCircumstance?.toSendName(): String? =
|
||||
when (this) {
|
||||
is UserState.SpecialCircumstance.ShareNewSend -> {
|
||||
when (data) {
|
||||
is IntentManager.ShareData.FileSend -> data.fileData.fileName
|
||||
is IntentManager.ShareData.TextSend -> data.subject
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the [UserState.SpecialCircumstance] requires the app to be closed after completing
|
||||
* the send.
|
||||
*/
|
||||
fun UserState.SpecialCircumstance?.shouldFinishOnComplete(): Boolean =
|
||||
when (this) {
|
||||
is UserState.SpecialCircumstance.ShareNewSend -> shouldFinishWhenComplete
|
||||
else -> false
|
||||
}
|
|
@ -51,8 +51,8 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
|
|||
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.theme.LocalExitManager
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
@ -72,7 +72,7 @@ fun VaultScreen(
|
|||
onNavigateToVerificationCodeScreen: () -> Unit,
|
||||
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
|
||||
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
exitManager: ExitManager = LocalExitManager.current,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsState()
|
||||
val context = LocalContext.current
|
||||
|
@ -107,7 +107,7 @@ fun VaultScreen(
|
|||
onNavigateToVaultItemListingScreen(event.itemListingType)
|
||||
}
|
||||
|
||||
VaultEvent.NavigateOutOfApp -> intentManager.exitApplication()
|
||||
VaultEvent.NavigateOutOfApp -> exitManager.exitApplication()
|
||||
is VaultEvent.ShowToast -> {
|
||||
Toast
|
||||
.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT)
|
||||
|
|
|
@ -23,6 +23,7 @@ import androidx.compose.ui.test.performTextInput
|
|||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
|
||||
|
@ -45,6 +46,9 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
|
||||
private var onNavigateBackCalled = false
|
||||
|
||||
private val exitManager: ExitManager = mockk(relaxed = true) {
|
||||
every { exitApplication() } just runs
|
||||
}
|
||||
private val permissionsManager = FakePermissionManager()
|
||||
private val intentManager: IntentManager = mockk(relaxed = true) {
|
||||
every { shareText(any()) } just runs
|
||||
|
@ -61,6 +65,7 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
composeTestRule.setContent {
|
||||
AddSendScreen(
|
||||
viewModel = viewModel,
|
||||
exitManager = exitManager,
|
||||
intentManager = intentManager,
|
||||
permissionsManager = permissionsManager,
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
|
@ -74,6 +79,14 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
assert(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ExitApp should call exitApplication on ExitManager`() {
|
||||
mutableEventFlow.tryEmit(AddSendEvent.ExitApp)
|
||||
verify {
|
||||
exitManager.exitApplication()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on ShowShareSheet should call shareText on IntentManager`() {
|
||||
val text = "sharable stuff"
|
||||
|
@ -91,6 +104,14 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
verify { viewModel.trySendAction(AddSendAction.CloseClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `display navigation icon according to state`() {
|
||||
mutableStateFlow.update { it.copy(isShared = false) }
|
||||
composeTestRule.onNodeWithContentDescription("Close").assertIsDisplayed()
|
||||
mutableStateFlow.update { it.copy(isShared = true) }
|
||||
composeTestRule.onNodeWithContentDescription("Close").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on save click should send SaveClick`() {
|
||||
composeTestRule
|
||||
|
@ -282,7 +303,25 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
|
||||
@Test
|
||||
fun `segmented buttons should appear based on state`() {
|
||||
mutableStateFlow.update { it.copy(addSendType = AddSendType.AddItem) }
|
||||
mutableStateFlow.update { it.copy(isShared = true) }
|
||||
composeTestRule
|
||||
.onNodeWithText("Type")
|
||||
.assertDoesNotExist()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("File")
|
||||
.filterToOne(!isEditableText)
|
||||
.assertDoesNotExist()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Text")
|
||||
.filterToOne(!isEditableText)
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
isShared = false,
|
||||
addSendType = AddSendType.AddItem,
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("Type")
|
||||
.performScrollTo()
|
||||
|
@ -889,6 +928,8 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
addSendType = AddSendType.AddItem,
|
||||
viewState = DEFAULT_VIEW_STATE,
|
||||
dialogState = null,
|
||||
shouldFinishOnComplete = false,
|
||||
isShared = false,
|
||||
isPremiumUser = false,
|
||||
baseWebSendUrl = "https://vault.bitwarden.com/#/send/",
|
||||
)
|
||||
|
|
|
@ -56,6 +56,8 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
|
||||
private val authRepository: AuthRepository = mockk {
|
||||
every { specialCircumstance } returns null
|
||||
every { specialCircumstance = null } just runs
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
}
|
||||
private val environmentRepository: EnvironmentRepository = mockk {
|
||||
|
@ -108,8 +110,9 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `SaveClick with createSend success should emit NavigateBack and ShowShareSheet`() =
|
||||
fun `SaveClick with createSend success should emit NavigateBack and ShowShareSheet when not an external shared`() =
|
||||
runTest {
|
||||
val viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(name = "input"),
|
||||
|
@ -137,6 +140,76 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `SaveClick with createSend success should copy the send URL to the clipboard and emit NavigateBack`() =
|
||||
runTest {
|
||||
val viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(name = "input"),
|
||||
)
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
shouldFinishOnComplete = false,
|
||||
isShared = true,
|
||||
viewState = viewState,
|
||||
)
|
||||
val mockSendView = mockk<SendView>()
|
||||
every { viewState.toSendView(clock) } returns mockSendView
|
||||
val sendUrl = "www.test.com/send/test"
|
||||
val resultSendView = mockk<SendView> {
|
||||
every { toSendUrl(DEFAULT_ENVIRONMENT_URL) } returns sendUrl
|
||||
}
|
||||
coEvery {
|
||||
vaultRepository.createSend(sendView = mockSendView, fileUri = null)
|
||||
} returns CreateSendResult.Success(sendView = resultSendView)
|
||||
val viewModel = createViewModel(initialState)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AddSendAction.SaveClick)
|
||||
assertEquals(AddSendEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
assertEquals(initialState, viewModel.stateFlow.value)
|
||||
coVerify(exactly = 1) {
|
||||
vaultRepository.createSend(sendView = mockSendView, fileUri = null)
|
||||
authRepository.specialCircumstance = null
|
||||
clipboardManager.setText(sendUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `SaveClick with createSend success should copy the send URL to the clipboard and emit ExitApp`() =
|
||||
runTest {
|
||||
val viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(name = "input"),
|
||||
)
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
shouldFinishOnComplete = true,
|
||||
isShared = true,
|
||||
viewState = viewState,
|
||||
)
|
||||
val mockSendView = mockk<SendView>()
|
||||
every { viewState.toSendView(clock) } returns mockSendView
|
||||
val sendUrl = "www.test.com/send/test"
|
||||
val resultSendView = mockk<SendView> {
|
||||
every { toSendUrl(DEFAULT_ENVIRONMENT_URL) } returns sendUrl
|
||||
}
|
||||
coEvery {
|
||||
vaultRepository.createSend(sendView = mockSendView, fileUri = null)
|
||||
} returns CreateSendResult.Success(sendView = resultSendView)
|
||||
val viewModel = createViewModel(initialState)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AddSendAction.SaveClick)
|
||||
assertEquals(AddSendEvent.ExitApp, awaitItem())
|
||||
}
|
||||
assertEquals(initialState, viewModel.stateFlow.value)
|
||||
coVerify(exactly = 1) {
|
||||
vaultRepository.createSend(sendView = mockSendView, fileUri = null)
|
||||
authRepository.specialCircumstance = null
|
||||
clipboardManager.setText(sendUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SaveClick with createSend failure should show error dialog`() = runTest {
|
||||
val viewState = DEFAULT_VIEW_STATE.copy(
|
||||
|
@ -915,6 +988,8 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
|||
addSendType = AddSendType.AddItem,
|
||||
viewState = DEFAULT_VIEW_STATE,
|
||||
dialogState = null,
|
||||
shouldFinishOnComplete = false,
|
||||
isShared = false,
|
||||
isPremiumUser = false,
|
||||
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
package com.x8bit.bitwarden.ui.tools.feature.send.addsend.util
|
||||
|
||||
import android.net.Uri
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendState
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SpecialCircumstanceExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `toSendType with TextSend should return Text SendType with correct text`() {
|
||||
val text = "Share Text"
|
||||
val expected = AddSendState.ViewState.Content.SendType.Text(
|
||||
input = text,
|
||||
isHideByDefaultChecked = false,
|
||||
)
|
||||
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
|
||||
data = IntentManager.ShareData.TextSend(
|
||||
subject = "",
|
||||
text = text,
|
||||
),
|
||||
shouldFinishWhenComplete = false,
|
||||
)
|
||||
|
||||
val result = specialCircumstance.toSendType()
|
||||
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSendType with FileSend should return File SendType with correct data`() {
|
||||
val uri = mockk<Uri>()
|
||||
val fileName = "Share Name"
|
||||
val sizeBytes = 100L
|
||||
val expected = AddSendState.ViewState.Content.SendType.File(
|
||||
uri = uri,
|
||||
name = fileName,
|
||||
sizeBytes = sizeBytes,
|
||||
displaySize = null,
|
||||
)
|
||||
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
|
||||
data = IntentManager.ShareData.FileSend(
|
||||
fileData = IntentManager.FileData(
|
||||
fileName = fileName,
|
||||
uri = uri,
|
||||
sizeBytes = sizeBytes,
|
||||
),
|
||||
),
|
||||
shouldFinishWhenComplete = false,
|
||||
)
|
||||
|
||||
val result = specialCircumstance.toSendType()
|
||||
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSendType with null SpecialCircumstance should return null`() {
|
||||
val specialCircumstance: UserState.SpecialCircumstance? = null
|
||||
assertNull(specialCircumstance.toSendType())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSendName with TextSend should return subject`() {
|
||||
val subject = "Subject"
|
||||
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
|
||||
data = IntentManager.ShareData.TextSend(
|
||||
subject = subject,
|
||||
text = "",
|
||||
),
|
||||
shouldFinishWhenComplete = false,
|
||||
)
|
||||
|
||||
val result = specialCircumstance.toSendName()
|
||||
|
||||
assertEquals(subject, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSendName with FileSend should return file name`() {
|
||||
val fileName = "File Name"
|
||||
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
|
||||
data = IntentManager.ShareData.FileSend(
|
||||
fileData = IntentManager.FileData(
|
||||
fileName = fileName,
|
||||
uri = mockk(),
|
||||
sizeBytes = 0L,
|
||||
),
|
||||
),
|
||||
shouldFinishWhenComplete = false,
|
||||
)
|
||||
|
||||
val result = specialCircumstance.toSendName()
|
||||
|
||||
assertEquals(fileName, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSendName with null SpecialCircumstance should return null`() {
|
||||
val specialCircumstance: UserState.SpecialCircumstance? = null
|
||||
assertNull(specialCircumstance.toSendName())
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `shouldFinishOnComplete with ShareNewSend shouldFinishWhenComplete true should return true`() {
|
||||
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
|
||||
data = mockk(),
|
||||
shouldFinishWhenComplete = true,
|
||||
)
|
||||
assertTrue(specialCircumstance.shouldFinishOnComplete())
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `shouldFinishOnComplete with ShareNewSend shouldFinishWhenComplete false should return false`() {
|
||||
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
|
||||
data = mockk(),
|
||||
shouldFinishWhenComplete = false,
|
||||
)
|
||||
assertFalse(specialCircumstance.shouldFinishOnComplete())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shouldFinishOnComplete with null SpecialCircumstance should return false`() {
|
||||
val specialCircumstance: UserState.SpecialCircumstance? = null
|
||||
assertFalse(specialCircumstance.shouldFinishOnComplete())
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
|
|||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
|
||||
import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed
|
||||
import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed
|
||||
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||
|
@ -61,7 +61,7 @@ class VaultScreenTest : BaseComposeTest() {
|
|||
private var onNavigateToVaultItemListingType: VaultItemListingType? = null
|
||||
private var onDimBottomNavBarRequestCalled = false
|
||||
private var onNavigateToVerificationCodeScreen = false
|
||||
private val intentManager = mockk<IntentManager>(relaxed = true)
|
||||
private val exitManager = mockk<ExitManager>(relaxed = true)
|
||||
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<VaultEvent>()
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
|
@ -81,7 +81,7 @@ class VaultScreenTest : BaseComposeTest() {
|
|||
onNavigateToVaultItemListingScreen = { onNavigateToVaultItemListingType = it },
|
||||
onDimBottomNavBarRequest = { onDimBottomNavBarRequestCalled = true },
|
||||
onNavigateToVerificationCodeScreen = { onNavigateToVerificationCodeScreen = true },
|
||||
intentManager = intentManager,
|
||||
exitManager = exitManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -646,9 +646,9 @@ class VaultScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateOutOfApp event should call exitApplication on the IntentManager`() {
|
||||
fun `NavigateOutOfApp event should call exitApplication on the ExitManager`() {
|
||||
mutableEventFlow.tryEmit(VaultEvent.NavigateOutOfApp)
|
||||
verify { intentManager.exitApplication() }
|
||||
verify { exitManager.exitApplication() }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Add table
Reference in a new issue