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 the text
field in the initial propsNSExtensionActivationSupportsWebURLWithMaxCount: 1
, which is triggered when using the share button in Safari. The result is passed as the url
field in the initial propsNSExtensionActivationSupportsWebPageWithMaxCount: 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!
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.
https://github.com/MaxAst/expo-share-extension/assets/13224092/e5a6fb3d-6c85-4571-99c8-4efe0f862266
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:
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,
});
}
}
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:
npm run build
npm run build plugin
cd /example
and generate the iOS project: npm run prebuild
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.
~/Library/Developer/Xcode/DerivedData/
rm -rf
folders that are prefixed with your project namepod cache clean --all
pod deintegrate
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: