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

expo-share-extension

v2.0.2

Published

Expo config plugin to create an iOS share extension.

Downloads

6,323

Readme

Expo Share Extension

npm License Downloads GitHub stars

Note: Support for the New Architecture is under active development. For the time being, you need to disable it in your app.json/app.config.(j|t)s:

{
  "expo": {
    "newArchEnabled": false
  }
}

Overview

Create an iOS share extension with a custom view (similar to e.g. Pinterest). Supports Apple Sign-In, React Native Firebase (including shared auth session via access groups), custom background, custom height, and custom fonts.

https://github.com/MaxAst/expo-share-extension/assets/13224092/e5a6fb3d-6c85-4571-99c8-4efe0f862266

Compatibility

| Expo | expo-share-extension | | ---------- | ---------------------- | | SDK 52 | 2.0.0+ | | SDK 51 | 1.5.3+ | | SDK 50 | 1.0.0+ |

Quick Start

1. Installation

npx expo install expo-share-extension

2. Basic Configuration

  1. Update your app.json or app.config.js:
"expo": {
  ...
  "plugins": ["expo-share-extension"],
  ...
}
  1. Ensure your package.json has the correct main entry:
{
  ...
  "main": "index.js",
  ...
}
  1. Create the required entry points:

index.js (main app):

import { registerRootComponent } from "expo";

import App from "./App";

registerRootComponent(App);

// or if you're using expo-router:
// import "expo-router/entry";

index.share.js (share extension):

import { AppRegistry } from "react-native";

// could be any component you want to use as the root component of your share extension's bundle
import ShareExtension from "./ShareExtension";

