From 248df1fbb8d8080bd29d892575132555c061cbbc Mon Sep 17 00:00:00 2001 From: Zac West Date: Sat, 20 Jun 2020 22:26:08 -0700 Subject: [PATCH] Handle relative URLs in notifications to open in-app This handles the various ways that we could launch: - state restoration doesn't fire on notifications since the system knows it shouldn't - launching from cold uses a Promise to wait (since it's launched well before WebViewController exists) - launching from warm goes through the same Promise but happens instantly - handles one fun edge case of: start the app, get logged out due to deactivated token, open a notification with a url, finish onboarding -- this will correctly open the URL. Fixes #250 -- use a URL like `/lovelace-name-here/0` to open a page as if you navigated to it normally. --- HomeAssistant/AppDelegate.swift | 93 ++++++++++++++------- HomeAssistant/Views/WebViewController.swift | 23 ++++- 2 files changed, 87 insertions(+), 29 deletions(-) diff --git a/HomeAssistant/AppDelegate.swift b/HomeAssistant/AppDelegate.swift index a39fb3e9d..8306f616f 100644 --- a/HomeAssistant/AppDelegate.swift +++ b/HomeAssistant/AppDelegate.swift @@ -35,6 +35,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var safariVC: SFSafariViewController? + private var webViewControllerPromise: Guarantee + private var webViewControllerSeal: (WebViewController) -> Void + private(set) var regionManager: RegionManager! private var periodicUpdateTimer: Timer? { willSet { @@ -44,10 +47,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } -// override init() { -// super.init() -// UIFont.overrideInitialize() -// } + override init() { + (self.webViewControllerPromise, self.webViewControllerSeal) = Guarantee.pending() + super.init() + } enum StateRestorationKey: String { case mainWindow @@ -110,19 +113,41 @@ class AppDelegate: UIResponder, UIApplicationDelegate { self.window = window } + func updateRootViewController(to newValue: UIViewController) { + let newWebViewController = newValue.children.compactMap { $0 as? WebViewController }.first + + // must be before the seal fires, or it may request during deinit of an old one + window?.rootViewController = newValue + + if let newWebViewController = newWebViewController { + // any kind of ->webviewcontroller is the same, even if we are for some reason replacing an existing one + if webViewControllerPromise.isFulfilled { + webViewControllerPromise = .value(newWebViewController) + } else { + webViewControllerSeal(newWebViewController) + } + } else if webViewControllerPromise.isFulfilled { + // replacing one, so set up a new promise if necessary + (self.webViewControllerPromise, self.webViewControllerSeal) = Guarantee.pending() + } + } + func setupView() { if Current.appConfiguration == .FastlaneSnapshot { setupFastlaneSnapshotConfiguration() } if requiresOnboarding { Current.Log.info("showing onboarding") - window?.rootViewController = onboardingNavigationController() + updateRootViewController(to: onboardingNavigationController()) } else { if let rootController = window?.rootViewController, !rootController.children.isEmpty { Current.Log.info("state restoration loaded controller, not creating a new one") + // not changing anything, but handle the promises + updateRootViewController(to: rootController) } else { Current.Log.info("state restoration didn't load anything, constructing controllers manually") - let navController = webViewNavigationController(rootViewController: WebViewController()) - window?.rootViewController = navController + let webViewController = WebViewController() + let navController = webViewNavigationController(rootViewController: webViewController) + updateRootViewController(to: navController) } } @@ -142,7 +167,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { Current.signInRequiredCallback = { type in let controller = self.onboardingNavigationController() - self.window?.rootViewController = controller + self.updateRootViewController(to: controller) if type.shouldShowError { let alert = UIAlertController(title: L10n.Alerts.AuthRequired.title, @@ -155,7 +180,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } Current.onboardingComplete = { - self.window?.rootViewController = self.webViewNavigationController(rootViewController: WebViewController()) + self.updateRootViewController(to: self.webViewNavigationController(rootViewController: WebViewController())) } } @@ -913,27 +938,39 @@ extension AppDelegate: UNUserNotificationCenterDelegate { } - if let openUrl = userInfo["url"] as? String, - let url = URL(string: openUrl) { - if prefs.bool(forKey: "confirmBeforeOpeningUrl") { - let alert = UIAlertController(title: L10n.Alerts.OpenUrlFromNotification.title, - message: L10n.Alerts.OpenUrlFromNotification.message(openUrl), - preferredStyle: UIAlertController.Style.alert) - alert.addAction(UIAlertAction(title: L10n.noLabel, style: UIAlertAction.Style.default, handler: nil)) - alert.addAction(UIAlertAction(title: L10n.yesLabel, style: UIAlertAction.Style.default, handler: { _ in - UIApplication.shared.open(url, - options: [:], + if let openUrlRaw = userInfo["url"] as? String { + if let webviewURL = Current.settingsStore.connectionInfo?.webviewURL(from: openUrlRaw) { + webViewControllerPromise.done { webViewController in + webViewController.open(inline: webviewURL) + } + } else if let url = URL(string: openUrlRaw) { + if prefs.bool(forKey: "confirmBeforeOpeningUrl") { + let alert = UIAlertController(title: L10n.Alerts.OpenUrlFromNotification.title, + message: L10n.Alerts.OpenUrlFromNotification.message(openUrlRaw), + preferredStyle: UIAlertController.Style.alert) + alert.addAction(UIAlertAction( + title: L10n.noLabel, + style: UIAlertAction.Style.default, + handler: nil + )) + alert.addAction(UIAlertAction( + title: L10n.yesLabel, + style: UIAlertAction.Style.default + ) { _ in + UIApplication.shared.open(url, + options: [:], + completionHandler: nil) + }) + var rootViewController = UIApplication.shared.keyWindow?.rootViewController + if let navigationController = rootViewController as? UINavigationController { + rootViewController = navigationController.viewControllers.first + } + rootViewController?.present(alert, animated: true, completion: nil) + alert.popoverPresentationController?.sourceView = rootViewController?.view + } else { + UIApplication.shared.open(url, options: [:], completionHandler: nil) - })) - var rootViewController = UIApplication.shared.keyWindow?.rootViewController - if let navigationController = rootViewController as? UINavigationController { - rootViewController = navigationController.viewControllers.first } - rootViewController?.present(alert, animated: true, completion: nil) - alert.popoverPresentationController?.sourceView = rootViewController?.view - } else { - UIApplication.shared.open(url, options: [:], - completionHandler: nil) } } firstly { diff --git a/HomeAssistant/Views/WebViewController.swift b/HomeAssistant/Views/WebViewController.swift index b2cb43ce8..57f352394 100644 --- a/HomeAssistant/Views/WebViewController.swift +++ b/HomeAssistant/Views/WebViewController.swift @@ -225,8 +225,10 @@ class WebViewController: UIViewController, WKNavigationDelegate, WKUIDelegate, U self.navigationController?.setNavigationBarHidden(true, animated: false) + // if we aren't showing a url or it's an incorrect url, update it -- otherwise, leave it alone if let connectionInfo = Current.settingsStore.connectionInfo, - let webviewURL = connectionInfo.webviewURL() { + let webviewURL = connectionInfo.webviewURL(), + webView.url == nil || webView.url?.baseIsEqual(to: webviewURL) == false { let myRequest: URLRequest if Current.settingsStore.restoreLastURL, @@ -275,6 +277,11 @@ class WebViewController: UIViewController, WKNavigationDelegate, WKUIDelegate, U setNeedsStatusBarAppearanceUpdate() } + public func open(inline url: URL) { + loadViewIfNeeded() + webView.load(URLRequest(url: url)) + } + func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { if navigationAction.targetFrame == nil { @@ -922,5 +929,19 @@ extension ConnectionInfo { return try? components.asURL() } + + func webviewURL(from raw: String) -> URL? { + guard let baseURL = webviewURL() else { + return nil + } + + if raw.starts(with: "/") { + return baseURL.appendingPathComponent(raw) + } else if let url = URL(string: raw), url.baseIsEqual(to: baseURL) { + return url + } else { + return nil + } + } // swiftlint:disable:next file_length }