mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-10 17:46:07 -06:00
543 lines
19 KiB
Swift
543 lines
19 KiB
Swift
import AuthenticationServices
|
|
import BitwardenKit
|
|
import BitwardenResources
|
|
import BitwardenSdk
|
|
import SwiftUI
|
|
|
|
// swiftlint:disable file_length
|
|
|
|
// MARK: - SettingsCoordinatorDelegate
|
|
|
|
/// An object that is signaled when specific circumstances in the application flow have been encountered.
|
|
///
|
|
@MainActor
|
|
public protocol SettingsCoordinatorDelegate: AnyObject {
|
|
/// Called when the user completes the import navigation flow and should be navigated to the vault tab.
|
|
///
|
|
func didCompleteLoginsImport()
|
|
|
|
/// Called when the active user's account has been deleted.
|
|
///
|
|
func didDeleteAccount()
|
|
|
|
/// Called when the user has requested an account vault be locked.
|
|
/// - Parameters:
|
|
/// - userId: The user Id of the selected account. Defaults to the active user id if nil.
|
|
/// - isManuallyLocking: Whether the user is manually locking the account.
|
|
///
|
|
func lockVault(userId: String?, isManuallyLocking: Bool)
|
|
|
|
/// Called when the user has requested an account be logged out.
|
|
///
|
|
/// - Parameters:
|
|
/// - userId: The id of the account to log out.
|
|
/// - userInitiated: Did a user action initiate this logout?
|
|
///
|
|
func logout(userId: String?, userInitiated: Bool)
|
|
|
|
/// Called when the user requests an account switch.
|
|
///
|
|
/// - Parameters:
|
|
/// - isUserInitiated: Did the user trigger the account switch?
|
|
/// - userId: The user Id of the selected account.
|
|
///
|
|
func switchAccount(isAutomatic: Bool, userId: String)
|
|
}
|
|
|
|
// MARK: - SettingsCoordinator
|
|
|
|
/// A coordinator that manages navigation in the settings tab.
|
|
///
|
|
final class SettingsCoordinator: Coordinator, HasStackNavigator { // swiftlint:disable:this type_body_length
|
|
// MARK: Types
|
|
|
|
/// The module types required by this coordinator for creating child coordinators.
|
|
typealias Module = AddEditFolderModule
|
|
& AuthModule
|
|
& ExportCXFModule
|
|
& FlightRecorderModule
|
|
& ImportLoginsModule
|
|
& LoginRequestModule
|
|
& NavigatorBuilderModule
|
|
& PasswordAutoFillModule
|
|
|
|
typealias Services = HasAccountAPIService
|
|
& HasAppInfoService
|
|
& HasAuthRepository
|
|
& HasAuthService
|
|
& HasAutofillCredentialService
|
|
& HasBiometricsRepository
|
|
& HasConfigService
|
|
& HasEnvironmentService
|
|
& HasErrorAlertServices.ErrorAlertServices
|
|
& HasErrorReporter
|
|
& HasEventService
|
|
& HasExportCXFCiphersRepository
|
|
& HasExportVaultService
|
|
& HasFlightRecorder
|
|
& HasNotificationCenterService
|
|
& HasPasteboardService
|
|
& HasPolicyService
|
|
& HasSettingsRepository
|
|
& HasStateService
|
|
& HasSystemDevice
|
|
& HasTimeProvider
|
|
& HasTwoStepLoginService
|
|
& HasVaultRepository
|
|
& HasVaultTimeoutService
|
|
& HasWatchService
|
|
|
|
// MARK: Private Properties
|
|
|
|
/// The delegate for this coordinator, used to notify when the user logs out.
|
|
private weak var delegate: SettingsCoordinatorDelegate?
|
|
|
|
/// The module used to create child coordinators.
|
|
private let module: Module
|
|
|
|
/// The services used by this coordinator.
|
|
private let services: Services
|
|
|
|
// MARK: Properties
|
|
|
|
/// The stack navigator that is managed by this coordinator.
|
|
private(set) weak var stackNavigator: StackNavigator?
|
|
|
|
// MARK: Initialization
|
|
|
|
/// Creates a new `SettingsCoordinator`.
|
|
///
|
|
/// - Parameters:
|
|
/// - delegate: The delegate for this coordinator, used to notify when the user logs out.
|
|
/// - module: The module used to create child coordinators.
|
|
/// - services: The services used by this coordinator.
|
|
/// - stackNavigator: The stack navigator that is managed by this coordinator.
|
|
///
|
|
init(
|
|
delegate: SettingsCoordinatorDelegate?,
|
|
module: Module,
|
|
services: Services,
|
|
stackNavigator: StackNavigator,
|
|
) {
|
|
self.delegate = delegate
|
|
self.module = module
|
|
self.services = services
|
|
self.stackNavigator = stackNavigator
|
|
}
|
|
|
|
// MARK: Methods
|
|
|
|
func handleEvent(_ event: SettingsEvent, context: AnyObject?) async {
|
|
switch event {
|
|
case let .authAction(action):
|
|
switch action {
|
|
case let .lockVault(userId, isManuallyLocking):
|
|
delegate?.lockVault(userId: userId, isManuallyLocking: isManuallyLocking)
|
|
case let .logout(userId, userInitiated):
|
|
delegate?.logout(userId: userId, userInitiated: userInitiated)
|
|
case let .switchAccount(isAutomatic, userId, _):
|
|
delegate?.switchAccount(isAutomatic: isAutomatic, userId: userId)
|
|
}
|
|
case .didDeleteAccount:
|
|
stackNavigator?.dismiss {
|
|
self.delegate?.didDeleteAccount()
|
|
}
|
|
}
|
|
}
|
|
|
|
func navigate(to route: SettingsRoute, context: AnyObject?) { // swiftlint:disable:this function_body_length
|
|
switch route {
|
|
case .about:
|
|
showAbout()
|
|
case .accountSecurity:
|
|
showAccountSecurity()
|
|
case let .addEditFolder(folder):
|
|
showAddEditFolder(folder, delegate: context as? AddEditFolderDelegate)
|
|
case .appearance:
|
|
showAppearance()
|
|
case .appExtension:
|
|
showAppExtension()
|
|
case .appExtensionSetup:
|
|
showAppExtensionSetup(delegate: context as? AppExtensionSetupDelegate)
|
|
case .autoFill:
|
|
showAutoFill()
|
|
case .deleteAccount:
|
|
showDeleteAccount()
|
|
case .dismiss:
|
|
stackNavigator?.dismiss()
|
|
case .exportVault:
|
|
Task {
|
|
await showExportVault()
|
|
}
|
|
case .exportVaultToApp:
|
|
showExportVaultToApp()
|
|
case .exportVaultToFile:
|
|
showExportVaultToFile()
|
|
case let .flightRecorder(route):
|
|
showFlightRecorder(route: route)
|
|
case .folders:
|
|
showFolders()
|
|
case .importLogins:
|
|
showImportLogins()
|
|
case let .loginRequest(loginRequest):
|
|
showLoginRequest(loginRequest, delegate: context as? LoginRequestDelegate)
|
|
case .other:
|
|
showOtherScreen()
|
|
case .passwordAutoFill:
|
|
showPasswordAutoFill()
|
|
case .pendingLoginRequests:
|
|
showPendingLoginRequests()
|
|
case let .selectLanguage(currentLanguage: currentLanguage):
|
|
showSelectLanguage(currentLanguage: currentLanguage, delegate: context as? SelectLanguageDelegate)
|
|
case let .settings(presentationMode):
|
|
showSettings(presentationMode: presentationMode)
|
|
case let .shareURL(url):
|
|
showShareSheet([url])
|
|
case .vault:
|
|
showVault()
|
|
case .vaultUnlockSetup:
|
|
showAuthCoordinator(route: .vaultUnlockSetup(.settings))
|
|
}
|
|
}
|
|
|
|
func start() {
|
|
navigate(to: .settings(.tab))
|
|
}
|
|
|
|
// MARK: Private Methods
|
|
|
|
/// Shows the about screen.
|
|
///
|
|
private func showAbout() {
|
|
let processor = AboutProcessor(
|
|
coordinator: asAnyCoordinator(),
|
|
services: services,
|
|
state: AboutState(),
|
|
)
|
|
|
|
let view = AboutView(store: Store(processor: processor))
|
|
let viewController = UIHostingController(rootView: view)
|
|
viewController.navigationItem.largeTitleDisplayMode = .never
|
|
stackNavigator?.push(viewController, navigationTitle: Localizations.about)
|
|
}
|
|
|
|
/// Shows the account security screen.
|
|
///
|
|
private func showAccountSecurity() {
|
|
let processor = AccountSecurityProcessor(
|
|
coordinator: asAnyCoordinator(),
|
|
services: services,
|
|
state: AccountSecurityState(),
|
|
vaultUnlockSetupHelper: DefaultVaultUnlockSetupHelper(services: services),
|
|
)
|
|
|
|
let view = AccountSecurityView(store: Store(processor: processor))
|
|
let viewController = UIHostingController(rootView: view)
|
|
viewController.navigationItem.largeTitleDisplayMode = .never
|
|
stackNavigator?.push(viewController, navigationTitle: Localizations.accountSecurity)
|
|
}
|
|
|
|
/// Shows the add or edit folder screen.
|
|
///
|
|
/// - Parameter folder: The existing folder to edit, if applicable.
|
|
///
|
|
private func showAddEditFolder(_ folder: FolderView?, delegate: AddEditFolderDelegate?) {
|
|
let navigationController = module.makeNavigationController()
|
|
let coordinator = module.makeAddEditFolderCoordinator(stackNavigator: navigationController)
|
|
coordinator.start()
|
|
coordinator.navigate(to: .addEditFolder(folder: folder), context: delegate)
|
|
|
|
stackNavigator?.present(navigationController)
|
|
}
|
|
|
|
/// Shows the appearance screen.
|
|
///
|
|
private func showAppearance() {
|
|
let processor = AppearanceProcessor(
|
|
coordinator: asAnyCoordinator(),
|
|
services: services,
|
|
state: AppearanceState(),
|
|
)
|
|
|
|
let view = AppearanceView(store: Store(processor: processor))
|
|
let viewController = UIHostingController(rootView: view)
|
|
viewController.navigationItem.largeTitleDisplayMode = .never
|
|
stackNavigator?.push(viewController, navigationTitle: Localizations.appearance)
|
|
}
|
|
|
|
/// Shows the app extension screen.
|
|
///
|
|
private func showAppExtension() {
|
|
let processor = AppExtensionProcessor(
|
|
coordinator: asAnyCoordinator(),
|
|
state: AppExtensionState(),
|
|
)
|
|
let view = AppExtensionView(store: Store(processor: processor))
|
|
let viewController = UIHostingController(rootView: view)
|
|
viewController.navigationItem.largeTitleDisplayMode = .never
|
|
stackNavigator?.push(viewController, navigationTitle: Localizations.appExtension)
|
|
}
|
|
|
|
/// Shows the app extension setup screen.
|
|
///
|
|
/// - Parameter delegate: The `AppExtensionSetupDelegate` to notify when the user interacts with
|
|
/// the extension.
|
|
///
|
|
private func showAppExtensionSetup(delegate: AppExtensionSetupDelegate?) {
|
|
let extensionItem = NSExtensionItem()
|
|
extensionItem.attachments = [
|
|
NSItemProvider(
|
|
item: "" as NSString,
|
|
typeIdentifier: Constants.UTType.appExtensionSetup,
|
|
),
|
|
]
|
|
let viewController = UIActivityViewController(activityItems: [extensionItem], applicationActivities: nil)
|
|
viewController.completionWithItemsHandler = { activityType, completed, _, _ in
|
|
delegate?.didDismissExtensionSetup(
|
|
enabled: completed &&
|
|
activityType?.rawValue == Bundle.main.appExtensionIdentifier,
|
|
)
|
|
}
|
|
stackNavigator?.present(viewController)
|
|
}
|
|
|
|
/// Navigates to the specified auth coordinator route within the existing navigator.
|
|
///
|
|
/// - Parameter route: The auth route to navigate to.
|
|
///
|
|
private func showAuthCoordinator(route: AuthRoute) {
|
|
guard let stackNavigator else { return }
|
|
let coordinator = module.makeAuthCoordinator(
|
|
delegate: nil,
|
|
rootNavigator: nil,
|
|
stackNavigator: stackNavigator,
|
|
)
|
|
coordinator.navigate(to: route)
|
|
}
|
|
|
|
/// Shows the auto-fill screen.
|
|
///
|
|
private func showAutoFill() {
|
|
let processor = AutoFillProcessor(
|
|
coordinator: asAnyCoordinator(),
|
|
services: services,
|
|
state: AutoFillState(),
|
|
)
|
|
let view = AutoFillView(store: Store(processor: processor))
|
|
let viewController = UIHostingController(rootView: view)
|
|
viewController.navigationItem.largeTitleDisplayMode = .never
|
|
stackNavigator?.push(viewController, navigationTitle: Localizations.autofill)
|
|
}
|
|
|
|
/// Shows the delete account screen.
|
|
///
|
|
private func showDeleteAccount() {
|
|
let processor = DeleteAccountProcessor(
|
|
coordinator: asAnyCoordinator(),
|
|
services: services,
|
|
state: DeleteAccountState(),
|
|
)
|
|
stackNavigator?.present(DeleteAccountView(store: Store(processor: processor)))
|
|
}
|
|
|
|
/// Shows the share sheet to share one or more items.
|
|
///
|
|
/// - Parameter items: The items to share.
|
|
///
|
|
private func showShareSheet(_ items: [Any]) {
|
|
let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil)
|
|
stackNavigator?.present(activityVC)
|
|
}
|
|
|
|
/// Shows the export vault screen.
|
|
///
|
|
@MainActor
|
|
private func showExportVault() async {
|
|
guard await services.configService.getFeatureFlag(.cxpExportMobile) else {
|
|
navigate(to: .exportVaultToFile)
|
|
return
|
|
}
|
|
|
|
let processor = ExportSettingsProcessor(coordinator: asAnyCoordinator())
|
|
let view = ExportSettingsView(store: Store(processor: processor))
|
|
let viewController = UIHostingController(rootView: view)
|
|
viewController.navigationItem.largeTitleDisplayMode = .never
|
|
stackNavigator?.push(viewController, navigationTitle: Localizations.exportVault)
|
|
}
|
|
|
|
/// Shows the export vault to file screen.
|
|
///
|
|
private func showExportVaultToFile() {
|
|
let processor = ExportVaultProcessor(
|
|
coordinator: asAnyCoordinator(),
|
|
services: services,
|
|
)
|
|
stackNavigator?.present(ExportVaultView(store: Store(processor: processor)))
|
|
}
|
|
|
|
/// Shows the export vault to another app screen (Credential Exchange flow).
|
|
///
|
|
private func showExportVaultToApp() {
|
|
let navigationController = module.makeNavigationController()
|
|
let coordinator = module.makeExportCXFCoordinator(
|
|
stackNavigator: navigationController,
|
|
)
|
|
coordinator.start()
|
|
stackNavigator?.present(navigationController)
|
|
}
|
|
|
|
/// Shows a flight recorder view.
|
|
///
|
|
/// - Parameter route: A `FlightRecorderRoute` to navigate to.
|
|
///
|
|
private func showFlightRecorder(route: FlightRecorderRoute) {
|
|
guard let stackNavigator else { return }
|
|
let coordinator = module.makeFlightRecorderCoordinator(stackNavigator: stackNavigator)
|
|
coordinator.start()
|
|
coordinator.navigate(to: route)
|
|
}
|
|
|
|
/// Shows the folders screen.
|
|
///
|
|
private func showFolders() {
|
|
let processor = FoldersProcessor(
|
|
coordinator: asAnyCoordinator(),
|
|
services: services,
|
|
state: FoldersState(),
|
|
)
|
|
let view = FoldersView(store: Store(processor: processor))
|
|
let viewController = UIHostingController(rootView: view)
|
|
viewController.navigationItem.largeTitleDisplayMode = .never
|
|
stackNavigator?.push(viewController, navigationTitle: Localizations.folders)
|
|
}
|
|
|
|
/// Shows the import login items screen.
|
|
///
|
|
private func showImportLogins() {
|
|
let navigationController = module.makeNavigationController()
|
|
navigationController.modalPresentationStyle = .overFullScreen
|
|
let coordinator = module.makeImportLoginsCoordinator(
|
|
delegate: self,
|
|
stackNavigator: navigationController,
|
|
)
|
|
coordinator.start()
|
|
coordinator.navigate(to: .importLogins(.settings))
|
|
|
|
stackNavigator?.present(navigationController)
|
|
}
|
|
|
|
/// Shows the login request.
|
|
///
|
|
/// - Parameters:
|
|
/// - loginRequest: The login request to display.
|
|
/// - delegate: The delegate.
|
|
///
|
|
private func showLoginRequest(_ loginRequest: LoginRequest, delegate: LoginRequestDelegate?) {
|
|
let navigationController = module.makeNavigationController()
|
|
let coordinator = module.makeLoginRequestCoordinator(stackNavigator: navigationController)
|
|
coordinator.start()
|
|
coordinator.navigate(to: .loginRequest(loginRequest), context: delegate)
|
|
stackNavigator?.present(navigationController)
|
|
}
|
|
|
|
/// Shows the other settings screen.
|
|
///
|
|
private func showOtherScreen() {
|
|
let processor = OtherSettingsProcessor(
|
|
coordinator: asAnyCoordinator(),
|
|
services: services,
|
|
state: OtherSettingsState(),
|
|
)
|
|
let view = OtherSettingsView(store: Store(processor: processor))
|
|
let viewController = UIHostingController(rootView: view)
|
|
viewController.navigationItem.largeTitleDisplayMode = .never
|
|
stackNavigator?.push(viewController, navigationTitle: Localizations.other)
|
|
}
|
|
|
|
/// Shows the password auto-fill screen.
|
|
///
|
|
private func showPasswordAutoFill() {
|
|
guard let stackNavigator else { return }
|
|
let coordinator = module.makePasswordAutoFillCoordinator(
|
|
delegate: nil,
|
|
stackNavigator: stackNavigator,
|
|
)
|
|
coordinator.start()
|
|
coordinator.navigate(to: .passwordAutofill(mode: .settings))
|
|
}
|
|
|
|
/// Shows the pending login requests screen.
|
|
///
|
|
private func showPendingLoginRequests() {
|
|
let processor = PendingRequestsProcessor(
|
|
coordinator: asAnyCoordinator(),
|
|
services: services,
|
|
state: PendingRequestsState(),
|
|
)
|
|
stackNavigator?.present(PendingRequestsView(store: Store(processor: processor)))
|
|
}
|
|
|
|
/// Shows the select language screen.
|
|
///
|
|
private func showSelectLanguage(currentLanguage: LanguageOption, delegate: SelectLanguageDelegate?) {
|
|
let processor = SelectLanguageProcessor(
|
|
coordinator: asAnyCoordinator(),
|
|
delegate: delegate,
|
|
services: services,
|
|
state: SelectLanguageState(currentLanguage: currentLanguage),
|
|
)
|
|
stackNavigator?.present(SelectLanguageView(store: Store(processor: processor)))
|
|
}
|
|
|
|
/// Shows the settings screen.
|
|
///
|
|
private func showSettings(presentationMode: SettingsPresentationMode) {
|
|
let processor = SettingsProcessor(
|
|
coordinator: asAnyCoordinator(),
|
|
delegate: self,
|
|
services: services,
|
|
state: SettingsState(presentationMode: presentationMode),
|
|
)
|
|
let view = SettingsView(store: Store(processor: processor))
|
|
stackNavigator?.replace(view, animated: false)
|
|
}
|
|
|
|
/// Shows the vault screen.
|
|
///
|
|
private func showVault() {
|
|
let processor = VaultSettingsProcessor(
|
|
coordinator: asAnyCoordinator(),
|
|
services: services,
|
|
state: VaultSettingsState(),
|
|
)
|
|
let view = VaultSettingsView(store: Store(processor: processor))
|
|
let viewController = UIHostingController(rootView: view)
|
|
viewController.navigationItem.largeTitleDisplayMode = .never
|
|
stackNavigator?.push(viewController, navigationTitle: Localizations.vault)
|
|
}
|
|
}
|
|
|
|
// MARK: - HasErrorAlertServices
|
|
|
|
extension SettingsCoordinator: HasErrorAlertServices {
|
|
var errorAlertServices: ErrorAlertServices { services }
|
|
}
|
|
|
|
// MARK: - ImportLoginsCoordinatorDelegate
|
|
|
|
extension SettingsCoordinator: ImportLoginsCoordinatorDelegate {
|
|
func didCompleteLoginsImport() {
|
|
stackNavigator?.dismiss {
|
|
self.delegate?.didCompleteLoginsImport()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - SettingsProcessorDelegate
|
|
|
|
extension SettingsCoordinator: SettingsProcessorDelegate {
|
|
func updateSettingsTabBadge(_ badgeValue: String?) {
|
|
stackNavigator?.rootViewController?.tabBarItem.badgeValue = badgeValue
|
|
}
|
|
}
|