mirror of
https://github.com/bitwarden/android.git
synced 2025-12-11 04:39:19 -06:00
[PM-13626] Remember last opened view for 5 minutes (#4574)
Signed-off-by: Andre Rosado <arosado@bitwarden.com> Co-authored-by: Dave Severns <dseverns@livefront.com>
This commit is contained in:
parent
3f1f9983e3
commit
fe06bf48e7
@ -11,6 +11,7 @@ import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@ -19,6 +20,7 @@ import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityComp
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.ObserveScreenDataEffect
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
|
||||
@ -53,6 +55,7 @@ class MainActivity : AppCompatActivity() {
|
||||
@Inject
|
||||
lateinit var debugLaunchManager: DebugMenuLaunchManager
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
var shouldShowSplashScreen = true
|
||||
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
|
||||
@ -109,6 +112,15 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
|
||||
LocalManagerProvider {
|
||||
ObserveScreenDataEffect(
|
||||
onDataUpdate = remember(mainViewModel) {
|
||||
{
|
||||
mainViewModel.trySendAction(
|
||||
MainAction.ResumeScreenDataReceived(it),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
BitwardenTheme(theme = state.theme) {
|
||||
RootNavScreen(
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
|
||||
@ -18,8 +18,10 @@ import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2GetCredentialsReques
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
@ -71,6 +73,7 @@ class MainViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val appResumeManager: AppResumeManager,
|
||||
private val clock: Clock,
|
||||
) : BaseViewModel<MainState, MainEvent, MainAction>(
|
||||
initialState = MainState(
|
||||
@ -185,6 +188,14 @@ class MainViewModel @Inject constructor(
|
||||
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
|
||||
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
|
||||
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
|
||||
is MainAction.ResumeScreenDataReceived -> handleAppResumeDataUpdated(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAppResumeDataUpdated(action: MainAction.ResumeScreenDataReceived) {
|
||||
when (val data = action.screenResumeData) {
|
||||
null -> appResumeManager.clearResumeScreen()
|
||||
else -> appResumeManager.setResumeScreen(data)
|
||||
}
|
||||
}
|
||||
|
||||
@ -454,6 +465,11 @@ sealed class MainAction {
|
||||
*/
|
||||
data object OpenDebugMenu : MainAction()
|
||||
|
||||
/**
|
||||
* Receive event to save the app resume screen
|
||||
*/
|
||||
data class ResumeScreenDataReceived(val screenResumeData: AppResumeScreenData?) : MainAction()
|
||||
|
||||
/**
|
||||
* Actions for internal use by the ViewModel.
|
||||
*/
|
||||
|
||||
@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJso
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Primary access point for disk information.
|
||||
@ -352,4 +353,14 @@ interface AuthDiskSource {
|
||||
* Stores the new device notice state for the given [userId].
|
||||
*/
|
||||
fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?)
|
||||
|
||||
/**
|
||||
* Gets the last lock timestamp for the given [userId].
|
||||
*/
|
||||
fun getLastLockTimestamp(userId: String): Instant?
|
||||
|
||||
/**
|
||||
* Stores the last lock timestamp for the given [userId].
|
||||
*/
|
||||
fun storeLastLockTimestamp(userId: String, lastLockTimestamp: Instant?)
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
// These keys should be encrypted
|
||||
@ -49,6 +50,7 @@ private const val USES_KEY_CONNECTOR = "usesKeyConnector"
|
||||
private const val ONBOARDING_STATUS_KEY = "onboardingStatus"
|
||||
private const val SHOW_IMPORT_LOGINS_KEY = "showImportLogins"
|
||||
private const val NEW_DEVICE_NOTICE_STATE = "newDeviceNoticeState"
|
||||
private const val LAST_LOCK_TIMESTAMP = "lastLockTimestamp"
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthDiskSource].
|
||||
@ -154,6 +156,7 @@ class AuthDiskSourceImpl(
|
||||
storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = null)
|
||||
storeAuthenticatorSyncUnlockKey(userId = userId, authenticatorSyncUnlockKey = null)
|
||||
storeShowImportLogins(userId = userId, showImportLogins = null)
|
||||
storeLastLockTimestamp(userId = userId, lastLockTimestamp = null)
|
||||
|
||||
// Do not remove the DeviceKey or PendingAuthRequest on logout, these are persisted
|
||||
// indefinitely unless the TDE flow explicitly removes them.
|
||||
@ -502,6 +505,19 @@ class AuthDiskSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun getLastLockTimestamp(userId: String): Instant? {
|
||||
return getLong(key = LAST_LOCK_TIMESTAMP.appendIdentifier(userId))?.let {
|
||||
Instant.ofEpochMilli(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun storeLastLockTimestamp(userId: String, lastLockTimestamp: Instant?) {
|
||||
putLong(
|
||||
key = LAST_LOCK_TIMESTAMP.appendIdentifier(userId),
|
||||
value = lastLockTimestamp?.toEpochMilli(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateAndStoreUniqueAppId(): String =
|
||||
UUID
|
||||
.randomUUID()
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
@ -391,4 +392,14 @@ interface SettingsDiskSource {
|
||||
* Returns an [Flow] to observe updates to the "ShouldShowGeneratorCoachMark" value.
|
||||
*/
|
||||
fun getShouldShowGeneratorCoachMarkFlow(): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Stores the given [screenData] as the screen to resume to identified by [userId].
|
||||
*/
|
||||
fun storeAppResumeScreen(userId: String, screenData: AppResumeScreenData?)
|
||||
|
||||
/**
|
||||
* Gets the screen data to resume to for the device identified by [userId] or null if no screen
|
||||
*/
|
||||
fun getAppResumeScreen(userId: String): AppResumeScreenData?
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
@ -41,6 +42,7 @@ private const val COPY_ACTION_COUNT = "copyActionCount"
|
||||
private const val CREATE_ACTION_COUNT = "createActionCount"
|
||||
private const val SHOULD_SHOW_ADD_LOGIN_COACH_MARK = "shouldShowAddLoginCoachMark"
|
||||
private const val SHOULD_SHOW_GENERATOR_COACH_MARK = "shouldShowGeneratorCoachMark"
|
||||
private const val RESUME_SCREEN = "resumeScreen"
|
||||
|
||||
/**
|
||||
* Primary implementation of [SettingsDiskSource].
|
||||
@ -185,6 +187,7 @@ class SettingsDiskSourceImpl(
|
||||
storeClearClipboardFrequencySeconds(userId = userId, frequency = null)
|
||||
removeWithPrefix(prefix = ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY.appendIdentifier(userId))
|
||||
storeVaultRegisteredForExport(userId = userId, isRegistered = null)
|
||||
storeAppResumeScreen(userId = userId, screenData = null)
|
||||
|
||||
// The following are intentionally not cleared so they can be
|
||||
// restored after logging out and back in:
|
||||
@ -526,6 +529,16 @@ class SettingsDiskSourceImpl(
|
||||
emit(getShouldShowGeneratorCoachMark())
|
||||
}
|
||||
|
||||
override fun storeAppResumeScreen(userId: String, screenData: AppResumeScreenData?) {
|
||||
putString(
|
||||
key = RESUME_SCREEN.appendIdentifier(userId),
|
||||
value = screenData?.let { json.encodeToString(it) },
|
||||
)
|
||||
}
|
||||
|
||||
override fun getAppResumeScreen(userId: String): AppResumeScreenData? =
|
||||
getString(RESUME_SCREEN.appendIdentifier(userId))?.let { json.decodeFromStringOrNull(it) }
|
||||
|
||||
private fun getMutableLastSyncFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Instant?> =
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
|
||||
/**
|
||||
* Manages the screen from which the app should be resumed after unlock.
|
||||
*/
|
||||
interface AppResumeManager {
|
||||
|
||||
/**
|
||||
* Sets the screen from which the app should be resumed after unlock.
|
||||
*
|
||||
* @param screenData The screen identifier (e.g., "HomeScreen", "SettingsScreen").
|
||||
*/
|
||||
fun setResumeScreen(screenData: AppResumeScreenData)
|
||||
|
||||
/**
|
||||
* Gets the screen from which the app should be resumed after unlock.
|
||||
*
|
||||
* @return The screen identifier, or an empty string if not set.
|
||||
*/
|
||||
fun getResumeScreen(): AppResumeScreenData?
|
||||
|
||||
/**
|
||||
* Gets the special circumstance associated with the resume screen for the current user.
|
||||
*
|
||||
* @return The special circumstance, or null if no special circumstance
|
||||
* is associated with the resume screen.
|
||||
*/
|
||||
fun getResumeSpecialCircumstance(): SpecialCircumstance?
|
||||
|
||||
/**
|
||||
* Clears the saved resume screen for the current user.
|
||||
*/
|
||||
fun clearResumeScreen()
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import java.time.Clock
|
||||
|
||||
private const val UNLOCK_NAVIGATION_TIME_SECONDS: Long = 5 * 60
|
||||
|
||||
/**
|
||||
* Primary implementation of [AppResumeManager].
|
||||
*/
|
||||
class AppResumeManagerImpl(
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val authRepository: AuthRepository,
|
||||
private val vaultLockManager: VaultLockManager,
|
||||
private val clock: Clock,
|
||||
) : AppResumeManager {
|
||||
|
||||
override fun setResumeScreen(screenData: AppResumeScreenData) {
|
||||
authRepository.activeUserId?.let {
|
||||
settingsDiskSource.storeAppResumeScreen(
|
||||
userId = it,
|
||||
screenData = screenData,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getResumeScreen(): AppResumeScreenData? {
|
||||
return authRepository.activeUserId?.let { userId ->
|
||||
settingsDiskSource.getAppResumeScreen(userId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getResumeSpecialCircumstance(): SpecialCircumstance? {
|
||||
val userId = authRepository.activeUserId ?: return null
|
||||
val timeNowMinus5Min = clock.instant().minusSeconds(UNLOCK_NAVIGATION_TIME_SECONDS)
|
||||
val lastLockTimestamp = authDiskSource
|
||||
.getLastLockTimestamp(userId = userId)
|
||||
?: return null
|
||||
|
||||
if (timeNowMinus5Min.isAfter(lastLockTimestamp)) {
|
||||
settingsDiskSource.storeAppResumeScreen(userId = userId, screenData = null)
|
||||
return null
|
||||
}
|
||||
return when (val resumeScreenData = getResumeScreen()) {
|
||||
AppResumeScreenData.GeneratorScreen -> SpecialCircumstance.GeneratorShortcut
|
||||
AppResumeScreenData.SendScreen -> SpecialCircumstance.SendShortcut
|
||||
is AppResumeScreenData.SearchScreen -> SpecialCircumstance.SearchShortcut(
|
||||
searchTerm = resumeScreenData.searchTerm,
|
||||
)
|
||||
|
||||
AppResumeScreenData.VerificationCodeScreen -> {
|
||||
SpecialCircumstance.VerificationCodeShortcut
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearResumeScreen() {
|
||||
val userId = authRepository.activeUserId ?: return
|
||||
if (vaultLockManager.isVaultUnlocked(userId = userId)) {
|
||||
settingsDiskSource.storeAppResumeScreen(
|
||||
userId = userId,
|
||||
screenData = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,8 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterM
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
@ -66,6 +68,7 @@ import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@ -337,4 +340,22 @@ object PlatformManagerModule {
|
||||
fun provideKeyManager(
|
||||
@ApplicationContext context: Context,
|
||||
): KeyManager = KeyManagerImpl(context = context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppResumeManager(
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
authRepository: AuthRepository,
|
||||
vaultLockManager: VaultLockManager,
|
||||
clock: Clock,
|
||||
): AppResumeManager {
|
||||
return AppResumeManagerImpl(
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
authDiskSource = authDiskSource,
|
||||
authRepository = authRepository,
|
||||
vaultLockManager = vaultLockManager,
|
||||
clock = clock,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Data class representing the Screen Data for app resume.
|
||||
*/
|
||||
@Serializable
|
||||
sealed class AppResumeScreenData {
|
||||
|
||||
/**
|
||||
* Data object representing the Generator screen for app resume.
|
||||
*/
|
||||
@Serializable
|
||||
data object GeneratorScreen : AppResumeScreenData()
|
||||
|
||||
/**
|
||||
* Data object representing the Send screen for app resume.
|
||||
*/
|
||||
@Serializable
|
||||
data object SendScreen : AppResumeScreenData()
|
||||
|
||||
/**
|
||||
* Data class representing the Search screen for app resume.
|
||||
*/
|
||||
@Serializable
|
||||
data class SearchScreen(val searchTerm: String) : AppResumeScreenData()
|
||||
|
||||
/**
|
||||
* Data object representing the Verification Code screen for app resume.
|
||||
*/
|
||||
@Serializable
|
||||
data object VerificationCodeScreen : AppResumeScreenData()
|
||||
}
|
||||
@ -104,6 +104,24 @@ sealed class SpecialCircumstance : Parcelable {
|
||||
@Parcelize
|
||||
data object AccountSecurityShortcut : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* Deeplink to the Send.
|
||||
*/
|
||||
@Parcelize
|
||||
data object SendShortcut : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* Deeplink to the Search.
|
||||
*/
|
||||
@Parcelize
|
||||
data class SearchShortcut(val searchTerm: String) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* Deeplink to the Verification Code.
|
||||
*/
|
||||
@Parcelize
|
||||
data object VerificationCodeShortcut : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* A subset of [SpecialCircumstance] that are only relevant in a pre-login state and should be
|
||||
* cleared after a successful login.
|
||||
|
||||
@ -0,0 +1,80 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.LivecycleEventEffect
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalAppResumeStateManager
|
||||
|
||||
/**
|
||||
* Manages the state of the screen to resume to after the app is unlocked.
|
||||
*/
|
||||
interface AppResumeStateManager {
|
||||
/**
|
||||
* The current state of the screen to resume to.
|
||||
* It will be `null` if there is no screen to resume to.
|
||||
*/
|
||||
val appResumeState: State<AppResumeScreenData?>
|
||||
|
||||
/**
|
||||
* Updates the screen data to resume to.
|
||||
*
|
||||
* @param data The [AppResumeScreenData] for the screen to resume to, or `null` if there is no
|
||||
* screen to resume to.
|
||||
*/
|
||||
fun updateScreenData(data: AppResumeScreenData?)
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary implementation of [AppResumeStateManager].
|
||||
*/
|
||||
class AppResumeStateManagerImpl : AppResumeStateManager {
|
||||
private val mutableAppResumeState = mutableStateOf<AppResumeScreenData?>(null)
|
||||
override val appResumeState: State<AppResumeScreenData?> = mutableAppResumeState
|
||||
|
||||
override fun updateScreenData(data: AppResumeScreenData?) {
|
||||
mutableAppResumeState.value = data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumer
|
||||
*
|
||||
* onDataUpdate (call in central location: MainViewModel -> updates the data source through action
|
||||
* handling.
|
||||
*/
|
||||
@Composable
|
||||
fun ObserveScreenDataEffect(onDataUpdate: (AppResumeScreenData?) -> Unit) {
|
||||
val appResumeStateManager = LocalAppResumeStateManager.current
|
||||
LaunchedEffect(appResumeStateManager.appResumeState.value) {
|
||||
onDataUpdate(appResumeStateManager.appResumeState.value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Producer
|
||||
*
|
||||
* Add to screen where needed and pass in the necessary instance of [AppResumeScreenData]
|
||||
*/
|
||||
@Composable
|
||||
fun RegisterScreenDataOnLifecycleEffect(
|
||||
appResumeStateManager: AppResumeStateManager = LocalAppResumeStateManager.current,
|
||||
appResumeStateProvider: () -> AppResumeScreenData,
|
||||
) {
|
||||
LivecycleEventEffect { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
appResumeStateManager.updateScreenData(data = appResumeStateProvider())
|
||||
}
|
||||
|
||||
Lifecycle.Event.ON_STOP -> {
|
||||
appResumeStateManager.updateScreenData(data = null)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.sdk.AuthClient
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
|
||||
@ -281,6 +281,10 @@ class VaultLockManagerImpl(
|
||||
)
|
||||
if (!wasVaultLocked) {
|
||||
mutableVaultStateEventSharedFlow.tryEmit(VaultStateEvent.Locked(userId = userId))
|
||||
authDiskSource.storeLastLockTimestamp(
|
||||
userId = userId,
|
||||
lastLockTimestamp = clock.instant(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
@ -52,6 +53,7 @@ class VaultUnlockViewModel @Inject constructor(
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val fido2CredentialManager: Fido2CredentialManager,
|
||||
private val appResumeManager: AppResumeManager,
|
||||
environmentRepo: EnvironmentRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<VaultUnlockState, VaultUnlockEvent, VaultUnlockAction>(
|
||||
@ -71,7 +73,9 @@ class VaultUnlockViewModel @Inject constructor(
|
||||
// There is no valid way to unlock this app.
|
||||
authRepository.logout()
|
||||
}
|
||||
|
||||
val specialCircumstance = specialCircumstanceManager.specialCircumstance
|
||||
|
||||
val showAccountMenu =
|
||||
VaultUnlockArgs(savedStateHandle).unlockType == UnlockType.STANDARD &&
|
||||
(specialCircumstance !is SpecialCircumstance.Fido2GetCredentials &&
|
||||
@ -340,6 +344,11 @@ class VaultUnlockViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
VaultUnlockResult.Success -> {
|
||||
if (specialCircumstanceManager.specialCircumstance == null) {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
appResumeManager.getResumeSpecialCircumstance()
|
||||
}
|
||||
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
// Don't do anything, we'll navigate to the right place.
|
||||
}
|
||||
|
||||
@ -10,6 +10,8 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManager
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManagerImpl
|
||||
@ -49,6 +51,7 @@ fun LocalManagerProvider(
|
||||
LocalNfcManager provides NfcManagerImpl(activity),
|
||||
LocalFido2CompletionManager provides fido2CompletionManager,
|
||||
LocalAppReviewManager provides AppReviewManagerImpl(activity),
|
||||
LocalAppResumeStateManager provides AppResumeStateManagerImpl(),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
@ -103,3 +106,7 @@ val LocalFido2CompletionManager: ProvidableCompositionLocal<Fido2CompletionManag
|
||||
val LocalAppReviewManager: ProvidableCompositionLocal<AppReviewManager> = compositionLocalOf {
|
||||
error("CompositionLocal AppReviewManager not present")
|
||||
}
|
||||
|
||||
val LocalAppResumeStateManager = compositionLocalOf<AppResumeStateManager> {
|
||||
error("CompositionLocal AppResumeStateManager not present")
|
||||
}
|
||||
|
||||
@ -104,7 +104,9 @@ class RootNavViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
userState.activeAccount.isVaultUnlocked &&
|
||||
authRepository.checkUserNeedsNewDeviceTwoFactorNotice() -> RootNavState.NewDeviceTwoFactorNotice(userState.activeAccount.email)
|
||||
authRepository.checkUserNeedsNewDeviceTwoFactorNotice() -> RootNavState.NewDeviceTwoFactorNotice(
|
||||
userState.activeAccount.email,
|
||||
)
|
||||
|
||||
userState.activeAccount.isVaultUnlocked -> {
|
||||
when (specialCircumstance) {
|
||||
@ -157,6 +159,9 @@ class RootNavViewModel @Inject constructor(
|
||||
SpecialCircumstance.AccountSecurityShortcut,
|
||||
SpecialCircumstance.GeneratorShortcut,
|
||||
SpecialCircumstance.VaultShortcut,
|
||||
SpecialCircumstance.SendShortcut,
|
||||
is SpecialCircumstance.SearchShortcut,
|
||||
SpecialCircumstance.VerificationCodeShortcut,
|
||||
null,
|
||||
-> RootNavState.VaultUnlocked(activeUserId = userState.activeAccount.userId)
|
||||
|
||||
|
||||
@ -20,6 +20,9 @@ import androidx.core.net.toUri
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.RegisterScreenDataOnLifecycleEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenSearchTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||
@ -29,6 +32,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalAppResumeStateManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.handlers.SearchHandlers
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
@ -48,10 +52,20 @@ fun SearchScreen(
|
||||
onNavigateToViewCipher: (cipherId: String) -> Unit,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
viewModel: SearchViewModel = hiltViewModel(),
|
||||
appResumeStateManager: AppResumeStateManager = LocalAppResumeStateManager.current,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val searchHandlers = remember(viewModel) { SearchHandlers.create(viewModel) }
|
||||
val context = LocalContext.current
|
||||
|
||||
RegisterScreenDataOnLifecycleEffect(
|
||||
appResumeStateManager = appResumeStateManager,
|
||||
) {
|
||||
AppResumeScreenData.SearchScreen(
|
||||
searchTerm = state.searchTerm,
|
||||
)
|
||||
}
|
||||
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
SearchEvent.NavigateBack -> onNavigateBack()
|
||||
|
||||
@ -17,6 +17,7 @@ import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.toTotpDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
@ -86,9 +87,15 @@ class SearchViewModel @Inject constructor(
|
||||
val searchType = SearchArgs(savedStateHandle).type
|
||||
val userState = requireNotNull(authRepo.userStateFlow.value)
|
||||
val specialCircumstance = specialCircumstanceManager.specialCircumstance
|
||||
val searchTerm = (specialCircumstance as? SpecialCircumstance.SearchShortcut)
|
||||
?.searchTerm
|
||||
?.also {
|
||||
specialCircumstanceManager.specialCircumstance = null
|
||||
}
|
||||
.orEmpty()
|
||||
|
||||
SearchState(
|
||||
searchTerm = "",
|
||||
searchTerm = searchTerm,
|
||||
searchType = searchType.toSearchTypeData(),
|
||||
viewState = SearchState.ViewState.Loading,
|
||||
dialogState = null,
|
||||
|
||||
@ -91,7 +91,9 @@ fun VaultUnlockedNavBarScreen(
|
||||
navigateToVaultGraph(navOptions)
|
||||
}
|
||||
|
||||
VaultUnlockedNavBarEvent.NavigateToSendScreen -> {
|
||||
VaultUnlockedNavBarEvent.Shortcut.NavigateToSendScreen,
|
||||
VaultUnlockedNavBarEvent.NavigateToSendScreen,
|
||||
-> {
|
||||
navigateToSendGraph(navOptions)
|
||||
}
|
||||
|
||||
|
||||
@ -69,6 +69,29 @@ class VaultUnlockedNavBarViewModel @Inject constructor(
|
||||
sendEvent(VaultUnlockedNavBarEvent.Shortcut.NavigateToSettingsScreen)
|
||||
}
|
||||
|
||||
SpecialCircumstance.SendShortcut -> {
|
||||
sendEvent(VaultUnlockedNavBarEvent.Shortcut.NavigateToSendScreen)
|
||||
specialCircumstancesManager.specialCircumstance = null
|
||||
}
|
||||
|
||||
is SpecialCircumstance.SearchShortcut -> {
|
||||
sendEvent(
|
||||
VaultUnlockedNavBarEvent.Shortcut.NavigateToVaultScreen(
|
||||
labelRes = state.vaultNavBarLabelRes,
|
||||
contentDescRes = state.vaultNavBarContentDescriptionRes,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is SpecialCircumstance.VerificationCodeShortcut -> {
|
||||
sendEvent(
|
||||
VaultUnlockedNavBarEvent.Shortcut.NavigateToVaultScreen(
|
||||
labelRes = state.vaultNavBarLabelRes,
|
||||
contentDescRes = state.vaultNavBarContentDescriptionRes,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
@ -294,5 +317,12 @@ sealed class VaultUnlockedNavBarEvent {
|
||||
data object NavigateToSettingsScreen : Shortcut() {
|
||||
override val tab: VaultUnlockedNavBarTab = VaultUnlockedNavBarTab.Settings()
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the Send Screen.
|
||||
*/
|
||||
data object NavigateToSendScreen : Shortcut() {
|
||||
override val tab: VaultUnlockedNavBarTab = VaultUnlockedNavBarTab.Send
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,6 +38,9 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.RegisterScreenDataOnLifecycleEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.LivecycleEventEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.scrolledContainerBottomDivider
|
||||
@ -76,6 +79,7 @@ import com.x8bit.bitwarden.ui.platform.components.stepper.BitwardenStepper
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformation
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalAppResumeStateManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
@ -117,6 +121,7 @@ fun GeneratorScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onDimNavBarRequest: (Boolean) -> Unit,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
appResumeStateManager: AppResumeStateManager = LocalAppResumeStateManager.current,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val snackbarHostState = rememberBitwardenSnackbarHostState()
|
||||
@ -129,6 +134,11 @@ fun GeneratorScreen(
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
RegisterScreenDataOnLifecycleEffect(
|
||||
appResumeStateManager = appResumeStateManager,
|
||||
) {
|
||||
AppResumeScreenData.GeneratorScreen
|
||||
}
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
val coachMarkState = rememberLazyListCoachMarkState(
|
||||
|
||||
@ -21,6 +21,9 @@ import androidx.core.net.toUri
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.RegisterScreenDataOnLifecycleEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem
|
||||
@ -34,6 +37,7 @@ import com.x8bit.bitwarden.ui.platform.components.fab.BitwardenFloatingActionBut
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.rememberBitwardenPullToRefreshState
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalAppResumeStateManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
@ -54,6 +58,7 @@ fun SendScreen(
|
||||
onNavigateToSearchSend: (searchType: SearchType.Sends) -> Unit,
|
||||
viewModel: SendViewModel = hiltViewModel(),
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
appResumeStateManager: AppResumeStateManager = LocalAppResumeStateManager.current,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
@ -65,6 +70,12 @@ fun SendScreen(
|
||||
},
|
||||
)
|
||||
|
||||
RegisterScreenDataOnLifecycleEffect(
|
||||
appResumeStateManager = appResumeStateManager,
|
||||
) {
|
||||
AppResumeScreenData.SendScreen
|
||||
}
|
||||
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
is SendEvent.NavigateToSearch -> onNavigateToSearchSend(SearchType.Sends.All)
|
||||
|
||||
@ -12,10 +12,12 @@ import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||
@ -77,6 +79,7 @@ class VaultViewModel @Inject constructor(
|
||||
private val snackbarRelayManager: SnackbarRelayManager,
|
||||
private val reviewPromptManager: ReviewPromptManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
) : BaseViewModel<VaultState, VaultEvent, VaultAction>(
|
||||
initialState = run {
|
||||
val userState = requireNotNull(authRepository.userStateFlow.value)
|
||||
@ -204,6 +207,22 @@ class VaultViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleLifecycleResumed() {
|
||||
when (specialCircumstanceManager.specialCircumstance) {
|
||||
is SpecialCircumstance.SearchShortcut -> {
|
||||
sendEvent(VaultEvent.NavigateToVaultSearchScreen)
|
||||
// not clearing SpecialCircumstance as it contains necessary data
|
||||
return
|
||||
}
|
||||
|
||||
is SpecialCircumstance.VerificationCodeShortcut -> {
|
||||
sendEvent(VaultEvent.NavigateToVerificationCodeScreen)
|
||||
specialCircumstanceManager.specialCircumstance = null
|
||||
return
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
val shouldShowPrompt = reviewPromptManager.shouldPromptForAppReview() &&
|
||||
featureFlagManager.getFeatureFlag(FlagKey.AppReviewPrompt)
|
||||
if (shouldShowPrompt) {
|
||||
|
||||
@ -21,6 +21,9 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.RegisterScreenDataOnLifecycleEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
|
||||
@ -35,6 +38,7 @@ import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.rememberBitwardenPullToRefreshState
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalAppResumeStateManager
|
||||
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.handlers.VerificationCodeHandlers
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@ -51,6 +55,7 @@ fun VerificationCodeScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToSearch: () -> Unit,
|
||||
onNavigateToVaultItemScreen: (String) -> Unit,
|
||||
appResumeStateManager: AppResumeStateManager = LocalAppResumeStateManager.current,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val verificationCodeHandler = remember(viewModel) {
|
||||
@ -65,6 +70,12 @@ fun VerificationCodeScreen(
|
||||
},
|
||||
)
|
||||
|
||||
RegisterScreenDataOnLifecycleEffect(
|
||||
appResumeStateManager = appResumeStateManager,
|
||||
) {
|
||||
AppResumeScreenData.VerificationCodeScreen
|
||||
}
|
||||
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
is VerificationCodeEvent.NavigateBack -> onNavigateBack()
|
||||
|
||||
@ -21,8 +21,8 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialReques
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2AssertionRequestOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CreateCredentialRequestOrNull
|
||||
@ -34,9 +34,11 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
|
||||
@ -130,6 +132,11 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
private val savedStateHandle = SavedStateHandle()
|
||||
|
||||
private val appResumeManager: AppResumeManager = mockk {
|
||||
every { setResumeScreen(any()) } just runs
|
||||
every { clearResumeScreen() } just runs
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(
|
||||
@ -1061,6 +1068,28 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
verify { authRepository.switchAccount(userId) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ResumeScreenDataReceived with null value, should call AppResumeManager clearResumeScreen`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(
|
||||
MainAction.ResumeScreenDataReceived(screenResumeData = null),
|
||||
)
|
||||
|
||||
verify { appResumeManager.clearResumeScreen() }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ResumeScreenDataReceived with data value, should call AppResumeManager setResumeScreen`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(
|
||||
MainAction.ResumeScreenDataReceived(screenResumeData = AppResumeScreenData.GeneratorScreen),
|
||||
)
|
||||
|
||||
verify { appResumeManager.setResumeScreen(AppResumeScreenData.GeneratorScreen) }
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
initialSpecialCircumstance: SpecialCircumstance? = null,
|
||||
) = MainViewModel(
|
||||
@ -1079,6 +1108,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
savedStateHandle = savedStateHandle.apply {
|
||||
set(SPECIAL_CIRCUMSTANCE_KEY, initialSpecialCircumstance)
|
||||
},
|
||||
appResumeManager = appResumeManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -27,13 +27,13 @@ import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
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
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@ -1335,6 +1335,62 @@ class AuthDiskSourceTest {
|
||||
actual,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getLastLockTimestamp should pull from SharedPreferences`() {
|
||||
val storeKey = "bwPreferencesStorage:lastLockTimestamp"
|
||||
val mockUserId = "mockUserId"
|
||||
val expectedState = Instant.parse("2025-01-13T12:00:00Z")
|
||||
fakeSharedPreferences.edit {
|
||||
putLong(
|
||||
"${storeKey}_$mockUserId",
|
||||
expectedState.toEpochMilli(),
|
||||
)
|
||||
}
|
||||
val actual = authDiskSource.getLastLockTimestamp(userId = mockUserId)
|
||||
assertEquals(
|
||||
expectedState,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getLastLockTimestamp should pull null from SharedPreferences if there is no data`() {
|
||||
val mockUserId = "mockUserId"
|
||||
val expectedState = null
|
||||
val actual = authDiskSource.getLastLockTimestamp(userId = mockUserId)
|
||||
assertEquals(
|
||||
expectedState,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setLastLockTimestamp should update SharedPreferences`() {
|
||||
val mockUserId = "mockUserId"
|
||||
val expectedState = Instant.parse("2025-01-13T12:00:00Z")
|
||||
authDiskSource.storeLastLockTimestamp(
|
||||
userId = mockUserId,
|
||||
expectedState,
|
||||
)
|
||||
val actual = authDiskSource.getLastLockTimestamp(userId = mockUserId)
|
||||
assertEquals(
|
||||
expectedState,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setLastLockTimestamp should clear SharedPreferences when null is passed`() {
|
||||
val mockUserId = "mockUserId"
|
||||
val expectedState = null
|
||||
authDiskSource.storeLastLockTimestamp(
|
||||
userId = mockUserId,
|
||||
expectedState,
|
||||
)
|
||||
val actual = authDiskSource.getLastLockTimestamp(userId = mockUserId)
|
||||
assertNull(actual)
|
||||
}
|
||||
}
|
||||
|
||||
private const val USER_STATE_JSON = """
|
||||
|
||||
@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import org.junit.Assert.assertEquals
|
||||
import java.time.Instant
|
||||
|
||||
class FakeAuthDiskSource : AuthDiskSource {
|
||||
|
||||
@ -64,6 +65,7 @@ class FakeAuthDiskSource : AuthDiskSource {
|
||||
private val storedOnboardingStatus = mutableMapOf<String, OnboardingStatus?>()
|
||||
private val storedShowImportLogins = mutableMapOf<String, Boolean?>()
|
||||
private val storedNewDeviceNoticeState = mutableMapOf<String, NewDeviceNoticeState?>()
|
||||
private val storedLastLockTimestampState = mutableMapOf<String, Instant?>()
|
||||
|
||||
override var userState: UserStateJson? = null
|
||||
set(value) {
|
||||
@ -314,13 +316,21 @@ class FakeAuthDiskSource : AuthDiskSource {
|
||||
return storedNewDeviceNoticeState[userId] ?: NewDeviceNoticeState(
|
||||
displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN,
|
||||
lastSeenDate = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?) {
|
||||
storedNewDeviceNoticeState[userId] = newState
|
||||
}
|
||||
|
||||
override fun getLastLockTimestamp(userId: String): Instant? {
|
||||
return storedLastLockTimestampState[userId]
|
||||
}
|
||||
|
||||
override fun storeLastLockTimestamp(userId: String, lastLockTimestamp: Instant?) {
|
||||
storedLastLockTimestampState[userId] = lastLockTimestamp
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the the [isTdeLoginComplete] was stored successfully using the [userId].
|
||||
*/
|
||||
@ -471,6 +481,13 @@ class FakeAuthDiskSource : AuthDiskSource {
|
||||
assertEquals(policies, storedPolicies[userId])
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the [lastLockTimestamp] was stored successfully using the [userId].
|
||||
*/
|
||||
fun assertLastLockTimestamp(userId: String, expectedValue: Instant?) {
|
||||
assertEquals(expectedValue, storedLastLockTimestampState[userId])
|
||||
}
|
||||
|
||||
//region Private helper functions
|
||||
|
||||
private fun getMutableShouldUseKeyConnectorFlow(
|
||||
|
||||
@ -4,12 +4,15 @@ import androidx.core.content.edit
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeSharedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequency
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
@ -1314,4 +1317,52 @@ class SettingsDiskSourceTest {
|
||||
assertTrue(awaitItem() ?: false)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAppResumeScreen should pull from SharedPreferences`() {
|
||||
val mockUserId = "mockUserId"
|
||||
val resumeScreenKey = "bwPreferencesStorage:resumeScreen_$mockUserId"
|
||||
val expectedData = AppResumeScreenData.GeneratorScreen
|
||||
fakeSharedPreferences.edit {
|
||||
putString(
|
||||
resumeScreenKey,
|
||||
json.encodeToString<AppResumeScreenData>(expectedData),
|
||||
)
|
||||
}
|
||||
assertEquals(expectedData, settingsDiskSource.getAppResumeScreen(mockUserId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storeAppResumeScreen should update SharedPreferences`() {
|
||||
val mockUserId = "mockUserId"
|
||||
val resumeScreenKey = "bwPreferencesStorage:resumeScreen_$mockUserId"
|
||||
val expectedData = AppResumeScreenData.GeneratorScreen
|
||||
settingsDiskSource.storeAppResumeScreen(mockUserId, expectedData)
|
||||
assertEquals(
|
||||
expectedData,
|
||||
fakeSharedPreferences.getString(resumeScreenKey, "")?.let {
|
||||
Json.decodeFromStringOrNull<AppResumeScreenData>(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storeAppResumeScreen should save null when passed`() {
|
||||
val mockUserId = "mockUserId"
|
||||
val resumeScreenKey = "bwPreferencesStorage:resumeScreen_$mockUserId"
|
||||
val expectedData = AppResumeScreenData.GeneratorScreen
|
||||
settingsDiskSource.storeAppResumeScreen(mockUserId, expectedData)
|
||||
assertEquals(
|
||||
expectedData,
|
||||
fakeSharedPreferences.getString(resumeScreenKey, "")?.let {
|
||||
Json.decodeFromStringOrNull<AppResumeScreenData>(it)
|
||||
},
|
||||
)
|
||||
settingsDiskSource.storeAppResumeScreen(mockUserId, null)
|
||||
assertNull(
|
||||
fakeSharedPreferences.getString(resumeScreenKey, "")?.let {
|
||||
Json.decodeFromStringOrNull<AppResumeScreenData>(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk.util
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
@ -67,6 +70,7 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
||||
private val storedScreenCaptureAllowed = mutableMapOf<String, Boolean?>()
|
||||
private var storedSystemBiometricIntegritySource: String? = null
|
||||
private val storedAccountBiometricIntegrityValidity = mutableMapOf<String, Boolean?>()
|
||||
private val storedAppResumeScreenData = mutableMapOf<String, String?>()
|
||||
private val userSignIns = mutableMapOf<String, Boolean>()
|
||||
private val userShowAutoFillBadge = mutableMapOf<String, Boolean?>()
|
||||
private val userShowUnlockBadge = mutableMapOf<String, Boolean?>()
|
||||
@ -424,6 +428,14 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
||||
emit(hasSeenGeneratorCoachMark)
|
||||
}
|
||||
|
||||
override fun storeAppResumeScreen(userId: String, screenData: AppResumeScreenData?) {
|
||||
storedAppResumeScreenData[userId] = screenData.let { Json.encodeToString(it) }
|
||||
}
|
||||
|
||||
override fun getAppResumeScreen(userId: String): AppResumeScreenData? {
|
||||
return storedAppResumeScreenData[userId]?.let { Json.decodeFromStringOrNull(it) }
|
||||
}
|
||||
|
||||
//region Private helper functions
|
||||
private fun getMutableScreenCaptureAllowedFlow(userId: String): MutableSharedFlow<Boolean?> {
|
||||
return mutableScreenCaptureAllowedFlowMap.getOrPut(userId) {
|
||||
|
||||
@ -0,0 +1,171 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class AppResumeManagerTest {
|
||||
private val fakeSettingsDiskSource: SettingsDiskSource = FakeSettingsDiskSource()
|
||||
private val authRepository = mockk<AuthRepository> {
|
||||
every { activeUserId } returns USER_ID
|
||||
}
|
||||
private val vaultLockManager: VaultLockManager = mockk {
|
||||
every { isVaultUnlocked(USER_ID) } returns true
|
||||
}
|
||||
|
||||
private val fixedClock: Clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
|
||||
private val appResumeManager = AppResumeManagerImpl(
|
||||
settingsDiskSource = fakeSettingsDiskSource,
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
authRepository = authRepository,
|
||||
vaultLockManager = vaultLockManager,
|
||||
clock = fixedClock,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `setResumeScreen should update the app resume screen in the settings disk source`() =
|
||||
runTest {
|
||||
val expectedValue = AppResumeScreenData.SendScreen
|
||||
appResumeManager.setResumeScreen(expectedValue)
|
||||
val actualValue = fakeSettingsDiskSource.getAppResumeScreen(USER_ID)
|
||||
|
||||
assertEquals(expectedValue, actualValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getResumeScreen should return null when there is no app resume screen saved`() =
|
||||
runTest {
|
||||
val actualValue = appResumeManager.getResumeScreen()
|
||||
assertNull(actualValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getResumeScreen should return the saved AppResumeScreen`() =
|
||||
runTest {
|
||||
val expectedValue = AppResumeScreenData.GeneratorScreen
|
||||
fakeSettingsDiskSource.storeAppResumeScreen(
|
||||
userId = USER_ID,
|
||||
screenData = expectedValue,
|
||||
)
|
||||
val actualValue = appResumeManager.getResumeScreen()
|
||||
assertEquals(expectedValue, actualValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearResumeScreen should clear the app resume screen in the settings disk source`() =
|
||||
runTest {
|
||||
fakeSettingsDiskSource.storeAppResumeScreen(
|
||||
userId = USER_ID,
|
||||
screenData = AppResumeScreenData.GeneratorScreen,
|
||||
)
|
||||
appResumeManager.clearResumeScreen()
|
||||
val actualValue = fakeSettingsDiskSource.getAppResumeScreen(USER_ID)
|
||||
assertNull(actualValue)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getResumeSpecialCircumstance should return GeneratorShortcut when the resume screen is GeneratorScreen`() =
|
||||
runTest {
|
||||
fakeSettingsDiskSource.storeAppResumeScreen(
|
||||
userId = USER_ID,
|
||||
screenData = AppResumeScreenData.GeneratorScreen,
|
||||
)
|
||||
fakeAuthDiskSource.storeLastLockTimestamp(USER_ID, fixedClock.instant())
|
||||
val expectedValue = SpecialCircumstance.GeneratorShortcut
|
||||
val actualValue = appResumeManager.getResumeSpecialCircumstance()
|
||||
assertEquals(expectedValue, actualValue)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getResumeSpecialCircumstance should return SendShortcut when the resume screen is SendScreen`() =
|
||||
runTest {
|
||||
fakeSettingsDiskSource.storeAppResumeScreen(
|
||||
userId = USER_ID,
|
||||
screenData = AppResumeScreenData.SendScreen,
|
||||
)
|
||||
fakeAuthDiskSource.storeLastLockTimestamp(USER_ID, fixedClock.instant())
|
||||
val expectedValue = SpecialCircumstance.SendShortcut
|
||||
val actualValue = appResumeManager.getResumeSpecialCircumstance()
|
||||
assertEquals(expectedValue, actualValue)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getResumeSpecialCircumstance should return VerificationCodeShortcut when the resume screen is VerificationCodeScreen`() =
|
||||
runTest {
|
||||
fakeSettingsDiskSource.storeAppResumeScreen(
|
||||
userId = USER_ID,
|
||||
screenData = AppResumeScreenData.VerificationCodeScreen,
|
||||
)
|
||||
fakeAuthDiskSource.storeLastLockTimestamp(USER_ID, fixedClock.instant())
|
||||
val expectedValue = SpecialCircumstance.VerificationCodeShortcut
|
||||
val actualValue = appResumeManager.getResumeSpecialCircumstance()
|
||||
assertEquals(expectedValue, actualValue)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getResumeSpecialCircumstance should return SearchShortcut when the resume screen is SearchScreen`() =
|
||||
runTest {
|
||||
fakeSettingsDiskSource.storeAppResumeScreen(
|
||||
userId = USER_ID,
|
||||
screenData = AppResumeScreenData.SearchScreen("test"),
|
||||
)
|
||||
fakeAuthDiskSource.storeLastLockTimestamp(USER_ID, fixedClock.instant())
|
||||
val expectedValue = SpecialCircumstance.SearchShortcut("test")
|
||||
val actualValue = appResumeManager.getResumeSpecialCircumstance()
|
||||
assertEquals(expectedValue, actualValue)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getResumeSpecialCircumstance should should clear app resume screen if have passed 5 minutes`() {
|
||||
val delayedAuthDiskSource: AuthDiskSource = mockk {
|
||||
every { getLastLockTimestamp(any()) } returns fixedClock.instant()
|
||||
.minusSeconds(5 * 60 + 1)
|
||||
}
|
||||
|
||||
val delayedAppResumeManager = AppResumeManagerImpl(
|
||||
settingsDiskSource = fakeSettingsDiskSource,
|
||||
authDiskSource = delayedAuthDiskSource,
|
||||
authRepository = authRepository,
|
||||
vaultLockManager = vaultLockManager,
|
||||
clock = fixedClock,
|
||||
)
|
||||
fakeSettingsDiskSource.storeAppResumeScreen(
|
||||
userId = USER_ID,
|
||||
screenData = AppResumeScreenData.GeneratorScreen,
|
||||
)
|
||||
val actualValue = delayedAppResumeManager.getResumeSpecialCircumstance()
|
||||
assertNull(actualValue)
|
||||
|
||||
val actualSettingsValue = fakeSettingsDiskSource.getAppResumeScreen(
|
||||
userId = USER_ID,
|
||||
)
|
||||
assertNull(actualSettingsValue)
|
||||
}
|
||||
}
|
||||
|
||||
private const val USER_ID = "user_id"
|
||||
@ -0,0 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.util
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class AppResumeStateManagerTest {
|
||||
private val appStateManager = AppResumeStateManagerImpl()
|
||||
|
||||
@Test
|
||||
fun `AppResumeStateManagerImpl should update and retrieve screen data`() {
|
||||
val screenData = AppResumeScreenData.GeneratorScreen
|
||||
|
||||
appStateManager.updateScreenData(screenData)
|
||||
Assert.assertEquals(screenData, appStateManager.appResumeState.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AppResumeStateManagerImpl should retrieve null if not set`() {
|
||||
Assert.assertEquals(null, appStateManager.appResumeState.value)
|
||||
}
|
||||
}
|
||||
@ -167,6 +167,10 @@ class VaultLockManagerTest {
|
||||
vaultLockManager.vaultStateEventFlow.test {
|
||||
vaultLockManager.lockVault(userId = USER_ID)
|
||||
assertEquals(VaultStateEvent.Locked(userId = USER_ID), awaitItem())
|
||||
fakeAuthDiskSource.assertLastLockTimestamp(
|
||||
userId = USER_ID,
|
||||
FIXED_CLOCK.instant(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
@ -80,8 +81,14 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
||||
every { isUserVerified } returns true
|
||||
every { isUserVerified = any() } just runs
|
||||
}
|
||||
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager = mockk {
|
||||
every { specialCircumstance } returns null
|
||||
every { specialCircumstance = any() } answers { }
|
||||
}
|
||||
|
||||
private val appResumeManager: AppResumeManager = mockk {
|
||||
every { getResumeSpecialCircumstance() } returns null
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -1248,6 +1255,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
||||
biometricsEncryptionManager = biometricsEncryptionManager,
|
||||
fido2CredentialManager = fido2CredentialManager,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
appResumeManager = appResumeManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -762,34 +762,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||
val fido2GetCredentialsRequest = createMockFido2GetCredentialsRequest(number = 1)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2GetCredentials(fido2GetCredentialsRequest)
|
||||
mutableUserStateFlow.tryEmit(
|
||||
UserState(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "name",
|
||||
email = "email",
|
||||
avatarColorHex = "avatarHexColor",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isLoggedIn = true,
|
||||
isVaultUnlocked = true,
|
||||
needsPasswordReset = false,
|
||||
isBiometricsEnabled = false,
|
||||
organizations = emptyList(),
|
||||
needsMasterPassword = false,
|
||||
trustedDevice = null,
|
||||
hasMasterPassword = true,
|
||||
isUsingKeyConnector = false,
|
||||
onboardingStatus = OnboardingStatus.COMPLETE,
|
||||
firstTimeState = FirstTimeState(
|
||||
showImportLoginsCard = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
mutableUserStateFlow.tryEmit(MOCK_VAULT_UNLOCKED_USER_STATE)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
RootNavState.VaultUnlockedForFido2GetCredentials(
|
||||
@ -800,6 +773,45 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when the active user has an unlocked vault but there is an SendShortcut special circumstance the nav state should be VaultUnlocked`() {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.SendShortcut
|
||||
mutableUserStateFlow.tryEmit(MOCK_VAULT_UNLOCKED_USER_STATE)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
RootNavState.VaultUnlocked(activeUserId = "activeUserId"),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when the active user has an unlocked vault but there is an VerificationCodeShortcut special circumstance the nav state should be VaultUnlocked`() {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.VerificationCodeShortcut
|
||||
mutableUserStateFlow.tryEmit(MOCK_VAULT_UNLOCKED_USER_STATE)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
RootNavState.VaultUnlocked(activeUserId = "activeUserId"),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when the active user has an unlocked vault but there is an SearchShortcut special circumstance the nav state should be VaultUnlocked`() {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.SearchShortcut("")
|
||||
mutableUserStateFlow.tryEmit(MOCK_VAULT_UNLOCKED_USER_STATE)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
RootNavState.VaultUnlocked(activeUserId = "activeUserId"),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when there are no accounts but there is a CompleteRegistration special circumstance the nav state should be CompleteRegistration`() {
|
||||
@ -1386,3 +1398,28 @@ private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||
)
|
||||
|
||||
private const val ACCESS_TOKEN: String = "access_token"
|
||||
|
||||
private val MOCK_VAULT_UNLOCKED_USER_STATE = UserState(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "name",
|
||||
email = "email",
|
||||
avatarColorHex = "avatarColorHex",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isLoggedIn = true,
|
||||
isVaultUnlocked = true,
|
||||
needsPasswordReset = false,
|
||||
isBiometricsEnabled = false,
|
||||
organizations = emptyList(),
|
||||
needsMasterPassword = false,
|
||||
trustedDevice = null,
|
||||
hasMasterPassword = true,
|
||||
isUsingKeyConnector = false,
|
||||
firstTimeState = FirstTimeState(false),
|
||||
onboardingStatus = OnboardingStatus.COMPLETE,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@ -18,6 +18,7 @@ import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
|
||||
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
|
||||
@ -55,6 +56,8 @@ class SearchScreenTest : BaseComposeTest() {
|
||||
every { launchUri(any()) } just runs
|
||||
}
|
||||
|
||||
private val appResumeStateManager: AppResumeStateManager = mockk(relaxed = true)
|
||||
|
||||
private var onNavigateBackCalled = false
|
||||
private var onNavigateToEditSendId: String? = null
|
||||
private var onNavigateToEditCipherId: String? = null
|
||||
@ -70,6 +73,7 @@ class SearchScreenTest : BaseComposeTest() {
|
||||
onNavigateToEditSend = { onNavigateToEditSendId = it },
|
||||
onNavigateToEditCipher = { onNavigateToEditCipherId = it },
|
||||
onNavigateToViewCipher = { onNavigateToViewCipherId = it },
|
||||
appResumeStateManager = appResumeStateManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -248,6 +248,75 @@ class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() {
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on init with SendShortcut special circumstance should navigate to the send screen with shortcut event`() =
|
||||
runTest {
|
||||
every {
|
||||
specialCircumstancesManager.specialCircumstance
|
||||
} returns SpecialCircumstance.SendShortcut
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultUnlockedNavBarEvent.Shortcut.NavigateToSendScreen,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
specialCircumstancesManager.specialCircumstance
|
||||
specialCircumstancesManager.specialCircumstance = null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on init with VerificationCodeShortcut special circumstance should navigate to the Vault screen with shortcut event`() =
|
||||
runTest {
|
||||
every {
|
||||
specialCircumstancesManager.specialCircumstance
|
||||
} returns SpecialCircumstance.VerificationCodeShortcut
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultUnlockedNavBarEvent.Shortcut.NavigateToVaultScreen(
|
||||
labelRes = R.string.my_vault,
|
||||
contentDescRes = R.string.my_vault,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
specialCircumstancesManager.specialCircumstance
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on init with SearchShortcut special circumstance should navigate to the Vault screen with shortcut event`() =
|
||||
runTest {
|
||||
every {
|
||||
specialCircumstancesManager.specialCircumstance
|
||||
} returns SpecialCircumstance.SearchShortcut("")
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultUnlockedNavBarEvent.Shortcut.NavigateToVaultScreen(
|
||||
labelRes = R.string.my_vault,
|
||||
contentDescRes = R.string.my_vault,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
specialCircumstancesManager.specialCircumstance
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel() =
|
||||
VaultUnlockedNavBarViewModel(
|
||||
|
||||
@ -29,6 +29,7 @@ import androidx.compose.ui.test.performTouchInput
|
||||
import androidx.compose.ui.test.swipeRight
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
|
||||
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
|
||||
@ -60,16 +61,20 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||
private val intentManager: IntentManager = mockk {
|
||||
every { launchUri(any()) } just runs
|
||||
}
|
||||
private val appResumeStateManager: AppResumeStateManager = mockk(relaxed = true)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
composeTestRule.setContent {
|
||||
GeneratorScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateToPasswordHistory = { onNavigateToPasswordHistoryScreenCalled = true },
|
||||
onNavigateToPasswordHistory = {
|
||||
onNavigateToPasswordHistoryScreenCalled = true
|
||||
},
|
||||
onNavigateBack = {},
|
||||
onDimNavBarRequest = { onDimNavBarRequest = it },
|
||||
intentManager = intentManager,
|
||||
appResumeStateManager = appResumeStateManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
|
||||
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
|
||||
@ -60,6 +61,7 @@ class SendScreenTest : BaseComposeTest() {
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
}
|
||||
private val appResumeStateManager: AppResumeStateManager = mockk(relaxed = true)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
@ -72,6 +74,7 @@ class SendScreenTest : BaseComposeTest() {
|
||||
onNavigateToSendTextList = { onNavigateToSendTextListCalled = true },
|
||||
onNavigateToSearchSend = { onNavigateToSendSearchCalled = true },
|
||||
intentManager = intentManager,
|
||||
appResumeStateManager = appResumeStateManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,11 +13,13 @@ import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
@ -145,6 +147,11 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
} returns mutableSshKeyVaultItemsEnabledFlow.value
|
||||
}
|
||||
private val reviewPromptManager: ReviewPromptManager = mockk()
|
||||
private val mockAuthRepository = mockk<AuthRepository>(relaxed = true)
|
||||
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager = mockk {
|
||||
every { specialCircumstance } returns null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct and should trigger a syncIfNecessary call`() {
|
||||
@ -1841,6 +1848,41 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `init should send NavigateToVerificationCodeScreen when special circumstance is VerificationCodeShortcut`() =
|
||||
runTest {
|
||||
every {
|
||||
specialCircumstanceManager.specialCircumstance
|
||||
} returns SpecialCircumstance.VerificationCodeShortcut
|
||||
every { specialCircumstanceManager.specialCircumstance = null } just runs
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultAction.LifecycleResumed)
|
||||
assertEquals(
|
||||
VaultEvent.NavigateToVerificationCodeScreen, awaitItem(),
|
||||
)
|
||||
}
|
||||
verify { specialCircumstanceManager.specialCircumstance = null }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `init should send NavigateToVaultSearchScreen when special circumstance is SearchShortcut`() =
|
||||
runTest {
|
||||
every {
|
||||
specialCircumstanceManager.specialCircumstance
|
||||
} returns SpecialCircumstance.SearchShortcut("")
|
||||
every { specialCircumstanceManager.specialCircumstance = null } just runs
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultAction.LifecycleResumed)
|
||||
assertEquals(
|
||||
VaultEvent.NavigateToVaultSearchScreen, awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(): VaultViewModel =
|
||||
VaultViewModel(
|
||||
authRepository = authRepository,
|
||||
@ -1854,6 +1896,7 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
snackbarRelayManager = snackbarRelayManager,
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
@ -41,6 +42,7 @@ class VerificationCodeScreenTest : BaseComposeTest() {
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
}
|
||||
private val appResumeStateManager: AppResumeStateManager = mockk(relaxed = true)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
@ -50,6 +52,7 @@ class VerificationCodeScreenTest : BaseComposeTest() {
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
onNavigateToVaultItemScreen = { onNavigateToVaultItemId = it },
|
||||
onNavigateToSearch = { onNavigateToSearchCalled = true },
|
||||
appResumeStateManager = appResumeStateManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user