mirror of
https://github.com/luxfi/safe-ios.git
synced 2026-10-30 15:19:20 +00:00
434 lines
15 KiB
Swift
434 lines
15 KiB
Swift
//
|
|
// SceneDelegate.swift
|
|
// Multisig
|
|
//
|
|
// Created by Dmitry Bespalov. on 14.04.20.
|
|
// Copyright © 2020 Gnosis Ltd. All rights reserved.
|
|
//
|
|
import UIKit
|
|
import SwiftUI
|
|
import CustomAuth
|
|
|
|
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
|
|
var snackbarViewController = SnackbarViewController(nibName: nil, bundle: nil)
|
|
|
|
var updateAppWindow: UIWindow?
|
|
var tabBarWindow: UIWindow?
|
|
var privacyShieldWindow: UIWindow?
|
|
|
|
// the window to present
|
|
var presentedWindow: UIWindow?
|
|
|
|
private var shouldShowPasscode: Bool {
|
|
App.shared.auth.isPasscodeSetAndAvailable && AppSettings.passcodeOptions.contains(.useForLogin)
|
|
}
|
|
|
|
private var startedFromNotification: Bool {
|
|
get { App.shared.appReview.startedFromNotification }
|
|
set { App.shared.appReview.startedFromNotification = newValue }
|
|
}
|
|
|
|
weak var scene: UIWindowScene?
|
|
|
|
// MARK: - Scene Life Cycle
|
|
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
|
#if DEBUG
|
|
guard UIApplication.shared.delegate is AppDelegate else {
|
|
// assume we're in a testing mode, so exit any further configuration
|
|
return
|
|
}
|
|
#endif
|
|
|
|
App.shared.notificationHandler.appStarted()
|
|
startedFromNotification = connectionOptions.notificationResponse != nil
|
|
|
|
if let scene = scene as? UIWindowScene {
|
|
self.scene = scene
|
|
makeWindows(scene: scene)
|
|
}
|
|
|
|
App.shared.appReview.startedFromNotification = connectionOptions.notificationResponse != nil
|
|
|
|
if let userActivity = connectionOptions.userActivities.first {
|
|
handleUserActivity(userActivity)
|
|
}
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(handlePasscodeRequired),
|
|
name: .passcodeRequired,
|
|
object: nil)
|
|
}
|
|
|
|
func sceneWillEnterForeground(_ scene: UIScene) {
|
|
#if DEBUG
|
|
guard UIApplication.shared.delegate is AppDelegate else {
|
|
// assume we're in a testing mode, so exit any further configuration
|
|
return
|
|
}
|
|
#endif
|
|
|
|
App.shared.notificationHandler.appEnteredForeground()
|
|
|
|
if scene.activationState == .unattached && updateAppWindow?.rootViewController != nil {
|
|
showWindow(updateAppWindow)
|
|
} else {
|
|
onAppUpdateCompletion()
|
|
}
|
|
}
|
|
|
|
func sceneDidBecomeActive(_ scene: UIScene) {
|
|
#if DEBUG
|
|
guard UIApplication.shared.delegate is AppDelegate else {
|
|
// assume we're in a testing mode, so exit any further configuration
|
|
return
|
|
}
|
|
#endif
|
|
|
|
// Called when the scene has moved from an inactive state to an active state.
|
|
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
|
|
App.shared.clientGatewayHostObserver.startObserving()
|
|
|
|
PendingTransactionMonitor.scheduleMonitoring()
|
|
RelayedTransactionMonitor.scheduleMonitoring()
|
|
SafeCreationMonitor.scheduleMonitoring()
|
|
WebConnectionExpirationMonitor.scheduleMonitoring()
|
|
|
|
privacyShieldWindow?.isHidden = true
|
|
|
|
if let viewController = updateAppWindow?.rootViewController as? UpdateAppViewController, viewController.style == .required {
|
|
showWindow(updateAppWindow)
|
|
}
|
|
}
|
|
|
|
func sceneWillResignActive(_ scene: UIScene) {
|
|
#if DEBUG
|
|
guard UIApplication.shared.delegate is AppDelegate else {
|
|
// assume we're in a testing mode, so exit any further configuration
|
|
return
|
|
}
|
|
#endif
|
|
|
|
App.shared.clientGatewayHostObserver.stopObserving()
|
|
|
|
PendingTransactionMonitor.stopMonitoring()
|
|
WebConnectionExpirationMonitor.stopMonitoring()
|
|
|
|
if presentedWindow === tabBarWindow {
|
|
privacyShieldWindow?.isHidden = false
|
|
}
|
|
IntercomConfig.hide()
|
|
}
|
|
|
|
func sceneDidEnterBackground(_ scene: UIScene) {
|
|
#if DEBUG
|
|
guard UIApplication.shared.delegate is AppDelegate else {
|
|
// assume we're in a testing mode, so exit any further configuration
|
|
return
|
|
}
|
|
#endif
|
|
// Save changes in the application's managed object context when the application transitions to the background.
|
|
App.shared.coreDataStack.saveContext()
|
|
|
|
App.shared.securityCenter.lockDataStore()
|
|
}
|
|
|
|
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
|
#if DEBUG
|
|
guard UIApplication.shared.delegate is AppDelegate else {
|
|
// assume we're in a testing mode, so exit any further configuration
|
|
return
|
|
}
|
|
#endif
|
|
|
|
handleUserActivity(userActivity)
|
|
}
|
|
|
|
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
|
_ = handleURL(wcURL: URLContexts.first?.url.absoluteString)
|
|
}
|
|
|
|
|
|
|
|
// Handles opening of a universal link.
|
|
//
|
|
// Supported link types:
|
|
// - WalletConnect links from dapps to connect to the safe
|
|
// - 'connect' link to establish new connection
|
|
// - 'open' link to move the app to foreground so that it is able to process WalletConnect request or response.
|
|
// - Request To Add Owner
|
|
// - <web app url>/settings/setup?safe=<safe_address>&address=<owner_address>
|
|
// - Web3auth
|
|
// - handled by CustomAuth.handle()
|
|
private func handleUserActivity(_ userActivity: NSUserActivity) {
|
|
// Get URL components from the incoming user activity.
|
|
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
|
|
let incomingURL = userActivity.webpageURL,
|
|
let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else {
|
|
return
|
|
}
|
|
|
|
// handle wallet connect
|
|
let didHandleURL = handleURL(wcURL: components.queryItems?.first?.value)
|
|
if didHandleURL {
|
|
return
|
|
}
|
|
|
|
|
|
// handle request to add owner
|
|
if AddOwnerRequestValidator.isValid(url: incomingURL),
|
|
let params = AddOwnerRequestValidator.parameters(from: incomingURL) {
|
|
CompositeNavigationRouter.shared.navigate(to: .requestToAddOwner(params))
|
|
return
|
|
}
|
|
|
|
if let navigationRoute = CompositeNavigationRouter.shared.routeFrom(from: incomingURL) {
|
|
CompositeNavigationRouter.shared.navigate(to: navigationRoute)
|
|
}
|
|
}
|
|
|
|
private func handleURL(wcURL: String?) -> Bool {
|
|
guard let wcURL = wcURL, (try? Safe.getSelected()) != nil else { return false }
|
|
if WalletConnectManager.shared.canConnect(url: wcURL) {
|
|
WalletConnectManager.shared.pairClient(url: wcURL, trackingEvent: .dappConnectedWithUniversalLink)
|
|
return true
|
|
} else if WalletConnectSafesServerController.shared.canConnect(url: wcURL) {
|
|
try? WalletConnectSafesServerController.shared.connect(url: wcURL)
|
|
WalletConnectSafesServerController.shared.dappConnectedTrackingEvent = .dappConnectedWithUniversalLink
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// MARK: - Window Management
|
|
|
|
func present(_ controller: UIViewController) {
|
|
tabBarWindow?.rootViewController?.present(controller, animated: true)
|
|
}
|
|
|
|
private func makeWindow(scene: UIWindowScene) -> UIWindow {
|
|
let window = WindowWithViewOnTop(windowScene: scene)
|
|
window.tintColor = .primary
|
|
|
|
return window
|
|
}
|
|
|
|
private func showWindow(_ window: UIWindow?) {
|
|
guard let window = window else { return }
|
|
|
|
if let customWindow = window as? WindowWithViewOnTop {
|
|
snackbarViewController.view.frame = customWindow.bounds
|
|
customWindow.addSubviewAlwaysOnTop(snackbarViewController.view)
|
|
}
|
|
|
|
window.makeKeyAndVisible()
|
|
App.shared.theme.setUp()
|
|
presentedWindow = window
|
|
}
|
|
|
|
private func makeWindows(scene: UIWindowScene) {
|
|
// snack bar
|
|
SnackbarViewController.instance = snackbarViewController
|
|
snackbarViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
|
|
updateAppWindow = makeUpdateAppWindow()
|
|
tabBarWindow = makeTabBarWindow()
|
|
privacyShieldWindow = makePrivacyShieldWindow()
|
|
}
|
|
|
|
func makeUpdateAppWindow() -> UIWindow {
|
|
let updateAppWindow = makeWindow(scene: scene!)
|
|
if let controller = App.shared.updateController.makeUpdateAppViewController() {
|
|
updateAppWindow.rootViewController = controller
|
|
|
|
controller.completion = { [unowned self] in
|
|
onAppUpdateCompletion()
|
|
}
|
|
}
|
|
return updateAppWindow
|
|
}
|
|
|
|
func makeTabBarWindow() -> UIWindow {
|
|
let tabBarWindow = makeWindow(scene: scene!)
|
|
tabBarWindow.rootViewController = ViewControllerFactory.tabBarViewController(completion: { [unowned self] tabBar in
|
|
onTabBarAppearance(of: tabBar)
|
|
})
|
|
return tabBarWindow
|
|
}
|
|
|
|
func makeFaceIDUnlockWindow() -> UIWindow {
|
|
let faceIDUnlockWindow = makeWindow(scene: scene!)
|
|
faceIDUnlockWindow.rootViewController = ViewControllerFactory.faceIDUnlockViewController { [unowned self] in
|
|
onFaceIDCheckCompletion()
|
|
}
|
|
return faceIDUnlockWindow
|
|
}
|
|
|
|
|
|
func makeEnterPasscodeWindow(showsCloseButton: Bool = false,
|
|
completion: ((EnterPasscodeViewController.Result) -> Void)? = nil) -> UIWindow {
|
|
let enterPasscodeWindow = makeWindow(scene: scene!)
|
|
|
|
let vc = ViewControllerFactory.enterPasscodeViewController (showsCloseButton: showsCloseButton) { [unowned self] result in
|
|
if case let EnterPasscodeViewController.Result.success(passcode) = result {
|
|
onEnterPasscodeCompletion(userPassword: passcode)
|
|
} else {
|
|
showMainContentWindow()
|
|
}
|
|
|
|
completion?(result)
|
|
}
|
|
|
|
enterPasscodeWindow.rootViewController = vc
|
|
return enterPasscodeWindow
|
|
}
|
|
|
|
func makePrivacyShieldWindow() -> UIWindow {
|
|
let privacyShieldWindow = makeWindow(scene: scene!)
|
|
privacyShieldWindow.rootViewController = PrivacyProtectionScreenViewController()
|
|
return privacyShieldWindow
|
|
}
|
|
|
|
func makeTermsWindow() -> UIWindow {
|
|
let termsWindow = makeWindow(scene: scene!)
|
|
termsWindow.rootViewController = ViewControllerFactory.termsViewController { [unowned self] in
|
|
onTermsCompletion()
|
|
}
|
|
return termsWindow
|
|
}
|
|
|
|
func showOnboardingWindow() {
|
|
if let presentedWindow = presentedWindow,
|
|
let root = presentedWindow.rootViewController,
|
|
root is OnboardingViewController {
|
|
return
|
|
}
|
|
|
|
showWindow(makeOnboardingWindow())
|
|
}
|
|
|
|
func makeOnboardingWindow() -> UIWindow {
|
|
AppSettings.onboardingCompleted = false
|
|
|
|
let onboardingWindow = makeWindow(scene: scene!)
|
|
onboardingWindow.rootViewController = OnboardingViewController(completion: { [unowned self] in
|
|
AppSettings.onboardingCompleted = true
|
|
onOnboardingCompletion()
|
|
})
|
|
|
|
return onboardingWindow
|
|
}
|
|
|
|
func onAppUpdateCompletion() {
|
|
if !AppSettings.termsAccepted {
|
|
showWindow(makeTermsWindow())
|
|
// TODO: Enable when implemented new security center
|
|
} else if shouldShowPasscode && !AppConfiguration.FeatureToggles.securityCenter {
|
|
showWindow(makeEnterPasscodeWindow())
|
|
} else if App.shared.securityCenter.shouldShowFaceID() {
|
|
showWindow(makeFaceIDUnlockWindow())
|
|
} else if App.shared.securityCenter.shouldShowPasscode() {
|
|
showWindow(makeEnterPasscodeWindow())
|
|
} else if !AppSettings.onboardingCompleted {
|
|
showOnboardingWindow()
|
|
} else {
|
|
showMainContentWindow()
|
|
}
|
|
}
|
|
|
|
func showMainContentWindow() {
|
|
showWindow(tabBarWindow)
|
|
IntercomConfig.appDidShowMainContent()
|
|
}
|
|
|
|
func onTermsCompletion() {
|
|
showWindow(makeOnboardingWindow())
|
|
}
|
|
|
|
func onOnboardingCompletion() {
|
|
showMainContentWindow()
|
|
}
|
|
|
|
// userPassword can be nil if passcode is disabled
|
|
func onEnterPasscodeCompletion(userPassword: String? = nil) {
|
|
do {
|
|
if AppConfiguration.FeatureToggles.securityCenter {
|
|
try App.shared.securityCenter.unlockDataStore(userPassword: userPassword)
|
|
}
|
|
showMainContentWindow()
|
|
} catch {
|
|
LogService.shared.error("Failed to unlock", error: error)
|
|
}
|
|
}
|
|
|
|
func onFaceIDCheckCompletion() {
|
|
showMainContentWindow()
|
|
}
|
|
|
|
func onTabBarAppearance(of tabBar: MainTabBarViewController) {
|
|
if startedFromNotification, let safeTxHash = App.shared.notificationHandler.transactionDetailsPayload {
|
|
// present transaction details
|
|
App.shared.notificationHandler.transactionDetailsPayload = nil
|
|
let vc = ViewControllerFactory.transactionDetailsViewController(safeTxHash: safeTxHash)
|
|
tabBar.present(vc, animated: true, completion: nil)
|
|
|
|
} else if App.shared.notificationHandler.needsToRequestNotificationPermission {
|
|
App.shared.notificationHandler.requestUserPermissionAndRegister()
|
|
|
|
} else {
|
|
App.shared.appReview.pullAppReviewTrigger()
|
|
}
|
|
}
|
|
|
|
@objc private func handlePasscodeRequired(_ notification: Notification) {
|
|
guard let task = notification.userInfo?["accessTask"] as? (_ password: String?) -> Void else {
|
|
return
|
|
}
|
|
|
|
DispatchQueue.main.async { [unowned self] in
|
|
showWindow(makeEnterPasscodeWindow(showsCloseButton: true) { result in
|
|
switch result {
|
|
case .success(let password):
|
|
task(password)
|
|
case .close:
|
|
return
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SceneDelegate: NavigationRouter {
|
|
func routeFrom(from url: URL) -> NavigationRoute? {
|
|
nil
|
|
}
|
|
|
|
func canNavigate(to route: NavigationRoute) -> Bool {
|
|
guard let tabWindow = tabBarWindow, let tabBarVC = tabWindow.rootViewController as? MainTabBarViewController else { return false }
|
|
return tabBarVC.canNavigate(to: route)
|
|
}
|
|
|
|
func navigate(to route: NavigationRoute) {
|
|
guard let tabWindow = tabBarWindow, let tabBarVC = tabWindow.rootViewController as? MainTabBarViewController else { return }
|
|
tabBarVC.navigate(to: route)
|
|
}
|
|
}
|
|
|
|
// Window that can keep some view always on top of other views
|
|
class WindowWithViewOnTop: UIWindow {
|
|
|
|
private weak var keepInFront: UIView?
|
|
|
|
func addSubviewAlwaysOnTop(_ view: UIView) {
|
|
keepInFront = view
|
|
addSubview(view)
|
|
}
|
|
|
|
override func addSubview(_ view: UIView) {
|
|
super.addSubview(view)
|
|
if let v = keepInFront {
|
|
bringSubviewToFront(v)
|
|
}
|
|
}
|
|
}
|