What's new for Web In Play

Since Trusted Web Activity was introduced last year, the Chrome team continues to work on the product, making it easier to use with Bubblewrap, adding new features like the upcoming Google Play Billing integration, and enabling it to work on more platforms, like ChromeOS. This article will summarize the latest and upcoming updates for Trusted Web Activity.

New Bubblewrap and Trusted Web Activity features

Bubblewrap helps you create apps that launch your PWAs inside a Trusted Web Activity, without requiring knowledge of platform-specific tooling.

Simplified setup flow

Previously, using Bubblewrap required manually setting up the Java Development Kit and the Android SDK, both of which are error prone. The tool now offers to automatically download the external dependencies when run for the first time.

You can still choose to use an existing installation of the dependencies, if you prefer to do so, and the new doctor command helps find issues and recommends fixes to the configuration, which can now be updated from the command line using the updateConfig command.

Improved wizard

When creating a project with init, Bubblewrap needs information to generate the Android app. The tool extracts values from the Web App Manifest and provides defaults where possible.

You can change those values when creating a new project, but previously the meaning of each field was not clear. The initialization dialogs were rebuilt with better descriptions and validation for each input field.

display: fullscreen and orientation support

In some cases, you may want your application to use as much of the screen as possible and, when building PWAs, this is implemented by setting the display field from the Web App Manifest to fullscreen.

When Bubblewrap detects the fullscreen option in the Web App Manifest, it will configure the Android application to also launch in full screen, or immersive mode, in Android specific terms.

The orientation field from the Web App Manifest defines whether the application should be started in portrait mode, landscape mode, or in the orientation the device is currently using. Bubblewrap now reads the Web App Manifest field and uses it as a default when creating the Android app.

You can customize both configurations can be customized as part of the bubblewrap init flow.

AppBundles Output

App Bundles is a publishing format for apps that delegates the final APK generation and signing to Play. In practice, this enables smaller files to be served to users when downloading the app from the store.

Bubblewrap now packages the application as an App Bundle, in a file called app-release-bundle.aab. You should prefer this format when publishing apps to the Play Store as it will be required by the store starting in the second half of 2021.

Geolocation delegation

Users expect applications installed on their devices to behave consistently, regardless of technology. When used inside a Trusted Web Activity, the GeoLocation permission can now be delegated to the Operating System and, when enabled, users will see the same dialogs as apps built with Kotlin or Java, and find controls to manage the permission in the same place.

The feature can be added via Bubblewrap and, since it adds extra dependencies to the Android project, you should only enable it when the web app is using the Geolocation permission.

Optimized binaries

Devices with limited storage are common in certain areas of the world, and owners of those devices frequently prefer smaller applications. Applications using Trusted Web Activity produce small binaries, which removes some of the anxiety from those users.

Bubblewrap has been optimized by reducing the list of needed Android libraries, resulting in generated binaries that are 800k smaller. In practice, that's less than half the average size generated by previous versions. To take advantage of the smaller binaries, you only need to update your app using the latest version of Bubblewrap.

How to update an existing app

An application generated by Bubblewrap is composed of a web application and a lightweight Android wrapper that opens the PWA. Even though the PWA opened inside a Trusted Web Activity follows the same update cycles as any web app, the native wrapper can and should be updated.

You should update your app to ensure it is using the latest version of the wrapper, with the latest bug fixes and features. With the latest version of Bubblewrap installed, the update command will apply the latest version of the wrapper to an existing project:

npm update -g @bubblewrap/cli
bubblewrap update
bubblewrap build

Another reason to update those applications is ensuring that changes to the Web Manifest are applied to the application. Use the new merge command for that:

bubblewrap merge
bubblewrap update
bubblewrap build

Updates to the Quality Criteria

Chrome 86 introduced changes to the Trusted Web Activity Quality Criteria, which are explained in full in Changes to quality criteria for PWAs using Trusted Web Activity.

A quick summary is that you should ensure your applications handle the following scenarios to prevent them from crashing:

  • Failure to verify digital asset links at application launch
  • Failure to return HTTP 200 for an offline network resource request
  • Return of an HTTP 404 or 5xx error in the application.

Besides ensuring that the application passes the Digital Asset Links validation, the remaining scenarios can be handled by a service worker:

self.addEventListener('fetch', event => {
  event.respondWith((async () => {
    try {
      return await fetchAndHandleError(event.request);
    } catch {
      // Failed to load from the network. User is offline or the response
      // has a status code that triggers the Quality Criteria.
      // Try loading from cache.
      const cachedResponse = await caches.match(event.request);
      if (cachedResponse) {
        return cachedResponse;
      }
      // Response was not found on the cache. Send the error / offline
      // page. OFFLINE_PAGE should be pre-cached when the service worker
      // is activated.
      return await caches.match(OFFLINE_PAGE);
    }
  })());
});

async function fetchAndHandleError(request) {
  const cache = await caches.open(RUNTIME_CACHE);
  const response = await fetch(request);

  // Throw an error if the response returns one of the status
  // that trigger the Quality Criteria.
  if (response.status === 404 ||
      response.status >= 500 && response.status < 600) {
    throw new Error(`Server responded with status: ${response.status}`);
  }

  // Cache the response if the request is successful.
  cache.put(request, response.clone());
  return response;
}

Workbox bakes in best practices and removes boilerplate when using service workers. Alternatively, consider using a Workbox plugin to handle those scenarios:

export class FallbackOnErrorPlugin {
  constructor(offlineFallbackUrl, notFoundFallbackUrl, serverErrorFallbackUrl) {
    this.notFoundFallbackUrl = notFoundFallbackUrl;
    this.offlineFallbackUrl = offlineFallbackUrl;
    this.serverErrorFallbackUrl = serverErrorFallbackUrl;
  }

  checkTrustedWebActivityCrash(response) {
    if (response.status === 404 || response.status >= 500 && response.status <= 600) {
      const type = response.status === 404 ? 'E_NOT_FOUND' : 'E_SERVER_ERROR';
      const error = new Error(`Invalid response status (${response.status})`);
      error.type = type;
      throw error;
    }
  }

  // This is called whenever there's a network response,
  // but we want special behavior for 404 and 5**.
  fetchDidSucceed({response}) {
    // Cause a crash if this is a Trusted Web Activity crash.
    this.checkTrustedWebActivityCrash(response);

    // If it's a good response, it can be used as-is.
    return response;
  }

  // This callback is new in Workbox v6, and is triggered whenever
  // an error (including a NetworkError) is thrown when a handler runs.
  handlerDidError(details) {
    let fallbackURL;
    switch (details.error.details.error.type) {
      case 'E_NOT_FOUND': fallbackURL = this.notFoundFallbackUrl; break;
      case 'E_SERVER_ERROR': fallbackURL = this.serverErrorFallbackUrl; break;
      default: fallbackURL = this.offlineFallbackUrl;
    }

    return caches.match(fallbackURL, {
      // Use ignoreSearch as a shortcut to work with precached URLs
      // that have _WB_REVISION parameters.
      ignoreSearch: true,
    });
  }
}

Google Play Billing

Besides allowing your app to sell digital goods and subscriptions on the Play Store, Google Play Billing offers tools for managing your catalog, prices and subscriptions, useful reports, and a checkout flow powered by the Play Store that is already familiar to your users. It is also a requirement for applications published on the Play Store that sell digital goods.

Chrome 88 will launch with an origin trial on Android that enables the integration of Trusted Web Activities, the Payment Request API and the Digital Goods API to implement purchase flows via Google Play Billing. We expect this Origin Trial to also be available for ChromeOS on version 89.

Important: The Google Play Billing API has its own terminology and includes client and backend components. This section covers only a small part of the API that is specific to using the Digital Goods API and Trusted Web Activity. Make sure to read the Google Play Billing documentation and understand its concepts before integrating it into a production application.

The basic flow

Play Console menu

To provide digital goods via the Play Store you'll need to configure your catalog on the Play Store, as well as connect the Play Store as a payment method from your PWA.

When you are ready to configure your catalog, start by finding the Products section in the left hand side menu on the Play Console:

Here you'll find the option to view your existing in-app products and subscriptions and you'll also find the create button for adding new ones.

In-app Products

Product Details

To create a new in-app product you will need a product ID, name, description, and a price. It's important to create meaningful and easy to remember product IDs, you'll need them later and the IDs can't be changed once created.

When creating subscriptions you will also have to specify a billing period. You have the option to list your subscription benefits and add features like whether you have a free trial, an introductory price, a grace period, and a resubscribe option.

After creating each product, make them available from your app by activating them.

If you prefer you can add your products via the Play Developers API.

Once your catalog is configured, the next step is to configure the checkout flow from the PWA. You will use a combination of the Digital Goods API and the Payment Request API to achieve this.

Fetch a product price with the Digital Goods API

