EAS - React Native Branch - example config plugin (Working on iOS and Android)

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):

  1. First step was just to install npm install react-native-branch
  2. Then I split my app.json into static app.json and dynamic app.config.js (Expo Docs)
  3. 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,
        },
      ],
    ],
  1. 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.
  2. 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],
  ]);
  1. 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;
  1. Then setup react-native-branch in Javascript as you would normally, just follow their documentation.
4 Likes

Hi @svarto - many thanks for sharing this. I’ve been round the houses with Cordova, Ionic,and now Expo, trying to get branch working, as it’s key to this project. Hopefully this will help!

I have one question though. In step 4. you mention branch.config.js - what’s in that file?

Again, appreciate your help,
Andy

So sorry I missed to reply to this! You noticed a mistake I made in the instructions, actually step 5 is the branch.config.js - the app.config.js just integrating all my plugins into the app.json.

So what you are seeing on step 5 is the branch.config.js

Thanks for getting back to me.

Actually, in the meantime, I moved off Expo as the build times were too long, but hopefully this will be useful to someone else.

In case it’s helpful to anyone:

If you’re saying that building on your own machine is faster than waiting in the free queue on Expo’s build farm, then you can use eas build --local to build Expo apps on your own machine. See also turtle-cli for the equivalent of expo build:*.

Of course if you pay for one of their non-free plans then you will also get priority access to their build farm.

Thanks. Believe or not, I literally just read about the local eas build, so I’m going to give that a shot.

1 Like