diff --git a/Source/Turbo/Navigator/Navigator.swift b/Source/Turbo/Navigator/Navigator.swift index 084f86a4..dcc1d910 100644 --- a/Source/Turbo/Navigator/Navigator.swift +++ b/Source/Turbo/Navigator/Navigator.swift @@ -74,7 +74,7 @@ public class Navigator { /// /// - Parameter proposal: the proposal to visit public func route(_ proposal: VisitProposal) { - if routeDecision(for: proposal.url) == .cancel { + if routeDecision(for: proposal) == .cancel { return } @@ -161,9 +161,9 @@ public class Navigator { } } - private func routeDecision(for location: URL) -> Router.Decision { + private func routeDecision(for proposal: VisitProposal) -> Router.Decision { return Hotwire.config.router.decideRoute( - for: location, + for: proposal, configuration: configuration, navigator: self ) diff --git a/Source/Turbo/Navigator/Routing/Handlers/AppNavigationRouteDecisionHandler.swift b/Source/Turbo/Navigator/Routing/Handlers/AppNavigationRouteDecisionHandler.swift index 494ad45d..859d17ac 100644 --- a/Source/Turbo/Navigator/Routing/Handlers/AppNavigationRouteDecisionHandler.swift +++ b/Source/Turbo/Navigator/Routing/Handlers/AppNavigationRouteDecisionHandler.swift @@ -5,8 +5,9 @@ public final class AppNavigationRouteDecisionHandler: RouteDecisionHandler { public init() {} - public func matches(location: URL, + public func matches(proposal: VisitProposal, configuration: Navigator.Configuration) -> Bool { + let location = proposal.url if #available(iOS 16, *) { return configuration.startLocation.host() == location.host() } @@ -14,10 +15,10 @@ public final class AppNavigationRouteDecisionHandler: RouteDecisionHandler { return configuration.startLocation.host == location.host } - public func handle(location: URL, + public func handle(proposal: VisitProposal, configuration: Navigator.Configuration, navigator: Navigating) -> Router.Decision { - logger.info("Routing \(location.absoluteString)") + logger.info("Routing \(proposal.url.absoluteString)") return .navigate } } diff --git a/Source/Turbo/Navigator/Routing/Handlers/SafariViewControllerRouteDecisionHandler.swift b/Source/Turbo/Navigator/Routing/Handlers/SafariViewControllerRouteDecisionHandler.swift index e09459f2..62362d40 100644 --- a/Source/Turbo/Navigator/Routing/Handlers/SafariViewControllerRouteDecisionHandler.swift +++ b/Source/Turbo/Navigator/Routing/Handlers/SafariViewControllerRouteDecisionHandler.swift @@ -7,8 +7,9 @@ public final class SafariViewControllerRouteDecisionHandler: RouteDecisionHandle public init() {} - public func matches(location: URL, + public func matches(proposal: VisitProposal, configuration: Navigator.Configuration) -> Bool { + let location = proposal.url /// SFSafariViewController will crash if we pass along a URL that's not valid. guard location.scheme == "http" || location.scheme == "https" else { return false @@ -21,11 +22,11 @@ public final class SafariViewControllerRouteDecisionHandler: RouteDecisionHandle return configuration.startLocation.host != location.host } - public func handle(location: URL, + public func handle(proposal: VisitProposal, configuration: Navigator.Configuration, navigator: Navigating) -> HotwireNative.Router.Decision { Task { @MainActor in - await open(externalURL: location, + await open(externalURL: proposal.url, viewController: navigator.activeNavigationController) } diff --git a/Source/Turbo/Navigator/Routing/Handlers/SystemNavigationRouteDecisionHandler.swift b/Source/Turbo/Navigator/Routing/Handlers/SystemNavigationRouteDecisionHandler.swift index 7cc5b353..162b403f 100644 --- a/Source/Turbo/Navigator/Routing/Handlers/SystemNavigationRouteDecisionHandler.swift +++ b/Source/Turbo/Navigator/Routing/Handlers/SystemNavigationRouteDecisionHandler.swift @@ -7,8 +7,9 @@ public final class SystemNavigationRouteDecisionHandler: RouteDecisionHandler { public init() {} - public func matches(location: URL, + public func matches(proposal: VisitProposal, configuration: Navigator.Configuration) -> Bool { + let location = proposal.url if #available(iOS 16, *) { return configuration.startLocation.host() != location.host() } @@ -16,10 +17,10 @@ public final class SystemNavigationRouteDecisionHandler: RouteDecisionHandler { return configuration.startLocation.host != location.host } - public func handle(location: URL, + public func handle(proposal: VisitProposal, configuration: Navigator.Configuration, navigator: Navigating) -> Router.Decision { - UIApplication.shared.open(location) + UIApplication.shared.open(proposal.url) return .cancel } diff --git a/Source/Turbo/Navigator/Routing/RouteDecisionHandler.swift b/Source/Turbo/Navigator/Routing/RouteDecisionHandler.swift index 877eb031..967303a8 100644 --- a/Source/Turbo/Navigator/Routing/RouteDecisionHandler.swift +++ b/Source/Turbo/Navigator/Routing/RouteDecisionHandler.swift @@ -7,22 +7,23 @@ public protocol RouteDecisionHandler { /// The decision handler name used in debug logging. var name: String { get } - /// Determines whether the location matches this decision handler. - /// Use your own custom rules based on the location's domain, protocol, path, or any other factors. + /// Determines whether the proposed visit matches this decision handler. + /// Use your own custom rules based on the visit's location domain, protocol, path, + /// options, path properties, or any other factors. /// - Parameters: - /// - location: The location URL. + /// - proposal: The proposed visit, including its location, options, and path properties. /// - configuration: The configuration of the navigator where the navigation is taking place. - /// - Returns: `true` if location matches this decision handler, `false` otherwise. - func matches(location: URL, + /// - Returns: `true` if the proposed visit matches this decision handler, `false` otherwise. + func matches(proposal: VisitProposal, configuration: Navigator.Configuration) -> Bool /// Handle custom routing behavior when a match is found. /// For example, open an external browser or app for external domain urls. /// - Parameters: - /// - location: The location URL. + /// - proposal: The proposed visit, including its location, options, and path properties. /// - configuration: The configuration of the navigator where the navigation is taking place. /// - navigator: The navigator instance responsible for the navigation. - func handle(location: URL, + func handle(proposal: VisitProposal, configuration: Navigator.Configuration, navigator: Navigating) -> Router.Decision } diff --git a/Source/Turbo/Navigator/Routing/Router.swift b/Source/Turbo/Navigator/Routing/Router.swift index c8290aca..a9ecca9a 100644 --- a/Source/Turbo/Navigator/Routing/Router.swift +++ b/Source/Turbo/Navigator/Routing/Router.swift @@ -9,19 +9,19 @@ public final class Router { self.decisionHandlers = decisionHandlers } - func decideRoute(for location: URL, + func decideRoute(for proposal: VisitProposal, configuration: Navigator.Configuration, navigator: Navigating) -> Router.Decision { for handler in decisionHandlers { - if handler.matches(location: location, configuration: configuration) { - logger.debug("[Router] handler match found handler: \(handler.name) location: \(location)") - return handler.handle(location: location, + if handler.matches(proposal: proposal, configuration: configuration) { + logger.debug("[Router] handler match found handler: \(handler.name) proposal: \(proposal)") + return handler.handle(proposal: proposal, configuration: configuration, navigator: navigator) } } - logger.warning("[Router] no handler for location: \(location)") + logger.warning("[Router] no handler for proposal: \(proposal)") return .cancel } } diff --git a/Tests/Turbo/Routing/AppNavigationRouteDecisionHandlerTest.swift b/Tests/Turbo/Routing/AppNavigationRouteDecisionHandlerTest.swift index 4c228e32..ae11b115 100644 --- a/Tests/Turbo/Routing/AppNavigationRouteDecisionHandlerTest.swift +++ b/Tests/Turbo/Routing/AppNavigationRouteDecisionHandlerTest.swift @@ -14,29 +14,33 @@ final class AppNavigationRouteDecisionHandlerTest: XCTestCase { navigator = NavigationSpy() } + private func proposal(for url: URL) -> VisitProposal { + VisitProposal(url: url, options: VisitOptions()) + } + func test_handling_matching_result_navigates() { let url = URL(string: "https://my.app.com/page")! - let result = route.handle(location: url, configuration: navigatorConfiguration, navigator: navigator) + let result = route.handle(proposal: proposal(for: url), configuration: navigatorConfiguration, navigator: navigator) XCTAssertEqual(result, Router.Decision.navigate) } func test_url_on_app_domain_matches() { let url = URL(string: "https://my.app.com/page")! - let result = route.matches(location: url, configuration: navigatorConfiguration) + let result = route.matches(proposal: proposal(for: url), configuration: navigatorConfiguration) XCTAssertTrue(result) } func test_url_without_subdomain_does_not_match() { let url = URL(string: "https://app.com/page")! - let result = route.matches(location: url, configuration: navigatorConfiguration) + let result = route.matches(proposal: proposal(for: url), configuration: navigatorConfiguration) XCTAssertFalse(result) } func test_masqueraded_url_does_not_match() { let url = URL(string: "https://app.my.com@fake.domain")! - let result = route.matches(location: url, configuration: navigatorConfiguration) + let result = route.matches(proposal: proposal(for: url), configuration: navigatorConfiguration) XCTAssertFalse(result) } diff --git a/Tests/Turbo/Routing/RouterTests.swift b/Tests/Turbo/Routing/RouterTests.swift index 41ccebe7..57c17ef5 100644 --- a/Tests/Turbo/Routing/RouterTests.swift +++ b/Tests/Turbo/Routing/RouterTests.swift @@ -6,7 +6,10 @@ final class RouterTests: XCTestCase { name: "test", startLocation: URL(string: "https://my.app.com")! ) - let url = URL(string: "https://my.app.com/page")! + let proposal = VisitProposal( + url: URL(string: "https://my.app.com/page")!, + options: VisitOptions() + ) var router: Router! var navigator: NavigationSpy! @@ -18,7 +21,7 @@ final class RouterTests: XCTestCase { router = Router(decisionHandlers: []) let result = router.decideRoute( - for: url, + for: proposal, configuration: navigatorConfiguration, navigator: navigator ) @@ -38,7 +41,7 @@ final class RouterTests: XCTestCase { ) let result = router.decideRoute( - for: url, + for: proposal, configuration: navigatorConfiguration, navigator: navigator ) @@ -64,7 +67,7 @@ final class RouterTests: XCTestCase { ) let result = router.decideRoute( - for: url, + for: proposal, configuration: navigatorConfiguration, navigator: navigator ) @@ -84,12 +87,12 @@ final class NoMatchRouteDecisionHandlerSpy: RouteDecisionHandler { var matchesWasCalled = false var handleWasCalled = false - func matches(location: URL, configuration: HotwireNative.Navigator.Configuration) -> Bool { + func matches(proposal: VisitProposal, configuration: HotwireNative.Navigator.Configuration) -> Bool { matchesWasCalled = true return false } - - func handle(location: URL, configuration: HotwireNative.Navigator.Configuration, navigator: HotwireNative.Navigating) -> HotwireNative.Router.Decision { + + func handle(proposal: VisitProposal, configuration: HotwireNative.Navigator.Configuration, navigator: HotwireNative.Navigating) -> HotwireNative.Router.Decision { handleWasCalled = true return .cancel } @@ -100,12 +103,12 @@ final class MatchRouteDecisionHandlerSpy: RouteDecisionHandler { var matchesWasCalled = false var handleWasCalled = false - func matches(location: URL, configuration: HotwireNative.Navigator.Configuration) -> Bool { + func matches(proposal: VisitProposal, configuration: HotwireNative.Navigator.Configuration) -> Bool { matchesWasCalled = true return true } - func handle(location: URL, configuration: HotwireNative.Navigator.Configuration, navigator: HotwireNative.Navigating) -> HotwireNative.Router.Decision { + func handle(proposal: VisitProposal, configuration: HotwireNative.Navigator.Configuration, navigator: HotwireNative.Navigating) -> HotwireNative.Router.Decision { handleWasCalled = true return .navigate } diff --git a/Tests/Turbo/Routing/SafariViewControllerRouteDecisionHandlerTests.swift b/Tests/Turbo/Routing/SafariViewControllerRouteDecisionHandlerTests.swift index fda403ab..b359d6d6 100644 --- a/Tests/Turbo/Routing/SafariViewControllerRouteDecisionHandlerTests.swift +++ b/Tests/Turbo/Routing/SafariViewControllerRouteDecisionHandlerTests.swift @@ -14,36 +14,40 @@ final class SafariViewControllerRouteDecisionHandlerTests: XCTestCase { navigator = NavigationSpy() } + private func proposal(for url: URL) -> VisitProposal { + VisitProposal(url: url, options: VisitOptions()) + } + func test_handling_matching_result_stops_navigation() { let url = URL(string: "https://external.com/page")! - let result = route.handle(location: url, configuration: navigatorConfiguration, navigator: navigator) + let result = route.handle(proposal: proposal(for: url), configuration: navigatorConfiguration, navigator: navigator) XCTAssertEqual(result, Router.Decision.cancel) } func test_url_on_external_domain_matches() { let url = URL(string: "https://external.com/page")! - let result = route.matches(location: url, configuration: navigatorConfiguration) + let result = route.matches(proposal: proposal(for: url), configuration: navigatorConfiguration) XCTAssertTrue(result) } func test_url_without_subdomain_matches() { let url = URL(string: "https://app.com/page")! - let result = route.matches(location: url, configuration: navigatorConfiguration) + let result = route.matches(proposal: proposal(for: url), configuration: navigatorConfiguration) XCTAssertTrue(result) } func test_url_on_app_domain_does_not_match() { let url = URL(string: "https://my.app.com/page")! - let result = route.matches(location: url, configuration: navigatorConfiguration) + let result = route.matches(proposal: proposal(for: url), configuration: navigatorConfiguration) XCTAssertFalse(result) } func test_non_http_urls_do_not_match() { let url = URL(string: "file:///path/to/file")! - let result = route.matches(location: url, configuration: navigatorConfiguration) + let result = route.matches(proposal: proposal(for: url), configuration: navigatorConfiguration) XCTAssertFalse(result) } diff --git a/Tests/Turbo/Routing/SystemNavigationRouteDecisionHandlerTest.swift b/Tests/Turbo/Routing/SystemNavigationRouteDecisionHandlerTest.swift index ee87a33d..8965a340 100644 --- a/Tests/Turbo/Routing/SystemNavigationRouteDecisionHandlerTest.swift +++ b/Tests/Turbo/Routing/SystemNavigationRouteDecisionHandlerTest.swift @@ -14,43 +14,47 @@ final class SystemNavigationRouteDecisionHandlerTests: XCTestCase { navigator = NavigationSpy() } + private func proposal(for url: URL) -> VisitProposal { + VisitProposal(url: url, options: VisitOptions()) + } + func test_handling_matching_result_stops_navigation() { let url = URL(string: "https://external.com/page")! - let result = route.handle(location: url, configuration: navigatorConfiguration, navigator: navigator) + let result = route.handle(proposal: proposal(for: url), configuration: navigatorConfiguration, navigator: navigator) XCTAssertEqual(result, Router.Decision.cancel) } func test_url_on_external_domain_matches() { let url = URL(string: "https://external.com/page")! - let result = route.matches(location: url, configuration: navigatorConfiguration) + let result = route.matches(proposal: proposal(for: url), configuration: navigatorConfiguration) XCTAssertTrue(result) } func test_url_without_subdomain_matches() { let url = URL(string: "https://app.com/page")! - let result = route.matches(location: url, configuration: navigatorConfiguration) + let result = route.matches(proposal: proposal(for: url), configuration: navigatorConfiguration) XCTAssertTrue(result) } func test_url_on_app_domain_does_not_match() { let url = URL(string: "https://my.app.com/page")! - let result = route.matches(location: url, configuration: navigatorConfiguration) + let result = route.matches(proposal: proposal(for: url), configuration: navigatorConfiguration) XCTAssertFalse(result) } func test_non_http_urls_match() { let url = URL(string: "file:///path/to/file")! - let result = route.matches(location: url, configuration: navigatorConfiguration) + let result = route.matches(proposal: proposal(for: url), configuration: navigatorConfiguration) XCTAssertTrue(result) } func test_sms_urls_match() { let url = URL(string: "sms:1-408-555-1212")! - let result = route.matches(location: url, configuration: navigatorConfiguration) + let result = route.matches(proposal: proposal(for: url), configuration: navigatorConfiguration) XCTAssertTrue(result) }