mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-10 00:42:29 -06:00
659 lines
24 KiB
Swift
659 lines
24 KiB
Swift
import BitwardenKit
|
|
import BitwardenResources
|
|
import BitwardenSdk
|
|
import SwiftUI
|
|
|
|
// swiftlint:disable file_length
|
|
|
|
// MARK: - VaultListProcessor
|
|
|
|
/// The processor used to manage state and handle actions for the vault list screen.
|
|
///
|
|
final class VaultListProcessor: StateProcessor<
|
|
VaultListState,
|
|
VaultListAction,
|
|
VaultListEffect,
|
|
> {
|
|
// MARK: Types
|
|
|
|
typealias Services = HasApplication
|
|
& HasAuthRepository
|
|
& HasAuthService
|
|
& HasChangeKdfService
|
|
& HasErrorReporter
|
|
& HasEventService
|
|
& HasFlightRecorder
|
|
& HasNotificationService
|
|
& HasPasteboardService
|
|
& HasPolicyService
|
|
& HasReviewPromptService
|
|
& HasStateService
|
|
& HasTimeProvider
|
|
& HasVaultRepository
|
|
|
|
// MARK: Private Properties
|
|
|
|
/// The `Coordinator` that handles navigation.
|
|
private let coordinator: AnyCoordinator<VaultRoute, AuthAction>
|
|
|
|
/// Whether the cipher decryption failure alert was shown to the user, if the vault has any
|
|
/// ciphers which failed to decrypt.
|
|
private(set) var hasShownCipherDecryptionFailureAlert = false
|
|
|
|
/// The helper to handle master password reprompts.
|
|
private let masterPasswordRepromptHelper: MasterPasswordRepromptHelper
|
|
|
|
/// The task that schedules the app review prompt.
|
|
private(set) var reviewPromptTask: Task<Void, Never>?
|
|
|
|
/// The services used by this processor.
|
|
private let services: Services
|
|
|
|
/// The helper to handle the more options menu for a vault item.
|
|
private let vaultItemMoreOptionsHelper: VaultItemMoreOptionsHelper
|
|
|
|
// MARK: Initialization
|
|
|
|
/// Creates a new `VaultListProcessor`.
|
|
///
|
|
/// - Parameters:
|
|
/// - coordinator: The `Coordinator` that handles navigation.
|
|
/// - masterPasswordRepromptHelper: The helper to handle master password reprompts.
|
|
/// - services: The services used by this processor.
|
|
/// - state: The initial state of the processor.
|
|
/// - vaultItemMoreOptionsHelper: The helper to handle the more options menu for a vault item.
|
|
///
|
|
init(
|
|
coordinator: AnyCoordinator<VaultRoute, AuthAction>,
|
|
masterPasswordRepromptHelper: MasterPasswordRepromptHelper,
|
|
services: Services,
|
|
state: VaultListState,
|
|
vaultItemMoreOptionsHelper: VaultItemMoreOptionsHelper,
|
|
) {
|
|
self.coordinator = coordinator
|
|
self.masterPasswordRepromptHelper = masterPasswordRepromptHelper
|
|
self.services = services
|
|
self.vaultItemMoreOptionsHelper = vaultItemMoreOptionsHelper
|
|
super.init(state: state)
|
|
}
|
|
|
|
deinit {
|
|
reviewPromptTask?.cancel()
|
|
}
|
|
|
|
// MARK: Methods
|
|
|
|
override func perform(_ effect: VaultListEffect) async {
|
|
switch effect {
|
|
case .appeared:
|
|
await appeared()
|
|
case .checkAppReviewEligibility:
|
|
if await services.reviewPromptService.isEligibleForReviewPrompt() {
|
|
await scheduleReviewPrompt()
|
|
} else {
|
|
state.isEligibleForAppReview = false
|
|
}
|
|
case .dismissFlightRecorderToastBanner:
|
|
await dismissFlightRecorderToastBanner()
|
|
case .dismissImportLoginsActionCard:
|
|
await setImportLoginsProgress(.setUpLater)
|
|
case let .morePressed(item):
|
|
await vaultItemMoreOptionsHelper.showMoreOptionsAlert(
|
|
for: item,
|
|
handleDisplayToast: { [weak self] toast in
|
|
self?.state.toast = toast
|
|
},
|
|
handleOpenURL: { [weak self] url in
|
|
self?.state.url = url
|
|
},
|
|
)
|
|
case let .profileSwitcher(profileEffect):
|
|
await handleProfileSwitcherEffect(profileEffect)
|
|
case .refreshAccountProfiles:
|
|
await refreshProfileState()
|
|
case .refreshVault:
|
|
await refreshVault(syncWithPeriodicCheck: false)
|
|
case let .search(text):
|
|
await searchVault(for: text)
|
|
case .streamAccountSetupProgress:
|
|
await streamAccountSetupProgress()
|
|
case .streamFlightRecorderLog:
|
|
await streamFlightRecorderLog()
|
|
case .streamOrganizations:
|
|
await streamOrganizations()
|
|
case .streamShowWebIcons:
|
|
for await value in await services.stateService.showWebIconsPublisher().values {
|
|
state.showWebIcons = value
|
|
}
|
|
case .streamVaultList:
|
|
await streamVaultList()
|
|
case .tryAgainTapped:
|
|
state.loadingState = .loading(nil)
|
|
await appeared()
|
|
}
|
|
}
|
|
|
|
override func receive(_ action: VaultListAction) {
|
|
switch action {
|
|
case .addFolder:
|
|
coordinator.navigate(to: .addFolder)
|
|
case let .addItemPressed(type):
|
|
addItem(type: type)
|
|
case .clearURL:
|
|
state.url = nil
|
|
case .copyTOTPCode:
|
|
break
|
|
case .disappeared:
|
|
reviewPromptTask?.cancel()
|
|
case let .itemPressed(item):
|
|
handleItemTapped(item)
|
|
case .navigateToFlightRecorderSettings:
|
|
coordinator.navigate(to: .flightRecorderSettings)
|
|
case let .profileSwitcher(profileAction):
|
|
handleProfileSwitcherAction(profileAction)
|
|
case let .searchStateChanged(isSearching: isSearching):
|
|
guard isSearching else {
|
|
state.searchText = ""
|
|
state.searchResults = []
|
|
return
|
|
}
|
|
state.profileSwitcherState.isVisible = !isSearching
|
|
case let .searchTextChanged(newValue):
|
|
state.searchText = newValue
|
|
case let .searchVaultFilterChanged(newValue):
|
|
state.searchVaultFilterType = newValue
|
|
case .appReviewPromptShown:
|
|
state.isEligibleForAppReview = false
|
|
Task {
|
|
await services.reviewPromptService.setReviewPromptShownVersion()
|
|
await services.reviewPromptService.clearUserActions()
|
|
}
|
|
case .showImportLogins:
|
|
coordinator.navigate(to: .importLogins)
|
|
case let .toastShown(newValue):
|
|
state.toast = newValue
|
|
case .totpCodeExpired:
|
|
// No-op: TOTP codes aren't shown on the list view and can't be copied.
|
|
break
|
|
case let .vaultFilterChanged(newValue):
|
|
state.vaultFilterType = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
extension VaultListProcessor {
|
|
// MARK: Private Methods
|
|
|
|
/// Navigates to the add vault item screen.
|
|
///
|
|
/// - Parameter type: The type of vault item to add.
|
|
///
|
|
private func addItem(type: CipherType) {
|
|
setProfileSwitcher(visible: false)
|
|
switch state.vaultFilterType {
|
|
case let .organization(organization):
|
|
coordinator.navigate(to: .addItem(organizationId: organization.id, type: type))
|
|
default:
|
|
coordinator.navigate(to: .addItem(type: type))
|
|
}
|
|
reviewPromptTask?.cancel()
|
|
}
|
|
|
|
/// Called when the vault list appears on screen.
|
|
private func appeared() async {
|
|
await refreshVault(syncWithPeriodicCheck: true)
|
|
await handleNotifications()
|
|
await checkPendingLoginRequests()
|
|
await checkPersonalOwnershipPolicy()
|
|
await loadItemTypesUserCanCreate()
|
|
}
|
|
|
|
/// Checks if the user needs to update their KDF settings.
|
|
private func checkIfForceKdfUpdateRequired() async {
|
|
guard await services.changeKdfService.needsKdfUpdateToMinimums() else { return }
|
|
|
|
coordinator.showAlert(.updateEncryptionSettings { password in
|
|
self.coordinator.showLoadingOverlay(title: Localizations.updating)
|
|
defer { self.coordinator.hideLoadingOverlay() }
|
|
|
|
do {
|
|
try await self.services.changeKdfService.updateKdfToMinimums(password: password)
|
|
self.coordinator.showToast(Localizations.encryptionSettingsUpdated)
|
|
} catch {
|
|
self.services.errorReporter.log(error: error)
|
|
await self.coordinator.showErrorAlert(error: error)
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Check if there are any pending login requests for the user to deal with.
|
|
private func checkPendingLoginRequests() async {
|
|
do {
|
|
// If the user had previously received a notification for a login request
|
|
// but hasn't been able to view it yet, open the request now.
|
|
let userId = try await services.stateService.getActiveAccountId()
|
|
if let loginRequestData = await services.stateService.getLoginRequest(),
|
|
loginRequestData.userId == userId {
|
|
// Show the login request if it's still valid.
|
|
if let loginRequest = try await services.authService.getPendingLoginRequest(withId: loginRequestData.id)
|
|
.first,
|
|
!loginRequest.isAnswered,
|
|
!loginRequest.isExpired {
|
|
coordinator.navigate(to: .loginRequest(loginRequest))
|
|
}
|
|
|
|
// Since the request has been handled, remove it from local storage.
|
|
await services.stateService.setLoginRequest(nil)
|
|
}
|
|
} catch {
|
|
services.errorReporter.log(error: error)
|
|
}
|
|
}
|
|
|
|
/// Checks if the personal ownership policy is enabled.
|
|
///
|
|
private func checkPersonalOwnershipPolicy() async {
|
|
let isPersonalOwnershipDisabled = await services.policyService.policyAppliesToUser(.personalOwnership)
|
|
state.isPersonalOwnershipDisabled = isPersonalOwnershipDisabled
|
|
state.canShowVaultFilter = await services.vaultRepository.canShowVaultFilter()
|
|
}
|
|
|
|
/// Checks available item types user can create.
|
|
///
|
|
private func loadItemTypesUserCanCreate() async {
|
|
state.itemTypesUserCanCreate = await services.vaultRepository.getItemTypesUserCanCreate()
|
|
}
|
|
|
|
/// Dismisses the flight recorder toast banner for the active user.
|
|
///
|
|
private func dismissFlightRecorderToastBanner() async {
|
|
state.isFlightRecorderToastBannerVisible = false
|
|
await services.flightRecorder.setFlightRecorderBannerDismissed()
|
|
}
|
|
|
|
/// If the vault has ciphers which failed to decrypt, and the cipher decryption failure alert
|
|
/// hasn't been shown yet, notify the user that a cipher(s) failed to decrypt.
|
|
///
|
|
/// - Parameter cipherIds: The list of identifiers for ciphers which failed to decrypt.
|
|
///
|
|
private func handleCipherDecryptionFailures(cipherIds: [Uuid]) {
|
|
guard !cipherIds.isEmpty, !hasShownCipherDecryptionFailureAlert else { return }
|
|
coordinator.showAlert(.cipherDecryptionFailure(cipherIds: cipherIds, isFromCipherTap: false) { stringToCopy in
|
|
self.services.pasteboardService.copy(stringToCopy)
|
|
})
|
|
hasShownCipherDecryptionFailureAlert = true
|
|
}
|
|
|
|
/// Handles the primary action for when a `VaultListItem` is tapped in the list.
|
|
///
|
|
/// - Parameter item: The `VaultListItem` that was tapped.
|
|
///
|
|
private func handleItemTapped(_ item: VaultListItem) {
|
|
switch item.itemType {
|
|
case let .cipher(cipherListView, _):
|
|
if cipherListView.isDecryptionFailure, let cipherId = cipherListView.id {
|
|
coordinator.showAlert(.cipherDecryptionFailure(cipherIds: [cipherId]) { stringToCopy in
|
|
self.services.pasteboardService.copy(stringToCopy)
|
|
})
|
|
} else {
|
|
navigateToViewItem(cipherListView: cipherListView, id: item.id)
|
|
}
|
|
case let .group(group, _):
|
|
coordinator.navigate(to: .group(group, filter: state.vaultFilterType))
|
|
case let .totp(_, model):
|
|
navigateToViewItem(cipherListView: model.cipherListView, id: model.id)
|
|
}
|
|
}
|
|
|
|
/// Entry point to handling things around push notifications.
|
|
private func handleNotifications() async {
|
|
switch await services.notificationService.notificationAuthorization() {
|
|
case .authorized:
|
|
await registerForNotifications()
|
|
case .notDetermined:
|
|
await requestNotificationPermissions()
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
/// Navigates to the view item view for the specified cipher. If the cipher requires master
|
|
/// password reprompt, this will prompt the user before navigation.
|
|
///
|
|
/// - Parameters:
|
|
/// - cipherListView: The cipher list view item for the cipher that will be shown in the view item view.
|
|
/// - id: The cipher's identifier.
|
|
///
|
|
private func navigateToViewItem(cipherListView: CipherListView, id: String) {
|
|
Task {
|
|
await masterPasswordRepromptHelper.repromptForMasterPasswordIfNeeded(cipherListView: cipherListView) {
|
|
self.coordinator.navigate(to: .viewItem(id: id, masterPasswordRepromptCheckCompleted: true))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Refreshes the vault's contents.
|
|
///
|
|
/// - Parameter syncWithPeriodicCheck: Whether the sync should take into consideration
|
|
/// the periodic check.
|
|
private func refreshVault(syncWithPeriodicCheck: Bool) async {
|
|
do {
|
|
let takingTimeTask = Task {
|
|
try await Task.sleep(forSeconds: 5)
|
|
// If we already have data, don't show the toast
|
|
guard case .loading = self.state.loadingState else { return }
|
|
self.state.toast = Toast(title: Localizations.thisIsTakingLongerThanExpected, mode: .manualDismiss)
|
|
}
|
|
defer {
|
|
state.toast = nil
|
|
takingTimeTask.cancel()
|
|
}
|
|
|
|
try await services.vaultRepository.fetchSync(
|
|
forceSync: false,
|
|
filter: state.vaultFilterType,
|
|
isPeriodic: syncWithPeriodicCheck,
|
|
)
|
|
|
|
if try await services.vaultRepository.isVaultEmpty() {
|
|
// Normally after syncing the database will publish the contents of the vault which is
|
|
// used to transition from the loading to loaded state. If the vault is empty, nothing
|
|
// will be published by the database, so it needs to be manually updated.
|
|
state.loadingState = .data([])
|
|
}
|
|
|
|
await checkIfForceKdfUpdateRequired()
|
|
} catch URLError.cancelled {
|
|
// No-op: don't log or alert for cancellation errors.
|
|
} catch {
|
|
services.errorReporter.log(error: error)
|
|
|
|
let needsSync = try? await services.vaultRepository.needsSync()
|
|
if needsSync == true {
|
|
// If the vault needs a sync and there are cached items,
|
|
// display the cached data and show an error alert.
|
|
if let sections = state.loadingState.data, !sections.isEmpty {
|
|
await coordinator.showErrorAlert(error: error)
|
|
} else {
|
|
// If the vault needs a sync and there were no cached items,
|
|
// show the full screen error view.
|
|
state.loadingState = .error(
|
|
errorMessage: Localizations.weAreUnableToProcessYourRequestPleaseTryAgainOrContactUs,
|
|
)
|
|
}
|
|
} else {
|
|
await coordinator.showErrorAlert(error: error)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Attempts to register the device for push notifications.
|
|
/// We only need to register once a day.
|
|
private func registerForNotifications() async {
|
|
do {
|
|
let lastReg = try await services.stateService.getNotificationsLastRegistrationDate() ?? Date.distantPast
|
|
if services.timeProvider.timeSince(lastReg) >= 86400 { // One day
|
|
services.application?.registerForRemoteNotifications()
|
|
try await services.stateService.setNotificationsLastRegistrationDate(services.timeProvider.presentTime)
|
|
}
|
|
} catch {
|
|
services.errorReporter.log(error: error)
|
|
}
|
|
}
|
|
|
|
/// Request permission to send push notifications.
|
|
private func requestNotificationPermissions() async {
|
|
// Show the explanation alert before asking for permissions.
|
|
coordinator.showAlert(
|
|
.pushNotificationsInformation { [services] in
|
|
do {
|
|
let authorized = try await services.notificationService
|
|
.requestAuthorization(options: [.alert, .sound, .badge])
|
|
if authorized {
|
|
await self.registerForNotifications()
|
|
}
|
|
} catch {
|
|
self.services.errorReporter.log(error: error)
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
/// Searches the vault using the provided string and sets to state any matching results.
|
|
///
|
|
/// - Parameter searchText: The string to use when searching the vault.
|
|
///
|
|
private func searchVault(for searchText: String) async {
|
|
guard !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
state.searchResults = []
|
|
return
|
|
}
|
|
do {
|
|
let publisher = try await services.vaultRepository.vaultListPublisher(
|
|
filter: VaultListFilter(
|
|
filterType: state.searchVaultFilterType,
|
|
searchText: searchText,
|
|
),
|
|
)
|
|
for try await vaultListData in publisher {
|
|
let items = vaultListData.sections.first?.items ?? []
|
|
state.searchResults = items
|
|
}
|
|
} catch {
|
|
services.errorReporter.log(error: error)
|
|
}
|
|
}
|
|
|
|
/// Sets the user's import logins progress.
|
|
///
|
|
/// - Parameter progress: The user's import logins progress.
|
|
///
|
|
private func setImportLoginsProgress(_ progress: AccountSetupProgress) async {
|
|
do {
|
|
try await services.stateService.setAccountSetupImportLogins(progress)
|
|
} catch {
|
|
services.errorReporter.log(error: error)
|
|
coordinator.showAlert(.defaultAlert(error: error))
|
|
}
|
|
}
|
|
|
|
/// Sets the visibility of the profiles view and updates accessibility focus.
|
|
///
|
|
/// - Parameter visible: the intended visibility of the view.
|
|
///
|
|
private func setProfileSwitcher(visible: Bool) {
|
|
if !visible {
|
|
state.profileSwitcherState.hasSetAccessibilityFocus = false
|
|
}
|
|
state.profileSwitcherState.isVisible = visible
|
|
}
|
|
|
|
/// Triggers the app review prompt after a delay.
|
|
private func scheduleReviewPrompt() async {
|
|
reviewPromptTask?.cancel()
|
|
reviewPromptTask = Task {
|
|
do {
|
|
try await Task.sleep(nanoseconds: Constants.appReviewPromptDelay)
|
|
state.isEligibleForAppReview = true
|
|
} catch is CancellationError {
|
|
// Task was cancelled, no need to handle this error
|
|
} catch {
|
|
services.errorReporter.log(error: error)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Streams the user's account setup progress.
|
|
///
|
|
private func streamAccountSetupProgress() async {
|
|
do {
|
|
for await badgeState in try await services.stateService.settingsBadgePublisher().values {
|
|
state.importLoginsSetupProgress = badgeState.importLoginsSetupProgress
|
|
}
|
|
} catch {
|
|
services.errorReporter.log(error: error)
|
|
}
|
|
}
|
|
|
|
/// Streams the flight recorder enabled status.
|
|
///
|
|
private func streamFlightRecorderLog() async {
|
|
for await log in await services.flightRecorder.activeLogPublisher().values {
|
|
state.activeFlightRecorderLog = log
|
|
state.isFlightRecorderToastBannerVisible = !(log?.isBannerDismissed ?? true)
|
|
}
|
|
}
|
|
|
|
/// Streams the user's organizations.
|
|
private func streamOrganizations() async {
|
|
do {
|
|
for try await organizations in try await services.vaultRepository.organizationsPublisher() {
|
|
state.organizations = organizations
|
|
}
|
|
} catch {
|
|
services.errorReporter.log(error: error)
|
|
}
|
|
}
|
|
|
|
/// Streams the user's vault list.
|
|
private func streamVaultList() async {
|
|
do {
|
|
for try await vaultList in try await services.vaultRepository
|
|
.vaultListPublisher(
|
|
filter: VaultListFilter(
|
|
filterType: state.vaultFilterType,
|
|
options: [.addTOTPGroup, .addTrashGroup],
|
|
),
|
|
) {
|
|
// Check if the vault needs a sync.
|
|
let needsSync = try await services.vaultRepository.needsSync()
|
|
|
|
let value = vaultList.sections
|
|
|
|
// If the data is empty, check to ensure that a sync is not needed.
|
|
if !needsSync || !value.isEmpty {
|
|
// Dismiss the "this is taking a while" toast now that we have data,
|
|
// since this might not happen because of the sync in `refreshVault()`.
|
|
state.toast = nil
|
|
// If the data is not empty or if a sync is not needed, set the data.
|
|
state.loadingState = .data(value)
|
|
} else {
|
|
// Otherwise mark the state as `.loading` until the sync is complete.
|
|
state.loadingState = .loading(value)
|
|
}
|
|
|
|
// Dismiss the import logins action card once the vault has items in it.
|
|
if !value.isEmpty {
|
|
await setImportLoginsProgress(.complete)
|
|
}
|
|
// Dismiss the coach mark action cards once the vault has at least one login item in it.
|
|
if value.hasLoginItems {
|
|
await services.stateService.setLearnNewLoginActionCardStatus(.complete)
|
|
await services.stateService.setLearnGeneratorActionCardStatus(.complete)
|
|
}
|
|
// Alert the user of any cipher decryption failures.
|
|
handleCipherDecryptionFailures(cipherIds: vaultList.cipherDecryptionFailureIds)
|
|
}
|
|
} catch {
|
|
services.errorReporter.log(error: error)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - CipherItemOperationDelegate
|
|
|
|
extension VaultListProcessor: CipherItemOperationDelegate {
|
|
func itemDeleted() {
|
|
state.toast = Toast(title: Localizations.itemDeleted)
|
|
}
|
|
|
|
func itemSoftDeleted() {
|
|
state.toast = Toast(title: Localizations.itemSoftDeleted)
|
|
}
|
|
|
|
func itemRestored() {
|
|
state.toast = Toast(title: Localizations.itemRestored)
|
|
}
|
|
}
|
|
|
|
// MARK: - MoreOptionsAction
|
|
|
|
/// The actions available from the More Options alert.
|
|
enum MoreOptionsAction: Equatable {
|
|
/// Copy the `value` and show a toast with the `toast` string.
|
|
case copy(
|
|
toast: String,
|
|
value: String,
|
|
requiresMasterPasswordReprompt: Bool,
|
|
logEvent: EventType?,
|
|
cipherId: String?,
|
|
)
|
|
|
|
/// Generate and copy the TOTP code for the given `totpKey`.
|
|
case copyTotp(totpKey: TOTPKeyModel)
|
|
|
|
/// Navigate to the view to edit the `cipherView`.
|
|
case edit(cipherView: CipherView)
|
|
|
|
/// Launch the `url` in the device's browser.
|
|
case launch(url: URL)
|
|
|
|
/// Navigate to view the item with the given `id`.
|
|
case view(id: String)
|
|
}
|
|
|
|
// MARK: - ProfileSwitcherHandler
|
|
|
|
extension VaultListProcessor: ProfileSwitcherHandler {
|
|
var allowLockAndLogout: Bool {
|
|
true
|
|
}
|
|
|
|
var profileServices: ProfileServices {
|
|
services
|
|
}
|
|
|
|
var profileSwitcherState: ProfileSwitcherState {
|
|
get {
|
|
state.profileSwitcherState
|
|
}
|
|
set {
|
|
state.profileSwitcherState = newValue
|
|
}
|
|
}
|
|
|
|
var shouldHideAddAccount: Bool {
|
|
false
|
|
}
|
|
|
|
var toast: Toast? {
|
|
get {
|
|
state.toast
|
|
}
|
|
set {
|
|
state.toast = newValue
|
|
}
|
|
}
|
|
|
|
func handleAuthEvent(_ authEvent: AuthEvent) async {
|
|
guard case let .action(authAction) = authEvent else { return }
|
|
await coordinator.handleEvent(authAction)
|
|
}
|
|
|
|
func dismissProfileSwitcher() {
|
|
coordinator.navigate(to: .dismiss)
|
|
}
|
|
|
|
func showAddAccount() {
|
|
coordinator.navigate(to: .addAccount)
|
|
}
|
|
|
|
func showAlert(_ alert: BitwardenKit.Alert) {
|
|
coordinator.showAlert(alert)
|
|
}
|
|
|
|
func showProfileSwitcher() {
|
|
coordinator.navigate(to: .viewProfileSwitcher, context: self)
|
|
}
|
|
}
|