mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-09-26 21:41:48 +02:00
feat(deep-link): validate new intent URLs against configured deep links (#3186)
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"deep-link": patch
|
||||||
|
"deep-link-js": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Validate Android new intent is actually a deep link before triggering the onOpenUrl event.
|
||||||
@@ -6,9 +6,8 @@ package app.tauri.deep_link
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.PatternMatcher
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import app.tauri.Logger
|
|
||||||
import app.tauri.annotation.InvokeArg
|
import app.tauri.annotation.InvokeArg
|
||||||
import app.tauri.annotation.Command
|
import app.tauri.annotation.Command
|
||||||
import app.tauri.annotation.TauriPlugin
|
import app.tauri.annotation.TauriPlugin
|
||||||
@@ -16,18 +15,35 @@ import app.tauri.plugin.Channel
|
|||||||
import app.tauri.plugin.JSObject
|
import app.tauri.plugin.JSObject
|
||||||
import app.tauri.plugin.Plugin
|
import app.tauri.plugin.Plugin
|
||||||
import app.tauri.plugin.Invoke
|
import app.tauri.plugin.Invoke
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
@InvokeArg
|
@InvokeArg
|
||||||
class SetEventHandlerArgs {
|
class SetEventHandlerArgs {
|
||||||
lateinit var handler: Channel
|
lateinit var handler: Channel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InvokeArg
|
||||||
|
class AssociatedDomain {
|
||||||
|
var scheme: List<String> = listOf("https", "http")
|
||||||
|
var host: String? = null
|
||||||
|
var path: List<String> = listOf()
|
||||||
|
var pathPattern: List<String> = listOf()
|
||||||
|
var pathPrefix: List<String> = listOf()
|
||||||
|
var pathSuffix: List<String> = listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
@InvokeArg
|
||||||
|
class PluginConfig {
|
||||||
|
var mobile: List<AssociatedDomain> = listOf()
|
||||||
|
}
|
||||||
|
|
||||||
@TauriPlugin
|
@TauriPlugin
|
||||||
class DeepLinkPlugin(private val activity: Activity): Plugin(activity) {
|
class DeepLinkPlugin(private val activity: Activity): Plugin(activity) {
|
||||||
//private val implementation = Example()
|
//private val implementation = Example()
|
||||||
private var webView: WebView? = null
|
private var webView: WebView? = null
|
||||||
private var currentUrl: String? = null
|
private var currentUrl: String? = null
|
||||||
private var channel: Channel? = null
|
private var channel: Channel? = null
|
||||||
|
private var config: PluginConfig? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var instance: DeepLinkPlugin? = null
|
var instance: DeepLinkPlugin? = null
|
||||||
@@ -51,27 +67,105 @@ class DeepLinkPlugin(private val activity: Activity): Plugin(activity) {
|
|||||||
|
|
||||||
override fun load(webView: WebView) {
|
override fun load(webView: WebView) {
|
||||||
instance = this
|
instance = this
|
||||||
|
config = getConfig(PluginConfig::class.java)
|
||||||
val intent = activity.intent
|
|
||||||
|
|
||||||
if (intent.action == Intent.ACTION_VIEW) {
|
|
||||||
// TODO: check if it makes sense to split up init url and last url
|
|
||||||
this.currentUrl = intent.data.toString()
|
|
||||||
val event = JSObject()
|
|
||||||
event.put("url", this.currentUrl)
|
|
||||||
this.channel?.send(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.load(webView)
|
super.load(webView)
|
||||||
this.webView = webView
|
this.webView = webView
|
||||||
|
|
||||||
|
val intent = activity.intent
|
||||||
|
|
||||||
|
if (intent.action == Intent.ACTION_VIEW && intent.data != null) {
|
||||||
|
val url = intent.data.toString()
|
||||||
|
if (isDeepLink(url)) {
|
||||||
|
// TODO: check if it makes sense to split up init url and last url
|
||||||
|
this.currentUrl = url
|
||||||
|
val event = JSObject()
|
||||||
|
event.put("url", this.currentUrl)
|
||||||
|
this.channel?.send(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
if (intent.action == Intent.ACTION_VIEW) {
|
if (intent.action == Intent.ACTION_VIEW && intent.data != null) {
|
||||||
this.currentUrl = intent.data.toString()
|
val url = intent.data.toString()
|
||||||
val event = JSObject()
|
if (isDeepLink(url)) {
|
||||||
event.put("url", this.currentUrl)
|
this.currentUrl = url
|
||||||
this.channel?.send(event)
|
val event = JSObject()
|
||||||
|
event.put("url", this.currentUrl)
|
||||||
|
this.channel?.send(event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isDeepLink(url: String): Boolean {
|
||||||
|
val config = this.config ?: return false
|
||||||
|
|
||||||
|
if (config.mobile.isEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = try {
|
||||||
|
url.toUri()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// not a URL
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val scheme = uri.scheme ?: return false
|
||||||
|
val host = uri.host
|
||||||
|
val path = uri.path ?: ""
|
||||||
|
|
||||||
|
// Check if URL matches any configured mobile deep link
|
||||||
|
for (domain in config.mobile) {
|
||||||
|
// Check scheme
|
||||||
|
if (!domain.scheme.any { it.equals(scheme, ignoreCase = true) }) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check host (if configured)
|
||||||
|
if (domain.host != null) {
|
||||||
|
if (!host.equals(domain.host, ignoreCase = true)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check path constraints
|
||||||
|
// According to Android docs:
|
||||||
|
// - path: exact match, must begin with /
|
||||||
|
// - pathPrefix: matches initial part of path
|
||||||
|
// - pathSuffix: matches ending part, doesn't need to begin with /
|
||||||
|
// - pathPattern: simple glob pattern (., *, .*)
|
||||||
|
val pathMatches = when {
|
||||||
|
// Exact path match (must begin with /)
|
||||||
|
domain.path.isNotEmpty() && domain.path.any { it == path } -> true
|
||||||
|
// Path pattern match (simple glob: ., *, .*)
|
||||||
|
domain.pathPattern.isNotEmpty() && domain.pathPattern.any { pattern ->
|
||||||
|
try {
|
||||||
|
PatternMatcher(pattern, PatternMatcher.PATTERN_SIMPLE_GLOB).match(path)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} -> true
|
||||||
|
// Path prefix match
|
||||||
|
domain.pathPrefix.isNotEmpty() && domain.pathPrefix.any { prefix ->
|
||||||
|
path.startsWith(prefix)
|
||||||
|
} -> true
|
||||||
|
// Path suffix match
|
||||||
|
domain.pathSuffix.isNotEmpty() && domain.pathSuffix.any { suffix ->
|
||||||
|
path.endsWith(suffix)
|
||||||
|
} -> true
|
||||||
|
// If no path constraints, any path is allowed
|
||||||
|
domain.path.isEmpty() && domain.pathPattern.isEmpty() &&
|
||||||
|
domain.pathPrefix.isEmpty() && domain.pathSuffix.isEmpty() -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathMatches) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user