Skip to content

Commit

Permalink
Handle relative URLs in notifications to open in-app (#687)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
zacwest committed Jun 21, 2020
1 parent c6cac21 commit db9c015
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 29 deletions.
93 changes: 65 additions & 28 deletions HomeAssistant/AppDelegate.swift
Expand Up @@ -35,6 +35,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var safariVC: SFSafariViewController?

private var webViewControllerPromise: Guarantee<WebViewController>
private var webViewControllerSeal: (WebViewController) -> Void

private(set) var regionManager: RegionManager!
private var periodicUpdateTimer: Timer? {
willSet {
Expand All @@ -44,10 +47,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}

// override init() {
// super.init()
// UIFont.overrideInitialize()
// }
override init() {
(self.webViewControllerPromise, self.webViewControllerSeal) = Guarantee<WebViewController>.pending()
super.init()
}

enum StateRestorationKey: String {
case mainWindow
Expand Down Expand Up @@ -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<WebViewController>.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)
}
}

Expand All @@ -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,
Expand All @@ -155,7 +180,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}

Current.onboardingComplete = {
self.window?.rootViewController = self.webViewNavigationController(rootViewController: WebViewController())
self.updateRootViewController(to: self.webViewNavigationController(rootViewController: WebViewController()))
}
}

Expand Down Expand Up @@ -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 {
Expand Down
23 changes: 22 additions & 1 deletion HomeAssistant/Views/WebViewController.swift
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

0 comments on commit db9c015

Please sign in to comment.