Webview bridge communication — is it really that smooth?

Ben Yael
Ben Yael
Mar 9 · 7 min read

This post will dive into the implementation of a React-Native webview that communicates with the loading web page, its API and how we faced its challenges here in Soluto. More than once I’ve heard about the idea of a webview pattern, or Bridge, sounds like some magic that helps you “bridge” the gaps of communication between your app and its internal loading web app. My team and I imagined this would be smooth and easy — until we realised that something is missing. What would give us thee indication that something actually triggered and succeeded inside that black-box called webview? So let’s plunge into the insights we gained about this black-box called WebView.

Overview

Our task was to create a ReactNative package/component that internally loads a react-based “SDK” (could be any web application in your case), and implement it in our company, so our partners could easily integrate this sdk in their mobile apps.

Quickly we found that even a few libraries can help implement the bridge to both trigger and call API functions within the web-sdk, as well as observe events triggering onto the outer host native application.

Let’s call them by their names:

  • Web-sdk — Is a react-based implemented application that loads a chat view for our customers. It enables the customer to initiate a chat session and communicate with an expert, which provides them with 24/7 technical support. The sdk is a UI widget, which alongside API methods attached to the window object, is provided to its host’s applications. Let׳s call it SDK from now on. This SDK is the internal page we were intending to load onto the webview. From there, our partners just need to load this page onto their website with a script tag in order for their users to get a pop-up chat window.
  • ReactNative-sdk — Our final goal, the solution we wanted to provide for our partners, who want to load it into their mobile application. Also known as host mobile applications, or just RN-SDK.

What is a bridge?

Great question! It is the way to invoke functionalities inside the web-page, by calling it from the host app, using the window object of this webview.

Note:

ReactNativeApi() is a wrapper pattern that combines the original and traditional way of passing data between two applications. If you’re not familiar with this, please read more about “onMessage={this.onMessage}“ and “postMessage(“passing object”)}”.

When mentioning ReactNativeApi — I refer to the trick of implementing a callback in our host app that triggers and receives passing data from within the webview (internal web-sdk).

Here is our usage of the WebViewWithBridge library. We will discuss further details about its properties in upcoming posts.

<WebViewWithBridge
reactNativeApi={reactNativeApi}
onLoad={handleOnLoad}
source={{ uri:SdkUrl }}
ref={getWebviewRef}
{…rest}
/>

This wrapper, created by the library we used in our project, is called react-native-webview-bridge-seamless, but the same pattern could be achieved with the original implementation of webview, using onMessage/postMessage technique.

Note that the window is going to be used by the sdk, as it’s our gate for controlling sdk behaviours. Our original, and awesome, SDK supplies API calls. For example:

window.AE_SDK.initialize(‘your-own-app-key’);

This line of code is running inside the webview and initializing the sdk JS package for usage.

On the other hand — once the sdk wants to raise an event up to its host, it does so easily by registering to its known events:

eventEmitter.on(‘NEW_MESSAGE’, (args: any) => {
console.log(‘registerEventEmitter NewMessage’, args);
});

How is it being done with the context of webview? By injecting a JS code from the host to the webview:

webViewRef.injectJavaScript(`${fnStringToEnvoke}.
then((res)=>{
window.getReactNativeApi().onApiFunctionEnd({
functionName’: ‘${eventName}’,
success’:true,
value’: res,
correlationId’ : ‘${correlationId}’
});
}).
catch((e)=> {
window.getReactNativeApi().onApiFunctionEnd({
functionName’: ‘${eventName}’,
success’:false,
reason’:e && (e.name + ‘: ‘ + e.message),
correlationId’ : ‘${correlationId}’
});
});
`);

When fnStringToEnvoke could be any API needed in the flow, such as the initialize call above, and would populate this

window.AE_SDK.initialize(‘your-own-app-key’)

into the injected fnStringToEnvoke to the webview.

Therefor the final result would be:

webViewRef.injectJavaScript(`
window.AE_SDK.initialize(‘your-own-app-key’)
then((res)=>{
window.getReactNativeApi().onApiFunctionEnd({
functionName’: ‘${eventName}’,
success’:true,
value’: res,
correlationId’ : ‘${correlationId}’
});
}).
catch((e)=> {
window.getReactNativeApi().onApiFunctionEnd({
functionName’: ‘${eventName}’,
success’:false,
reason’:e && (e.name + ‘: ‘ + e.message),
correlationId’ : ‘${correlationId}’
});
});
`);

Here you can see our use of ‘window’ as our gate to initiate internal API’s in SDK. However, you can adapt it to your case and use any applicable function inside the webview instead. Your take-away from this part is the idea of injecting JS code the way webview lets you, and kind of “wait around the corner” for the indication to end. Again, we did it by passing the relevant data to this function:

