Web In Play の最新情報

Trusted Web Activity が昨年導入されて以来、Chrome チームは、Bubblewrap を使いやすくする、今後の Google Play Billing 統合などの新機能の追加、ChromeOS など、より多くのプラットフォームで動作するように、このプロダクトの開発に引き続き取り組んでいます。この記事では、信頼できるウェブ アクティビティの最新情報と今後の更新について概説します。

新しい Bubblewrap と Trusted Web Activity 機能

Bubblewrap を使用すると、プラットフォーム固有のツールの知識がなくても、信頼できるウェブ アクティビティ内で PWA を起動するアプリを作成できます。

シンプルな設定フロー

以前は、Bubblewrap を使用するには、Java 開発キットと Android SDK を手動で設定する必要がありましたが、どちらもエラーが発生しやすいものでした。このツールの初回実行時に、外部依存関係が自動的にダウンロードされるようになりました。

必要に応じて、既存の依存関係のインストールを使用することもできます。また、新しい doctor コマンドを使用すると、問題の検出と構成の修正の推奨が可能になります。修正は、コマンドラインから updateConfig コマンドを使用して更新できます。

ウィザードの改善

init を使用してプロジェクトを作成する場合、Bubblewrap は Android アプリを生成するための情報を必要とします。このツールはウェブアプリ マニフェストから値を抽出し、可能な場合はデフォルト値を提供します。

これらの値は新しいプロジェクトの作成時に変更できますが、以前は各フィールドの意味が明確ではありませんでした。初期化ダイアログが再構築され、各入力フィールドの説明と検証が改善されました。

display: 全画面と画面の向きのサポート

場合によっては、アプリでできるだけ多くの画面を使用したい場合、PWA を構築する際に、ウェブアプリ マニフェストの display フィールドを fullscreen に設定することで実装します。

Bubblewrap は、ウェブアプリ マニフェストで全画面オプションを検出すると、全画面表示(没入モード)でも起動するように Android アプリを構成します。

ウェブアプリ マニフェストの orientation フィールドでは、アプリを縦向き、横向き、デバイスの現在使用している向きのいずれで起動するかを定義します。Bubblewrap がウェブアプリ マニフェストのフィールドを読み取り、Android アプリの作成時にデフォルトとして使用するようになりました。

どちらの構成も、bubblewrap init フローの一部としてカスタマイズできます。

AppBundle の出力

App Bundle は、最終的な APK の生成と Google Play への署名をアプリ向けに行う公開形式です。実際には、これにより、ストアからアプリをダウンロードする際に、サイズの小さいファイルをユーザーに提供できます。

Bubblewrap は、アプリを App Bundle として app-release-bundle.aab というファイルにパッケージ化します。2021 年後半より Google Play ストアで必須となるため、Google Play ストアにアプリを公開する場合はこの形式を使用することをおすすめします。

位置情報の委任

ユーザーは、デバイスにインストールされたアプリが、テクノロジーに関係なく常に動作することを期待しています。信頼できるウェブ アクティビティ内で使用する場合、GeoLocation 権限をオペレーティング システムに委任できるようになりました。有効にした場合、ユーザーには Kotlin または Java でビルドしたアプリと同じダイアログが表示され、同じ場所で権限を管理するためのコントロールが表示されます。

この機能は Bubblewrap を使用して追加できます。Android プロジェクトに依存関係が追加されるため、ウェブアプリが位置情報の権限を使用している場合にのみ有効にしてください。

最適化されたバイナリ

ストレージが限られているデバイスは世界の特定地域では一般的であり、そのようなデバイスの所有者は、しばしば小規模なアプリを好む傾向にあります。Trusted Web Activity を使用するアプリでは、小さなバイナリが生成されるため、ユーザーの不安を軽減できます。

必要な Android ライブラリの数を減らすことで Bubblewrap を最適化し、生成されるバイナリのサイズを 800 KB 小さくしました。実際には、これは以前のバージョンで生成された平均サイズの半分未満です。サイズの小さなバイナリを利用するために、Bubblewrap の最新バージョンを使用してアプリを更新するだけで済みます。

