Hi everyone,
I have gotten react-native-branch to work on EAS Build and thought I would share my code for others coming after me. A big shout out to @jvincent who is really the person who made this all work first, he helped me through my setup and couldn’t have done it without him.
After setting up EAS Build and for me migrating from expo build, I commented out all my expo-branch code and uninstalled the package. And off I went, instructions are below but a couple of comments before:
- The was I did this is not 100% foolproof, I had to use withDangerousMod which can change in the future and also directly edit files. I have seen once where building it again duplicated the onNewIntent() in MainActivity.java, this was quickly fixed by just rebuilding with the --clean flag. However, just a disclaimer.
- The way I did testing was to always run
expo prebuild --no-install --clean
, this meant I could check the configs in ios/ and android/ folder really fast and iterate my config plugin until it ended up the way I wanted it to. - There is no helper function yet to edit MainApplication.java, so I had to edit this through fs and actually directly read/write it. This will likely change in the future as EAS matures.
INSTRUCTIONS (THE PARTS YOU NEED TO ADAPT TO YOUR SETUP ARE IN CAPITAL LETTERS):
- First step was just to install
npm install react-native-branch
- Then I split my app.json into static app.json and dynamic app.config.js (Expo Docs)
- In the app.config.js, I added in the plugins:
plugins: [
"sentry-expo", // This is for EAS Build support, and will auto-configure your native projects if you ever eject
[
"./branch.config.js",
{
branch_app_domain: BRANCH_DOMAIN,
branch_key: BRANCH_API_KEY,
},
],
],
- I then created the config plugin file,
branch.config.js
, it is placed in the root in the same directory where app.json / app.config.js is located. I am thinking of moving this later when I get more config plugins. - My app.config.js is split between iOS and Android, and looks like this:
// EAS config plugin for react-native-branch for both Android and iOS
let {
AndroidConfig,
withAppDelegate,
withAndroidManifest,
withPlugins,
withDangerousMod,
withMainActivity,
withInfoPlist,
} = require("@expo/config-plugins");
let InsertLinesHelper = require("./src/services/utilities/insertLinesHelper");
let fs = require("fs");
const { addMetaDataItemToMainApplication, getMainApplicationOrThrow } =
AndroidConfig.Manifest;
// Starting with iOS
function withBranchIos(config, data) {
// Ensure object exist
if (!config.ios) {
config.ios = {};
}
// Update the infoPlist with the branch key and branch domain
config = withInfoPlist(config, (config) => {
config.modResults.branch_app_domain = data.branch_app_domain;
config.modResults.branch_key = data.branch_key;
return config;
});
// Update the AppDelegate.m
config = withAppDelegate(config, (config) => {
config.modResults.contents = InsertLinesHelper(
"#import <RNBranch/RNBranch.h>",
"start",
config.modResults.contents
);
config.modResults.contents = InsertLinesHelper(
"[RNBranch initSessionWithLaunchOptions:launchOptions isReferrable:YES]; // <-- add this",
"didFinishLaunchingWithOptions",
config.modResults.contents,
2
);
config.modResults.contents = InsertLinesHelper(
"return [RNBranch continueUserActivity:userActivity];",
"restorationHandler",
config.modResults.contents,
1,
3
);
return config;
});
return config;
}
function withBranchAndroid(config, data) {
// Insert the branch_key into the AndroidManifest
config = withAndroidManifest(config, async (config) => {
// Modifiers can be async, but try to keep them fast.
config.modResults = await setCustomConfigAsync(
config,
config.modResults,
data
);
return config;
});
// Directly edit MainApplication.java
config = withDangerousMod(config, [
"android",
async (config) => {
fs.readFile(
`${config.modRequest.platformProjectRoot}/**PATH_TO_YOUR_FILE**/MainApplication.java`,
"utf-8",
function (err, data) {
data = InsertLinesHelper(
"import io.branch.rnbranch.RNBranchModule;",
**I USED MY BUNDLER NAME TO INSERT THIS AT THE RIGHT PLACE**,
data
);
data = InsertLinesHelper(
"RNBranchModule.getAutoInstance(this);",
"super.onCreate();",
data
);
fs.writeFile(
`${config.modRequest.platformProjectRoot}/**PATH_TO_YOUR_FILE**/MainApplication.java`,
data,
"utf-8",
function (err) {
if (err) console.log("Error writing MainApplication.java");
}
);
}
);
return config;
},
]);
// Update proguard rules directly
config = withDangerousMod(config, [
"android",
async (config) => {
fs.readFile(
`${config.modRequest.platformProjectRoot}/app/proguard-rules.pro`,
"utf-8",
function (err, data) {
data = InsertLinesHelper("-dontwarn io.branch.**", "end", data);
fs.writeFile(
`${config.modRequest.platformProjectRoot}/app/proguard-rules.pro`,
data,
"utf-8",
function (err) {
if (err) console.log("Error writing proguard rules");
}
);
}
);
return config;
},
]);
// Insert the required Branch code into MainActivity.java
config = withMainActivity(config, (config) => {
config.modResults.contents = InsertLinesHelper(
"import android.content.Intent; // <-- and this",
**I USED MY BUNDLER NAME TO INSERT THIS AT THE RIGHT PLACE**,
config.modResults.contents
);
config.modResults.contents = InsertLinesHelper(
"import io.branch.rnbranch.*; // <-- add this",
**I USED MY BUNDLER NAME TO INSERT THIS AT THE RIGHT PLACE**,
config.modResults.contents
);
config.modResults.contents = InsertLinesHelper(
" // Override onStart, onNewIntent:\n" +
" @Override\n" +
" protected void onStart() {\n" +
" super.onStart();\n" +
" RNBranchModule.initSession(getIntent().getData(), this);\n" +
" }\n",
"getMainComponentName",
config.modResults.contents,
3
);
config.modResults.contents = InsertLinesHelper(
"RNBranchModule.onNewIntent(intent);",
"super.onNewIntent(intent);",
config.modResults.contents
);
return config;
});
return config;
}
// Splitting this function out of the mod makes it easier to test.
async function setCustomConfigAsync(config, androidManifest, data) {
// Get the <application /> tag and assert if it doesn't exist.
const mainApplication = getMainApplicationOrThrow(androidManifest);
addMetaDataItemToMainApplication(
mainApplication,
// value for `android:name`
"io.branch.sdk.BranchKey",
// value for `android:value`
data.branch_key
);
return androidManifest;
}
module.exports = (config, data) =>
withPlugins(config, [
[withBranchIos, data],
[withBranchAndroid, data],
]);
- You will notice that I used a helper function to insert text at specific anchors, this helper function I wrote looks like this:
// Helper function to replace text in AppDelegate etc. by target anchors in the text, making it more robust
// when new native modules are installed as the text is always inserted as specified before / after the anchors
function InsertLinesHelper(insert, target, contents, offset = 1, replace = 0) {
// Check that what you want to insert does not already exist
if (!contents.includes(insert)) {
const array = contents.split("\n");
let newArray = [];
if (target == "start") {
newArray = [...array.slice(0, 1), insert, ...array.slice(1)];
} else if (target == "end") {
newArray = [...array, insert];
} else {
// Find the index of the target text you want to anchor your insert on
let index = array.findIndex((str) => {
return str.includes(target);
});
// Insert the wanted text around this anchor (i.e. offset / replace options)
newArray = [
...array.slice(0, index + offset),
insert,
...array.slice(index + offset + replace),
];
}
return newArray.join("\n");
} else {
return contents;
}
}
module.exports = InsertLinesHelper;
- Then setup react-native-branch in Javascript as you would normally, just follow their documentation.