Skip to content

@capawesome-team/capacitor-share-target

Capacitor plugin to receive content such as text, links, and files from other apps.

Features

We are proud to offer one of the most complete and feature-rich Capacitor plugins for receiving content from other apps. Here are some of the key features:

  • 🖥️ Cross-platform: Supports Android, iOS and Web.
  • 📝 Multi-content types: Handle text, URLs, images, videos, and files.
  • 🌐 Web Share Target API: Leverage the native sharing capabilities of the web.
  • 📦 Large File Support: Efficient file caching without memory limitations.
  • 📦 SPM: Supports Swift Package Manager for iOS.
  • 🔁 Up-to-date: Always supports the latest Capacitor version.
  • ⭐️ Support: Priority support from the Capawesome Team.

Missing a feature? Just open an issue and we'll take a look!

Compatibility

Plugin Version Capacitor Version Status
7.x.x >=7.x.x Active support

Demo

A working example can be found here.

Android iOS Web
Android Demo iOS Demo Web Demo

Installation

This plugin is only available to Capawesome Insiders. First, make sure you have the Capawesome npm registry set up. You can do this by running the following commands:

npm config set @capawesome-team:registry https://npm.registry.capawesome.io
npm config set //npm.registry.capawesome.io/:_authToken <YOUR_LICENSE_KEY>

Attention: Replace <YOUR_LICENSE_KEY> with the license key you received from Polar. If you don't have a license key yet, you can get one by becoming a Capawesome Insider.

Next, install the package:

npm install @capawesome-team/capacitor-share-target
npx cap sync

Android

Intent Filters

To enable your app to receive shared content from other apps, you need to add intent filters to your main activity in AndroidManifest.xml (usually android/app/src/main/AndroidManifest.xml).

For example, to handle text, URLs, and other text-based content, add the following intent filter:

<intent-filter>
    <action android:name="android.intent.action.SEND" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="text/plain" />
</intent-filter>

To handle a single image, add the following intent filter:

<intent-filter>
    <action android:name="android.intent.action.SEND" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="image/*" />
</intent-filter>

To handle multiple images, add the following intent filter:

<intent-filter>
    <action android:name="android.intent.action.SEND_MULTIPLE" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="image/*" />
</intent-filter>

