mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 20:39:24 +00:00
Initial setup ready for PIN/Biometric app lock. (#1876)
* Add AppLockCoordinator and WindowManager.
This commit is contained in:
@@ -20,7 +20,7 @@ import MatrixRustSDK
|
||||
import SwiftUI
|
||||
import Version
|
||||
|
||||
class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, NotificationManagerDelegate {
|
||||
class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, NotificationManagerDelegate, WindowManagerDelegate {
|
||||
private let stateMachine: AppCoordinatorStateMachine
|
||||
private let navigationRootCoordinator: NavigationRootCoordinator
|
||||
private let userSessionStore: UserSessionStoreProtocol
|
||||
@@ -43,8 +43,9 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
}
|
||||
}
|
||||
|
||||
private var userSessionFlowCoordinator: UserSessionFlowCoordinator?
|
||||
private var authenticationCoordinator: AuthenticationCoordinator?
|
||||
private let appLockFlowCoordinator: AppLockFlowCoordinator
|
||||
private var userSessionFlowCoordinator: UserSessionFlowCoordinator?
|
||||
private var softLogoutCoordinator: SoftLogoutScreenCoordinator?
|
||||
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
@@ -54,6 +55,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
private var clientProxyObserver: AnyCancellable?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
let windowManager = WindowManager()
|
||||
let notificationManager: NotificationManagerProtocol
|
||||
|
||||
private let appRouteURLParser: AppRouteURLParser
|
||||
@@ -94,10 +96,20 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
UIApplication.shared
|
||||
}
|
||||
|
||||
userSessionStore = UserSessionStore(backgroundTaskService: backgroundTaskService)
|
||||
|
||||
let keychainController = KeychainController(service: .sessions,
|
||||
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
||||
userSessionStore = UserSessionStore(keychainController: keychainController,
|
||||
backgroundTaskService: backgroundTaskService)
|
||||
|
||||
let appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings)
|
||||
appLockFlowCoordinator = AppLockFlowCoordinator(appLockService: appLockService,
|
||||
navigationCoordinator: NavigationRootCoordinator())
|
||||
|
||||
notificationManager = NotificationManager(notificationCenter: UNUserNotificationCenter.current(),
|
||||
appSettings: appSettings)
|
||||
|
||||
windowManager.delegate = self
|
||||
|
||||
notificationManager.delegate = self
|
||||
notificationManager.start()
|
||||
|
||||
@@ -117,6 +129,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
|
||||
observeApplicationState()
|
||||
observeNetworkState()
|
||||
observeAppLockChanges()
|
||||
|
||||
registerBackgroundAppRefresh()
|
||||
}
|
||||
@@ -178,6 +191,12 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
stateMachine.processEvent(.createdUserSession)
|
||||
}
|
||||
|
||||
// MARK: - WindowManagerDelegate
|
||||
|
||||
func windowManagerDidConfigureWindows(_ windowManager: WindowManager) {
|
||||
windowManager.alternateWindow.rootViewController = UIHostingController(rootView: appLockFlowCoordinator.toPresentable())
|
||||
}
|
||||
|
||||
// MARK: - NotificationManagerDelegate
|
||||
|
||||
func registerForRemoteNotifications() {
|
||||
@@ -553,6 +572,19 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func observeAppLockChanges() {
|
||||
appLockFlowCoordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .lockApp:
|
||||
windowManager.switchToAlternate()
|
||||
case .unlockApp:
|
||||
windowManager.switchToMain()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func handleAppRoute(_ appRoute: AppRoute) {
|
||||
if let userSessionFlowCoordinator {
|
||||
userSessionFlowCoordinator.handleAppRoute(appRoute, animated: UIApplication.shared.applicationState == .active)
|
||||
|
||||
@@ -25,6 +25,13 @@ enum AppDelegateCallback {
|
||||
|
||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
let callbacks = PassthroughSubject<AppDelegateCallback, Never>()
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
// Add a SceneDelegate to the SwiftUI scene so that we can connect up the WindowManager.
|
||||
let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
|
||||
configuration.delegateClass = SceneDelegate.self
|
||||
return configuration
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
||||
NSTextAttachment.registerViewProviderClass(PillAttachmentViewProvider.self, forFileType: InfoPlistReader.main.pillsUTType)
|
||||
|
||||
@@ -42,6 +42,7 @@ final class AppSettings {
|
||||
case swiftUITimelineEnabled
|
||||
case voiceMessageEnabled
|
||||
case mentionsEnabled
|
||||
case appLockFlowEnabled
|
||||
}
|
||||
|
||||
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
||||
@@ -257,4 +258,7 @@ final class AppSettings {
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.mentionsEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||
var mentionsEnabled
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.appLockFlowEnabled, defaultValue: false, storageType: .volatile)
|
||||
var appLockFlowEnabled
|
||||
}
|
||||
|
||||
@@ -18,8 +18,9 @@ import SwiftUI
|
||||
|
||||
@main
|
||||
struct Application: App {
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self) private var applicationDelegate
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
private var appCoordinator: AppCoordinatorProtocol!
|
||||
|
||||
init() {
|
||||
@@ -28,7 +29,9 @@ struct Application: App {
|
||||
} else if ProcessInfo.isRunningUnitTests {
|
||||
appCoordinator = UnitTestsAppCoordinator()
|
||||
} else {
|
||||
appCoordinator = AppCoordinator(appDelegate: applicationDelegate)
|
||||
let coordinator = AppCoordinator(appDelegate: appDelegate)
|
||||
SceneDelegate.windowManager = coordinator.windowManager
|
||||
appCoordinator = coordinator
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,13 +57,18 @@ class NavigationRootCoordinator: ObservableObject, CoordinatorProtocol, CustomSt
|
||||
|
||||
/// Sets or replaces the presented coordinator
|
||||
/// - Parameter coordinator: the coordinator to display
|
||||
func setRootCoordinator(_ coordinator: (any CoordinatorProtocol)?, dismissalCallback: (() -> Void)? = nil) {
|
||||
guard let coordinator else {
|
||||
rootModule = nil
|
||||
return
|
||||
}
|
||||
func setRootCoordinator(_ coordinator: (any CoordinatorProtocol)?, animated: Bool = true, dismissalCallback: (() -> Void)? = nil) {
|
||||
var transaction = Transaction()
|
||||
transaction.disablesAnimations = !animated
|
||||
|
||||
rootModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
|
||||
withTransaction(transaction) {
|
||||
guard let coordinator else {
|
||||
rootModule = nil
|
||||
return
|
||||
}
|
||||
|
||||
rootModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
|
||||
}
|
||||
}
|
||||
|
||||
/// - dismissalCallback: called when the sheet has been dismissed, programatically or otherwise
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A basic window scene delegate used to configure the `WindowManager`.
|
||||
///
|
||||
/// We don't support multiple scenes right now, so the implementation is pretty basic.
|
||||
class SceneDelegate: NSObject, UIWindowSceneDelegate {
|
||||
weak static var windowManager: WindowManager!
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
guard let windowScene = scene as? UIWindowScene, !ProcessInfo.isRunningTests else { return }
|
||||
Self.windowManager.configure(with: windowScene)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
protocol WindowManagerDelegate: AnyObject {
|
||||
/// The window manager has configured its windows.
|
||||
func windowManagerDidConfigureWindows(_ windowManager: WindowManager)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// A window manager that supports switching between a main app window with an overlay and
|
||||
/// an alternate window to switch contexts whilst also preserving the main view hierarchy.
|
||||
class WindowManager {
|
||||
weak var delegate: WindowManagerDelegate?
|
||||
|
||||
/// The app's main window (we only support a single scene).
|
||||
private(set) var mainWindow: UIWindow!
|
||||
/// Presented on top of the main window, to display e.g. user indicators.
|
||||
private(set) var overlayWindow: UIWindow!
|
||||
/// A secondary window that can be presented instead of the main/overlay window combo.
|
||||
private(set) var alternateWindow: UIWindow!
|
||||
|
||||
/// Configures the window manager to operate on the supplied scene.
|
||||
func configure(with windowScene: UIWindowScene) {
|
||||
mainWindow = windowScene.keyWindow
|
||||
|
||||
overlayWindow = UIWindow(windowScene: windowScene)
|
||||
overlayWindow.backgroundColor = .clear
|
||||
// We don't support user interaction on our indicators so disable interaction, to pass
|
||||
// touches through to the main window. If this changes, there's another solution here:
|
||||
// https://www.fivestars.blog/articles/swiftui-windows/
|
||||
overlayWindow.isUserInteractionEnabled = false
|
||||
|
||||
alternateWindow = UIWindow(windowScene: windowScene)
|
||||
|
||||
delegate?.windowManagerDidConfigureWindows(self)
|
||||
}
|
||||
|
||||
/// Shows the main and overlay window combo, hiding the alternate window.
|
||||
func switchToMain() {
|
||||
mainWindow.isHidden = false
|
||||
overlayWindow.isHidden = false
|
||||
alternateWindow.isHidden = true
|
||||
}
|
||||
|
||||
/// Shows the alternate window, hiding the main and overlay combo.
|
||||
func switchToAlternate() {
|
||||
alternateWindow.isHidden = false
|
||||
overlayWindow.isHidden = true
|
||||
mainWindow.isHidden = true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user