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
11 changes: 11 additions & 0 deletions core/src/main/kotlin/dev/hotwire/core/turbo/visit/VisitProposal.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package dev.hotwire.core.turbo.visit

import android.os.Bundle
import dev.hotwire.core.turbo.config.PathConfigurationProperties

data class VisitProposal(
val location: String,
val options: VisitOptions,
val properties: PathConfigurationProperties,
val bundle: Bundle?
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import dev.hotwire.core.turbo.config.PathConfigurationProperties
import dev.hotwire.core.turbo.config.context
import dev.hotwire.core.turbo.nav.PresentationContext
import dev.hotwire.core.turbo.visit.VisitAction
import dev.hotwire.core.turbo.visit.VisitProposal
import dev.hotwire.navigation.fragments.HotwireFragmentDelegate
import dev.hotwire.navigation.fragments.HotwireFragmentViewModel
import dev.hotwire.navigation.navigator.Navigator
Expand Down Expand Up @@ -115,7 +116,7 @@ interface HotwireDestination : BridgeDestination {
* Return `null` to use the global [Router.RouteDecisionHandler] instances to determine
* routing logic.
*/
fun customRouteDecision(newLocation: String): Router.Decision? {
fun customRouteDecision(proposal: VisitProposal): Router.Decision? {
return null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import dev.hotwire.core.turbo.nav.PresentationContext
import dev.hotwire.core.turbo.session.Session
import dev.hotwire.core.turbo.visit.VisitAction
import dev.hotwire.core.turbo.visit.VisitOptions
import dev.hotwire.core.turbo.visit.VisitProposal
import dev.hotwire.navigation.activities.HotwireActivity
import dev.hotwire.navigation.config.HotwireNavigation
import dev.hotwire.navigation.destinations.HotwireDestination
Expand Down Expand Up @@ -138,7 +139,7 @@ class Navigator(
extras: FragmentNavigator.Extras? = null
) {

if (getRouteDecision(location) == Router.Decision.CANCEL) {
if (getRouteDecision(location, options, bundle) == Router.Decision.CANCEL) {
return
}

Expand Down Expand Up @@ -423,11 +424,22 @@ class Navigator(
return customNavigator?.navController ?: navController
}

private fun getRouteDecision(location: String): Router.Decision {
val customDecision = currentDestination?.customRouteDecision(location)
private fun getRouteDecision(
location: String,
options: VisitOptions,
bundle: Bundle?
): Router.Decision {
val proposal = VisitProposal(
location = location,
options = options,
properties = Hotwire.config.pathConfiguration.properties(location),
bundle = bundle
)

val customDecision = currentDestination?.customRouteDecision(proposal)

val decision = customDecision ?: HotwireNavigation.router.decideRoute(
location = location,
proposal = proposal,
configuration = configuration,
activity = activity
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.hotwire.navigation.routing

import androidx.core.net.toUri
import dev.hotwire.core.turbo.visit.VisitProposal
import dev.hotwire.navigation.activities.HotwireActivity
import dev.hotwire.navigation.navigator.NavigatorConfiguration

Expand All @@ -11,14 +12,14 @@ class AppNavigationRouteDecisionHandler : Router.RouteDecisionHandler {
override val name = "app-navigation"

override fun matches(
location: String,
proposal: VisitProposal,
configuration: NavigatorConfiguration
): Boolean {
return configuration.startLocation.toUri().host == location.toUri().host
return configuration.startLocation.toUri().host == proposal.location.toUri().host
}

override fun handle(
location: String,
proposal: VisitProposal,
configuration: NavigatorConfiguration,
activity: HotwireActivity
): Router.Decision {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri
import com.google.android.material.R
import dev.hotwire.core.turbo.visit.VisitProposal
import dev.hotwire.navigation.activities.HotwireActivity
import dev.hotwire.navigation.logging.logError
import dev.hotwire.navigation.navigator.NavigatorConfiguration
Expand All @@ -21,17 +22,17 @@ class BrowserTabRouteDecisionHandler : Router.RouteDecisionHandler {
override val name = "browser-tab"

override fun matches(
location: String,
proposal: VisitProposal,
configuration: NavigatorConfiguration
): Boolean {
val locationUri = location.toUri()
val locationUri = proposal.location.toUri()

return configuration.startLocation.toUri().host != locationUri.host &&
(locationUri.scheme?.lowercase() == "https" || locationUri.scheme?.lowercase() == "http")
}

override fun handle(
location: String,
proposal: VisitProposal,
configuration: NavigatorConfiguration,
activity: HotwireActivity
): Router.Decision {
Expand All @@ -48,7 +49,7 @@ class BrowserTabRouteDecisionHandler : Router.RouteDecisionHandler {
.setUrlBarHidingEnabled(false)
.setDefaultColorSchemeParams(colorParams)
.build()
.launchUrl(activity, location.toUri())
.launchUrl(activity, proposal.location.toUri())
} catch (e: ActivityNotFoundException) {
logError("BrowserTabRouteDecisionHandler", e)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.hotwire.navigation.routing

import dev.hotwire.core.turbo.visit.VisitProposal
import dev.hotwire.navigation.activities.HotwireActivity
import dev.hotwire.navigation.logging.logDebug
import dev.hotwire.navigation.navigator.NavigatorConfiguration
Expand All @@ -21,12 +22,12 @@ class Router(private val decisionHandlers: List<RouteDecisionHandler>) {
val name: String

/**
* 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 visit proposal matches this decision handler. Use
* your own custom rules based on the proposal's location domain, protocol,
* path, options, path configuration properties, or any other factors.
*/
fun matches(
location: String,
proposal: VisitProposal,
configuration: NavigatorConfiguration
): Boolean

Expand All @@ -36,7 +37,7 @@ class Router(private val decisionHandlers: List<RouteDecisionHandler>) {
* [Decision.CANCEL].
*/
fun handle(
location: String,
proposal: VisitProposal,
configuration: NavigatorConfiguration,
activity: HotwireActivity
): Decision
Expand All @@ -55,22 +56,22 @@ class Router(private val decisionHandlers: List<RouteDecisionHandler>) {
}

internal fun decideRoute(
location: String,
proposal: VisitProposal,
configuration: NavigatorConfiguration,
activity: HotwireActivity
): Decision {
decisionHandlers.forEach { handler ->
if (handler.matches(location, configuration)) {
if (handler.matches(proposal, configuration)) {
logDebug("handlerMatch", listOf(
"handler" to handler.name,
"location" to location
"proposal" to proposal
))

return handler.handle(location, configuration, activity)
return handler.handle(proposal, configuration, activity)
}
}

logDebug("noHandlerForLocation", location)
logDebug("noHandlerForProposal", proposal.toString())
return Decision.CANCEL
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dev.hotwire.navigation.routing
import android.content.ActivityNotFoundException
import android.content.Intent
import androidx.core.net.toUri
import dev.hotwire.core.turbo.visit.VisitProposal
import dev.hotwire.navigation.activities.HotwireActivity
import dev.hotwire.navigation.logging.logError
import dev.hotwire.navigation.navigator.NavigatorConfiguration
Expand All @@ -14,18 +15,18 @@ class SystemNavigationRouteDecisionHandler : Router.RouteDecisionHandler {
override val name = "system-navigation"

override fun matches(
location: String,
proposal: VisitProposal,
configuration: NavigatorConfiguration
): Boolean {
return configuration.startLocation.toUri().host != location.toUri().host
return configuration.startLocation.toUri().host != proposal.location.toUri().host
}

override fun handle(
location: String,
proposal: VisitProposal,
configuration: NavigatorConfiguration,
activity: HotwireActivity
): Router.Decision {
val intent = Intent(Intent.ACTION_VIEW, location.toUri())
val intent = Intent(Intent.ACTION_VIEW, proposal.location.toUri())

try {
activity.startActivity(intent)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package dev.hotwire.navigation.routing

import dev.hotwire.core.turbo.config.PathConfigurationProperties
import dev.hotwire.core.turbo.visit.VisitOptions
import dev.hotwire.core.turbo.visit.VisitProposal
import dev.hotwire.navigation.activities.HotwireActivity
import dev.hotwire.navigation.navigator.NavigatorConfiguration
import org.junit.Assert.assertEquals
Expand Down Expand Up @@ -29,28 +32,35 @@ class AppNavigationRouteDecisionHandlerTest {

@Test
fun `matching result navigates`() {
val decision = route.handle(config.startLocation, config, activity)
val decision = route.handle(proposal(config.startLocation), config, activity)
assertEquals(Router.Decision.NAVIGATE, decision)
}

@Test
fun `url on app domain matches`() {
val url = "https://my.app.com/page"
assertTrue(route.matches(url, config))
assertTrue(route.matches(proposal(url), config))
}

@Test
fun `url without subdomain does not match`() {
val url = "https://app.com/page"
assertFalse(route.matches(url, config))
assertFalse(route.matches(proposal(url), config))
}

@Test
fun `masqueraded url does not match`() {
val url = "https://app.my.com@fake.domain"
assertFalse(route.matches(url, config))
assertFalse(route.matches(proposal(url), config))
}

private fun proposal(location: String) = VisitProposal(
location = location,
options = VisitOptions(),
properties = PathConfigurationProperties(),
bundle = null
)

private class TestActivity : HotwireActivity() {
override fun navigatorConfigurations() = emptyList<NavigatorConfiguration>()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package dev.hotwire.navigation.routing

import dev.hotwire.core.turbo.config.PathConfigurationProperties
import dev.hotwire.core.turbo.visit.VisitOptions
import dev.hotwire.core.turbo.visit.VisitProposal
import dev.hotwire.navigation.activities.HotwireActivity
import dev.hotwire.navigation.navigator.NavigatorConfiguration
import org.junit.Assert.assertEquals
Expand Down Expand Up @@ -29,34 +32,41 @@ class BrowserTabRouteDecisionHandlerTest {

@Test
fun `matching result stops navigation`() {
val decision = route.handle("https://external.com/page", config, activity)
val decision = route.handle(proposal("https://external.com/page"), config, activity)
assertEquals(Router.Decision.CANCEL, decision)
}

@Test
fun `url on external domain matches`() {
val url = "https://external.com/page"
assertTrue(route.matches(url, config))
assertTrue(route.matches(proposal(url), config))
}

@Test
fun `url without subdomain matches`() {
val url = "https://app.com/page"
assertTrue(route.matches(url, config))
assertTrue(route.matches(proposal(url), config))
}

@Test
fun `url on app domain does not match`() {
val url = "https://my.app.com/page"
assertFalse(route.matches(url, config))
assertFalse(route.matches(proposal(url), config))
}

@Test
fun `non-http scheme does not match`() {
val url = "sms:555-555-5555"
assertFalse(route.matches(url, config))
assertFalse(route.matches(proposal(url), config))
}

private fun proposal(location: String) = VisitProposal(
location = location,
options = VisitOptions(),
properties = PathConfigurationProperties(),
bundle = null
)

private class TestActivity : HotwireActivity() {
override fun navigatorConfigurations() = emptyList<NavigatorConfiguration>()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package dev.hotwire.navigation.routing

import dev.hotwire.core.turbo.config.PathConfigurationProperties
import dev.hotwire.core.turbo.visit.VisitOptions
import dev.hotwire.core.turbo.visit.VisitProposal
import dev.hotwire.navigation.activities.HotwireActivity
import dev.hotwire.navigation.navigator.NavigatorConfiguration
import org.junit.Assert.assertEquals
Expand Down Expand Up @@ -29,34 +32,41 @@ class SystemNavigationRouteDecisionHandlerTest {

@Test
fun `matching result stops navigation`() {
val decision = route.handle("https://external.com/page", config, activity)
val decision = route.handle(proposal("https://external.com/page"), config, activity)
assertEquals(Router.Decision.CANCEL, decision)
}

@Test
fun `url on external domain matches`() {
val url = "https://external.com/page"
assertTrue(route.matches(url, config))
assertTrue(route.matches(proposal(url), config))
}

@Test
fun `url without subdomain matches`() {
val url = "https://app.com/page"
assertTrue(route.matches(url, config))
assertTrue(route.matches(proposal(url), config))
}

@Test
fun `url on app domain does not match`() {
val url = "https://my.app.com/page"
assertFalse(route.matches(url, config))
assertFalse(route.matches(proposal(url), config))
}

@Test
fun `non-http scheme matches`() {
val url = "sms:555-555-5555"
assertTrue(route.matches(url, config))
assertTrue(route.matches(proposal(url), config))
}

private fun proposal(location: String) = VisitProposal(
location = location,
options = VisitOptions(),
properties = PathConfigurationProperties(),
bundle = null
)

private class TestActivity : HotwireActivity() {
override fun navigatorConfigurations() = emptyList<NavigatorConfiguration>()
}
Expand Down
Loading