PostMessage for TWA

Sayed El-Abady
Sayed El-Abady

From Chrome 115 Trusted Web Activities (TWA) can send messages using postMessage. This document walks through the setup needed to communicate between your app and the web.

By the end of this guide you will:

  • Understand how the client and web content validation works.
  • Know how to initialize the communication channel between client and webcontent.
  • Know how to send messages to and receive messages from webcontent.

To follow this guide you'll need:

  • To add the latest androidx.browser (min v1.6.0-alpha02) library to your build.gradle file.
  • Chrome version 115.0.5790.13 or greater for TWA.

The window.postMessage() method safely enables cross-origin communication between Window objects. For example, between a page and a pop-up that it spawned, or between a page and an iframe embedded within it.

Usually, scripts on different pages are allowed to access each other only if the pages they originate from the same origin, they share the same protocol, port number, and host (also known as the same-origin policy). The window.postMessage() method provides a controlled mechanism to securely communicate between different origins. This can be useful for implementing chat applications, collaborative tools and others. For example, a chat application could use postMessage to send messages between users who are on different websites. Using postMessage in Trusted Web Activities (TWA) can be a bit tricky, this guide walks you through how to use postMessage in TWA client to send messages to and receive messages from the web page.

Add the app to web validation

The postMessage API allows two valid origins to communicate to each other, a source and a target origin. For the Android application to be able to send messages to the target origin, it needs to declare which source origin it is equivalent to. This can be done with Digital Asset Links (DAL) by adding the app's package name in your assetlinks.json file with relation as use_as_origin so it will be as following:

[{
  "relation": ["delegate_permission/common.use_as_origin"],
  "target" : { "namespace": "android_app", "package_name": "com.example.app", "sha256_cert_fingerprints": [""] }
}]

Note that setup on the origin associated with the TWA, it is required to provide an origin for the MessageEvent.origin field, but postMessage can be used to communicate with other sites that don't include the Digital Assets Link. For example, if you own www.example.com you will have to prove that through DAL but you can communicate with any other websites, www.wikipedia.org for example.

Add the PostMessageService to your manifest

To receive postMessage communication you need to setup the service, you do so by adding the PostMessageService in your Android manifest:

<service android:name="androidx.browser.customtabs.PostMessageService"
android:exported="true"/>

Get a CustomTabsSession instance

After adding the service to the manifest, use the CustomTabsClient class to bind the service. Once connected you can use the provided client for creating a new session as follows. CustomTabsSession is the core class for handling the postMessage API. The following code shows how once the service is connected, the client is used to create a new session, this session is used to postMessage:

private CustomTabsClient mClient;
private CustomTabsSession mSession;

// We use this helper method to return the preferred package to use for
// Custom Tabs.
String packageName = CustomTabsClient.getPackageName(this, null);

// Binding the service to (packageName).
CustomTabsClient.bindCustomTabsService(this, packageName, new CustomTabsServiceConnection() {
 @Override
 public void onCustomTabsServiceConnected(@NonNull ComponentName name,
     @NonNull CustomTabsClient client) {
   mClient = client;

   // Note: validateRelationship requires warmup to have been called.
   client.warmup(0L);

   mSession = mClient.newSession(customTabsCallback);
 }

 @Override
 public void onServiceDisconnected(ComponentName componentName) {
   mClient = null;
 }
});

You are now wondering what’s this customTabsCallback instance right? We will be creating this in the next section.

Create CustomTabsCallback

CustomTabsCallback is a callback class for CustomTabsClient to get messages regarding events in their custom tabs. One of these events is onPostMessage and this gets called when the app receives a message from the web. Add the callback to the client to initialize the postMessage channel to start communication, as shown in the following code.

private final String TAG = "TWA/CCT-PostMessageDemo";

// The origin the TWA is equivalent to, where the Digital Asset Links file
// was created with the "use_as_origin" relationship.
private Uri SOURCE_ORIGIN = Uri.parse("https://source-origin.example.com");

// The origin the TWA will communicate with. In most cases, SOURCE_ORIGIN and
// TARGET_ORIGIN will be the same.
private Uri TARGET_ORIGIN = Uri.parse("https://target-origin.example.com");

// It stores the validation result so you can check on it before requesting
// postMessage channel, since without successful validation it is not possible
// to use postMessage.
boolean mValidated;

CustomTabsCallback customTabsCallback = new CustomTabsCallback() {

    // Listens for the validation result, you can use this for any kind of
    // logging purposes.
    @Override
    public void onRelationshipValidationResult(int relation, @NonNull Uri requestedOrigin,
        boolean result, @Nullable Bundle extras) {
        // If this fails:
        // - Have you called warmup?
        // - Have you set up Digital Asset Links correctly?
        // - Double check what browser you're using.
        Log.d(TAG, "Relationship result: " + result);
        mValidated = result;
    }

    // Listens for any navigation happens, it waits until the navigation finishes
    // then requests post message channel using
    // CustomTabsSession#requestPostMessageChannel(sourceUri, targetUri, extrasBundle)

    // The targetOrigin in requestPostMessageChannel means that you can be certain their messages are delivered only to the website you expect.
    @Override
    public void onNavigationEvent(int navigationEvent, @Nullable Bundle extras) {
        if (navigationEvent != NAVIGATION_FINISHED) {
            return;
        }

        if (!mValidated) {
            Log.d(TAG, "Not starting PostMessage as validation didn't succeed.");
        }

        // If this fails:
        // - Have you included PostMessageService in your AndroidManifest.xml ?
        boolean result = mSession.requestPostMessageChannel(SOURCE_ORIGIN, TARGET_ORIGIN, new Bundle());
        Log.d(TAG, "Requested Post Message Channel: " + result);
    }

    // This gets called when the channel we requested is ready for sending/receiving messages.
    @Override
    public void onMessageChannelReady(@Nullable Bundle extras) {
        Log.d(TAG, "Message channel ready.");

        int result = mSession.postMessage("First message", null);
        Log.d(TAG, "postMessage returned: " + result);
    }

    // Listens for upcoming messages from Web.
    @Override
    public void onPostMessage(@NonNull String message, @Nullable Bundle extras) {
        super.onPostMessage(message, extras);
        // Handle the received message.
    }
};

Communicating from the web

Now we can send and receive messages from our host app, how do we do the same from the web? Communication has to start from the host app, then the web page needs to get the port from the first message. This port is used to communicate back. Your JavaScript file will look something like the following example:

window.addEventListener("message", function (event) {
  // We are receiveing messages from any origin, you can check of the origin by
  // using event.origin

  // get the port then use it for communication.
  var port = event.ports[0];
  if (typeof port === 'undefined') return;

  // Post message on this port.
  port.postMessage("Test")

  // Receive upcoming messages on this port.
  port.onmessage = function(event) {
    console.log("[PostMessage1] Got message" + event.data);
  };
});

You can find a full complete sample here

Photo by Joanna Kosinska on Unsplash