既存のアプリを更新する方法

Bubblewrap によって生成されたアプリは、ウェブアプリと、PWA を開く軽量の Android ラッパーで構成されます。Trusted Web Activity 内で開いた PWA は、他のウェブアプリと同じ更新サイクルに従いますが、ネイティブ ラッパーは更新可能であり、更新する必要があります。

アプリを更新して、最新のバグ修正と機能を備えた最新バージョンのラッパーを使用するようにしてください。Bubblewrap の最新バージョンがインストールされている状態で、update コマンドを使用すると、最新バージョンのラッパーが既存のプロジェクトに適用されます。

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

このようなアプリケーションを更新するもう 1 つの理由は、ウェブ マニフェストに加えた変更がアプリケーションに適用されるようにすることです。そのためには、新しい merge コマンドを使用します。

bubblewrap merge
bubblewrap update
bubblewrap build

品質基準の更新

Chrome 86 では、Trusted Web Activity の品質基準が変更されました。詳細については、Trusted Web Activity を使用する PWA の品質基準の変更をご覧ください。

簡単にまとめると、アプリケーションが次のシナリオに対処してクラッシュを防ぐ必要があります。

  • アプリの起動時にデジタル アセット リンクを確認できない
  • オフライン ネットワーク リソース リクエストに対して HTTP 200 が返されない
  • アプリケーションで HTTP 404 または 5xx エラーが返されます。

アプリケーションがデジタル アセット リンクの検証に合格することを確認するだけでなく、残りのシナリオは 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 はベスト プラクティスを組み込み、Service Worker の使用時にボイラープレートを削除します。または、Workbox プラグインを使用してこれらのシナリオに対処することを検討してください。

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 請求サービス

Google Play 請求サービスは、アプリが Play ストアでデジタル商品と定期購入を販売できるようにするだけでなく、カタログ、価格、定期購入を管理するためのツール、便利なレポート、およびユーザーが使い慣れた Play ストアを利用した購入手続きフローも提供します。また、Google Play ストアで公開するデジタル商品を販売するアプリの要件でもあります。

Chrome 88 では、Android のオリジン トライアルがリリースされます。これにより、Trusted Web ActivityPayment Request APIDigital Goods API を統合し、Google Play 請求サービスによる購入フローを実装できるようになります。このオリジン トライアルは ChromeOS バージョン 89 でも利用できるようになる予定です。

重要: Google Play Billing API には独自の用語があり、クライアント コンポーネントとバックエンド コンポーネントが含まれています。このセクションでは、Digital Goods API と Trusted Web Activity の使用に特化した API のごく一部について説明します。本番環境のアプリに統合する前に、Google Play 請求サービスのドキュメントを読み、そのコンセプトを理解しておいてください。

基本的なフロー

Google Play Console のメニュー

Google Play ストアでデジタル商品を提供するには、Google Play ストアでカタログを設定し、PWA から支払い方法として Google Play ストアを接続する必要があります。

カタログを構成する準備ができたら、まず Google Play Console の左側のサイドメニューで [商品] セクションを探します。

ここには、既存のアプリ内アイテムと定期購入を表示するオプションと、新しいアイテムを追加するための [作成] ボタンがあります。

アプリ内アイテム

プロダクトの詳細

新しいアプリ内アイテムを作成するには、アイテム ID、名前、説明、価格が必要です。意味があり覚えやすい商品 ID を作成することが重要です。プロダクト ID は後で必要になります。作成した ID は変更できません。

定期購入を作成する際は、請求対象期間も指定する必要があります。定期購入の特典を一覧表示したり、無料試用、お試し価格、猶予期間、再度定期購入が可能かどうかなどの機能を追加したりすることもできます。

各アイテムを作成したら、有効にしてアプリから利用できるようにします。

必要に応じて、Play Developers API を使用して商品を追加することもできます。

カタログを構成したら、次のステップでは PWA からの購入手続きフローを構成します。そのためには、Digital Goods APIPayment Request API を組み合わせて使用します。