You can also add additional intent filters for other MIME types as needed, such as application/pdf for PDF files or video/* for videos.

Attention: Make sure to add these intent filters inside the <activity> tag of your main activity, typically MainActivity. Also, make sure the activity has the android:exported="true" attribute set, which allows other apps to send intents to it. The android:launchMode="singleTask" attribute is recommended to prevent multiple instances of your app from being created when receiving shared content.

Here's an example of how your AndroidManifest.xml might look like:

<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTask">
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="text/plain" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="image/*" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="video/*" />
    </intent-filter>
</activity>

Proguard

If you are using Proguard, you need to add the following rules to your proguard-rules.pro file:

-keep class io.capawesome.capacitorjs.plugins.** { *; }

iOS

On iOS, it's not possible to receive shared content directly in the main app. Instead, you need to create a share extension that can handle the shared content and then communicate it back to your main app. This involves setting up a URL scheme and configuring the share extension to handle the shared content. The communication between the share extension and the main app is done using URL schemes, which allows the share extension to open the main app with the shared content as parameters in the URL.

URL Scheme

To enable your app to be opened by the share extension, you need to set up a URL scheme in your iOS app. This is done by adding a URL type in your app's Info.plist file. Add the following code to your Info.plist file:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>example</string>
        </array>
    </dict>
</array>

Attention: Replace example with your desired URL scheme. This will allow your app to be opened with URLs like example://?text=Hello%20World.

Share Extension

To enable your app to receive shared content from other apps on iOS, you need to create a share extension. First, you need to add a new target to your Xcode project:

  1. Open your Xcode project.
  2. Go to File > New > Target....
  3. Select Share Extension from the list of templates.
  4. Name your extension AppShare and click Finish.

This will create a new target in your Xcode project with the necessary files for a share extension. There is one file called MainInterface.storyboard that you can delete, as we will not need it.

After this, you need to configure the share extension to handle the shared content. Open the Info.plist file of your share extension and replace its content with the following:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>NSExtension</key>
    <dict>
        <key>NSExtensionAttributes</key>
        <dict>
            <key>NSExtensionActivationRule</key>
            <dict>
                <key>NSExtensionActivationDictionaryVersion</key>
                <integer>2</integer>
                <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
                <integer>1</integer>
                <key>NSExtensionActivationSupportsText</key>
                <true/>
                <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
                <integer>10</integer>
                <key>NSExtensionActivationSupportsFileWithMaxCount</key>
                <integer>10</integer>
                <key>NSExtensionActivationSupportsImageWithMaxCount</key>
                <integer>10</integer>
                <key>NSExtensionActivationSupportsMovieWithMaxCount</key>
                <integer>10</integer>
                <key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
                <integer>1</integer>
            </dict>
        </dict>
        <key>NSExtensionPrincipalClass</key>
        <string>AppShare.ShareViewController</string>
        <key>NSExtensionPointIdentifier</key>
        <string>com.apple.share-services</string>
    </dict>
</dict>
</plist>

Feel free to adjust the NSExtensionActivationRule keys to match the types of content you want to support. The example above supports text, web URLs, files, images, and movies.

Next, you need to create a new Swift file in your share extension target. Name it ShareViewController.swift and replace its content with the following code:

import MobileCoreServices
import Social
import UIKit
import UniformTypeIdentifiers

class ShareViewController: UIViewController {

    private let appGroupIdentifier = "PLACEHOLDER_APP_GROUP_IDENTIFIER"
    private let urlScheme = "PLACEHOLDER_URL_SCHEME"

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        Task {
            processSharedContent()
        }
    }

    private func openURL(_ url: URL) {
        var responder: UIResponder? = self
        while responder != nil {
            if let application = responder as? UIApplication {
                application.open(url, options: [:], completionHandler: nil)
                return
            }
            responder = responder?.next
        }
    }

    private func copyFileToSharedContainer(_ url: URL) -> String? {
        guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
            return nil
        }

        let fileName = url.lastPathComponent
        let destinationURL = containerURL.appendingPathComponent(fileName)

        do {
            // Remove file if it already exists
            if FileManager.default.fileExists(atPath: destinationURL.path) {
                try FileManager.default.removeItem(at: destinationURL)
            }

            // Copy file to shared container
            try FileManager.default.copyItem(at: url, to: destinationURL)
            return destinationURL.absoluteString
        } catch {
            print("Error copying file to shared container: \(error)")
            return nil
        }
    }

    private func sendData(with textValues: [String], fileValues: [String], title: String) {
        var urlComps = URLComponents(string: "\(urlScheme)://?")!
        var queryItems: [URLQueryItem] = []

        if !title.isEmpty {
            queryItems.append(URLQueryItem(name: "title", value: title))
        }

        for text in textValues {
            if !text.isEmpty {
                queryItems.append(URLQueryItem(name: "text", value: text))
            }
        }

        for file in fileValues {
            if !file.isEmpty {
                queryItems.append(URLQueryItem(name: "file", value: file))
            }
        }

        urlComps.queryItems = queryItems.isEmpty ? nil : queryItems
        openURL(urlComps.url!)
    }

    private func processSharedContent() {
        guard let extensionContext = extensionContext else {
            self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
            return
        }

        guard let item = extensionContext.inputItems.first as? NSExtensionItem else {
            self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
            return
        }

        guard let attachments = item.attachments else {
            self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
            return
        }

        var textValues: [String] = []
        var fileValues: [String] = []
        let title = item.attributedTitle?.string ?? item.attributedContentText?.string ?? ""
        let dispatchGroup = DispatchGroup()

        for attachment in attachments {
            // Handle images
            if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
                dispatchGroup.enter()
                attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (item, _) in
                    defer { dispatchGroup.leave() }

                    if let url = item as? URL {
                        if let sharedPath = self?.copyFileToSharedContainer(url) {
                            fileValues.append(sharedPath)
                        }
                    } else if let image = item as? UIImage {
                        if let data = image.pngData() {
                            let base64String = data.base64EncodedString()
                            fileValues.append("data:image/png;base64,\(base64String)")
                        }
                    }
                }
            }
            // Handle movies
            if attachment.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
                dispatchGroup.enter()
                attachment.loadItem(forTypeIdentifier: UTType.movie.identifier, options: nil) { [weak self] (item, _) in
                    defer { dispatchGroup.leave() }

                    if let url = item as? URL {
                        if let sharedPath = self?.copyFileToSharedContainer(url) {
                            fileValues.append(sharedPath)
                        }
                    }
                }
            }
            // Handle plain text content
            if attachment.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
                dispatchGroup.enter()
                attachment.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { (item, _) in
                    if let text = item as? String {
                        textValues.append(text)
                    }
                    dispatchGroup.leave()
                }
            }
            // Handle URL content
            if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
                dispatchGroup.enter()
                attachment.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { (item, _) in
                    if let url = item as? URL {
                        textValues.append(url.absoluteString)
                    }
                    dispatchGroup.leave()
                }
            }
        }

        dispatchGroup.notify(queue: .main) { [weak self] in
            self?.sendData(with: textValues, fileValues: fileValues, title: title)
        }
    }
}

Attention: Replace <YOUR_URL_SCHEME> with the URL scheme you defined in your main app's Info.plist file.

Finally, you need to modify the AppDelegate.swift file of your main app target to handle the URLs opened by the share extension. Add the missing import and the following code to the application(_:open:options:) method:

+ import CapawesomeTeamCapacitorShareTarget

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
+    // Handle share target URLs
+    let _ = ShareTargetPlugin.handleOpenUrl(url)
+
    // Called when the app was launched with a url. Feel free to add additional processing here,
    // but if you want the App API to support tracking app url opens, make sure to keep this call
    return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}

Capabilities

If you want to receive files (e.g., images, videos) from other apps, you need to enable the "App Groups" capability for both your main app and the share extension. This allows both targets to share files in a common container. To do this, follow these steps:

  1. Open your Xcode project.
  2. Select your app target.
  3. Go to the "Signing & Capabilities" tab.
  4. Click the "+" button to add a new capability.
  5. Select "App Groups" from the list.
  6. Create a new app group (must begin with group.) and note its identifier (e.g., group.com.example.app).
  7. Repeat the same steps for your share extension target, ensuring that both targets use the same app group identifier.

If you don't want to receive files, you can skip this step.

Configuration

No configuration required for this plugin.

Web

To use this plugin on the web, you need to set up a Progressive Web App (PWA) with a web manifest and service worker that handles share targets.

Manifest

To allow your PWA to act as a share target, you need to add a share_target configuration to your web manifest (manifest.json):

{
  "share_target": {
    "action": "/_share-target",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "title": "title",
      "text": "text", 
      "url": "url",
      "files": [
        {
          "name": "files",
          "accept": ["*/*"]
        }
      ]
    }
  }
}

