Skip to content

Commit

Permalink
PM-14836: Fix vault state on app re-entry from background (#1149)
Browse files Browse the repository at this point in the history
  • Loading branch information
ezimet-livefront authored Nov 23, 2024
1 parent e4efd6c commit 8293f41
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 0 deletions.
13 changes: 13 additions & 0 deletions BitwardenShared/UI/Platform/Application/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ class AppCoordinator: Coordinator, HasRootNavigator {

func handleEvent(_ event: AppEvent, context: AnyObject?) async {
switch event {
case let .accountBecameActive(
account,
attemptAutomaticBiometricUnlock,
didSwitchAccountAutomatically
):
await handleAuthEvent(
.accountBecameActive(
account,
animated: true,
attemptAutomaticBiometricUnlock: attemptAutomaticBiometricUnlock,
didSwitchAccountAutomatically: didSwitchAccountAutomatically
)
)
case let .didLogout(userId, userInitiated):
await handleAuthEvent(.didLogout(userId: userId, userInitiated: userInitiated))
case .didStart:
Expand Down
18 changes: 18 additions & 0 deletions BitwardenShared/UI/Platform/Application/AppCoordinatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,24 @@ class AppCoordinatorTests: BitwardenTestCase { // swiftlint:disable:this type_bo
)
}

/// `handleEvent(_:)` with `.accountBecameActive` has the router handle account activation.
@MainActor
func test_handleEvent_accountBecameActive() async {
let account = Account.fixtureAccountLogin()
await subject.handleEvent(
.accountBecameActive(account, attemptAutomaticBiometricUnlock: true, didSwitchAccountAutomatically: true)
)
XCTAssertEqual(
router.events,
[.accountBecameActive(
account,
animated: true,
attemptAutomaticBiometricUnlock: true,
didSwitchAccountAutomatically: true
)]
)
}

/// `navigate(to:)` with `.onboarding` starts the auth coordinator and navigates to the proper auth route.
@MainActor
func test_navigateTo_auth() throws {
Expand Down
22 changes: 22 additions & 0 deletions BitwardenShared/UI/Platform/Application/AppProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public class AppProcessor {
startEventTimer()
await checkIfExtensionSwitchedAccounts()
await checkAccountsForTimeout()
await handleNeverTimeOutAccountBecameActive()
await completeAutofillAccountSetupIfEnabled()
#if DEBUG
debugWillEnterForeground?()
Expand Down Expand Up @@ -373,6 +374,27 @@ extension AppProcessor {
}
}

/// Handles unlocking the vault for a manually locked account that uses never lock
/// and was previously unlocked in an extension.
///
private func handleNeverTimeOutAccountBecameActive() async {
guard
appExtensionDelegate?.isInAppExtension != true,
await (try? services.authRepository.isLocked()) == true,
await (try? services.authRepository.sessionTimeoutValue()) == .never,
await (try? services.stateService.getManuallyLockedAccount(userId: nil)) == false,
let account = try? await services.stateService.getActiveAccount()
else { return }

await coordinator?.handleEvent(
.accountBecameActive(
account,
attemptAutomaticBiometricUnlock: true,
didSwitchAccountAutomatically: false
)
)
}

/// Checks if the active account was switched while in the extension. If this occurs, the app
/// needs to also switch to the updated active account.
///
Expand Down
29 changes: 29 additions & 0 deletions BitwardenShared/UI/Platform/Application/AppProcessorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,35 @@ class AppProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_body
XCTAssertEqual(stateService.accountSetupAutofill, ["1": .complete])
}

/// `init()` subscribes to will enter foreground events and handles accountBecameActive if the
/// never timeout account is unlocked in extension.
@MainActor
func test_init_appForeground_checkAccountBecomeActive() async throws {
// The processor checks for switched accounts when entering the foreground. Wait for the
// initial check to finish when the test starts before continuing.
try await waitForAsync { self.willEnterForegroundCalled == 1 }
let account: Account = .fixture(profile: .fixture(userId: "2"))
let userId = account.profile.userId
stateService.activeAccount = account
authRepository.activeAccount = account
stateService.didAccountSwitchInExtensionResult = .success(true)
authRepository.vaultTimeout = [userId: .never]
authRepository.isLockedResult = .success(true)
stateService.manuallyLockedAccounts = [userId: false]

notificationCenterService.willEnterForegroundSubject.send()
try await waitForAsync { self.willEnterForegroundCalled == 2 }

XCTAssertEqual(
coordinator.events.last,
AppEvent.accountBecameActive(
account,
attemptAutomaticBiometricUnlock: true,
didSwitchAccountAutomatically: false
)
)
}

/// `init()` subscribes to will enter foreground events and logs an error if one occurs while
/// checking if the active account was changed in an extension.
@MainActor
Expand Down
13 changes: 13 additions & 0 deletions BitwardenShared/UI/Platform/Application/AppRoute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ public enum AppRoute: Equatable {
}

public enum AppEvent: Equatable {
/// When the router should check the lock status of an account and propose a route.
///
/// - Parameters:
/// - account: The account to unlock the vault for.
/// - attemptAutomaticBiometricUnlock: If `true` and biometric unlock is enabled/available,
/// the processor should attempt an automatic biometric unlock.
/// - didSwitchAccountAutomatically: A flag indicating if the active account was switched automatically.
///
case accountBecameActive(
Account,
attemptAutomaticBiometricUnlock: Bool,
didSwitchAccountAutomatically: Bool
)
/// When the user logs out from an account.
///
/// - Parameters:
Expand Down

0 comments on commit 8293f41

Please sign in to comment.