Top level deeplink handling (#1660)

* Handle link opening on the top most levels and prepare for percolating them throughout the app
* Add support for a custom app scheme
* Add specific AppRoute parsers.
* Integrate custom scheme in the AppRouteURLParser
* Switch to `element://call` and cleanup route parsing
This commit is contained in:
Stefan Ceriu
2023-09-11 12:31:31 +03:00
committed by GitHub
parent 0813e13325
commit ee4040141d
19 changed files with 140 additions and 44 deletions
@@ -55,6 +55,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
let notificationManager: NotificationManagerProtocol
private let appRouteURLParser: AppRouteURLParser
@Consumable private var storedAppRoute: AppRoute?
init() {
@@ -75,6 +76,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
}
self.appSettings = appSettings
appRouteURLParser = AppRouteURLParser(appSettings: appSettings)
navigationRootCoordinator = NavigationRootCoordinator()
@@ -137,10 +139,10 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
)
}
func handleUniversalLink(_ url: URL) {
func handleDeepLink(_ url: URL) -> Bool {
// Parse into an AppRoute to redirect these in a type safe way.
if let route = AppRouteURLParser.route(from: url) {
if let route = appRouteURLParser.route(from: url) {
switch route {
case .genericCallLink(let url):
if let userSessionFlowCoordinator {
@@ -152,13 +154,15 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
break
}
return
return true
}
// Until we have an OIDC callback AppRoute, handle it manually.
if url.absoluteString.starts(with: appSettings.oidcRedirectURL.absoluteString) {
MXLog.error("OIDC callback through Universal Links not implemented.")
}
return false
}
// MARK: - AuthenticationCoordinatorDelegate
@@ -18,5 +18,5 @@ import Foundation
protocol AppCoordinatorProtocol: CoordinatorProtocol {
var notificationManager: NotificationManagerProtocol { get }
func handleUniversalLink(_ url: URL)
@discardableResult func handleDeepLink(_ url: URL) -> Bool
}
@@ -35,7 +35,14 @@ struct Application: App {
WindowGroup {
appCoordinator.toPresentable()
.statusBarHidden(shouldHideStatusBar)
.onOpenURL { appCoordinator.handleUniversalLink($0) }
.environment(\.openURL, OpenURLAction { url in
if appCoordinator.handleDeepLink(url) {
return .handled
}
return .systemAction
})
.onOpenURL { appCoordinator.handleDeepLink($0) }
.introspect(.window, on: .iOS(.v16)) { window in
// Workaround for SwiftUI not consistently applying the tint colour to Alerts/Confirmation Dialogs.
window.tintColor = .compound.textActionPrimary
@@ -17,6 +17,7 @@
import Foundation
enum AppRoute: Equatable {
case oidcCallback(url: URL)
case roomList
case room(roomID: String)
case roomDetails(roomID: String)
@@ -24,32 +25,87 @@ enum AppRoute: Equatable {
case genericCallLink(url: URL)
}
enum AppRouteURLParser {
private enum KnownHosts: String, CaseIterable {
case elementIo = "element.io"
case appElementIo = "app.element.io"
case stagingElementIo = "staging.element.io"
case developElementIo = "develop.element.io"
case mobileElementIo = "mobile.element.io"
case callElementIo = "call.element.io"
struct AppRouteURLParser {
let urlParsers: [URLParser]
init(appSettings: AppSettings) {
urlParsers = [
ElementAppURLParser(appSettings: appSettings),
ElementCallURLParser()
]
}
static func route(from url: URL) -> AppRoute? {
guard let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false),
let host = urlComponents.host else {
MXLog.error("Failed parsing URL: \(url)")
return nil
func route(from url: URL) -> AppRoute? {
for parser in urlParsers {
if let appRoute = parser.route(from: url) {
return appRoute
}
}
guard KnownHosts.allCases.map(\.rawValue).contains(host) else {
return .genericCallLink(url: url)
}
if host == KnownHosts.callElementIo.rawValue {
return .genericCallLink(url: url)
}
// Deep linking not supported at the moment
return nil
}
}
/// Represents a type that can parse a `URL` into an `AppRoute`.
///
/// The following Universal Links are missing parsers.
/// - app.element.io
/// - mobile.element.io
protocol URLParser {
func route(from url: URL) -> AppRoute?
}
/// The parser for the main Element website.
struct ElementAppURLParser: URLParser {
private let knownHosts = ["app.element.io", "staging.element.io", "develop.element.io"]
let appSettings: AppSettings
func route(from url: URL) -> AppRoute? {
guard let host = url.host, knownHosts.contains(host) else {
return nil
}
let pathComponents = url.pathComponents
// OIDC callback URL.
if pathComponents.count == 3, pathComponents[0] == "mobile", pathComponents[1] == "oidc" {
return .oidcCallback(url: url)
}
return nil
}
}
/// The parser for Element Call links. This always returns a `.genericCallLink`
struct ElementCallURLParser: URLParser {
private let knownHosts = ["call.element.io"]
private let customSchemeHost = "call"
private let customSchemeURLQueryParameterName = "url"
func route(from url: URL) -> AppRoute? {
// First try processing URLs with custom schemes
if let scheme = url.scheme,
scheme == InfoPlistReader.app.appScheme {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
components.host == customSchemeHost else {
return nil
}
guard let encodedURLString = components.queryItems?.first(where: { $0.name == customSchemeURLQueryParameterName })?.value,
let callURL = URL(string: encodedURLString) else {
MXLog.error("Invalid custom scheme call parameters: \(url)")
return nil
}
return .genericCallLink(url: callURL)
}
// Otherwise try to interpret it as an universal link
guard let host = url.host, knownHosts.contains(host) else {
return nil
}
return .genericCallLink(url: url)
}
}