What's new for Web In Play

Published: December 2, 2020

Since Trusted Web Activity was introduced, the Chrome team has made it easier to use with Bubblewrap. We've added additional features, such as Google Play Billing integration, and enabled it to work on more platforms, like ChromeOS.

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 configures 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's required by the store as 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 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 applies 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,
    });
  }
}