Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-13626] Remember last opened view for 5 minutes #4574

Open
wants to merge 29 commits into
base: main
Choose a base branch
from

Conversation

aj-rosado
Copy link
Contributor

@aj-rosado aj-rosado commented Jan 16, 2025

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-13626

📔 Objective

After lock, for the next 5 minutes, the user should be redirected to the previous screen if they were:

  • Send
  • Verification Code
  • Generator
  • Search

To achieve this, a new manager was added to control the behaviour and saving the last screen on SettingsDiskSource (should this be on a different disk source?). The navigation is provided by deeplink using the existing SpecialCircumstance.

The current screen is always saved onResume and cleared onPause (to ensure we don't navigate to previous screens)
Before clearing, checking if the user is not locked, does the trick to not clear in case of lock when the app was on background

A different task will add more screens

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed
    issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

Copy link
Contributor

github-actions bot commented Jan 16, 2025

Logo
Checkmarx One – Scan Summary & Details0661fedf-9ff8-4f93-a3e2-56efffd64e68

Great job, no security vulnerabilities found in this Pull Request

Copy link

codecov bot commented Jan 16, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 88.32%. Comparing base (31ccf49) to head (3548304).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4574      +/-   ##
==========================================
- Coverage   88.35%   88.32%   -0.04%     
==========================================
  Files         603      605       +2     
  Lines       40768    40887     +119     
  Branches     5738     5772      +34     
==========================================
+ Hits        36022    36112      +90     
- Misses       2742     2757      +15     
- Partials     2004     2018      +14     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

) : BaseViewModel<SearchState, SearchEvent, SearchAction>(
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE]
?: run {
val searchType = SearchArgs(savedStateHandle).type
val userState = requireNotNull(authRepo.userStateFlow.value)
val specialCircumstance = specialCircumstanceManager.specialCircumstance
val searchTerm = if (specialCircumstance is SpecialCircumstance.SearchShortcut) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could also be:

(specialCircumstance as? SpecialCircumstance.SearchShortcut)
                ?.searchTerm
                ?.also {
                    specialCircumstanceManager.specialCircumstance = null
                }
                .orEmpty()

) : BaseViewModel<SearchState, SearchEvent, SearchAction>(
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE]
?: run {
val searchType = SearchArgs(savedStateHandle).type
val userState = requireNotNull(authRepo.userStateFlow.value)
val specialCircumstance = specialCircumstanceManager.specialCircumstance
val searchTerm = if (specialCircumstance is SpecialCircumstance.SearchShortcut) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could also be:

(specialCircumstance as? SpecialCircumstance.SearchShortcut)
                ?.searchTerm
                ?.also {
                    specialCircumstanceManager.specialCircumstance = null
                }
                .orEmpty()

/**
* Gets the last lock timestamp for the given [userId].
*/
fun getLastLockTimestamp(userId: String): Long?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should return the same types, either both longs or both Instants. I kinda prefer Instant but I'll leave it up to you

) : AppResumeManager {
private companion object {
// 5 minutes
private const val UNLOCK_NAVIGATION_TIME: Long = 5 * 60
Copy link
Collaborator

@david-livefront david-livefront Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add _SECONDS to the end of this to indicate units.

Also, this can be a top level property of the file.

authDiskSource.getLastLockTimestamp(
userId = userId,
) ?: Long.MIN_VALUE,
)
Copy link
Collaborator

@david-livefront david-livefront Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to handle a null timestamp at all:

val lastLockTimestamp = authDiskSource
    .getLastLockTimestamp(userId = userId)
    ?.let { Instant.ofEpochMilli(it) }
    ?: return null

@@ -268,6 +270,10 @@ class VaultLockManagerImpl(
)
if (!wasVaultLocked) {
mutableVaultStateEventSharedFlow.tryEmit(VaultStateEvent.Locked(userId = userId))
authDiskSource.storeLastLockTimestamp(
userId = userId,
lastLockTimestamp = clock.instant(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -142,6 +153,8 @@ class SearchViewModel @Inject constructor(
is SearchAction.SearchTermChange -> handleSearchTermChange(action)
is SearchAction.VaultFilterSelect -> handleVaultFilterSelect(action)
is SearchAction.OverflowOptionClick -> handleOverflowItemClick(action)
is SearchAction.LifecycleResume -> handleOnResumed()
is SearchAction.LifecycleStop -> handleOnPaused()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function is still called handleOnPaused even though you are stopping. Can we rename this


Lifecycle.Event.ON_STOP -> {
viewModel.trySendAction(GeneratorAction.LifecycleStop)
}
Copy link
Collaborator

@david-livefront david-livefront Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wanna run the formatter here, there should be some newlines.

I would recommend adding a macro for this. There are instructions here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatter is not making any changes on this file

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, are you sure you have the correct formatter configured. Because this when I run the formatter it does this:

    LivecycleEventEffect { _, event ->
        when (event) {
            Lifecycle.Event.ON_RESUME -> {
                viewModel.trySendAction(GeneratorAction.LifecycleResume)
            }

            Lifecycle.Event.ON_STOP -> {
                viewModel.trySendAction(GeneratorAction.LifecycleStop)
            }

            else -> Unit
        }
    }

@@ -2139,6 +2150,11 @@ sealed class GeneratorAction {
*/
data object LifecycleResume : GeneratorAction()

/**
* Indicates the UI has been entered a resumed lifecycle state.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This refers to resume instead of stop

@@ -314,13 +317,21 @@ class FakeAuthDiskSource : AuthDiskSource {
return storedNewDeviceNoticeState[userId] ?: NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN,
lastSeenDate = null,
)
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -53,6 +53,31 @@ class RootNavViewModelTest : BaseViewModelTest() {
dispatcherManager = FakeDispatcherManager(),
)

private val mockVaultUnlockedUserState = UserState(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just make this a constant at the bottom of the file.

import java.time.Instant

// 5 minutes
private const val UNLOCK_NAVIGATION_TIME_MS: Long = 5 * 60
Copy link
Collaborator

@dseverns-livefront dseverns-livefront Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ I appreciate the comment here, but the _MS immediately makes me think milliseconds, UNLOCK_NAVIGATION_TIME_MINUTES can never go wrong being UBER explicit.

Copy link
Collaborator

@david-livefront david-livefront Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is my bad, I told him to add _MS because I thought it was milliseconds.

It should be UNLOCK_NAVIGATION_TIME_SECONDS since the value is in seconds

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants