Writing custom expo plugin to integrate a third party library.

Krishan Madushanka
7 min readFeb 4, 2025

--

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 at src/main/assets (This should be at the same level as your java and res 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 using withPlugins, ensuring that withAndroidConfigFile gets executed.
  • We have passed src and dest to withAndroidConfigFile 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 😊

--

--

Krishan Madushanka
Krishan Madushanka

Written by Krishan Madushanka

Software Engineer | Android | iOS | Flutter | React Native | IoT | aws certified

No responses yet