Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Source/Turbo/Navigator/Navigator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@ 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()
}

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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@ 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()
}

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
}
Expand Down
15 changes: 8 additions & 7 deletions Source/Turbo/Navigator/Routing/RouteDecisionHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
10 changes: 5 additions & 5 deletions Source/Turbo/Navigator/Routing/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
12 changes: 8 additions & 4 deletions Tests/Turbo/Routing/AppNavigationRouteDecisionHandlerTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
21 changes: 12 additions & 9 deletions Tests/Turbo/Routing/RouterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand All @@ -18,7 +21,7 @@ final class RouterTests: XCTestCase {
router = Router(decisionHandlers: [])

let result = router.decideRoute(
for: url,
for: proposal,
configuration: navigatorConfiguration,
navigator: navigator
)
Expand All @@ -38,7 +41,7 @@ final class RouterTests: XCTestCase {
)

let result = router.decideRoute(
for: url,
for: proposal,
configuration: navigatorConfiguration,
navigator: navigator
)
Expand All @@ -64,7 +67,7 @@ final class RouterTests: XCTestCase {
)

let result = router.decideRoute(
for: url,
for: proposal,
configuration: navigatorConfiguration,
navigator: navigator
)
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
16 changes: 10 additions & 6 deletions Tests/Turbo/Routing/SystemNavigationRouteDecisionHandlerTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading