App-to-app communication with React Native on Android

Eric Rozell

The ixo Foundation was created to build a decentralized impact evaluation protocol using blockchain and the W3C decentralized identifier specification. Impact, in this case, refers to impact investments, which are funding for projects that can make a measurable change in society. Impact projects are based on the UN’s 17 Sustainable Development Goals, including projects to eliminate hunger, improve education, supply clean water, and increase gender equality. One of the key challenges with impact projects is producing, measuring, and valuing impact data at scale, and this is what ixo aims to address with the ixo protocol. In addition to the protocol, ixo is also building sample apps and SDKs for interacting with the impact protocol on the blockchain. They developed a Web client in React.js and an SDK based on ethjs to run using MetaMask or any other DApps (Decentralized Application) browser.

We recently worked with ixo to develop a sample client in React Native, with the goals of sharing as much code as possible with the Web client and investigating strategies to allow different organizations to set their own standards for signing impact project documents.

Problem

When using a DApps browser like MetaMask, the user is already authenticated in the browser context. On mobile, ixo needed a way to store the user’s decentralized identifier (DID) and a mechanism to sign impact projects and verifications.

One of ixo’s use cases is to support multiple cryptographic signing algorithms and to allow different organizations to set their own standards for what and how an impact project or verification is signed. The approach to this use case that we investigated was to separate the impact project creation and verification app (henceforth, the impact app) from the signing app, so organizations would be allowed to publish their own signing apps with their own standards.

ixo chose React Native because they were already familiar with React.js, and they wanted to leverage their existing JavaScript SDK for managing DIDs and signing documents as much as possible. We needed to find a way to support app-to-app communication in React Native apps. Specifically, we needed to enable the impact app to send a JSON document to a signing app and await the signed response. While ixo does intend to eventually support Android and iOS, the first version of the app is only targeting Android.

Attempt #1: Linking Module

Our first attempt at app-to-app communication was the Linking module that already existed in the core React Native library. The benefit of this approach was that it was cross-platform out-of-the-box; iOS, Android, and even Windows 10 support the Linking module. Our primary concern with this approach was that you are limited to sending data to other apps using URL parameters.

Linking.openURL(url).catch(err => console.error('An error occurred', err));

For the impact app to send the JSON document to a signing app using the Linking module, we’d have to URL encode the JSON document and send it as a URL parameter. Before we did this, we checked for any limitations on the number of characters that could be used in a URL for the Linking module and found that, on Android, the limit was between 100K and 200K characters. While this limit may have been reasonable for most JSON documents, ixo did not want to impose an arbitrary limit on the length of data being signed.

Solution: Custom Native Modules for Android

While looking at the native implementation of the Linking module on Android, we discovered that the Linking module only supports triggering new activities using the Activity.startActivity API. What we really wanted to use was the Activity.startActivityForResult API, which supported getting a response back when the launched activity finished. Since React Native does not have any core modules for invoking this API, we had to create our own custom native module, which we called StartActivityModule. Snippets of the StartActivityModule are provided throughout this section, but you can find the full implementation on GitHub.

Starting an Activity from React Native Android

To create a native module for React Native on Android, you need to create a new class that implements the NativeModule interface. It’s rare that you implement this interface directly, as React Native also provides a few useful base classes, including BaseJavaModule and ReactContextBaseJavaModule. In this case of StartActivityModule, we chose the abstract ReactContextBaseJavaModule because we required access to the ReactContext inside the module. To derive from ReactContextBaseJavaModule, you must supply an implementation for the getName method and a constructor that takes the ReactApplicationContext as a parameter.

public class StartActivityModule extends ReactContextBaseJavaModule implements ActivityEventListener {

  private final SparseArray<Promise> mPromises;

  public StartActivityModule(ReactApplicationContext reactContext) {
    super(reactContext);
    mPromises = new SparseArray<>();
  }

  @Override
  public String getName() {
    return "StartActivity";
  }
}

To add a native method that gets exposed to JavaScript, use the ReactMethod attribute. The method must be void-returning, and parameters can only use:

  • a few primitive types, i.e., strings, ints, doubles, bools, etc.
  • the JSON array data model type, ReadableArray
  • the JSON object data model type, ReadableMap
  • React Native specific types that represent callbacks, Callback, and promises, Promise

More information about implementing native modules for React Native Android can be found in the documentation.

The method we use to expose Activity.startActivityForResult in StartActivityModule is below. It takes four parameters: requestCode, a unique request ID; action, the name of the Android intent (henceforth, just intent) to call; data, the JSON data to pass via the intent extra data; and promise, a promise to resolve once an activity result matching the requestCode is triggered. We store the promise parameter in a SparseArray (the mPromises field from the class stub above) so that it can be looked up later to resolve the promise.  

