npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

capacitor-shared-extension

v2.0.7

Published

A Capacitor Plugin that provides native capabilities to retrieve media files sent via iOS Share Extension and Android Send Intent events

Downloads

2

Readme

@calvinckho/capacitor-share-extension

This Capacitor Plugin provides native capabilities to retrieve media files sent via iOS Share Extension and Android Send Intent events

Compatibility to Capacitor Versions

Installation

Github Branch

npm i git+ssh://[email protected]/calvinckho/capacitor-share-extension#[branch-name]]

NPM

npm i capacitor-share-extension@[version number]

Usage

Capacitor 2:

import 'capacitor-share-extension';
const { ShareExtension } = Plugins;

Capacitor 3 & 4:

import { ShareExtension } from 'capacitor-share-extension';
// run this as part of the app launch
if (this.platform.is('cordova') && Capacitor.isPluginAvailable('ShareExtension')) {
    window.addEventListener('sendIntentReceived',  () => {
        this.checkIntent();
    });
    this.checkIntent();
}

async checkIntent() {
    try {
        const result: any = await ShareExtension.checkSendIntentReceived();
        /* sample result::
        { payload: [
            {
                "type":"image%2Fjpg",
                "description":"",
                "title":"IMG_0002.JPG",
                // url contains a full, platform-specific file URL that can be read later using the Filsystem API.
                "url":"file%3A%2F%2F%2FUsers%2Fcalvinho%2FLibrary%2FDeveloper%2FCoreSimulator%2FDevices%2FE4C13502-3A0B-4DF4-98ED-9F31DDF03672%2Fdata%2FContainers%2FShared%2FAppGroup%2FF41DC1F5-54D7-4EC5-9785-5248BAE06588%2FIMG_0002.JPG",
                // webPath returns a path that can be used to set the src attribute of an image for efficient loading and rendering.
                "webPath":"capacitor%3A%2F%2Flocalhost%2F_capacitor_file_%2FUsers%2Fcalvinho%2FLibrary%2FDeveloper%2FCoreSimulator%2FDevices%2FE4C13502-3A0B-4DF4-98ED-9F31DDF03672%2Fdata%2FContainers%2FShared%2FAppGroup%2FF41DC1F5-54D7-4EC5-9785-5248BAE06588%2FIMG_0002.JPG",
            }]
         } 
         */
        if (result && result.payload && result.payload.length) {
            console.log('Intent received: ', JSON.stringify(result));
        }
    } catch (err) {
        console.log(err);
    }
}

// in Android, call finish when done processing the Intent
await ShareExtension.finish()

// iOS keychain methods
try {
    // load an authentication token
    const token = 'token XXYYZZ';
    // use the extension to save the auth token to iOS Keychain
    await ShareExtension.saveDataToKeychain({ key: 'token', data: token });
} catch (err) {
    console.log(err);
}

// when user is about to log out, remove the token from iOS Keychain
try {
    await ShareExtension.clearKeychainData( { key: 'token' });
} catch (err) {
    console.log(err);
}

Android

Configure AndroidManifest.xml to allow file types to be received by your main app. See here for a list of available mimeTypes.

<activity
        android:name="com.myown.app.MainActivity"
        android:label="@string/app_name"
        android:exported="true"
        android:theme="@style/AppTheme.NoActionBar">
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <action android:name="android.intent.action.SEND_MULTIPLE" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="text/plain" />
        <data android:mimeType="image/*" />
        <data android:mimeType="application/pdf" />
        <data android:mimeType="application/msword" />
        <data android:mimeType="application/vnd.openxmlformats-officedocument.wordprocessingml.document" />
        <data android:mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" />
        <data android:mimeType="application/vnd.openxmlformats-officedocument.presentationml.presentation" />
    </intent-filter>
</activity>

Update your Android main app's /android/app/src/main/java/.../MainActivity.java with these code:

package com.myown.app;
import com.getcapacitor.BridgeActivity;

+import android.content.Intent;
+import android.webkit.ValueCallback;

public class MainActivity extends BridgeActivity {
   @Override
   public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     ...
   }
   
+  @Override
+  protected void onNewIntent(Intent intent) {
+    super.onNewIntent(intent);
+    String action = intent.getAction();
+    String type = intent.getType();
+    if ((Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) && type != null) {
+      bridge.getActivity().setIntent(intent);
+      bridge.eval("window.dispatchEvent(new Event('sendIntentReceived'))", new ValueCallback<String>() {
+        @Override
+        public void onReceiveValue(String s) {
+        }
+      });
+    }
+  }
}

On Android, after having processed the Send Intent in your app, it is a good practice to end the Intent using the finish() method. Not doing so can lead to app state issues (because you have two instances running) or trigger the same intent again if your app reloads from idle mode.

ShareExtension.finish();

iOS

Create a new "Share Extension" in Xcode (Creating an App extension)

Set the activation rules in the extension's Info.plist, so that your app will be displayed as an option in the share view. To add more types, see here.

...
<key>NSExtensionAttributes</key>
<dict>
    <key>NSExtensionActivationRule</key>
        <string>SUBQUERY (
              extensionItems,
              $extensionItem,
              SUBQUERY (
                  $extensionItem.attachments,
                  $attachment,
                   ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image" ||
                           ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.spreadsheet" ||
                           ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.presentation" ||
                           ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.openxmlformats.wordprocessingml.document" ||
                           ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf" ||
                           ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.png" ||
                           ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg" ||
                           ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg-2000" ||
                           ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.plain-text" ||
                           ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" ||
                           ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.compuserve.gif"
                ).@count == $extensionItem.attachments.@count
                ).@count == 1
        </string>
</dict>
...            

Overwrite ShareViewController.swift with this code. In Target - [This Extension's Name] - Build Settings, set the iOS Deployment Target to iOS 13 or higher, as the syntax in the following code is not compatible with older iOS version.

import UIKit
import Social
import MobileCoreServices
import Foundation.NSURLSession

class ShareItem {
    public var title: String?
    public var type: String?
    public var url: String?
    public var webPath: String?
    public var qrStrings: String?
}

class ShareViewController:  UIViewController {
    private var shareItems: [ShareItem] = []
    override public func viewDidAppear(_ animated: Bool) {
       super.viewDidAppear(animated)
       self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
    }
    
    override public func viewDidLoad() {
        super.viewDidLoad()
        shareItems.removeAll()
        let extensionItem = extensionContext?.inputItems[0] as! NSExtensionItem
        Task {
            try await withThrowingTaskGroup(
                of: ShareItem.self,
                body: { taskGroup in
                    for attachment in extensionItem.attachments! {
                        if attachment.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
                            taskGroup.addTask {
                                return try await self.handleTypeUrl(attachment)
                            }
                        } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
                            taskGroup.addTask {
                                return try await self.handleTypeText(attachment)
                            }
                        } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) {
                            taskGroup.addTask {
                                return try await self.handleTypeMovie(attachment)
                            }
                        } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
                            taskGroup.addTask {
                                return try await self.handleTypeImage(attachment)
                            }
                        }
                    }
                    for try await item in taskGroup {
                        self.shareItems.append(item)
                    }
                })
            self.sendData()
        }
    }
    
    private func sendData() {
        let queryItems = shareItems.map {
            [
                URLQueryItem(
                    name: "title",
                    value: $0.title?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
                URLQueryItem(name: "description", value: ""),
                URLQueryItem(
                    name: "type",
                    value: $0.type?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
                URLQueryItem(
                    name: "url",
                    value: $0.url?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
                URLQueryItem(
                    name: "webPath",
                    value: $0.webPath?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
                URLQueryItem(
                    name: "qrStrings",
                    value: $0.qrStrings?.description.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "")
            ]
        }.flatMap({ $0 })
        var urlComps = URLComponents(string: "restvo://;")!
        urlComps.queryItems = queryItems
        openURL(urlComps.url!)
    }
    
    fileprivate func createSharedFileUrl(_ url: URL?) -> String {
        let fileManager = FileManager.default
        print("share url: " + url!.absoluteString)
        let copyFileUrl =
        fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.com.restvo.test")!
            .absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! + url!
            .lastPathComponent.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
        try? Data(contentsOf: url!).write(to: URL(string: copyFileUrl)!)
        
        return copyFileUrl
    }
    
    func saveScreenshot(_ image: UIImage) -> String {
        let fileManager = FileManager.default
        
        let copyFileUrl =
        fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.com.restvo.test")!
            .absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
        + "/screenshot.png"
        do {
            try image.pngData()?.write(to: URL(string: copyFileUrl)!)
            return copyFileUrl
        } catch {
            print(error.localizedDescription)
            return ""
        }
    }
    
    fileprivate func handleTypeUrl(_ attachment: NSItemProvider)
    async throws -> ShareItem
    {
        let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil)
        let url = results as! URL?
        let shareItem: ShareItem = ShareItem()
        
        if url!.isFileURL {
            shareItem.title = url!.lastPathComponent
            shareItem.type = "application/" + url!.pathExtension.lowercased()
            shareItem.url = createSharedFileUrl(url)
            shareItem.webPath = "capacitor://localhost/_capacitor_file_" + URL(string: shareItem.url ?? "")!.path
        } else {
            shareItem.title = url!.absoluteString
            shareItem.url = url!.absoluteString
            shareItem.webPath = url!.absoluteString
            shareItem.type = "text/plain"
        
            do {
                let _coding = try await attachment.loadPreviewImage();
                if let image = _coding as? UIImage {
                    self.processQR(image: image, shareItem: shareItem)
                }
            }
            catch {
            }
        }
        return shareItem
    }
    
    fileprivate func handleTypeText(_ attachment: NSItemProvider)
    async throws -> ShareItem
    {
        let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil)
        let shareItem: ShareItem = ShareItem()
        let text = results as! String
        shareItem.title = text
        shareItem.type = "text/plain"
        return shareItem
    }
    
    fileprivate func handleTypeMovie(_ attachment: NSItemProvider)
    async throws -> ShareItem
    {
        let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeMovie as String, options: nil)
        let shareItem: ShareItem = ShareItem()
        let url = results as! URL?
        shareItem.title = url!.lastPathComponent
        shareItem.type = "video/" + url!.pathExtension.lowercased()
        shareItem.url = createSharedFileUrl(url)
        shareItem.webPath = "capacitor://localhost/_capacitor_file_" + URL(string: shareItem.url ?? "")!.path
        return shareItem
    }
    
    fileprivate func handleTypeImage(_ attachment: NSItemProvider)
    async throws -> ShareItem
    {
        let data = try await attachment.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil)
        let shareItem: ShareItem = ShareItem()
        switch data {
        case let image as UIImage:
            shareItem.title = "screenshot"
            shareItem.type = "image/png"
            shareItem.url = self.saveScreenshot(image)
            shareItem.webPath = "capacitor://localhost/_capacitor_file_" + URL(string: shareItem.url ?? "")!.path
            self.processQR(image: image, shareItem: shareItem)
        case let url as URL:
            shareItem.title = url.lastPathComponent
            shareItem.type = "image/" + url.pathExtension.lowercased()
            shareItem.url = self.createSharedFileUrl(url)
            shareItem.webPath = "capacitor://localhost/_capacitor_file_" + URL(string: shareItem.url ?? "")!.path
            if let ciImage = CIImage(contentsOf: url) {
                self.processQR(image: ciImage, shareItem: shareItem)
            }
        default:
            print("Unexpected image data:", type(of: data))
        }
        return shareItem
    }
    
    private func processQR( image: UIImage, shareItem: ShareItem){
        let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: [ CIDetectorAccuracy : CIDetectorAccuracyHigh ])
        if let _cgImage = image.cgImage {
            self.processQR(image: CIImage(cgImage: _cgImage), shareItem: shareItem);
            
        }

    }

    private func processQR( image: CIImage, shareItem: ShareItem){
            let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: [ CIDetectorAccuracy : CIDetectorAccuracyHigh ])
            let features = detector?.features(in: image)
            if let _features = features as? [CIQRCodeFeature] {
                var _qrStrings: [String]=[]
                if (!_features.isEmpty){
                    for _feature in _features {
                        if let _messageString = _feature.messageString {
                            _qrStrings.append("\""+_messageString+"\"");
                        }
                    }
                }
                if (_qrStrings.count>0){
                    shareItem.qrStrings = "["+_qrStrings.joined(separator: ",")+"]";
                }
            };
    }

    @objc func openURL(_ url: URL) -> Bool {
        var responder: UIResponder? = self
        while responder != nil {
            if let application = responder as? UIApplication {
                return application.perform(#selector(openURL(_:)), with: url) != nil
            }
            responder = responder?.next
        }
        return false
    }
}

