1--- 2title: 'Tutorial: Create a module with a config plugin' 3sidebar_title: Create a module with a config plugin 4description: A tutorial on creating a native module with a config plugin using Expo modules API. 5--- 6 7import { CodeBlocksTable } from '~/components/plugins/CodeBlocksTable'; 8import { PlatformTag } from '~/ui/components/Tag'; 9import { APIBox } from '~/components/plugins/APIBox'; 10import { Terminal } from '~/ui/components/Snippet'; 11 12Config plugins allow you to customize native Android and iOS projects when they are generated with `npx expo prebuild`. It is often useful to add properties in native config files, to copy assets to native projects, and for advanced configurations such as adding an app extension target. As an app developer, applying customizations not exposed in the default [Expo config](/workflow/configuration) can be helpful. As a library author, it allows you to configure native projects for the developers using your library automatically. 13 14This guide will walk you through creating a new config plugin from scratch and show you how to read custom values injected into **AndroidManifest.xml** and **Info.plist** by your plugin from an Expo module. 15 16## 1. Initialize a module 17 18Start by initializing a new Expo module project using `create-expo-module`, which will provide scaffolding for Android, iOS, and TypeScript. It will also provide an example project to interact with the module from within an app. Run the following command to initialize it: 19 20<Terminal cmd={['$ npx create-expo-module expo-native-configuration']} /> 21 22We will use the name `expo-native-configuration`/`ExpoNativeConfiguration` for the project. You can name it whatever you like. 23 24## 2. Set up our workspace 25 26In our example, we won't need the view module included by `create-expo-module`. Let's clean up the default module a little bit with the following command: 27 28<Terminal 29 cmdCopy="cd expo-native-configuration && rm ios/ExpoNativeConfigurationView.swift && rm android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationView.kt && rm src/ExpoNativeConfigurationView.tsx src/ExpoNativeConfiguration.types.ts && rm src/ExpoNativeConfigurationView.web.tsx src/ExpoNativeConfigurationModule.web.ts" 30 cmd={[ 31 '$ cd expo-native-configuration', 32 '$ rm ios/ExpoNativeConfigurationView.swift', 33 '$ rm android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationView.kt', 34 '$ rm src/ExpoNativeConfigurationView.tsx src/ExpoNativeConfiguration.types.ts', 35 '$ rm src/ExpoNativeConfigurationView.web.tsx src/ExpoNativeConfigurationModule.web.ts', 36 ]} 37/> 38 39We also need to find **ExpoNativeConfigurationModule.swift**, **ExpoNativeConfigurationModule.kt**, **src/index.ts** and **example/App.tsx** and replace them with the provided minimal boilerplate: 40 41```swift ios/ExpoNativeConfigurationModule.swift 42import ExpoModulesCore 43 44public class ExpoNativeConfigurationModule: Module { 45 public func definition() -> ModuleDefinition { 46 Name("ExpoNativeConfiguration") 47 48 Function("getApiKey") { () -> String in 49 "api-key" 50 } 51 } 52} 53``` 54 55```kotlin android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationModule.kt 56package expo.modules.nativeconfiguration 57 58import expo.modules.kotlin.modules.Module 59import expo.modules.kotlin.modules.ModuleDefinition 60 61class ExpoNativeConfigurationModule : Module() { 62 override fun definition() = ModuleDefinition { 63 Name("ExpoNativeConfiguration") 64 65 Function("getApiKey") { 66 return@Function "api-key" 67 } 68 } 69} 70``` 71 72```typescript src/index.ts 73import ExpoNativeConfigurationModule from './ExpoNativeConfigurationModule'; 74 75export function getApiKey(): string { 76 return ExpoNativeConfigurationModule.getApiKey(); 77} 78``` 79 80```typescript example/App.tsx 81import * as ExpoNativeConfiguration from 'expo-native-configuration'; 82import { Text, View } from 'react-native'; 83 84export default function App() { 85 return ( 86 <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> 87 <Text>API key: {ExpoNativeConfiguration.getApiKey()}</Text> 88 </View> 89 ); 90} 91``` 92 93## 3. Run the example project 94 95Now let's run the example project to make sure everything is working. Start the TypeScript compiler to watch for changes and rebuild the module JavaScript. 96 97<Terminal 98 cmdCopy="npm run build" 99 cmd={[ 100 '# Run this in the root of the project to start the TypeScript compiler', 101 '$ npm run build', 102 ]} 103/> 104 105In another terminal window, compile and run the example app: 106 107<Terminal 108 cmdCopy="cd example && npx expo run:ios" 109 cmd={[ 110 '$ cd example', 111 '# Run the example app on iOS', 112 '$ npx expo run:ios', 113 '# Run the example app on Android', 114 '$ npx expo run:android', 115 ]} 116/> 117 118We should see a screen with a text saying `API key: api-key`. Now let's develop the plugin to inject our custom API key. 119 120## 4. Creating a new config plugin 121 122Let's start developing our new config plugin. Plugins are synchronous functions that accept an `ExpoConfig` and return a modified `ExpoConfig`. By convention, these functions are prefixed by the word `with`. We will name our plugin `withMyApiKey`. Feel free to call it whatever you like, as long as it follows the convention. 123 124Here is an example of how a basic config plugin function looks: 125 126```javascript 127const withMyApiKey = config => { 128 return config; 129}; 130``` 131 132Additionally, you can use `mods`, which are async functions that modify files in native projects such as source code or configuration (plist, xml) files. The `mods` object is different from the rest of the Expo config because it doesn't get serialized after the initial reading. This means you can use it to perform actions *during* code generation. 133 134However, there are a few considerations that we should follow when writing config plugins: 135 136- Plugins should be synchronous and their return value should be serializable, except for any `mods` that are added. 137- `plugins` are always invoked when the config is read by the `expo/config` method `getConfig`. However, `mods` are only invoked during the "syncing" phase of `npx expo prebuild`. 138 139> Although not required, we can use [`expo-module-scripts`](https://www.npmjs.com/package/expo-module-scripts) to make plugin development easier — it provides a recommended default configuration for TypeScript and Jest. For more information, see [config plugins guide](https://github.com/expo/expo/tree/main/packages/expo-module-scripts#-config-plugin). 140 141Let's start by creating our plugin with this minimal boilerplate. This will create a **plugin** folder where we will write the plugin in TypeScript and add a **app.plugin.js** file in the project root, which will be the entry file for the plugin. 142 1431. Create a **plugin/tsconfig.json** file: 144 145 ```json plugin/tsconfig.json 146 { 147 "extends": "expo-module-scripts/tsconfig.plugin", 148 "compilerOptions": { 149 "outDir": "build", 150 "rootDir": "src" 151 }, 152 "include": ["./src"], 153 "exclude": ["**/__mocks__/*", "**/__tests__/*"] 154 } 155 ``` 156 1572. Create a **plugin/src/index.ts** file for our plugin: 158 159 ```typescript plugin/src/index.ts 160 import { ConfigPlugin } from 'expo/config-plugins'; 161 162 const withMyApiKey: ConfigPlugin = config => { 163 console.log('my custom plugin'); 164 return config; 165 }; 166 167 export default withMyApiKey; 168 ``` 169 1703. Finally, create an **app.plugin.js** file in the root directory. That will configure the entry file for our plugin: 171 172 ```javascript app.plugin.js 173 module.exports = require('./plugin/build'); 174 ``` 175 176At the root of your project, run `npm run build plugin` to start the TypeScript compiler in watch mode. The only thing left to configure is our example project to use our plugin. We can achieve this by adding the following line to the **example/app.json** file. 177 178```json example/app.json 179{ 180 "expo": { 181 ... 182 "plugins": ["../app.plugin.js"] 183 } 184} 185``` 186 187Now when running `npx expo prebuild` inside our **example** folder we should see our 'my custom plugin’ console.log statement in the terminal. 188 189<Terminal cmd={['$ cd example', '$ npx expo prebuild --clean']} /> 190 191To inject our custom API keys into **AndroidManifest.xml** and **Info.plist** we can use a few helper [`mods` provided by `expo/config-plugins`](/config-plugins/plugins-and-mods/#what-are-mods), which makes it easy to modify native files. In our example, we will use two of them, `withAndroidManifest` and `withInfoPlist`. 192 193As the name suggests, `withInfoPlist` allows us to read and modify **Info.plist** values. Using the `modResults` property, we can add custom values as demonstrated in the code snippet below: 194 195```typescript 196const withMyApiKey: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => { 197 config = withInfoPlist(config, config => { 198 config.modResults['MY_CUSTOM_API_KEY'] = apiKey; 199 return config; 200 }); 201 202 return config; 203}; 204``` 205 206Similarly, we can use `withAndroidManifest` to modify the **AndroidManifest.xml** file. In this case, we will utilize `AndroidConfig` helpers to add a meta data item to the main application: 207 208```typescript 209const withMyApiKey: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => { 210 config = withAndroidManifest(config, config => { 211 const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults); 212 213 AndroidConfig.Manifest.addMetaDataItemToMainApplication( 214 mainApplication, 215 'MY_CUSTOM_API_KEY', 216 apiKey 217 ); 218 return config; 219 }); 220 221 return config; 222}; 223``` 224 225We can create our custom plugin by merging everything into a single function: 226 227```typescript plugin/src/index.ts 228import { 229 withInfoPlist, 230 withAndroidManifest, 231 AndroidConfig, 232 ConfigPlugin, 233} from 'expo/config-plugins'; 234 235const withMyApiKey: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => { 236 config = withInfoPlist(config, config => { 237 config.modResults['MY_CUSTOM_API_KEY'] = apiKey; 238 return config; 239 }); 240 241 config = withAndroidManifest(config, config => { 242 const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults); 243 244 AndroidConfig.Manifest.addMetaDataItemToMainApplication( 245 mainApplication, 246 'MY_CUSTOM_API_KEY', 247 apiKey 248 ); 249 return config; 250 }); 251 252 return config; 253}; 254 255export default withMyApiKey; 256``` 257 258Now with the plugin ready to be used, let's update the example app to pass our API key to the plugin as a configuration option. Modify the `"plugins"` field in **example/app.json** as shown below: 259 260```json example/app.json 261{ 262 "expo": { 263 ... 264 "plugins": [["../app.plugin.js", { "apiKey": "custom_secret_api" }]] 265 } 266} 267``` 268 269After making this change, we can test that the plugin is working correctly by running the command `npx expo prebuild --clean` inside the **example** folder. This will execute our plugin and update native files, injecting "MY_CUSTOM_API_KEY" into **AndroidManifest.xml** and **Info.plist**. You can verify this by checking the contents of **example/android/app/src/main/AndroidManifest.xml**. 270 271## 5. Reading native values from the module 272 273Now let's make our native module read the fields we added to **AndroidManifest.xml** and **Info.plist**. This can be done by using platform-specific methods to access the contents of these files. 274 275On iOS, we can read the content of an **Info.plist** property by using the `Bundle.main.object(forInfoDictionaryKey: "")` instance Method. To read the `"MY_CUSTOM_API_KEY"` value that we added earlier, update the **ios/ExpoNativeConfigurationModule.swift** file: 276 277```swift ios/ExpoNativeConfigurationModule.swift 278import ExpoModulesCore 279 280public class ExpoNativeConfigurationModule: Module { 281 public func definition() -> ModuleDefinition { 282 Name("ExpoNativeConfiguration") 283 284 Function("getApiKey") { 285 return Bundle.main.object(forInfoDictionaryKey: "MY_CUSTOM_API_KEY") as? String 286 } 287 } 288} 289``` 290 291On Android, we can access metadata information from the **AndroidManifest.xml** file using the `packageManager` class. To read the `"MY_CUSTOM_API_KEY"` value, update the **android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationModule.kt** file: 292 293```kotlin android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationModule.kt 294package expo.modules.nativeconfiguration 295 296import expo.modules.kotlin.modules.Module 297import expo.modules.kotlin.modules.ModuleDefinition 298import android.content.pm.PackageManager 299 300class ExpoNativeConfigurationModule() : Module() { 301 override fun definition() = ModuleDefinition { 302 Name("ExpoNativeConfiguration") 303 304 Function("getApiKey") { 305 val applicationInfo = appContext?.reactContext?.packageManager?.getApplicationInfo(appContext?.reactContext?.packageName.toString(), PackageManager.GET_META_DATA) 306 307 return@Function applicationInfo?.metaData?.getString("MY_CUSTOM_API_KEY") 308 } 309 } 310} 311``` 312 313## 6. Running your module 314 315With our native modules reading the fields we added to the native files, we can now run the example app and access our custom API key through the `ExamplePlugin.getApiKey()` function. 316 317<Terminal 318 cmdCopy="cd example && npx expo run:ios" 319 cmd={[ 320 '$ cd example', 321 '# execute our plugin and update native files', 322 '$ npx expo prebuild', 323 '# Run the example app on iOS', 324 '$ npx expo run:ios', 325 '# Run the example app on Android', 326 '$ npx expo run:android', 327 ]} 328/> 329 330## Next steps 331 332Congratulations, you have created a simple yet non-trivial config plugin that interacts with an Expo module for Android and iOS! 333 334If you want to challenge yourself and make the plugin more versatile we leave this exercise open to you. Try modifying the plugin to allow for any arbitrary set of config keys/values to be passed in and adding the functionality to allow for the reading of arbitrary keys from the module. 335