@ReactMethod
public void startActivityForResult(int requestCode, String action, ReadableMap data, Promise promise) {
  Activity activity = getReactApplicationContext().getCurrentActivity();
  Intent intent = new Intent(action);
  intent.putExtras(Arguments.toBundle(data));
  activity.startActivityForResult(intent, requestCode);
  mPromises.put(requestCode, promise);
}

Part of the problem was packaging the JSON document for signing in a way that it could be passed along with the intent. As it turns out, there is a very useful helper called Arguments inside React Native for converting React Native’s Java representation of a JSON object to an Android Bundle.

Listening for Activity results in React Native Android

We also needed a way to resolve the promise we generated for the startActivityForResults call. Typically, to listen for results from activities triggered by your Android app, you need to override the onActivityResult method on your main activity. However, the base class for the MainActivity in a React Native Android project already overrides these methods to expose a much simpler mechanism for subscribing to these notifications in native modules, via the ActivityEventListener interface. Once you add the ActivityEventListener interface to your native module, you can add the logic you would have added to your main activity directly in the native module. You also need to register your ActivityEventListener implementation with the ReactContext, which is typically done through the NativeModule initialize and onCatalystInstanceDestroy lifecycle methods. This is also outlined in the React Native documentation.

@Override
public void initialize() {
  super.initialize();
  getReactApplicationContext().addActivityEventListener(this);
}

@Override
public void onCatalystInstanceDestroy() {
  super.onCatalystInstanceDestroy();
  getReactApplicationContext().removeActivityEventListener(this);
}

In the case of the StartActivityModule, when we get an onActivityResult callback from the ActivityEventListener interface, we look up the provided requestCode in the map of pending promises. If we find a match, we resolve the promise with the converted data from the intent extra data Bundle (again using the Arguments helpers from React Native).

@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
  Promise promise = mPromises.get(requestCode);
  if (promise != null) {
    WritableMap result = new WritableNativeMap();
    result.putInt("resultCode", resultCode);
    result.putMap("data", Arguments.makeNativeMap(data.getExtras()));
    promise.resolve(result);
  }
}

Checking for Intent handling app

We needed a mechanism to alert the user to install the signing app if it had not yet been installed on the same device. Without such a check, the startActivityForResult call would result in an exception, as no handler for the custom intent would have been registered. To implement this, we exposed an additional ReactMethod called resolveActivity, which allows us to check if there is an app on the device registered to handle the intent. If the result of the resolveActivity call is null, we can trigger an alert from JavaScript, and potentially display a link to install the required app from.

@ReactMethod
public void resolveActivity(String action, Promise promise) {
  Activity activity = getReactApplicationContext().getCurrentActivity();
  Intent intent = new Intent(action);
  ComponentName componentName = intent.resolveActivity(activity.getPackageManager());
  if (componentName == null) {
    promise.resolve(null);
    return;
  }

  WritableMap map = new WritableNativeMap();
  map.putString("class", componentName.getClassName());
  map.putString("package", componentName.getPackageName());
  promise.resolve(map);
}

Exposing the native module to React Native JavaScript

React Native Android uses the ReactPackage interface to bundle together related native modules in a plugin. The ReactNativeHost, configured in the MainActivity.java file generated by the React Native CLI, returns a list of ReactPackage instances that are used to configure the native modules exposed to JavaScript. For the impact app, we bundled the StartActivityModule into a simple package called IxoMobileReactPackage, which we added to the MainApplication.java file for the project.

public class IxoMobileReactPackage implements ReactPackage {
  @Override
  public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
    return Arrays.<NativeModule>asList(
      new StartActivityModule(reactContext)
    );
  }

  @Override
  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
    return Collections.emptyList();
  }
}

public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    /* ... */

    @Override
    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
        new MainReactPackage(),
        new IxoMobileReactPackage()
      );
    }

    /* ... */
}

Invoking the native method from JavaScript

Once the app is configured to expose the native module and its methods to JavaScript, it is added to the NativeModules JavaScript module in React Native. Since we included the Promise parameter as the last parameter to our native method, React Native automatically returns a JavaScript promise when the method is called, so we can invoke the native method for startActivityForResults using async/await, or your pattern of choice for handling promises, as follows:

import {
  NativeModules
} from 'react-native';

let requestCode = 0;

