@@ -4,9 +4,11 @@ import android.os.Parcelable
44import androidx.lifecycle.SavedStateHandle
55import androidx.lifecycle.viewModelScope
66import com.bitwarden.annotation.OmitFromCoverage
7+ import com.bitwarden.core.data.manager.model.FlagKey
78import com.bitwarden.core.data.repository.model.DataState
89import com.bitwarden.data.repository.util.baseIconUrl
910import com.bitwarden.data.repository.util.baseWebSendUrl
11+ import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault
1012import com.bitwarden.network.model.PolicyTypeJson
1113import com.bitwarden.send.SendType
1214import com.bitwarden.ui.platform.base.BackgroundEvent
@@ -28,6 +30,7 @@ import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySele
2830import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
2931import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
3032import com.x8bit.bitwarden.data.autofill.util.login
33+ import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
3134import com.x8bit.bitwarden.data.platform.manager.PolicyManager
3235import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
3336import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
@@ -40,6 +43,7 @@ import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
4043import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
4144import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
4245import com.x8bit.bitwarden.data.vault.repository.VaultRepository
46+ import com.x8bit.bitwarden.data.vault.repository.model.ArchiveCipherResult
4347import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
4448import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
4549import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
@@ -95,10 +99,11 @@ class SearchViewModel @Inject constructor(
9599 private val organizationEventManager : OrganizationEventManager ,
96100 private val vaultRepo : VaultRepository ,
97101 private val authRepo : AuthRepository ,
98- environmentRepo : EnvironmentRepository ,
102+ private val environmentRepo : EnvironmentRepository ,
99103 settingsRepo : SettingsRepository ,
100104 snackbarRelayManager : SnackbarRelayManager <SnackbarRelay >,
101105 specialCircumstanceManager : SpecialCircumstanceManager ,
106+ featureFlagManager : FeatureFlagManager ,
102107) : BaseViewModel<SearchState, SearchEvent, SearchAction>(
103108 // We load the state from the savedStateHandle for testing purposes.
104109 initialState = savedStateHandle[KEY_STATE ]
@@ -134,6 +139,7 @@ class SearchViewModel @Inject constructor(
134139 hasMasterPassword = userState.activeAccount.hasMasterPassword,
135140 isPremium = userState.activeAccount.isPremium,
136141 restrictItemTypesPolicyOrgIds = persistentListOf(),
142+ isArchiveEnabled = featureFlagManager.getFeatureFlag(FlagKey .ArchiveItems ),
137143 )
138144 },
139145) {
@@ -171,6 +177,12 @@ class SearchViewModel @Inject constructor(
171177 .map { SearchAction .Internal .SnackbarDataReceived (it) }
172178 .onEach(::sendAction)
173179 .launchIn(viewModelScope)
180+
181+ featureFlagManager
182+ .getFeatureFlagFlow(FlagKey .ArchiveItems )
183+ .map { SearchAction .Internal .ArchiveItemsFlagUpdateReceive (it) }
184+ .onEach(::sendAction)
185+ .launchIn(viewModelScope)
174186 }
175187
176188 override fun handleAction (action : SearchAction ) {
@@ -187,6 +199,7 @@ class SearchViewModel @Inject constructor(
187199 is SearchAction .SearchTermChange -> handleSearchTermChange(action)
188200 is SearchAction .VaultFilterSelect -> handleVaultFilterSelect(action)
189201 is SearchAction .OverflowOptionClick -> handleOverflowItemClick(action)
202+ SearchAction .UpgradeToPremiumClick -> handleUpgradeToPremiumClick()
190203 is SearchAction .Internal -> handleInternalAction(action)
191204 }
192205 }
@@ -300,6 +313,13 @@ class SearchViewModel @Inject constructor(
300313 recalculateViewState()
301314 }
302315
316+ private fun handleUpgradeToPremiumClick () {
317+ mutableStateFlow.update { it.copy(dialogState = null ) }
318+ val baseUrl = environmentRepo.environment.environmentUrlData.baseWebVaultUrlOrDefault
319+ val url = " $baseUrl /#/settings/subscription/premium?callToAction=upgradeToPremium"
320+ sendEvent(SearchEvent .NavigateToUrl (url = url))
321+ }
322+
303323 private fun handleOverflowItemClick (action : SearchAction .OverflowOptionClick ) {
304324 when (val overflowAction = action.overflowAction) {
305325 is ListingItemOverflowAction .SendAction .CopyUrlClick -> {
@@ -355,6 +375,10 @@ class SearchViewModel @Inject constructor(
355375 is ListingItemOverflowAction .VaultAction .CopyTotpClick -> {
356376 handleCopyTotpClick(overflowAction)
357377 }
378+
379+ is ListingItemOverflowAction .VaultAction .ArchiveClick -> {
380+ handleArchiveClick(overflowAction)
381+ }
358382 }
359383 }
360384
@@ -404,6 +428,34 @@ class SearchViewModel @Inject constructor(
404428 }
405429 }
406430
431+ private fun handleArchiveClick (action : ListingItemOverflowAction .VaultAction .ArchiveClick ) {
432+ if (! state.isPremium) {
433+ mutableStateFlow.update {
434+ it.copy(dialogState = SearchState .DialogState .ArchiveRequiresPremium )
435+ }
436+ return
437+ }
438+ mutableStateFlow.update {
439+ it.copy(
440+ dialogState = SearchState .DialogState .Loading (
441+ message = BitwardenString .archiving.asText(),
442+ ),
443+ )
444+ }
445+ viewModelScope.launch {
446+ decryptCipherViewOrNull(cipherId = action.cipherId)?.let {
447+ sendAction(
448+ SearchAction .Internal .ArchiveCipherReceive (
449+ result = vaultRepo.archiveCipher(
450+ cipherId = action.cipherId,
451+ cipherView = it,
452+ ),
453+ ),
454+ )
455+ }
456+ }
457+ }
458+
407459 private fun handleRemovePasswordClick (
408460 action : ListingItemOverflowAction .SendAction .RemovePasswordClick ,
409461 ) {
@@ -558,6 +610,12 @@ class SearchViewModel @Inject constructor(
558610 is SearchAction .Internal .DecryptCipherErrorReceive -> {
559611 handleDecryptCipherErrorReceive(action)
560612 }
613+
614+ is SearchAction .Internal .ArchiveItemsFlagUpdateReceive -> {
615+ handleArchiveItemsFlagUpdateReceive(action)
616+ }
617+
618+ is SearchAction .Internal .ArchiveCipherReceive -> handleArchiveCipherReceive(action)
561619 }
562620 }
563621
@@ -575,6 +633,33 @@ class SearchViewModel @Inject constructor(
575633 }
576634 }
577635
636+ private fun handleArchiveItemsFlagUpdateReceive (
637+ action : SearchAction .Internal .ArchiveItemsFlagUpdateReceive ,
638+ ) {
639+ mutableStateFlow.update { it.copy(isArchiveEnabled = action.isEnabled) }
640+ }
641+
642+ private fun handleArchiveCipherReceive (action : SearchAction .Internal .ArchiveCipherReceive ) {
643+ when (val result = action.result) {
644+ is ArchiveCipherResult .Error -> {
645+ mutableStateFlow.update {
646+ it.copy(
647+ dialogState = SearchState .DialogState .Error (
648+ title = BitwardenString .an_error_has_occurred.asText(),
649+ message = BitwardenString .unable_to_archive_selected_item.asText(),
650+ throwable = result.error,
651+ ),
652+ )
653+ }
654+ }
655+
656+ ArchiveCipherResult .Success -> {
657+ mutableStateFlow.update { it.copy(dialogState = null ) }
658+ sendEvent(SearchEvent .ShowSnackbar (BitwardenString .item_archived.asText()))
659+ }
660+ }
661+ }
662+
578663 private fun handleIconLoadingSettingReceive (
579664 action : SearchAction .Internal .IconLoadingSettingReceive ,
580665 ) {
@@ -879,6 +964,7 @@ class SearchViewModel @Inject constructor(
879964 isIconLoadingDisabled = state.isIconLoadingDisabled,
880965 isAutofill = state.isAutofill,
881966 isPremiumUser = state.isPremium,
967+ isArchiveEnabled = state.isArchiveEnabled,
882968 )
883969 }
884970
@@ -945,6 +1031,7 @@ data class SearchState(
9451031 val hasMasterPassword : Boolean ,
9461032 val isPremium : Boolean ,
9471033 val restrictItemTypesPolicyOrgIds : ImmutableList <String >,
1034+ val isArchiveEnabled : Boolean ,
9481035) : Parcelable {
9491036
9501037 /* *
@@ -1027,6 +1114,12 @@ data class SearchState(
10271114 data class Loading (
10281115 val message : Text ,
10291116 ) : DialogState()
1117+
1118+ /* *
1119+ * Displays a dialog to the user indicating that archiving requires a premium account.
1120+ */
1121+ @Parcelize
1122+ data object ArchiveRequiresPremium : DialogState ()
10301123 }
10311124
10321125 /* *
@@ -1296,6 +1389,11 @@ sealed class SearchAction {
12961389 val overflowAction : ListingItemOverflowAction ,
12971390 ) : SearchAction()
12981391
1392+ /* *
1393+ * User clicked the upgrade to premium button.
1394+ */
1395+ data object UpgradeToPremiumClick : SearchAction ()
1396+
12991397 /* *
13001398 * Models actions that the [SearchViewModel] itself might send.
13011399 */
@@ -1321,6 +1419,13 @@ sealed class SearchAction {
13211419 val result : GenerateTotpResult ,
13221420 ) : Internal()
13231421
1422+ /* *
1423+ * Indicates that the archive cipher result has been received.
1424+ */
1425+ data class ArchiveCipherReceive (
1426+ val result : ArchiveCipherResult ,
1427+ ) : Internal()
1428+
13241429 /* *
13251430 * Indicates a result for removing the password protection from a send has been received.
13261431 */
@@ -1372,6 +1477,13 @@ sealed class SearchAction {
13721477 data class DecryptCipherErrorReceive (
13731478 val error : Throwable ? ,
13741479 ) : Internal()
1480+
1481+ /* *
1482+ * Indicates that the Archive Items flag has been updated.
1483+ */
1484+ data class ArchiveItemsFlagUpdateReceive (
1485+ val isEnabled : Boolean ,
1486+ ) : Internal()
13751487 }
13761488}
13771489
0 commit comments