The share extension is like a little standalone program. The extension receives the media, and issues an openURL call. In order for your main app to respond to the openURL call, you have to define a URL scheme (Register Your URL Scheme). The code above calls a URL scheme named "restvo://", so just replace it with your scheme. To allow sharing of files between the extension and your main app, you need to create an app group which is checked for both your extension and main app. Search and replace "group.com.restvo.test" with your app group's name.

Finally, in your main app's AppDelegate.swift, override the following function. This is the function that is activated when the main app is opened by URL.

import UIKit
import Capacitor
// ...
import CapacitorShareExtension
// ...

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    // ...
    let store = ShareStore.store
    // ...

    private func parseAndPopulateJSONString(shareItem: inout JSObject, name: String, items: [URLQueryItem], index: Int){
        if let _value = items[index].value {
            if !_value.isEmpty {
                shareItem[name]=_value.removingPercentEncoding;
            }
        }
    }

    private func parseAndPopulateJSONArray(shareItem: inout JSObject, name: String, items: [URLQueryItem], index: Int){
        if let _value = items[index].value {
            if !_value.isEmpty {
                if let _percentage_removed = _value.removingPercentEncoding {
                    let data = Data(_percentage_removed.utf8);
                    do {
                        if let _json_array = try JSONSerialization.jsonObject(with: data, options: []) as? [String] {
                            shareItem[name] = _json_array;
                        }
                    } catch  {
                        print("ERROR: parsing json: \(error)");
                    }
                }
            }
        }
    }

    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
                
            var success = true
            if CAPBridge.handleOpenUrl(url, options) {
                success = ApplicationDelegateProxy.shared.application(app, open: url, options: options)
            }
        
            guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
                  let params = components.queryItems else {
                      return false
                  }
            let titles = params.filter { $0.name == "title" }
            let descriptions = params.filter { $0.name == "description" }
            let types = params.filter { $0.name == "type" }
            let urls = params.filter { $0.name == "url" }
            let webPaths = params.filter { $0.name == "webPath" }
            let qrStrings = params.filter { $0.name == "qrStrings" }

            store.shareItems.removeAll()
        
            if (titles.count > 0){
                for index in 0...titles.count-1 {
                    var shareItem: JSObject = JSObject()
                    self.parseAndPopulateJSONString(shareItem: &shareItem, name: "title", items: titles, index: index);
                    self.parseAndPopulateJSONString(shareItem: &shareItem, name: "description", items: descriptions, index: index);
                    self.parseAndPopulateJSONString(shareItem: &shareItem, name: "type", items: types, index: index);
                    self.parseAndPopulateJSONString(shareItem: &shareItem, name: "url", items: urls, index: index);
                    self.parseAndPopulateJSONString(shareItem: &shareItem, name: "webPath", items: webPaths, index: index);
                    self.parseAndPopulateJSONArray(shareItem: &shareItem, name: "qrStrings", items: qrStrings, index: index);

                    store.shareItems.append(shareItem)
                }
            }
        
            store.processed = false
            let nc = NotificationCenter.default
            nc.post(name: Notification.Name("triggerSendIntent"), object: nil )
        
            return success
        }
    // ...
}