async function startActivityForResult(intent, extraData) {
  const { StartActivity } = NativeModules;
  const componentName = await StartActivity.resolveActivity(intent);
  if (!componentName) {
    // You could also display a dialog with the link to the app store.
    throw new Error(`Cannot resolve activity for intent ${intent}. Did you install the app?`);
  }

  const response = await StartActivity.startActivityForResult(
    ++requestCode,
    action,
    extraData);

  if (response.resultCode !== StartActivity.OK) {
    throw new Error('Invalid result from child activity.');
  }

  return response.data;
};

export default startActivityForResult;

In the case of the impact app, after the signing app does its work, the promise resolves with the signed JSON document, and the continuation after the await in the above code resumes execution.

Solution: Receiving the Intent on React Native Android

Both the sample impact app and the signing app were being written in React Native. We needed a mechanism to listen for the custom intent generated from the impact app in the signing app and pass the JSON document to be signed to the JavaScript runtime.

We created a custom ReactActivityDelegate implementation for our app that stored the Bundle from the main activity intent and returned that value for in the overridden getLaunchOptions method. The value returned from getLaunchOptions is converted into a JSON object and provided to the root level React component as props.

public class MainActivity extends ReactActivity {

  /* ... */

  @Override
  protected ReactActivityDelegate createReactActivityDelegate() {
    return new InitialPropsReactActivityDelegate(this, getMainComponentName());
  }

  public static class InitialPropsReactActivityDelegate extends ReactActivityDelegate {
    private final @Nullable Activity mActivity;
    private @Nullable Bundle mInitialProps;

    public InitialPropsReactActivityDelegate(Activity activity, String mainComponentName) {
      super(activity, mainComponentName);
      this.mActivity = activity;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      mInitialProps = mActivity.getIntent().getExtras();
      super.onCreate(savedInstanceState);
    }

    @Override
    protected Bundle getLaunchOptions() {
      return mInitialProps;
    }
  }
}

In the case of the ixo signing app, the root level props include a content prop, which we can use to decide if the purpose of the app running is to sign a JSON document. When the content prop is set, the app asks the user to confirm the contents of the JSON document to be signed and validate their identity via a fingerprint or PIN code. The app then proceeds to sign the JSON document and return the signed JSON to the calling app.

To return results for an activity in Android, the app needs to call Activity.setResult and then Activity.finish. There is currently no native module for invoking this API in React Native, so, using the same approach we described for creating a native module in the impact app, we created a native module called ActivityCompletionModule to expose this API. Below is the ReactMethod implementation, and the full native module implementation can be found on GitHub.

public class ActivityCompletionModule extends ReactContextBaseJavaModule {

  public ActivityCompletionModule(ReactApplicationContext reactContext) {
    super(reactContext);
  }

  @Override
  public String getName() {
    return "ActivityCompletion";
  }

  @ReactMethod
  public void finish(int result, String action, ReadableMap map) {
    Activity activity = getReactApplicationContext().getCurrentActivity();
    Intent intent = new Intent(action);
    intent.putExtras(Arguments.toBundle(map));
    activity.setResult(result, intent);
    activity.finish();
  }
}

Alternate Approach: Reading Intent Bundle from native module

We decided to pass the JSON document to be signed through the getLaunchOptions method in ReactActivityDelegate, but we could have just as easily created another native module to check the original intent at a later point after the React Native app had initialized. The reason we chose not to do this was that JavaScript in React Native runs on a separate thread from the native modules and UI main thread, and any communication to the native layer is asynchronous. Passing the JSON document to JavaScript in this way would have created the need for a loading dialog or activity indicator of some sort, while we waited for the asynchronous native module calls.

Conclusion

We worked with ixo not only to unblock the usage of their SDK for signing impact documents in React Native but also to implement the impact project management and signing behaviors in separate apps on Android, using custom native modules for React Native. Once ixo starts to develop and test the app for iOS and Windows 10, there is remaining work to implement the app-to-app communication modules for those platforms.

The pattern we applied for app-to-app communication between the project management and signing apps can be generalized to any app-to-app communication scenario for React Native Android. In fact, we merged the native modules described above for the startActivityForResults and finish native methods in a single React Native plugin, react-native-activity-result. The README includes instructions for getting started with the module, which you can easily install from NPM and add to your React Native Android app using the react-native link command. Any feedback or issues with using the plugin or anything described in this code story can be filed on GitHub.

ixo also created a declarative representation of impact form templates using JSON-LD that they use to dynamically render form entry UI on their Web app. We helped them re-implement the form module from React.js to ReactXP so the UI code, and any improvements or maintenance to it, could be shared across their Web and mobile apps. You can learn more about this work from another one of our code stories (coming soon!).

0 comments

Discussion is closed.

Feedback usabilla icon