For more information on setting up the share target for your PWA, refer to the Web Share Target documentation.

Service Worker

Since service workers are the only way to intercept network requests in a PWA, you'll need to create one to handle the share target requests. Just create a file named sw.js in your project's root directory and add the following code:

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  if (event.request.method === 'POST' && url.pathname === '/_share-target') {
    event.respondWith(handleShareTarget(event.request));
  } else if (url.pathname.startsWith('/_share-file/')) {
    event.respondWith(handleFileRequest(event.request));
  }
});

async function handleFileRequest(request) {
  try {
    const url = new URL(request.url);
    const fileId = url.pathname.substring(13); // Remove '/_share-file/' prefix
    const cache = await caches.open('share-target-files');
    const cacheKey = `/${fileId}`;

    const response = await cache.match(cacheKey);
    if (response) {
      return response;
    } else {
      return new Response('File not found', { status: 404 });
    }
  } catch (error) {
    console.error('Error serving file:', error);
    return new Response('Internal error', { status: 500 });
  }
}

async function handleShareTarget(request) {
  try {
    const formData = await request.formData();
    const title = formData.get('title') || '';
    const text = formData.get('text') || '';
    const url = formData.get('url') || '';
    const files = formData.getAll('files');

    const texts = [];
    if (text) texts.push(text);
    if (url) texts.push(url);

    const shareData = {
      title: title,
      texts: texts.length > 0 ? texts : undefined,
      files: undefined,
    };

    if (files.length > 0) {
      const fileUrls = [];
      const cache = await caches.open('share-target-files');

      for (const file of files) {
        if (file instanceof File && file.size > 0) {
          const fileId = `share-file-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
          const cacheKey = `/${fileId}`;

          const response = new Response(file, {
            headers: {
              'Content-Type': file.type,
              'Content-Length': file.size.toString(),
              'X-File-Name': file.name || 'unknown',
            }
          });
          await cache.put(cacheKey, response);

          fileUrls.push(`/_share-file/${fileId}`);
        }
      }

      if (fileUrls.length > 0) {
        shareData.files = fileUrls;
      }
    }

    const redirectUrl = new URL('/', self.location.origin);

    if (shareData.title) {
      redirectUrl.searchParams.set('title', shareData.title);
    }

    if (shareData.texts && shareData.texts.length > 0) {
      shareData.texts.forEach((text, index) => {
        redirectUrl.searchParams.set(`text${index}`, text);
      });
    }

    if (shareData.files && shareData.files.length > 0) {
      shareData.files.forEach((fileUrl, index) => {
        redirectUrl.searchParams.set(`file${index}`, fileUrl);
      });
    }

    return Response.redirect(redirectUrl.href, 303);
  } catch (error) {
    console.error('Error handling share target:', error);
    return Response.redirect('/', 303);
  }
}

Now, register your service worker in your main JavaScript file:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js');
}

Usage

import { ShareTarget } from '@capawesome-team/capacitor-share-target';

const addListener = async () => {
    await ShareTarget.addListener('shareReceived', (event) => {
        console.log('Share received:', event);

        // Handle shared files
        if (event.files) {
          event.files.forEach(async (fileUrl) => {
            const response = await fetch(fileUrl);
            const blob = await response.blob();
            // Process the file...
          });
        }
    });
};

API

addListener('shareReceived', ...)

addListener(eventName: 'shareReceived', listenerFunc: (event: ShareReceivedEvent) => void) => Promise<PluginListenerHandle>

Called when a share is received.

Param Type
eventName 'shareReceived'
listenerFunc (event: ShareReceivedEvent) => void

Returns: Promise<PluginListenerHandle>

Since: 0.1.0


removeAllListeners()

removeAllListeners() => Promise<void>

Remove all listeners for this plugin.

Since: 0.1.0


Interfaces

PluginListenerHandle

Prop Type
remove () => Promise<void>

ShareReceivedEvent

Prop Type Description Since
title string The title of the shared content. 0.1.0
texts string[] The text content that was shared. 0.1.0
files string[] The files that were shared. On Android and iOS, this will contain the file paths or base64 encoded data URLs of the shared files. On Web, this will contain cached file URLs that can be fetched directly. 0.1.0

Changelog

See CHANGELOG.md.

Breaking Changes

See BREAKING.md.

License

See LICENSE.