When using Google Play Billing, you will want to ensure that the price displayed to users matches the price from the store listing. Manually keeping those prices in sync would be impossible, so the Digital Goods API provides a way for the web application to query the underlying payment provider for prices:

// The SKU for the product, as defined in the Play Store interface
async function populatePrice(sku) {
  try {
    // Check if the Digital Goods API is supported by the browser.
    if (window.getDigitalGoodsService) {
      // The Digital Goods API can be supported by other Payments provider.
      // In this case, we're retrieving the Google Play Billing provider.
      const service =
          await window.getDigitalGoodsService("https://play.google.com/billing");

      // Fetch product details using the `getDetails()` method.
      const details = await service.getDetails([sku]);

      if (details.length === 0) {
        console.log(`Could not get SKU: "${sku}".`);
        return false;
      }

      // The details will contain both the price and the currenncy.
      item = details[0];
      const value = item.price.value;
      const currency = item.price.currency;

      const formattedPrice = new Intl.NumberFormat(navigator.language, {
        style: 'currency', currency: currency }).format(value);

      // Display the price to the user.
      document.getElementById("price").innerHTML = formattedPrice;
    } else {
      console.error("Could not get price for SKU \"" + sku + "\".");
    }
  } catch (error) {
    console.log(error);
  }
  return false;
}

You can detect support for the Digital Goods API by checking if getDigitalGoodsService() is available on the window object.

Then call window.getDigitalGoodsService() with the Google Play Billing identifier as a parameter. This will return a service instance for Google Play Billing and other vendors can implement support for the Digital Goods API and will have different identifiers.

Finally, call getDetails() on the reference to the Google Play Billing object passing the SKU for the item as a parameter. The method will return a detail object containing both the price and the currency for the item that can be displayed to the user.

Start the purchase flow

The Payment Request API enables purchase flows on the web and is also used for the Google Play Billing integration. Check out this How Payment Request API Works to learn more if you are new to the Payment Request API.

In order to use the API with Google Play Billing you will need to add a payment instrument which has a supported metod called https://play.google.com/billing and add the SKU as part of the data for the instrument:

const supportedInstruments = [{
  supportedMethods: "https://play.google.com/billing",
  data: {
    sku: sku
  }
}];

Then, build a PaymentRequest object as usual and use the API as usual

const request = new PaymentRequest(supportedInstruments, details);

Acknowledge the purchase

Once the transaction is complete, you will need to use the Digital Goods API to acknowledge the payment. The response object from the PaymentRequest will contain a token you will use to acknowledge the transaction:

const response = await request.show();
const token = response.details.token;
const service =
          await window.getDigitalGoodsService("https://play.google.com/billing");
await service.acknowledge(token, 'onetime');

The Digital Goods API and the Payment Request API don't have knowledge on the user's identity. As a result, it's up to you to associate the purchase to the user in your backend and ensure they have access to the purchased items. When associating the purchase to a user, remember to save the purchase token, as you may need it to verify if the purchase has been cancelled or refunded, or if a subscription is still active. Check out the Real Time Developer Notifications API and the Google Play Developer API as they provide endpoints for handling those cases in your backend.

Check for existing entitlements

A user may have redeemed a promo code or may have an existing subscription to your product. In order to validate that the user has the appropriate entitlements, you can call the listPurchases() command on the digital goods service. This will return all purchases that your customer has made in your app. This would also be the place to acknowledge any unacknowledged purchases to ensure that the user correctly redeems their entitlements.

const purchases = await itemService.listPurchases();
for (p of purchases) {
  if (!p.acknowledged) {
    await itemService.acknowledge(p.purchaseToken, 'onetime');
  }
}

Upload to the ChromeOS Play Store

Trusted Web Activities are also available since Chrome 85 in the ChromeOS Play Store. The process to list your app in the store is the same for ChromeOS as it is for Android.

Once you've created your app bundle, the Play Console will guide you through the required steps to list the app on the Play Store. In the Play Console documentation you can find help to create your app listing, manage your apk files and other settings, as well as instructions for testing and safely releasing your app.

To restrict your application to Chromebooks only, add the --chromeosonly flag when initializing the application in Bubblewrap:

bubblewrap init --manifest="https://example.com/manifest.json" --chromeosonly

When building your application manually, without Bubblewrap, add a uses-feature flag to your Android manifest:

<uses-feature  android:name="org.chromium.arc" android:required="true"/>

If your listing is shared with an Android app, the ChromeOS only package version will always have to be higher than the Android app package version. You can set up the ChromeOS bundle version to a way higher number than the Android version, so you don't have to update both versions with each release.