217 lines
7.4 KiB
Swift

import Combine
import Foundation
import UIKit
/// The `AppProcessor` processes actions received at the application level and contains the logic
/// to control the top-level flow through the app.
///
@MainActor
public class AppProcessor {
// MARK: Properties
/// The root module to use to create sub-coordinators.
let appModule: AppModule
/// The root coordinator of the app.
var coordinator: AnyCoordinator<AppRoute, AppEvent>?
/// The services used by the app.
let services: ServiceContainer
// MARK: Initialization
/// Initializes an `AppProcessor`.
///
/// - Parameters:
/// - appModule: The root module to use to create sub-coordinators.
/// - services: The services used by the app.
///
public init(
appModule: AppModule,
services: ServiceContainer
) {
self.appModule = appModule
self.services = services
self.services.notificationService.setDelegate(self)
self.services.syncService.delegate = self
UI.initialLanguageCode = services.appSettingsStore.appLocale ?? Locale.current.languageCode
UI.applyDefaultAppearances()
Task {
for await _ in services.notificationCenterService.willEnterForegroundPublisher() {
let userId = try await self.services.stateService.getActiveAccountId()
let shouldTimeout = try await services.vaultTimeoutService.hasPassedSessionTimeout(userId: userId)
if shouldTimeout {
// Allow the AuthCoordinator to handle the timeout.
await coordinator?.handleEvent(.didTimeout(userId: userId))
}
}
}
Task {
for await _ in services.notificationCenterService.didEnterBackgroundPublisher() {
let userId = try await self.services.stateService.getActiveAccountId()
try await services.vaultTimeoutService.setLastActiveTime(userId: userId)
}
}
}
// MARK: Methods
/// Starts the application flow by navigating the user to the first flow.
///
/// - Parameters:
/// - appContext: The context that the app is running within.
/// - initialRoute: The initial route to navigate to. If `nil` this, will navigate to the
/// unlock or landing auth route based on if there's an active account. Defaults to `nil`.
/// - navigator: The object that will be used to navigate between routes.
/// - window: The window to use to set the app's theme.
///
public func start(
appContext: AppContext,
initialRoute: AppRoute? = nil,
navigator: RootNavigator,
window: UIWindow?
) async {
let coordinator = appModule.makeAppCoordinator(appContext: appContext, navigator: navigator)
coordinator.start()
self.coordinator = coordinator
Task {
for await appTheme in await services.stateService.appThemePublisher().values {
navigator.appTheme = appTheme
window?.overrideUserInterfaceStyle = appTheme.userInterfaceStyle
}
}
await loadFlags()
await services.migrationService.performMigrations()
await services.environmentService.loadURLsForActiveAccount()
services.application?.registerForRemoteNotifications()
if let initialRoute {
coordinator.navigate(to: initialRoute)
} else {
await coordinator.handleEvent(.didStart)
}
}
// MARK: Notification Methods
/// Called when the app has registered for push notifications.
///
/// - Parameter tokenData: The device token for push notifications.
///
public func didRegister(withToken tokenData: Data) {
Task {
await services.notificationService.didRegister(withToken: tokenData)
}
}
/// Called when the app failed to register for push notifications.
///
/// - Parameter error: The error received.
///
public func failedToRegister(_ error: Error) {
services.errorReporter.log(error: error)
}
/// Called when the app has received data from a push notification.
///
/// - Parameters:
/// - message: The content of the push notification.
/// - notificationDismissed: `true` if a notification banner has been dismissed.
/// - notificationTapped: `true` if a notification banner has been tapped.
///
public func messageReceived(
_ message: [AnyHashable: Any],
notificationDismissed: Bool? = nil,
notificationTapped: Bool? = nil
) async {
await services.notificationService.messageReceived(
message,
notificationDismissed: notificationDismissed,
notificationTapped: notificationTapped
)
}
}
// MARK: - NotificationServiceDelegate
extension AppProcessor: NotificationServiceDelegate {
/// Users are logged out, route to landing page.
///
func routeToLanding() async {
coordinator?.navigate(to: .auth(.landing))
}
/// Show the login request.
///
/// - Parameter loginRequest: The login request.
///
func showLoginRequest(_ loginRequest: LoginRequest) {
coordinator?.navigate(to: .loginRequest(loginRequest))
}
/// Switch the active account in order to show the login request, prompting the user if necessary.
///
/// - Parameters:
/// - account: The account associated with the login request.
/// - loginRequest: The login request to show.
/// - showAlert: Whether to show the alert or simply switch the account.
///
func switchAccounts(to account: Account, for loginRequest: LoginRequest, showAlert: Bool) {
DispatchQueue.main.async {
if showAlert {
self.coordinator?.showAlert(.confirmation(
title: Localizations.logInRequested,
message: Localizations.loginAttemptFromXDoYouWantToSwitchToThisAccount(account.profile.email)
) {
self.switchAccounts(to: account.profile.userId, for: loginRequest)
})
} else {
self.switchAccounts(to: account.profile.userId, for: loginRequest)
}
}
}
/// Switch to the specified account and show the login request.
///
/// - Parameters:
/// - userId: The userId of the account to switch to.
/// - loginRequest: The login request to show.
///
private func switchAccounts(to userId: String, for loginRequest: LoginRequest) {
(coordinator as? VaultCoordinatorDelegate)?.didTapAccount(userId: userId)
coordinator?.navigate(to: .loginRequest(loginRequest))
}
}
// MARK: - SyncServiceDelegate
extension AppProcessor: SyncServiceDelegate {
func securityStampChanged(userId: String) async {
// Log the user out if their security stamp changes.
coordinator?.hideLoadingOverlay()
try? await services.authRepository.logout(userId: userId)
await coordinator?.handleEvent(.didLogout(userId: userId, userInitiated: false))
}
}
// MARK: - Feature flags
extension AppProcessor {
/// Loads feature flags.
///
func loadFlags() async {
do {
try await services.clientPlatform.loadFlags(flags: [FeatureFlagsConstants.enableCipherKeyEncryption: true])
} catch {
services.errorReporter.log(error: error)
}
}
}