// IMPORTANT: the first argument to registerComponent, must be "shareExtension"
AppRegistry.registerComponent("shareExtension", () => ShareExtension);
  1. Update metro.config.js (if you don't have one, run: npx expo customize metro.config.js first):
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require("expo/metro-config");

/**
 * Add support for share.js as a recognized extension to the Metro config.
 * This allows creating an index.share.js entry point for our iOS share extension
 *
 * @param {import('expo/metro-config').MetroConfig} config
 * @returns {import('expo/metro-config').MetroConfig}
 */
function withShareExtension(config) {
  config.transformer.getTransformOptions = () => ({
    resolver: {
      sourceExts: [...config.resolver.sourceExts, "share.js"], // Add 'share.js' as a recognized extension
    },
  });
  return config;
}

module.exports = withShareExtension(getDefaultConfig(__dirname), {
  // [Web-only]: Enables CSS support in Metro.
  isCSSEnabled: true,
});

Accessing Shared Data

The shared data is passed to the share extension's root component as an initial prop based on this type:

export type InitialProps = {
  files?: string[];
  images?: string[];
  videos?: string[];
  text?: string;
  url?: string;
  preprocessingResults?: unknown;
};

You can import InitialProps from expo-share-extension to use it as a type for your root component's props.

Activation Rules

The config plugin supports almost all NSExtensionActivationRules. It currently supports.

  • NSExtensionActivationSupportsText, which is triggered e.g. when sharing a WhatsApp message's contents or when selecting a text on a webpage and sharing it via the iOS tooltip menu. The result is passed as the text field in the initial props
  • NSExtensionActivationSupportsWebURLWithMaxCount: 1, which is triggered when using the share button in Safari. The result is passed as the url field in the initial props
  • NSExtensionActivationSupportsWebPageWithMaxCount: 1, which is triggered when using the share button in Safari. The result is passed as the preprocessingResults field in the initial props. When using this rule, you will no longer receive url as part of initial props, unless you extract it in your preprocessing JavaScript file. You can learn more about this in the Preprocessing JavaScript section.
  • NSExtensionActivationSupportsImageWithMaxCount: 1, which is triggered when using the share button on an image. The result is passed as part of the images array in the initial props.
  • NSExtensionActivationSupportsMovieWithMaxCount: 1, which is triggered when using the share button on a video. The result is passed as part of the videos array in the initial props.
  • NSExtensionActivationSupportsFileWithMaxCount: 1, which is triggered when using the share button on a file. The result is passed as part of the files array in the initial props.

You need to list the activation rules you want to use in your app.json/app.config.(j|t)s file like so:

[
  "expo-share-extension",
  {
    "activationRules": [
      {
        "type": "file",
        "max": 3
      },
      {
        "type": "image",
        "max": 2
      },
      {
        "type": "video",
        "max": 1
      },
      {
        "type": "text"
      },
      {
        "type": "url",
        "max": 1
      }
    ]
  }
]

If no values for max are provided, the default value is 1. The type field can be one of the following: file, image, video, text, url.

If you want to use the image and video types, you need to make sure to add this to your app.json:

{
  // ...
  "ios": {
    // ...
    "privacyManifests": {
      "NSPrivacyAccessedAPITypes": [
        {
          "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryFileTimestamp",
          "NSPrivacyAccessedAPITypeReasons": ["C617.1"]
        }
        // ...
      ]
    }
  }
}

If you do not specify the activationRules option, expo-share-extension enables the url and text rules by default, for backwards compatibility.

Contributions to support the remaining NSExtensionActivationRules (NSExtensionActivationSupportsAttachmentsWithMaxCount and NSExtensionActivationSupportsAttachmentsWithMinCount) are welcome!

Basic Usage

Need a way to close the share extension? Use the close method from expo-share-extension:

import { close } from "expo-share-extension"
import { Button, Text, View } from "react-native";

// if ShareExtension is your root component, url is available as an initial prop
export default function ShareExtension({ url }: { url: string }) {
  return (
    <View style={{ flex: 1 }}>
      <Text>{url}</Text>
      <Button title="Close" onPress={close} />
    </View>
  );
}

If you want to open the host app from the share extension, use the openHostApp method from expo-share-extension with a valid path:

import { openHostApp } from "expo-share-extension"
import { Button, Text, View } from "react-native";

// if ShareExtension is your root component, url is available as an initial prop
export default function ShareExtension({ url }: { url: string }) {
  const handleOpenHostApp = () => {
    openHostApp(`create?url=${url}`)
  }

  return (
    <View style={{ flex: 1 }}>
      <Text>{url}</Text>
      <Button title="Open Host App" onPress={handleOpenHostApp} />
    </View>
  );
}

When you share images and videos, expo-share-extension stores them in a sharedData directory in your app group's container. These files are not automatically cleaned up, so you should delete them when you're done with them. You can use the clearAppGroupContainer method from expo-share-extension to delete them:

import { clearAppGroupContainer } from "expo-share-extension"
import { Button, Text, View } from "react-native";

// if ShareExtension is your root component, url is available as an initial prop
export default function ShareExtension({ url }: { url: string }) {
  const handleCleanUp = async () => {
    await clearAppGroupContainer()
  }

  return (
    <View style={{ flex: 1 }}>
      <Text>I have finished processing all shared images and videos</Text>
      <Button title="Clear App Group Container" onPress={handleOpenHostApp} />
    </View>
  );
}

Configuration Options

Exlude Expo Modules

Exclude unneeded expo modules to reduce the share extension's bundle size by adding the following to your app.json/app.config.(j|t)s:

[
  "expo-share-extension",
    {
      "excludedPackages": [
        "expo-dev-client",
        "expo-splash-screen",
        "expo-updates",
        "expo-font",
      ],
    },
],

Note: The share extension does not support expo-updates as it causes the share extension to crash. Since version 1.5.0, expo-updates is excluded from the share extension's bundle by default. If you're using an older version, you must exclude it by adding it to the excludedPackages option in your app.json/app.config.(j|t)s. See the Exlude Expo Modules section for more information.

React Native Firebase

Using React Native Firebase? Given that share extensions are separate iOS targets, they have their own bundle IDs, so we need to create a dedicated GoogleService-Info.plist in the Firebase console, just for the share extension target. The bundle ID of your share extension is your existing bundle ID with .ShareExtension as the suffix, e.g. com.example.app.ShareExtension.

[
  "expo-share-extension",
    {
      "googleServicesFile": "./path-to-your-separate/GoogleService-Info.plist",
    },
],

You can share a firebase auth session between your main app and the share extension by using the useUserAccessGroup hook. The value for userAccessGroup is your main app's bundle ID with the group. prefix, e.g. group.com.example.app. For a full example, check this.

Custom Background Color

Want to customize the share extension's background color? Add the following to your app.json/app.config.(j|t)s:

[
  "expo-share-extension",
    {
      "backgroundColor": {
        "red": 255,
        "green": 255,
        "blue": 255,
        "alpha": 0.8 // if 0, the background will be transparent
      },
    },
],

Custom Height

Want to customize the share extension's height? Do this in your app.json/app.config.(j|t)s:

[
  "expo-share-extension",
    {
      "height": 500
    },
],

Custom Fonts

This plugin automatically adds custom fonts to the share extension target if they are embedded in the native project via the expo-font config plugin.

It currently does not support custom fonts that are loaded at runtime, due to an NSURLSesssion error. To fix this, Expo would need to support defining a sharedContainerIdentifier for NSURLSessionConfiguration instances, where the value would be set to the main app's and share extension's app group identifier (e.g. group.com.example.app).

Preprocessing JavaScript

As explained in Accessing a Webpage, we can use a JavaScript file to preprocess the webpage before the share extension is activated. This is useful if you want to extract the title and URL of the webpage, for example. To use this feature, add the following to your app.json/app.config.(j|t)s:

[
  "expo-share-extension",
    {
      "preprocessingFile": "./preprocessing.js"
    },
],

The preprocessingFile option adds NSExtensionActivationSupportsWebPageWithMaxCount: 1 as an NSExtensionActivationRule. Your preprocessing file must adhere to some rules:

  1. You must create a class with a run method, which receives an object with a completionFunction method as its argument. This completionFunction method must be invoked at the end of your run method. The argument you pass to it, is what you will receive as the preprocessingResults object as part of initial props.
class ShareExtensionPreprocessor {
  run(args) {
    args.completionFunction({
      title: document.title,
    });
  }
}
  1. Your file must create an instance of a class using var, so that it is globally accessible.
var ExtensionPreprocessingJS = new ShareExtensionPreprocessor();

For a full example, check this.

WARNING: Using this option enables NSExtensionActivationSupportsWebPageWithMaxCount: 1 and this is mutually exclusive with NSExtensionActivationSupportsWebURLWithMaxCount: 1, which expo-share-extension enables by default. This means that once you set the preprocessingFile option, you will no longer receive url as part of initial props. However, you can still get the URL via preprocessingResults by using window.location.href in your preprocessing file:

class ShareExtensionPreprocessor {
  run(args) {
    args.completionFunction({
      url: window.location.href,
      title: document.title,
    });
  }
}

Development

If you want to contribute to this project, you can use the example app to test your changes. Run the following commands to get started:

  1. Start the expo module build in watch mode: npm run build
  2. Start the config plugin build in watch mode: npm run build plugin
  3. cd /example and generate the iOS project: npm run prebuild
  4. Run the app from the /example folder: npm run ios

Troubleshooting

Command PhaseScriptExecution failed with a nonzero exit code

If you encounter this error when building your app in XCode and you use yarn as a package manager, it is most likely caused by XCode using the wrong node binary. To fix this, navigate into your project's ios directory and replace the contents in the .xcode.env.local file with the contents of the .xcode.env file.

Clear XCode Cache

  1. navigate to ~/Library/Developer/Xcode/DerivedData/
  2. rm -rf folders that are prefixed with your project name

Clear CocoaPods Cache

  1. pod cache clean --all
  2. pod deintegrate

Attach Debugger to Share Extension Process:

  1. In XCode in the top menu, navigate to Debug > Attach to Process.
  2. In the submenu, you should see a list of running processes. Find your share extension's name in this list. If you don't see it, you can try typing its name into the search box at the bottom.
  3. Once you've located your share extension's process, click on it to attach the debugger to that process.
  4. With the debugger attached, you can also set breakpoints within your share extension's code. If these breakpoints are hit, Xcode will pause execution and allow you to inspect variables and step through your code, just like you would with your main app.

Check Device Logs

  1. Open the Console app from the Applications/Utilities folder
  2. Select your device from the Devices list
  3. Filter the log messages by process name matching your share extension target name

Check Crash Logs

  1. On your Mac, open Finder.
  2. Select Go > Go to Folder from the menu bar or press Shift + Cmd + G.
  3. Enter ~/Library/Logs/DiagnosticReports/ and click Go.
  4. Look for any recent crash logs related to your share extension. These logs should have a .crash or .ips extension.

Credits

This project would not be possible without existing work in the react native ecosystem. I'd like to give credit to the following projects and their authors:

  • https://github.com/Expensify/react-native-share-menu
  • https://github.com/andrewsardone/react-native-ios-share-extension
  • https://github.com/alinz/react-native-share-extension
  • https://github.com/ajith-ab/react-native-receive-sharing-intent
  • https://github.com/timedtext/expo-config-plugin-ios-share-extension
  • https://github.com/achorein/expo-share-intent-demo
  • https://github.com/andrewsardone/react-native-ios-share-extension
  • https://github.com/EvanBacon/pillar-valley/tree/master/targets/widgets
  • https://github.com/andrew-levy/react-native-safari-extension
  • https://github.com/bndkt/react-native-app-clip