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.
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.
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 thetext
field in the initial propsNSExtensionActivationSupportsWebURLWithMaxCount: 1
, which is triggered when using the share button in Safari. The result is passed as theurl
field in the initial propsNSExtensionActivationSupportsWebPageWithMaxCount: 1
, which is triggered when using the share button in Safari. The result is passed as thepreprocessingResults
field in the initial props. When using this rule, you will no longer receiveurl
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 theimages
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 thevideos
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 thefiles
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!
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.
expo-share-extension-720.mov
Install the package
npx expo install expo-share-extension
Update app.json/app.config.js
"expo": {
...
"plugins": ["expo-share-extension"],
...
}
Update package.json
{
...
"main": "index.js",
...
}
Create an index.js
in the root of your project
import { registerRootComponent } from "expo";
import App from "./App";
registerRootComponent(App);
or if you're using expo-router:
import "expo-router/entry";
Create an index.share.js
in the root of your project
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);
Update metro.config.js so that it resolves index.share.js as the entry point for the share extension
// 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,
});
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>
);
}
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",
],
},
],
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.
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
},
},
],
Want to customize the share extension's height? Do this in your app.json
/app.config.(j|t)s
:
[
"expo-share-extension",
{
"height": 500
},
],
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
).
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:
- You must create a class with a
run
method, which receives an object with acompletionFunction
method as its argument. ThiscompletionFunction
method must be invoked at the end of yourrun
method. The argument you pass to it, is what you will receive as thepreprocessingResults
object as part of initial props.
class ShareExtensionPreprocessor {
run(args) {
args.completionFunction({
title: document.title,
});
}
}
- 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,
});
}
}
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:
- Start the expo module build in watch mode:
npm run build
- Start the config plugin build in watch mode:
npm run build plugin
cd /example
and generate the iOS project:npm run prebuild
- Run the app from the /example folder:
npm run ios
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.
- navigate to
~/Library/Developer/Xcode/DerivedData/
rm -rf
folders that are prefixed with your project name
pod cache clean --all
pod deintegrate
- In XCode in the top menu, navigate to Debug > Attach to Process.
- 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.
- Once you've located your share extension's process, click on it to attach the debugger to that process.
- 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.
- Open the Console app from the Applications/Utilities folder
- Select your device from the Devices list
- Filter the log messages by process name matching your share extension target name
- On your Mac, open Finder.
- Select Go > Go to Folder from the menu bar or press Shift + Cmd + G.
- Enter ~/Library/Logs/DiagnosticReports/ and click Go.
- Look for any recent crash logs related to your share extension. These logs should have a .crash or .ips extension.
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