window.getReactNativeApi().onApiFunctionEnd()

Wait, so what’s the problem? Everything seems to be implemented already. YEAH!

Or that’s what we thought…

The problem

The solution above is pretty straight forward for anyone implementing this communication.

But let’s examine this from a broader perspective.

The business goal was to supply our partners with a native NPM module that “bridges” the RN-SDK to the interface of the SDK.

If I were to use this package as described until now, as a developer, I’d rather kill myself, because with real-world application, the API, gets far more complicated and complex.

And What if I wanted to initiate an asynchronous flow, so instead of window.AE_SDK.initialize(‘your-own-app-key’); I would need and must call

await window.AE_SDK.initialize(‘your-own-app-key’);

Legit, isn’t it?

Let’s go back to the developer experience — what we’re expecting is an indication (ideally) that action was resolved successfully or rejected. Sound familiar, right?

Once you “await for action”, you expect to be triggered by a callback of either resolve or reject. That’s what we’re here for.

We called this function, “await…”, and we hoped it would work properly. However, in practice, once the ReactNativeApi that was supplied by the web view package injects that JS code to the sdk, everything works great, without much effort. By using this ability, it’s possible to easily react on this data coming from the sdk.

But wouldn’t you want your native app to seamlessly and smoothly react and know the status of this async operation? After all, asking the partner’s team of developers to translate returning data as a success indication or an error would not translate to a good experience,.especially since this is something I would know some time after the operation was called. They would want to react on this flow immediately once invoked.

You probably understand the challenge we faced by now,, and how straight forward the solution was.Actually, how did no one ever do it before?

Solution:

Using a react native event emitter, we implemented the registration of each of these API calls in the emitter under the hood. We wanted to create a dev experience that was as close as possible to real asynchronous programming. Same as when a promise is pending for an answer once this operation ends, we wanted our API calls via that bridge to be called from the Host-app, but to actually be running in a different context in the internal app (Two different apps) that need to “flow together”.

Following that, we could tell the developers on the native side: “Don’t worry, even though you call this X function, and it actually runs in a different context, we assure you that you can face this call as a promise, just like any other JS function you know”. Therefore, we allowed the host’s devs to smoothly integrate this module in their mobile app and interact with its abilities, just like any other library they clone from the web. This is all we wanted in the first place.

NativeEmitter.once(`${eventName}_${correlationId}`, (data) => {

if (data?.success) resolve(data?.value);
else reject(data?.reason);
});const reactNativeApi = React.useMemo(() => ({

onSdkEventTriggered: (args: any) => {
NativeEmitter.emit(
JSON.parse(args).name,
JSON.parse(args).data
);
},
onApiFunctionEnd: (args: any) => {
NativeEmitter.emit(
`${args?.functionName}_${args?.correlationId}`,
args);
},
}), [NativeEmitter]);

As you can see here, using a very common tool (An EventEmitter called NativeEmitter), we can now control the flow of this function by internally storing the host’s provided resolve\reject callbacks. By emitting these calls once, respectively, a success/error indication returns via the reactNativeApi interface that we passed to the webview (see WebViewWithBridge constructor above).

In our case, we tried wrapping SDK-API’s call, in particular those which are asynchronous, with the EventEmitter registration and callback, in order to provide the host with the ability to call. For example:

sdkApiRef.sendTeamMessage(‘This is a demo team message’)

You can easily use this pattern for any implementation and functionalities you will want to call between your host native app and the internal webview application you use within it.

Oh wait.,one last thing

What if an API function was called twice or more in a short period of time? This emitter would trigger its resolving behaviour on all of the calls from the same api type, as soon as just one call was completed.

We fixed that by adding an additional correlation ID (created in the promise creation method, and returned from the webview on “onApiFunctionEnd”) to pass alongside the function name and relevant arguments. Using this ID we could identify the specific one-to-one function we aimed to resolve/reject specifically.

Let’s sum it up

The challenge we were facing might pop-up in anytime there’s an attempt to implement a both-way communication between a host app that loads a webview of an application that exports an API for usage and a non-static page. This circumstance is very common and webview is the expected solution.
But be warned — you too might encounter the challenge of controlling and fading this API as a promise.
Out solution is fairly simple and could be adapted to any case, using a simple pattern for a very useful, friendly, and smooth experience, both for the devs and technically. Our solution provides a package that holds this internal implementation and lets its user forget about async abilities, and control the web app which runs within its application in a different context.

Additional reading:

https://www.npmjs.com/package/react-native-webview-bridge-seamless

https://www.npmjs.com/package/react-native-eventemitter