MetaMask / MetaMask/metamask-mobile
Expo CNG Support
- Dominant language
- TypeScript
- Stars
- 3k
- Forks
- 1.7k
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 669
Description
# MetaMask Mobile CNG Migration Analysis
This document analyzes what would be required to adopt Expo CNG (Continuous Native Generation) for the MetaMask Mobile project.
## What is Expo CNG?
**Expo CNG (Continuous Native Generation)** is a workflow where:
1. **Native directories (`ios/` and `android/`) are not committed to git** - Instead, they're generated on-demand
2. **Configuration-driven** - Native code is generated from `app.config.js` and config plugins
3. **Regenerated as needed** - Using `npx expo prebuild` to create or update native code
### Benefits of CNG
- Cleaner git history (no native code diffs to review)
- Easier React Native upgrades (regenerate rather than migrate)
- Portable projects (config plugins handle native modifications)
- Simplified native dependency management
---
## Executive Summary
| Metric | Assessment |
|--------|------------|
| **CNG Feasibility** | π’ Achievable |
| **Estimated Effort** | 7-10 weeks ([see detailed table](#migration-effort-estimate)) |
| **Risk Level** | Medium during migration |
| **Blocking Issues** | **None** - all have solutions |
| **CI/CD Impact** | **Minimal** - keep existing Bitrise |
| **Recommendation** | Proceed with gradual migration |
This is an **achievable undertaking**. All initially-identified blockers have Expo/EAS solutions:
| Initially Identified | Status | Expo Solution |
|---------------------|--------|---------------|
| Product flavors | π’ Solvable | [App Variants](https://docs.expo.dev/build-reference/variants/) |
| 11 signing configs | π’ Solvable | [EAS Credentials](https://docs.expo.dev/app-signing/managed-credentials/) |
| Multiple Xcode targets | π’ Solvable | App Variants + EAS profiles |
| Bundled Branch SDK | π’ Solvable | Use existing `react-native-branch` |
| Custom AAR libraries | π’ Solvable | Config plugin with `withDangerousMod` |
---
## Current State
The project currently uses a **hybrid approach**:
- Native directories (`ios/`, `android/`) are committed to git
- Some Expo config plugins are already in use
- Build scripts directly interact with native projects (`xcodebuild`, `gradlew`)
### Existing Config Plugins
```javascript
// app.config.js
plugins: [
['expo-build-properties', { ... }],
['@config-plugins/detox', { ... }],
'expo-apple-authentication',
['expo-screen-orientation', { ... }],
]
```
---
## Native Code Analysis
### 1. Custom Native Modules
These would need to be converted to **Expo Modules** or have config plugins written:
#### iOS Modules
| Module | Location | Complexity | Solution |
|--------|----------|------------|----------|
| `RCTMinimizer` | `ios/MetaMask/NativeModules/RCTMinimizer/` | π’ Low | Convert to Expo Module |
| `RCTScreenshotDetect` | `ios/RCTScreenshotDetect.m` | π’ Low | Convert to Expo Module |
| `RNTar` | `ios/RnTar.swift` | π‘ Medium | **Explore removal** (see note below) |
#### Android Modules
| Module | Location | Complexity | Solution |
|--------|----------|------------|----------|
| `PreventScreenshot` | `android/.../nativeModules/PreventScreenshot.java` | π’ Low | Convert to Expo Module |
| `RCTMinimizer` | `android/.../nativeModules/RCTMinimizer.java` | π’ Low | Convert to Expo Module |
| `RNTar` | `android/.../nativeModules/RNTar/` | π’ Low | **Explore removal** (see note below) |
| `NotificationModule` | `android/.../nativeModules/NotificationModule.kt` | π’ Low | Convert to Expo Module |
| `SplashActivity` | `android/.../SplashActivity.java` | π‘ Medium | Use `expo-splash-screen` + config plugin |
> **π Note on RNTar**: This module may no longer be needed. Currently referenced in `app/core/Snaps/location/npm.ts` for extracting Snap packages from tgz files. Before CNG migration, explore whether:
> 1. This functionality is still actively used
> 2. It can be replaced with a JS-based solution (e.g., `pako` + `tar-stream`)
> 3. An existing npm package like `react-native-zip-archive` could replace it
>
> Removing `RNTar` would also eliminate the `GzipSwift` iOS dependency.
#### NativeModules Usage in App Code
These are all the `NativeModules.*` references found in the app:
| Module | Source | Status |
|--------|--------|--------|
| `NativeModules.CommunicationClient` | `nativesdk.aar` | β οΈ Keep (AAR config plugin) |
| `NativeModules.Aes` | `react-native-aes-crypto` | β
npm package (autolinks) |
| `NativeModules.AesForked` | `react-native-aes-crypto-forked` | β
npm package (autolinks) |
| `NativeModules.ScreenshotDetect` | Custom `RCTScreenshotDetect` | β οΈ Convert to Expo Module |
| `NativeModules.NotificationModule` | Custom `NotificationModule` | β οΈ Convert to Expo Module |
| `NativeModules.RNTar` | Custom `RNTar` | β Explore removal |
| `NativeModules.RCTDeviceEventEmitter` | React Native built-in | β
Standard module |
---
### 2. π’ SOLVABLE: Custom AAR Libraries
```
android/libs/
βββ ecies.aar # ECIES encryption (dependency of nativesdk)
βββ nativesdk.aar # MetaMask Native SDK for Android
```
#### What These AARs Do
**`nativesdk.aar`** - MetaMask SDK for Android native integration:
- `NativeSDKPackage` - React Native package (registered in `MainApplication.kt`)
- `MessageService` - Android service for IPC with external dApps
- `CommunicationClient` - Native module used by `AndroidService.ts`
- Enables external Android apps to connect to MetaMask wallet
**`ecies.aar`** - ECIES encryption library:
- Internal dependency of `nativesdk.aar`
- Used for encrypted communication between dApps and wallet
- No direct usage in app code (JS layer uses `eciesjs` npm package)
#### Solution: Config Plugin
**1. Create the plugin (`plugins/withMetaMaskAAR.ts`):**
```typescript
import {
ConfigPlugin,
withProjectBuildGradle,
withAppBuildGradle,
withDangerousMod,
} from '@expo/config-plugins';
import * as fs from 'fs';
import * as path from 'path';
const withMetaMaskAAR: ConfigPlugin = (config) => {
// Step 1: Copy AAR files to android/app/libs during prebuild
config = withDangerousMod(config, [
'android',
async (cfg) => {
const projectRoot = cfg.modRequest.projectRoot;
const libsDir = path.join(projectRoot, 'android', 'app', 'libs');
const sourceDir = path.join(projectRoot, 'plugins', 'assets');
// Create libs directory if it doesn't exist
if (!fs.existsSync(libsDir)) {
fs.mkdirSync(libsDir, { recursive: true });
}
// Copy AAR files
const aarFiles = ['ecies.aar', 'nativesdk.aar'];
for (const aar of aarFiles) {
fs.copyFileSync(
path.join(sourceDir, aar),
path.join(libsDir, aar)
);
}
return cfg;
},
]);
// Step 2: Add flatDir repository to project build.gradle
config = withProjectBuildGradle(config, (cfg) => {
if (!cfg.modResults.contents.includes('flatDir')) {
cfg.modResults.contents = cfg.modResults.contents.replace(
/allprojects\s*{\s*repositories\s*{/,
`allprojects {
repositories {
flatDir {
dirs 'libs'
}`
);
}
return cfg;
});
// Step 3: Add AAR dependencies to app build.gradle
config = withAppBuildGradle(config, (cfg) => {
if (!cfg.modResults.contents.includes('ecies.aar')) {
cfg.modResults.contents = cfg.modResults.contents.replace(
/dependencies\s*{/,
`dependencies {
implementation(files("libs/ecies.aar"))
implementation(files("libs/nativesdk.aar"))`
);
}
return cfg;
});
return config;
};
export default withMetaMaskAAR;
```
**2. Store AAR files in plugin assets:**
```
plugins/
βββ assets/
β βββ ecies.aar
β βββ nativesdk.aar
βββ withMetaMaskAAR.ts
```
**3. Register in `app.config.js`:**
```javascript
module.exports = {
plugins: [
'./plugins/withMetaMaskAAR',
// ... other plugins
],
};
```
**4. Run prebuild:**
```bash
npx expo prebuild --clean
# AAR files will be copied and build.gradle updated automatically
```
#### Alternative: Publish to Maven (Recommended for Production)
For a cleaner long-term solution, publish the AARs to a private Maven repository:
```javascript
// app.config.js
module.exports = {
plugins: [
[
'expo-build-properties',
{
android: {
extraMavenRepos: [
'https://maven.pkg.github.com/AriaMoradi/metamask-android-sdk',
],
},
},
],
],
};
```
Then add dependencies via config plugin:
```groovy
implementation 'io.metamask:nativesdk:1.0.0'
implementation 'io.metamask:ecies:1.0.0'
```
---
### 3. π’ SOLVABLE: Android Product Flavors β Expo App Variants
```gradle
// Current: android/app/build.gradle
productFlavors {
qa {
dimension "version"
applicationIdSuffix ".qa"
}
prod {
dimension "version"
}
flask {
dimension "version"
applicationIdSuffix ".flask"
}
}
```
**Solution**: Use [Expo App Variants](https://docs.expo.dev/build-reference/variants/) - a first-class feature for this exact use case.
#### Implementation
**1. Update `app.config.js`:**
```javascript
const IS_DEV = process.env.APP_VARIANT === 'development';
const IS_QA = process.env.APP_VARIANT === 'qa';
const IS_FLASK = process.env.APP_VARIANT === 'flask';
const getAppName = () => {
if (IS_DEV) return 'MetaMask (Dev)';
if (IS_QA) return 'MetaMask QA';
if (IS_FLASK) return 'MetaMask Flask';
return 'MetaMask';
};
const getBundleId = () => {
if (IS_QA) return 'io.metamask.qa';
if (IS_FLASK) return 'io.metamask.flask';
return 'io.metamask';
};
module.exports = {
name: getAppName(),
ios: {
bundleIdentifier: getBundleId(),
},
android: {
package: getBundleId(),
},
// ... rest of config
};
```
**2. Configure `eas.json`:**
```json
{
"build": {
"development": {
"developmentClient": true,
"env": { "APP_VARIANT": "development" }
},
"qa": {
"env": { "APP_VARIANT": "qa" }
},
"flask": {
"env": { "APP_VARIANT": "flask" }
},
"production": {
"env": { "APP_VARIANT": "production" }
}
}
}
```
**3. Build specific variant:**
```bash
# Build QA variant
eas build --profile qa --platform all
# Build Flask variant
eas build --profile flask --platform android
```
This approach is **cleaner than Android product flavors** because:
- Configuration is in JavaScript, not Gradle
- Works identically on iOS and Android
- Easy to add new variants
- No native code modifications needed
---
### 4. π’ SOLVABLE: Complex Signing Configurations
The project has **11 signing configurations** - these can be handled in multiple ways:
| Current Config | EAS Build Profile |
|----------------|-------------------|
| `mainProd` | `production` |
| `mainBeta` | `beta` |
| `mainRc` | `rc` |
| `mainTest` | `internal-test` |
| `mainE2e` | `e2e` |
| `mainExp` | `experimental` |
| `mainDev` | `development` |
| `flaskProd` | `flask-production` |
| `flaskE2e` | `flask-e2e` |
| `flaskTest` | `flask-test` |
| `flaskDev` | `flask-development` |
| `qaProd` | `qa-production` |
**Solution**: Two options available:
#### Option A: Keep Current Signing (Recommended for existing CI/CD)
Your current signing setup in `build.gradle` continues to work! CNG generates the `build.gradle` file, and you can use a config plugin to add your signing configurations:
```typescript
// plugins/withMetaMaskSigning.ts
const withMetaMaskSigning: ConfigPlugin = (config) => {
return withAppBuildGradle(config, (cfg) => {
// Add your existing signing configs via string replacement
// The same 11 signing configs you have now
return cfg;
});
};
```
Your CI/CD continues to use:
- Keystores stored in CI secrets
- Environment variables for passwords
- Same `gradlew` commands
#### Option B: EAS Build Credentials (If migrating to EAS Build)
If you choose to use EAS Build in the future, it can manage credentials automatically.
#### Current Approach (No Change Needed)
Since you're keeping your existing CI/CD:
1. **Config plugin** adds signing configs to generated `build.gradle`
2. **Keystores** remain in your CI secrets
3. **Environment variables** work the same way
4. **Build commands** stay the same (`./gradlew assembleRelease`)
The signing configuration is just Gradle config - CNG doesn't change how signing works, it just generates the base `build.gradle` that you then customize via config plugins.
---
### 5. π’ SOLVABLE: Bundled Branch SDK β Use react-native-branch
```
ios/branch-ios-sdk/ # 400+ files bundled directly (can be removed)
```
**Current State**: Branch SDK is bundled directly in `ios/branch-ios-sdk/` instead of using the npm package.
**Solution**: `react-native-branch` is already in `package.json` dependencies!
1. Remove bundled `ios/branch-ios-sdk/` directory
2. Ensure `react-native-branch` is properly linked (handled by autolinking)
3. Configure Branch keys via `app.config.js`:
```javascript
// app.config.js
module.exports = {
plugins: [
[
'@config-plugins/react-native-branch',
{
apiKey: process.env.MM_BRANCH_KEY_LIVE,
iosAppDomain: 'metamask.app.link',
},
],
],
};
```
This is a cleanup task, not a blocker.
---
### 6. iOS Customizations
#### Info.plist Customizations
The following would need a config plugin:
```xml
CFBundleURLTypes
CFBundleURLSchemes
ethereum
metamask
dapp
wc
expo-metamask
UIAppFonts
Entypo.ttf
Metamask.ttf
MM Poly Regular.otf
UIBackgroundModes
fetch
remote-notification
branch_key
branch_universal_link_domains
NSBluetoothAlwaysUsageDescription
NSCameraUsageDescription
NSFaceIDUsageDescription
NSLocationWhenInUseUsageDescription
NSMicrophoneUsageDescription
NSPhotoLibraryUsageDescription
```
#### Entitlements
```xml
aps-environment
com.apple.developer.applesignin
com.apple.developer.associated-domains
com.apple.developer.in-app-payments
```
#### Multiple Xcode Targets β Expo App Variants (Solvable)
| Current Target | Expo Variant |
|----------------|--------------|
| MetaMask | `production` |
| MetaMask-QA | `qa` |
| MetaMask-Flask | `flask` |
**Solution**: Use [Expo App Variants](https://docs.expo.dev/build-reference/variants/) with EAS Build profiles.
Each variant gets its own:
- Bundle identifier (e.g., `io.metamask`, `io.metamask.qa`, `io.metamask.flask`)
- App name
- App icon (via variant-specific asset configuration)
- Entitlements (can be configured per variant in `app.config.js`)
#### Podfile Customizations
```ruby
# Custom pods
pod 'ReactNativePayments'
pod 'FirebaseCore', :modular_headers => true
pod 'Permission-BluetoothPeripheral'
pod 'GzipSwift'
pod 'OpenSSL-Universal', :modular_headers => true
# Post-install hooks
post_install do |installer|
# RCT-Folly modifications
# Code signing configuration
end
```
---
### 7. Android Customizations
#### AndroidManifest.xml
```xml
```
#### JNI/C++ Code
```
android/app/src/main/jni/
βββ CMakeLists.txt
βββ MainApplicationModuleProvider.cpp
βββ MainApplicationModuleProvider.h
βββ MainApplicationTurboModuleManagerDelegate.cpp
βββ MainApplicationTurboModuleManagerDelegate.h
βββ MainComponentsRegistry.cpp
βββ MainComponentsRegistry.h
βββ OnLoad.cpp
```
**Status**: This is New Architecture boilerplate - may be auto-generated by newer Expo versions.
#### build.gradle Customizations
- Sentry integration with custom configuration
- ProGuard rules including Detox rules
- Custom packaging options for native libraries
- Environment-based build config fields
- NDK path configuration for CI
---
## Dependencies Analysis
The project has **110+ `react-native-*` packages**. Key ones requiring config plugins:
| Package | Has Config Plugin? | Action Needed |
|---------|-------------------|---------------|
| `react-native-branch` | β οΈ Partial | Need custom plugin |
| `react-native-ble-plx` | β
Yes | Use existing |
| `react-native-keychain` | β οΈ Partial | May need enhancement |
| `react-native-quick-crypto` | β No | Need custom plugin |
| `react-native-vision-camera` | β
Yes | Use existing |
| `@react-native-firebase/app` | β
Yes | Use existing |
| `@react-native-firebase/messaging` | β
Yes | Use existing |
| `react-native-permissions` | β
Yes | Use existing |
| `react-native-mmkv` | β
Yes | Use existing |
| `react-native-video` | β οΈ Partial | May need enhancement |
| `react-native-share` | β
Yes | Use existing |
| `react-native-device-info` | β
Yes | Use existing |
---
## Required Config Plugins
| Plugin | Purpose |
|--------|---------|
| `withMetaMaskInfoPlist` | iOS Info.plist (URL schemes, permissions, Branch keys) |
| `withMetaMaskEntitlements` | iOS entitlements (release + debug) |
| `withMetaMaskAndroidManifest` | Android manifest (deep links, services) |
| `withMetaMaskBuildGradle` | Android build config (Sentry, ProGuard, signing) |
| `withMetaMaskAAR` | Copy AAR libraries |
**Handled automatically** (no custom plugins needed):
- App Variants β [Expo App Variants](https://docs.expo.dev/build-reference/variants/)
- Fonts β Expo handles automatically
- Firebase β `@react-native-firebase` has plugins
- Branch β `react-native-branch` autolinks
- Detox β Already using `@config-plugins/detox`
- Privacy Manifest β Expo aggregates automatically
---
## Additional Items (Auto-handled or Minor)
### iOS Items
| Item | Location | CNG Handling |
|------|----------|--------------|
| `PrivacyInfo.xcprivacy` | `ios/MetaMask/` | β
Auto-aggregated by Expo |
| `MetaMaskDebug.entitlements` | `ios/MetaMask/` | β οΈ Include in `withMetaMaskEntitlements` |
| `debug.xcconfig` / `release.xcconfig` | `ios/` | β
Replace with EAS secrets + `app.config.js` |
| `LaunchScreen.xib` | `ios/MetaMask/Base.lproj/` | β
Use `expo-splash-screen` |
| App Icons (3 variants) | `ios/MetaMask/Images.xcassets/` | β
Configure in `app.config.js` per variant |
| Multiple Xcode schemes | `ios/MetaMask.xcodeproj/xcshareddata/` | β
Replaced by App Variants |
### Android Items
| Item | Location | CNG Handling |
|------|----------|--------------|
| `styles.xml` (AppTheme, SplashTheme) | `android/app/src/main/res/values/` | β
Use `expo-splash-screen` config |
| `colors.xml` | `android/app/src/main/res/values/` | β
Expo generates standard colors |
| Night mode resources | `android/app/src/main/res/values-night/` | β οΈ May need config plugin for dark theme |
| `react_native_config.xml` | `android/app/src/main/res/xml/` | β
Expo generates network security config |
| `filepaths.xml` | `android/app/src/main/res/xml/` | β οΈ Include in `withMetaMaskAndroidManifest` |
| `proguard-rules.pro` | `android/app/` | β οΈ Include in `withMetaMaskBuildGradle` |
| Notification icons | `android/app/src/main/res/mipmap-*/` | β
Configure via `expo-notifications` |
| `DetoxTest.java` | `android/app/src/androidTest/` | β
Handled by `@config-plugins/detox` |
| `google-services.json` | `android/app/` | β
Handled by `@react-native-firebase` plugin |
| `SplashActivity.java` | `android/app/src/main/java/` | β
Use `expo-splash-screen` |
### Environment Variables (xcconfig β EAS)
Current xcconfig files contain:
```
MM_FOX_CODE=...
MM_BRANCH_KEY_TEST=...
MM_BRANCH_KEY_LIVE=...
MM_MIXPANEL_TOKEN=...
MM_FLASK_MIXPANEL_TOKEN=...
```
**Solution**: Migrate to EAS Secrets + `app.config.js`:
```javascript
// app.config.js
module.exports = {
ios: {
infoPlist: {
fox_code: process.env.MM_FOX_CODE,
mixpanel_token: process.env.MM_MIXPANEL_TOKEN,
},
},
};
```
```bash
# Set in EAS
eas secret:create --name MM_FOX_CODE --value "..."
eas secret:create --name MM_BRANCH_KEY_LIVE --value "..."
```
### Example Config Plugin
```typescript
// plugins/withMetaMaskInfoPlist.ts
import { ConfigPlugin, withInfoPlist } from '@expo/config-plugins';
const withMetaMaskInfoPlist: ConfigPlugin = (config) => {
return withInfoPlist(config, (cfg) => {
// URL Schemes
cfg.modResults.CFBundleURLTypes = [
{
CFBundleURLSchemes: ['ethereum', 'metamask', 'dapp', 'wc', 'expo-metamask'],
},
];
// Branch configuration
cfg.modResults.branch_key = {
live: '$(MM_BRANCH_KEY_LIVE)',
test: '$(MM_BRANCH_KEY_TEST)',
};
cfg.modResults.branch_universal_link_domains = [
'metamask.app.link',
'link.metamask.io',
'link-test.metamask.io',
'metamask-alternate.app.link',
'metamask.test.app.link',
'metamask-alternate.test.app.link',
];
// Background modes
cfg.modResults.UIBackgroundModes = ['fetch', 'remote-notification'];
return cfg;
});
};
export default withMetaMaskInfoPlist;
```
---
## Migration Effort Estimate
| # | Task | Category | Effort | Priority |
|---|------|----------|--------|----------|
| 1 | Convert native modules to Expo Modules | Native | 2-3 weeks | π΄ High |
| 2 | Create `withMetaMaskInfoPlist` plugin | Native | 2-3 days | π΄ High |
| 3 | Create `withMetaMaskEntitlements` plugin | Native | 1 day | π΄ High |
| 4 | Create `withMetaMaskAndroidManifest` plugin | Native | 2-3 days | π΄ High |
| 5 | Create `withMetaMaskBuildGradle` plugin | Native | 3-4 days | π΄ High |
| 6 | Create `withMetaMaskAAR` plugin | Native | 2-3 days | π΄ High |
| 7 | App Variants setup (`app.config.js`) | Native | 3-4 days | π΄ High |
| 8 | Configure existing library plugins | Native | 1 week | π‘ Medium |
| 9 | Update build scripts (add `expo prebuild`) | Non-Native | 2-3 days | π΄ High |
| 10 | Verify Metro config compatibility | Non-Native | 1 day | π΄ High |
| 11 | Verify patches still apply | Non-Native | 1 day | π‘ Medium |
| 12 | Verify LavaMoat compatibility | Non-Native | 1 day | π‘ Medium |
| 13 | Update Detox binary paths (if needed) | Non-Native | 1 day | π‘ Medium |
| 14 | Testing & validation (all variants) | Testing | 2-3 weeks | π΄ High |
| | **TOTAL** | | **7-10 weeks** | |
### Key Simplifications
- β
**Keep existing CI/CD** (Bitrise) - just add `expo prebuild` step
- β
**Keep existing signing** - add via config plugin
- β
**Keep existing build commands** - `xcodebuild`, `gradlew`
- β
**OTA updates** - already Expo-compatible
- β
**Most npm packages** - autolink without changes
---
## Blocking Issues Summary
| Issue | Severity | Workaround Available? | Notes |
|-------|----------|----------------------|-------|
| Custom AAR libraries | π’ Solvable | β
Yes | Config plugin with `withDangerousMod` |
| Product flavors (3) | π’ Solvable | β
Native | [Expo App Variants](https://docs.expo.dev/build-reference/variants/) |
| Signing configs (11) | π’ Solvable | β
Native | [EAS Build Credentials](https://docs.expo.dev/app-signing/managed-credentials/) |
| Bundled Branch SDK | π’ Solvable | β
Yes | Already have `react-native-branch` in dependencies |
| Multiple Xcode targets | π’ Solvable | β
Native | Expo App Variants + EAS profiles |
| JNI/C++ code | π’ Auto | β
Yes | Auto-generated by Expo |
**π No blocking issues!** All originally-identified blockers have clear solutions.
---
## Recommended Approach
### Option A: Full CNG Migration
**Not Recommended at this time**
- 3-5 months of dedicated work
- High risk during migration
- Requires significant testing across all build variants
- Benefit: Cleaner upgrades long-term
### Option B: Hybrid Enhancement β (Recommended)
Continue current approach but incrementally improve:
1. **Document all native modifications** in a central location
2. **Create config plugins** for reproducible changes (can be used even without full CNG)
3. **Convert native modules** to Expo Modules format
4. **Validate with prebuild** periodically to track drift
### Option C: Gradual Migration
A phased approach over 6-12 months:
| Phase | Duration | Goal |
|-------|----------|------|
| **Phase 1** | 2-3 weeks | Convert native modules to Expo Modules (keep native dirs) |
| **Phase 2** | 4-6 weeks | Create config plugins for current customizations |
| **Phase 3** | 2-3 weeks | Test with `expo prebuild --clean` to validate |
| **Phase 4** | 2-3 weeks | Remove native directories, go full CNG |
---
## 8. Non-Native Considerations
### Metro Configuration β οΈ
Heavily customized `metro.config.js` with LavaMoat lockdown, Node.js polyfills, custom resolvers. **Should work** but verify after prebuild.
### Babel Configuration β
Custom plugins (React Compiler, Reanimated). **No changes needed** - separate from native code.
### Patches (34 files) β οΈ
Yarn patches for various packages. **Should continue to work** - verify after prebuild.
### Build Scripts β οΈ
Update `build.sh` and `setup.mjs` to include `expo prebuild` step:
```bash
# Updated CI/CD workflow:
1. yarn install
2. npx expo prebuild --clean # NEW: Generate ios/ and android/
3. cd ios && pod install
4. xcodebuild / gradlew # Same as before
```
### OTA Updates β
Already Expo-compatible via `ota.config.js`. **No changes needed**.
### Detox E2E Testing β οΈ
Hardcoded binary paths in `.detoxrc.js` may need updating if build output locations change.
### LavaMoat Security β οΈ
Verify `@lavamoat/react-native-lockdown` works after CNG migration.
### CI/CD (Bitrise) β
**Keep using it!** CNG does NOT require EAS Build. Just add `expo prebuild` step.
### Sentry β
Use `@sentry/react-native` config plugin. **Has built-in support**.
---
## Validation Script
To check if the project is ready for CNG, run:
```bash
# 1. Backup current native directories
cp -r ios ios-backup
cp -r android android-backup
# 2. Run prebuild
npx expo prebuild --clean
# 3. Compare generated vs original
diff -r ios/ ios-backup/ > ios-diff.txt
diff -r android/ android-backup/ > android-diff.txt
# 4. Review differences
# Each difference represents something that needs a config plugin
```
---
## Key Native Dependencies (70+ packages)
Most `react-native-*` packages autolink automatically. These are the ones that may need attention:
### Packages with Known Config Plugins β
| Package | Config Plugin |
|---------|---------------|
| `@react-native-firebase/app` | Has built-in plugin |
| `@react-native-firebase/messaging` | Has built-in plugin |
| `react-native-permissions` | Has built-in plugin |
| `react-native-vision-camera` | Has built-in plugin |
| `react-native-ble-plx` | Has built-in plugin |
| `react-native-branch` | Autolinks, manual config for keys |
| `expo-screen-orientation` | Already in `app.config.js` |
| `expo-apple-authentication` | Already in `app.config.js` |
### Packages That Just Autolink β
Most packages work out of the box: `react-native-keychain`, `react-native-mmkv`, `react-native-device-info`, `react-native-gesture-handler`, `react-native-reanimated`, `react-native-screens`, `react-native-svg`, etc.
### Packages Needing Verification β οΈ
| Package | Notes |
|---------|-------|
| `react-native-quick-crypto` | May need OpenSSL config (verify with prebuild) |
| `react-native-aes-crypto-forked` | Custom fork - verify autolinking works |
| `react-native-payments` | Custom `@metamask/react-native-payments` - verify |
---
## References
- [Expo Prebuild Documentation](https://docs.expo.dev/workflow/prebuild/)
- [Expo App Variants](https://docs.expo.dev/build-reference/variants/) - **Key for handling QA/Flask/Production builds**
- [Expo Config Plugins](https://docs.expo.dev/config-plugins/introduction/)
- [Creating Config Plugins](https://docs.expo.dev/config-plugins/plugins-and-mods/)
- [Expo Modules API](https://docs.expo.dev/modules/overview/)
- [EAS Build](https://docs.expo.dev/build/introduction/)
- [EAS Build Credentials](https://docs.expo.dev/app-signing/managed-credentials/)
---
## Conclusion
**Can this project adopt CNG?** Yes, it's achievable in **7-10 weeks**.
**Recommended phases**:
| Phase | Duration | Goal |
|-------|----------|------|
| 1 | 1-2 weeks | App Variants setup + config plugins skeleton |
| 2 | 2-3 weeks | Convert native modules to Expo Modules |
| 3 | 2-3 weeks | Complete config plugins + integration |
| 4 | 2-3 weeks | Testing, validation, remove native directories |
**Key insights**:
- β
All blockers have solutions (see [estimates table](#migration-effort-estimate))
- β
Keep existing CI/CD - just add `expo prebuild` step
- β
Keep existing signing - add via config plugin
- β
Most work is creating 5 config plugins + converting 3-5 native modules
Contributor guide
Research direction
Start by reviewing app.config.js, the existing config plugins, and the committed ios/ and android/ directories to understand the current native setup. Run npx expo prebuild --clean only after mapping the custom modules, AAR libraries, variants, and signing configurations. Done means the migration scope is resolved and the generated projects support the existing mobile builds without committed native directories.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, react-native, typescript
- Domain
- build-system, mobile
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100