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 { Terminal } from '~/ui/components/Snippet'; 8 9Config 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 [app config](/workflow/configuration) can be helpful. As a library author, it allows you to configure native projects for the developers using your library automatically. 10 11This 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. 12 13## 1. Initialize a module 14 15Start 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: 16 17<Terminal cmd={['$ npx create-expo-module expo-native-configuration']} /> 18 19We will use the name `expo-native-configuration`/`ExpoNativeConfiguration` for the project. You can name it whatever you like. 20 21## 2. Set up our workspace 22 23In 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: 24 25<Terminal 26 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" 27 cmd={[ 28 '$ cd expo-native-configuration', 29 '$ rm ios/ExpoNativeConfigurationView.swift', 30 '$ rm android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationView.kt', 31 '$ rm src/ExpoNativeConfigurationView.tsx src/ExpoNativeConfiguration.types.ts', 32 '$ rm src/ExpoNativeConfigurationView.web.tsx src/ExpoNativeConfigurationModule.web.ts', 33 ]} 34/> 35 36We also need to find **ExpoNativeConfigurationModule.swift**, **ExpoNativeConfigurationModule.kt**, **src/index.ts** and **example/App.tsx** and replace them with the provided minimal boilerplate: 37 38```swift ios/ExpoNativeConfigurationModule.swift 39import ExpoModulesCore 40 41public class ExpoNativeConfigurationModule: Module { 42 public func definition() -> ModuleDefinition { 43 Name("ExpoNativeConfiguration") 44 45 Function("getApiKey") { () -> String in 46 "api-key" 47 } 48 } 49} 50``` 51 52```kotlin android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationModule.kt 53package expo.modules.nativeconfiguration 54 55import expo.modules.kotlin.modules.Module 56import expo.modules.kotlin.modules.ModuleDefinition 57 58class ExpoNativeConfigurationModule : Module() { 59 override fun definition() = ModuleDefinition { 60 Name("ExpoNativeConfiguration") 61 62 Function("getApiKey") { 63 return@Function "api-key" 64 } 65 } 66} 67``` 68 69```typescript src/index.ts 70import ExpoNativeConfigurationModule from './ExpoNativeConfigurationModule'; 71 72export function getApiKey(): string { 73 return ExpoNativeConfigurationModule.getApiKey(); 74} 75``` 76 77```typescript example/App.tsx 78import * as ExpoNativeConfiguration from 'expo-native-configuration'; 79import { Text, View } from 'react-native'; 80 81export default function App() { 82 return ( 83 <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> 84 <Text>API key: {ExpoNativeConfiguration.getApiKey()}</Text> 85 </View> 86 ); 87} 88``` 89 90## 3. Run the example project 91 92Now 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. 93 94<Terminal 95 cmdCopy="npm run build" 96 cmd={[ 97 '# Run this in the root of the project to start the TypeScript compiler', 98 '$ npm run build', 99 ]} 100/> 101 102In another terminal window, compile and run the example app: 103 104<Terminal 105 cmdCopy="cd example && npx expo run:ios" 106 cmd={[ 107 '$ cd example', 108 '# Run the example app on iOS', 109 '$ npx expo run:ios', 110 '# Run the example app on Android', 111 '$ npx expo run:android', 112 ]} 113/> 114 115We should see a screen with a text saying `API key: api-key`. Now let's develop the plugin to inject our custom API key. 116 117## 4. Creating a new config plugin 118 119Let'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. 120 121Here is an example of how a basic config plugin function looks: 122 123```javascript 124const withMyApiKey = config => { 125 return config; 126}; 127``` 128 129Additionally, 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 app config because it doesn't get serialized after the initial reading. This means you can use it to perform actions *during* code generation. 130 131However, there are a few considerations that we should follow when writing config plugins: 132 133- Plugins should be synchronous and their return value should be serializable, except for any `mods` that are added. 134- `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`. 135 136> 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). 137 138Let'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. 139 1401. Create a **plugin/tsconfig.json** file: 141 142 ```json plugin/tsconfig.json 143 { 144 "extends": "expo-module-scripts/tsconfig.plugin", 145 "compilerOptions": { 146 "outDir": "build", 147 "rootDir": "src" 148 }, 149 "include": ["./src"], 150 "exclude": ["**/__mocks__/*", "**/__tests__/*"] 151 } 152 ``` 153 1542. Create a **plugin/src/index.ts** file for our plugin: 155 156 ```typescript plugin/src/index.ts 157 import { ConfigPlugin } from 'expo/config-plugins'; 158 159 const withMyApiKey: ConfigPlugin = config => { 160 console.log('my custom plugin'); 161 return config; 162 }; 163 164 export default withMyApiKey; 165 ``` 166 1673. Finally, create an **app.plugin.js** file in the root directory. That will configure the entry file for our plugin: 168 169 ```javascript app.plugin.js 170 module.exports = require('./plugin/build'); 171 ``` 172 173At 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. 174 175```json example/app.json 176{ 177 "expo": { 178 ... 179 "plugins": ["../app.plugin.js"] 180 } 181} 182``` 183 184Now when running `npx expo prebuild` inside our **example** folder we should see our 'my custom plugin’ console.log statement in the terminal. 185 186<Terminal cmd={['$ cd example', '$ npx expo prebuild --clean']} /> 187 188To 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`. 189 190As 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: 191 192```typescript 193const withMyApiKey: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => { 194 config = withInfoPlist(config, config => { 195 config.modResults['MY_CUSTOM_API_KEY'] = apiKey; 196 return config; 197 }); 198 199 return config; 200}; 201``` 202 203Similarly, 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: 204 205```typescript 206const withMyApiKey: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => { 207 config = withAndroidManifest(config, config => { 208 const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults); 209 210 AndroidConfig.Manifest.addMetaDataItemToMainApplication( 211 mainApplication, 212 'MY_CUSTOM_API_KEY', 213 apiKey 214 ); 215 return config; 216 }); 217 218 return config; 219}; 220``` 221 222We can create our custom plugin by merging everything into a single function: 223 224```typescript plugin/src/index.ts 225import { 226 withInfoPlist, 227 withAndroidManifest, 228 AndroidConfig, 229 ConfigPlugin, 230} from 'expo/config-plugins'; 231 232const withMyApiKey: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => { 233 config = withInfoPlist(config, config => { 234 config.modResults['MY_CUSTOM_API_KEY'] = apiKey; 235 return config; 236 }); 237 238 config = withAndroidManifest(config, config => { 239 const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults); 240 241 AndroidConfig.Manifest.addMetaDataItemToMainApplication( 242 mainApplication, 243 'MY_CUSTOM_API_KEY', 244 apiKey 245 ); 246 return config; 247 }); 248 249 return config; 250}; 251 252export default withMyApiKey; 253``` 254 255Now 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: 256 257```json example/app.json 258{ 259 "expo": { 260 ... 261 "plugins": [["../app.plugin.js", { "apiKey": "custom_secret_api" }]] 262 } 263} 264``` 265 266After 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**. 267 268## 5. Reading native values from the module 269 270Now 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. 271 272On 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: 273 274```swift ios/ExpoNativeConfigurationModule.swift 275import ExpoModulesCore 276 277public class ExpoNativeConfigurationModule: Module { 278 public func definition() -> ModuleDefinition { 279 Name("ExpoNativeConfiguration") 280 281 Function("getApiKey") { 282 return Bundle.main.object(forInfoDictionaryKey: "MY_CUSTOM_API_KEY") as? String 283 } 284 } 285} 286``` 287 288On 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: 289 290```kotlin android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationModule.kt 291package expo.modules.nativeconfiguration 292 293import expo.modules.kotlin.modules.Module 294import expo.modules.kotlin.modules.ModuleDefinition 295import android.content.pm.PackageManager 296 297class ExpoNativeConfigurationModule() : Module() { 298 override fun definition() = ModuleDefinition { 299 Name("ExpoNativeConfiguration") 300 301 Function("getApiKey") { 302 val applicationInfo = appContext?.reactContext?.packageManager?.getApplicationInfo(appContext?.reactContext?.packageName.toString(), PackageManager.GET_META_DATA) 303 304 return@Function applicationInfo?.metaData?.getString("MY_CUSTOM_API_KEY") 305 } 306 } 307} 308``` 309 310## 6. Running your module 311 312With 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. 313 314<Terminal 315 cmdCopy="cd example && npx expo run:ios" 316 cmd={[ 317 '$ cd example', 318 '# execute our plugin and update native files', 319 '$ npx expo prebuild', 320 '# Run the example app on iOS', 321 '$ npx expo run:ios', 322 '# Run the example app on Android', 323 '$ npx expo run:android', 324 ]} 325/> 326 327## Next steps 328 329Congratulations, you have created a simple yet non-trivial config plugin that interacts with an Expo module for Android and iOS! 330 331If 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. 332