Tauri Plugin mobile-sharetarget
Description
This plugin is an alternative to tauri-plugin-sharetarget. It is command based rather than event based, making the management of incoming intents more reliable, especially when your webview isn't able to listen to events yet.
Showcase

Installation
Android
This plugin works by linking a Rust function to some native code (Kotlin).
Thus the plugin needs to load the lib generated by Tauri.
Your Tauri app lib name sits in the src-tauri/Cargo.toml file :
[]
...
= "tauri_app_lib"
...
Thus to use this plugin, you need to fork this repo. Here are the steps :
- In
src-tauri/gen/android/gradle.properties, add this linetauri_app_lib_name=YOUR_LIB_NAME, and replace the placeholder with your app's lib name as described above. - Add the packages to your app:
Cargo.toml
...
[]
= "2"
lib.rs
package.json
...
"tauri-plugin-mobile-sharetarget-api": "^2.0.0"
...
- Add the required intent filters. By default all kind of text is accepted, but you can tweak that to your needs.
AndroidManifest.xml
...
...
...
<!-- Support receiving share events. -->
<!-- You can scope any MIME type here. You'll see what Intent Android returns. -->
...
...
- Add the required permissions to your capabilities (don't forget to use it in your
tauri.conf.json).src-tauri/capabilities/mobile.json
iOS
This plugin doesn't properly relies on a Tauri swift plugin, but it uses the same Rust queue to keep the same behaviour between platforms. Instead it makes use of the official Tauri Deep-Link plugin to read the incoming URLs sent by a native Swift Tauri extension. Here are the installation steps :
- Add the required packages to your app:
Cargo.toml
...
[]
= "2"
= "2" # Note: you can also make this crate an ios only dependency
lib.rs
package.json
...
"tauri-plugin-mobile-sharetarget-api": "^2.0.0"
...
tauri.conf.json
...
"plugins": ,
...
- In Xcode, create an additionnal target for your iOS app, with the "Share Extension" template, name it (e.g. "ShareExtension") and activate the scheme when prompted.
- In the new generated "ShareExtension" folder replace the content of these files by the following
src-tauri/gen/apple/NAME_OF_YOUR_SHARE_EXTENSION/Info.plist
NSExtension
NSExtensionAttributes
NSExtensionActivationRule
NSExtensionActivationSupportsWebURLWithMaxCount
1
NSExtensionActivationSupportsWebPageWithMaxCount
1
NSExtensionActivationSupportsText
NSExtensionPointIdentifier
com.apple.share-services
NSExtensionPrincipalClass
$(PRODUCT_MODULE_NAME).ShareViewController
LSApplicationQueriesSchemes
REPLACE-BY-YOUR-SCHEME
src-tauri/gen/apple/NAME_OF_YOUR_SHARE_EXTENSION/ShareViewController.swift
import UIKit
import UniformTypeIdentifiers
import Foundation
class ShareViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Minimal UI: Transparent with spinner
self.view.backgroundColor = .clear
let spinner = UIActivityIndicatorView(style: .large)
spinner.center = self.view.center
spinner.startAnimating()
self.view.addSubview(spinner)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("🟢 Share Extension: View Did Appear")
// 1. Extract Data safely
extractSharedURL { [weak self] sharedURL in
guard let self = self else { return }
guard let url = sharedURL else {
print("🔴 Share Extension: No URL found in shared content.")
self.closeExtension()
return
}
// 2. Build Deeplink (WITH ENCODING)
// If the shared URL has special chars, it MUST be encoded or URL(string:) returns nil
let originalString = url.absoluteString
// Prepare the query item
// e.g., myapp://share?url=https%3A%2F%2Fgoogle.com
var components = URLComponents()
components.scheme = "REPLACE-BY-YOUR-APP-SCHEME"
components.host = "share"
components.queryItems = [
URLQueryItem(name: "url", value: originalString)
]
guard let deepLink = components.url else {
print("🔴 Share Extension: Could not construct deep link.")
self.closeExtension()
return
}
print("🟢 Share Extension: Attempting to open -> \(deepLink)")
// 3. Attempt to Open
let success = self.openURL(deepLink)
if success {
print("🟢 Share Extension: Open command sent successfully.")
// Give the system time to switch apps before killing this extension
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.closeExtension()
}
} else {
print("🔴 Share Extension: Trampoline failed. Responder not found.")
// Fallback: Show an alert so the user isn't left confusingly
self.showErrorAndClose()
}
}
}
// MARK: - Helper Methods
private func closeExtension() {
self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
private func showErrorAndClose() {
let alert = UIAlertController(title: "Error", message: "Could not open the main app.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
self.closeExtension()
}))
self.present(alert, animated: true)
}
// MARK: - Data Extraction
private func extractSharedURL(completion: @escaping (URL?) -> Void) {
// Safely unwrap extension items
guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
let attachments = extensionItem.attachments else {
completion(nil)
return
}
let urlType = UTType.url.identifier // "public.url"
let textTypes: [String] = {
if #available(iOS 16.0, *) {
return [UTType.text.identifier, UTType.plainText.identifier]
} else {
return [UTType.text.identifier]
}
}()
// Helper to extract the first URL from a string using a data detector
func urlFromString(_ string: String) -> URL? {
let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let range = NSRange(location: 0, length: (string as NSString).length)
let match = detector?.firstMatch(in: string, options: [], range: range)
if let match = match, match.resultType == .link, let foundURL = match.url {
return foundURL
}
// Fallback: direct initializer if the text is exactly a URL
return URL(string: string.trimmingCharacters(in: .whitespacesAndNewlines))
}
// 1) Prefer a real URL item provider
for provider in attachments {
if provider.hasItemConformingToTypeIdentifier(urlType) {
provider.loadItem(forTypeIdentifier: urlType, options: nil) { (item, error) in
DispatchQueue.main.async {
if let error = error { print("🔴 Load Error (URL): \(error.localizedDescription)") }
if let url = item as? URL {
completion(url)
} else if let url = item as? NSURL {
completion(url as URL)
} else if let string = item as? String, let url = urlFromString(string) {
completion(url)
} else {
completion(nil)
}
}
}
return
}
}
// 2) Fall back to text providers that may contain a URL
for provider in attachments {
for type in textTypes {
if provider.hasItemConformingToTypeIdentifier(type) {
provider.loadItem(forTypeIdentifier: type, options: nil) { (item, error) in
DispatchQueue.main.async {
if let error = error { print("🔴 Load Error (Text): \(error.localizedDescription)") }
if let string = item as? String, let url = urlFromString(string) {
completion(url)
} else if let data = item as? Data, let string = String(data: data, encoding: .utf8), let url = urlFromString(string) {
completion(url)
} else {
completion(nil)
}
}
}
return
}
}
}
// No suitable provider found
completion(nil)
}
// MARK: - The Trampoline (The Magic)
@discardableResult
@objc func openURL(_ url: URL) -> Bool {
var responder: UIResponder? = self
while responder != nil {
if let application = responder as? UIApplication {
application.open(url)
return true
}
responder = responder?.next
}
return false
}
}
- Define and setup an "App Group" capability for both your main app and Share Extension in Xcode
- Add this to the
src-tauri/Info.ios.plistfile of your app :
LSApplicationQueriesSchemes
REPLACE-BY-YOUR-SCHEME
- Add the required permissions to your capabilities (don't forget to use it in your
tauri.conf.json).src-tauri/capabilities/mobile.json
Usage
Intents are stored in a Rust queue. Each call to the queue pops the latest intent in the queue.
You can call popIntentQueue() to retrieve a raw intent in the FIFO queue.
You can also call popIntentQueueAndExtractText() that extracts the text payload of a textual share intent.
Svelte
Here's a Svelte 5 snippet that uses the plugin to consume the queue automatically when the app is launched or when it is focused. This is the equivalent to the event based approach but we can retry to consume the queue if we missed an event for some reason.
import { popIntentQueueAndExtractText } from 'tauri-plugin-mobile-sharetarget-api';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
import { platform } from '@tauri-apps/plugin-os';
let openDrawer = $state(false);
let sharedTargetUrl: string | undefined = $state();
const currentPlatform = platform();
let focusUnlistener: UnlistenFn;
onMount(async () => {
if (currentPlatform === 'android' || currentPlatform === 'ios') {
popIntentAndOpenDrawer();
// iOS tauri://focus isn't reliable for some reason, so we can use a custom event instead.
focusUnlistener = await listen(
currentPlatform === 'android' ? 'tauri://focus' : 'new-intent',
async () => {
await popIntentAndOpenDrawer();
}
);
}
});
const popIntentAndOpenDrawer = async () => {
let potentialIntent = await popIntentQueueAndExtractText();
if (potentialIntent) {
sharedTargetUrl = decodeURIComponent(potentialIntent);
// Or whatever state you need to update when a new intent is received.
openDrawer = true;
}
};
onDestroy(() => {
focusUnlistener();
});
Example
An example app is provided in this repo under examples/tauri-app.