Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.ui.platform.base.util.EventsEffect
Expand All @@ -21,6 +22,7 @@ import com.bitwarden.ui.platform.components.content.BitwardenErrorContent
import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
import com.bitwarden.ui.platform.components.snackbar.model.rememberBitwardenSnackbarHostState
Expand Down Expand Up @@ -54,7 +56,7 @@ fun AttachmentsScreen(
EventsEffect(viewModel = viewModel) { event ->
when (event) {
AttachmentsEvent.NavigateBack -> onNavigateBack()

is AttachmentsEvent.NavigateToUri -> intentManager.launchUri(event.uri.toUri())
AttachmentsEvent.ShowChooserSheet -> {
fileChooserLauncher.launch(
intentManager.createFileChooserIntent(withCameraIntents = false),
Expand All @@ -70,7 +72,7 @@ fun AttachmentsScreen(

AttachmentsDialogs(
dialogState = state.dialogState,
onDismissRequest = attachmentsHandlers.onDismissRequest,
attachmentsHandlers = attachmentsHandlers,
)

val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Expand Down Expand Up @@ -123,13 +125,23 @@ fun AttachmentsScreen(
@Composable
private fun AttachmentsDialogs(
dialogState: AttachmentsState.DialogState?,
onDismissRequest: () -> Unit,
attachmentsHandlers: AttachmentsHandlers,
) {
when (dialogState) {
AttachmentsState.DialogState.RequiresPremium -> BitwardenTwoButtonDialog(
title = stringResource(id = BitwardenString.attachments_unavailable),
message = stringResource(id = BitwardenString.attachments_are_a_premium_feature),
confirmButtonText = stringResource(id = BitwardenString.upgrade_to_premium),
onConfirmClick = attachmentsHandlers.onUpgradeToPremiumClick,
dismissButtonText = stringResource(id = BitwardenString.cancel),
onDismissClick = attachmentsHandlers.onDismissRequest,
onDismissRequest = attachmentsHandlers.onDismissRequest,
)

is AttachmentsState.DialogState.Error -> BitwardenBasicDialog(
title = dialogState.title?.invoke(),
message = dialogState.message(),
onDismissRequest = onDismissRequest,
onDismissRequest = attachmentsHandlers.onDismissRequest,
throwable = dialogState.throwable,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.model.FileData
Expand All @@ -17,6 +18,7 @@ import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
Expand Down Expand Up @@ -46,6 +48,7 @@ private const val MAX_FILE_SIZE_BYTES: Long = 100 * 1024 * 1024
@HiltViewModel
class AttachmentsViewModel @Inject constructor(
private val authRepo: AuthRepository,
private val environmentRepo: EnvironmentRepository,
private val vaultRepo: VaultRepository,
featureFlagManager: FeatureFlagManager,
savedStateHandle: SavedStateHandle,
Expand All @@ -57,10 +60,7 @@ class AttachmentsViewModel @Inject constructor(
AttachmentsState(
cipherId = savedStateHandle.toAttachmentsArgs().cipherId,
viewState = AttachmentsState.ViewState.Loading,
dialogState = AttachmentsState.DialogState.Error(
title = null,
message = BitwardenString.premium_required.asText(),
)
dialogState = AttachmentsState.DialogState.RequiresPremium
.takeUnless { isPremiumUser },
isPremiumUser = isPremiumUser,
isAttachmentUpdatesEnabled = featureFlagManager.getFeatureFlag(
Expand Down Expand Up @@ -94,6 +94,7 @@ class AttachmentsViewModel @Inject constructor(
AttachmentsAction.BackClick -> handleBackClick()
AttachmentsAction.SaveClick -> handleSaveClick()
AttachmentsAction.DismissDialogClick -> handleDismissDialogClick()
AttachmentsAction.UpgradeToPremiumClick -> handleUpgradeToPremiumClick()
AttachmentsAction.ChooseFileClick -> handleChooseFileClick()
is AttachmentsAction.FileNameChange -> handleFileNameChange(action)
is AttachmentsAction.FileChoose -> handleFileChoose(action)
Expand All @@ -111,12 +112,7 @@ class AttachmentsViewModel @Inject constructor(
onContent { content ->
if (!state.isPremiumUser) {
mutableStateFlow.update {
it.copy(
dialogState = AttachmentsState.DialogState.Error(
title = BitwardenString.an_error_has_occurred.asText(),
message = BitwardenString.premium_required.asText(),
),
)
it.copy(dialogState = AttachmentsState.DialogState.RequiresPremium)
}
return@onContent
}
Expand Down Expand Up @@ -170,6 +166,19 @@ class AttachmentsViewModel @Inject constructor(
mutableStateFlow.update { it.copy(dialogState = null) }
}

private fun handleUpgradeToPremiumClick() {
mutableStateFlow.update { it.copy(dialogState = null) }
val baseUrl = environmentRepo
.environment
.environmentUrlData
.baseWebVaultUrlOrDefault
sendEvent(
AttachmentsEvent.NavigateToUri(
uri = "$baseUrl/#/settings/subscription/premium?callToAction=upgradeToPremium",
),
)
}

private fun handleChooseFileClick() {
sendEvent(AttachmentsEvent.ShowChooserSheet)
}
Expand Down Expand Up @@ -227,6 +236,12 @@ class AttachmentsViewModel @Inject constructor(
}

private fun handleItemClick(action: AttachmentsAction.ItemClick) {
if (!state.isPremiumUser) {
mutableStateFlow.update {
it.copy(dialogState = AttachmentsState.DialogState.RequiresPremium)
}
return
}
sendEvent(
AttachmentsEvent.NavigateToPreview(
cipherId = state.cipherId,
Expand Down Expand Up @@ -465,6 +480,12 @@ data class AttachmentsState(
* Represents the current state of any dialogs on the screen.
*/
sealed class DialogState : Parcelable {
/**
* Represents a dismissible dialog indicating that you must have premium.
*/
@Parcelize
data object RequiresPremium : DialogState()

/**
* Represents a dismissible dialog with the given error [message].
*/
Expand Down Expand Up @@ -494,6 +515,11 @@ sealed class AttachmentsEvent {
*/
data object NavigateBack : AttachmentsEvent()

/**
* Navigates to upgrade to the given Uri.
*/
data class NavigateToUri(val uri: String) : AttachmentsEvent()

/**
* Navigates to preview the attachment.
*/
Expand Down Expand Up @@ -549,6 +575,11 @@ sealed class AttachmentsAction {
*/
data object DismissDialogClick : AttachmentsAction()

/**
* User clicked to upgrade top Premium.
*/
data object UpgradeToPremiumClick : AttachmentsAction()

/**
* User clicked to select a new attachment file.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ data class AttachmentsHandlers(
val onItemClick: (attachment: AttachmentsState.AttachmentItem) -> Unit,
val onDismissRequest: () -> Unit,
val onFileNameChange: (String) -> Unit,
val onUpgradeToPremiumClick: () -> Unit,
) {
@Suppress("UndocumentedPublicClass")
companion object {
Expand All @@ -38,6 +39,9 @@ data class AttachmentsHandlers(
onFileNameChange = {
viewModel.trySendAction(AttachmentsAction.FileNameChange(it))
},
onUpgradeToPremiumClick = {
viewModel.trySendAction(AttachmentsAction.UpgradeToPremiumClick)
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.core.net.toUri
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.platform.manager.IntentManager
import com.bitwarden.ui.util.asText
Expand Down Expand Up @@ -64,6 +65,15 @@ class AttachmentsScreenTest : BitwardenComposeTest() {
assertTrue(onNavigateBackCalled)
}

@Test
fun `NavigateToUri should call launchUri`() {
val uriString = "https://www.bitwarden.com"
mutableEventFlow.tryEmit(AttachmentsEvent.NavigateToUri(uri = uriString))
verify(exactly = 1) {
intentManager.launchUri(uriString.toUri())
}
}

@Test
fun `NavigateToPreview should call onNavigateToPreview`() {
mutableEventFlow.tryEmit(
Expand Down Expand Up @@ -240,6 +250,31 @@ class AttachmentsScreenTest : BitwardenComposeTest() {
}
}

@Test
fun `requires Premium dialog should be displayed according to state`() {
val requiresPremiumMessage = "Attachments unavailable"
composeTestRule.onNode(isDialog()).assertDoesNotExist()
composeTestRule.onNodeWithText(requiresPremiumMessage).assertDoesNotExist()

mutableStateFlow.update {
it.copy(dialogState = AttachmentsState.DialogState.RequiresPremium)
}

composeTestRule
.onNodeWithText(requiresPremiumMessage)
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()

composeTestRule
.onNodeWithText("Upgrade to Premium")
.assert(hasAnyAncestor(isDialog()))
.performClick()

verify(exactly = 1) {
viewModel.trySendAction(AttachmentsAction.UpgradeToPremiumClick)
}
}

@Test
fun `error dialog should be displayed according to state`() {
val errorMessage = "Fail"
Expand Down
Loading
Loading