Digital Goods API で商品価格を取得する

Google Play 請求サービスを使用する場合は、ユーザーに表示される価格とストアの掲載情報の価格を一致させる必要があります。これらの価格を手動で同期することは不可能であるため、Digital Goods API は、ウェブ アプリケーションが基盤となる決済機関に価格を照会する方法を提供します。

// 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;
}

Digital Goods API のサポートは、window オブジェクトで getDigitalGoodsService() が利用可能かどうかをチェックすることで検出できます。

次に、Google Play 請求サービス ID をパラメータとして window.getDigitalGoodsService() を呼び出します。これにより、Google Play 請求サービスのサービス インスタンスが返されます。他のベンダーは Digital Goods API のサポートを実装でき、異なる識別子を持ちます。

最後に、Google Play 請求サービス オブジェクトへの参照で getDetails() を呼び出して、アイテムの SKU をパラメータとして渡します。このメソッドは、ユーザーに表示できる商品アイテムの価格と通貨の両方を含む詳細オブジェクトを返します。

購入手続きを開始します。

Payment Request API を使用すると、ウェブでの購入フローが可能になります。また、この API は Google Play 請求サービスの統合にも使用されます。Payment Request API を初めて使用する場合は、Payment Request API の仕組みで詳細をご確認ください。

Google Play 請求サービスで API を使用するには、https://play.google.com/billing というサポートされている支払い方法を持つ支払い方法を追加し、その支払い方法のデータの一部として SKU を追加する必要があります。

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

次に、通常どおりに PaymentRequest オブジェクトを作成し、通常どおり API を使用します。

const request = new PaymentRequest(supportedInstruments, details);

購入を承認する

取引が完了したら、Digital Goods API を使用して支払いを承認する必要があります。PaymentRequest からのレスポンス オブジェクトには、トランザクションの承認に使用するトークンが含まれています。

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');

Digital Goods API と Payment Request API にユーザーの ID に関する情報はありません。そのため、ご自身でバックエンドでユーザーに購入を関連付け、ユーザーが購入したアイテムにアクセスできるようにする必要があります。購入をユーザーに関連付ける際は、購入トークンを忘れずに保存してください。購入トークンは、購入がキャンセルまたは払い戻しされたかどうか、または定期購入がまだ有効かどうかを確認するために必要になる場合があります。Real Time Developer Notifications APIGoogle Play Developer API を確認します。これらの API は、こうしたケースをバックエンドで処理するためのエンドポイントを提供します。

既存の利用資格を確認する

ユーザーがプロモーション コードを利用したか、プロダクトの既存の定期購入を利用している可能性があります。ユーザーが適切な利用資格を持っていることを確認するには、デジタル商品サービスで listPurchases() コマンドを呼び出します。これにより、ユーザーがアプリで行ったすべての購入が返されます。ここで、未確認の購入を承認し、ユーザーが利用資格を正しく利用したことを確認する必要もあります。

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

ChromeOS Play ストアにアップロードする

Chrome 85 以降では、ChromeOS Play ストアで Trusted Web Activity も利用できます。ストアにアプリを掲載するプロセスは、ChromeOS と Android の場合と同じです。

App Bundle を作成したら、Google Play Console のガイドに沿って、Google Play ストアにアプリを公開できます。Google Play Console のドキュメントには、アプリの掲載情報の作成、apk ファイルなどの設定の管理、アプリのテストと安全なリリースの手順が記載されています。

アプリケーションを Chromebook のみに制限するには、Bubblewrap でアプリケーションを初期化するときに --chromeosonly フラグを追加します。

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

Bubblewrap を使用せずにアプリを手動でビルドする場合は、Android マニフェストに uses-feature フラグを追加します。

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

掲載情報を Android アプリと共有している場合、ChromeOS のみのパッケージのバージョンは常に Android アプリのパッケージのバージョンよりも新しい必要があります。ChromeOS バンドルのバージョンは Android バージョンよりも大きく設定できるため、リリースごとに両方のバージョンを更新する必要はありません。