Writing custom expo plugin to integrate a third party library.
I was working on a react native Expo project in last months and I had the opportunity to learn how to write a expo plugin while the existing plugins don’t help me with my task. I had to run through lot of trial and error scenarios since the resources for this topic is limited. Therefore, I thought of sharing my experience in writing a simple Expo plugin while also sharing the knowledge I gained after burning a few midnights.
When Do You Need an Expo Config Plugin ?
An Expo config plugin is needed when you have to modify the native code or configuration of an Expo project that managed workflow does not support out of the box. Since Expo projects do not include native iOS or Android code by default, a config plugin allows you to make custom native changes.
Simply, if you want to work on Expo bare react native project you have to run command,
npx expo prebuild
inside your project directory which results generating ios and android directories. If you want to do any changes inside those native folders you have to use a config plugin. There are existing plugins that we can use in this case.
For example, if you want to chnge any build properties in android and ios side you can use expo-build-properties plugin for that. For that you have to install it as mentioned in the documentation and add in app.json
or app.config.js
file.
{
"expo": {
"plugins": [
[
"expo-build-properties",
{
"android": {
"compileSdkVersion": 34,
"targetSdkVersion": 34,
"buildToolsVersion": "34.0.0"
},
"ios": {
"deploymentTarget": "13.4"
}
}
]
]
}
}
After adding above values in app.json
and running npx expo prebuild command, the properties will be added automatically (bythe plugin) in native side. Always remember that there is no point that we manually do changes in native side since prebuild command will destroy every change that we did manually since it generates android and ios directories newly.
When Do You Need an Custom Plugin ?
Think of a situation where there is no prebuilt plugins that does the task you want to do. For example think of a third party library that needs native side configurations that doesn’t support expo and only supports bare react native. In that case you have to write your own plugin to do the necessary changes in native side.
In my case it was the integrating of a library called smile id used for KYC integration. From now on we’ll see how we can integrate this library with a custom expo plugin.
Configuring smile id library in expo project
First install the library in to your project using the npm command,
npm i @smile_identity/react-native
Now what we have to do here in native side is (given in the documentation),
Android
Place the
smile_config.json
file under your application's assets, located atsrc/main/assets
(This should be at the same level as yourjava
andres
directories)
iOS
Drag the
smile_config.json
file into your projects file inspector and ensure that the file is added to your app's target. Confirm that it is by checking the Copy Bundle Resources drop down in the Build Phases tab of Xcode.
I copied above from the documentation and it says how to integrate this with a bare react native project but, since our project is expo, we have to do above changes in native side with a custom plugin. All the configurations related to smile id is inside this smile_config.json
file and we have to add that file to native side as mentioned above.
I have created a directory in root of the expo project called plugins and added a file called copy-file-config.js.
Inside config
directory I have placed the file that we want to move to native side.
Writing the plugin
First we have open the app.json
file and add our plugin inside plugins array.
{
"expo": {
// other configurations
"plugins": [
// other plugins
[
"./plugins/copy-file-config",
{
"src": "./config/smile_config.json",
"iosDest": "smile_config.json",
"androidDest": "app/src/main/assets/smile_config.json",
"groupName": "<ios group name here>"
}
],
],
// other configurations
}
}
src — path to config file in js side
iosDest — ios destination that we have to copy the file
androidDest — android destination that we have to copy the file
groupName — Xcode group name where we should add the file
We’ll use above variables while writing the plugin.
Now lets start writing plugin with android part first. Add the following code in copy-file-config.js.
const fs = require("fs");
const path = require("path");
const {
withPlugins,
withAndroidManifest,
} = require("@expo/config-plugins");
const withMyConfigFile = (config, { src, iosDest, androidDest, groupName }) => {
return withPlugins(config, [
(config) => withAndroidConfigFile(config, { src, dest: androidDest }),
]);
};
const withAndroidConfigFile = (config, { src, dest }) => {
return withAndroidManifest(config, async (config) => {
const sourcePath = path.resolve(__dirname, src);
const destinationPath = path.resolve(
config.modRequest.platformProjectRoot,
dest
);
try {
// Copy the file to the destination directory
if (!fs.existsSync(sourcePath)) {
throw new Error(`Source file not found at ${sourcePath}`);
}
const destDir = path.dirname(destinationPath);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
fs.copyFileSync(sourcePath, destinationPath);
} catch (error) {
console.error(`Error copying ${src} to Android:`, error.message);
throw error;
}
return config;
});
};
module.exports = withMyConfigFile;
With android it is pretty straightforward since we have to copy the file from source to destination. You have to import withPlugins
and withAndroidManifest
modules from @expo/config-plugins
and we write the file copying inside withAndroidConfigFile
which is passed to withPlugins
inside withMyConfigFile
.
const withMyConfigFile = (config, { src, iosDest, androidDest, groupName }) => {
return withPlugins(config, [
(config) => withAndroidConfigFile(config, { src, dest: androidDest }),
]);
};
- Registers a custom Expo Config Plugin that adds a config file (smile_config.json) to the Android project.
- It wraps
config
usingwithPlugins
, ensuring thatwithAndroidConfigFile
gets executed. - We have passed
src
anddest
towithAndroidConfigFile
so that function can copy the file from given source path to destination.
I won’t discuss the code inside withAndroidConfigFile
since it is a simple file copying code.
Now let’s move into ios part. If you noted what I quoted from smile id documentation, copying the file is not enough for ios. It also asks to add the file for target. Doing that was bit tricky since there is no exact, well documented source anywhere in the internet as of now for the functions and methods that we can use while writing plugins. For that finally I had to go through the @expo/config-plugins
library and read comments and understand the functionality of each methods available.
Let’s add the ios code also to copy-file-config.js
const fs = require("fs");
const path = require("path");
const {
withPlugins,
withXcodeProject,
withAndroidManifest,
IOSConfig,
} = require("@expo/config-plugins");
const withMyConfigFile = (config, { src, iosDest, androidDest, groupName }) => {
return withPlugins(config, [
(config) => withIOSConfigFile(config, { src, dest: iosDest, groupName }),
(config) => withAndroidConfigFile(config, { src, dest: androidDest }),
]);
};
//For ios
const withIOSConfigFile = (config, { src, dest, groupName }) => {
return withXcodeProject(config, async (config) => {
try {
const sourcePath = path.resolve(__dirname, src);
const destinationPath = path.resolve(
config.modRequest.platformProjectRoot,
dest
);
if (!fs.existsSync(sourcePath)) {
throw new Error(`Source file not found at ${sourcePath}`);
}
//copy file to root directory
fs.copyFileSync(sourcePath, destinationPath);
const project = config.modResults;
//Add file to target and copy bundle resources
IOSConfig.XcodeUtils.addResourceFileToGroup({
filepath: destinationPath,
groupName: groupName,
isBuildFile: true,
project,
verbose: true,
});
} catch (error) {
console.error(`Error copying ${src} to ios:`, error.message);
throw error;
}
return config;
});
};
//For android
const withAndroidConfigFile = (config, { src, dest }) => {
return withAndroidManifest(config, async (config) => {
const sourcePath = path.resolve(__dirname, src);
const destinationPath = path.resolve(
config.modRequest.platformProjectRoot,
dest
);
try {
// Copy the file to the destination directory
if (!fs.existsSync(sourcePath)) {
throw new Error(`Source file not found at ${sourcePath}`);
}
const destDir = path.dirname(destinationPath);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
fs.copyFileSync(sourcePath, destinationPath);
} catch (error) {
console.error(`Error copying ${src} to Android:`, error.message);
throw error;
}
return config;
});
};
module.exports = withMyConfigFile;
If you notice withIOSConfigFile
function, there we copy the file first and then add to target using following code snippet.
const project = config.modResults;
IOSConfig.XcodeUtils.addResourceFileToGroup({
filepath: destinationPath,
groupName: groupName,
isBuildFile: true,
project,
verbose: true,
});
- Fetches the Xcode project (
config.modResults
). - Uses
IOSConfig.XcodeUtils.addResourceFileToGroup
to: - Add the copied file to the Xcode group (
groupName
). - Mark it as a build resource, so it’s included in the final app package(copy bundle resources).
- Prints logs (
verbose: true
) to debug if necessary.
Before I find this addResourceFileToGroup
function, I burned few midnights while trying to modify Xcode project
object. It didn’t work.
Finally run npx expo prebuild
commad which will generate ios and android directories with the changes.
Final thoughts
It was pretty interesting doing this with limited resources available to refer. If you want to write a different plugin please dig in to the @expo/config-plugins
library bit and try to understand the functions available. The library is not documented anywhere and you cannot find everything you want expo documentation. Found this video useful though.
Hit on clap, share and comment to motivate me 😊