Google Play for Native PC Billing Library をアプリに統合する

Play 請求サービスを使用してデジタル アイテムを販売することで、ゲームを収益化できます。この SDK には、購入可能なアイテムを表示する API、購入フローを開始する API、購入を処理する API が用意されています。これらの課金 API の呼び出しは、Google Play ゲーム クライアント内でゲームを起動した Google アカウントを使用して行われるため、追加のログイン手順は必要ありません。

Android Play Billing Library と統合している場合、これらの Play 請求サービス API は使い慣れたものだと思います。Play 請求サービスとのサーバーサイド統合は、Android と PC で同じであるため、PC タイトルで再利用できます。

前提条件

ステップ 1: BillingClient を作成する

26.3.312.0 以降

SDK バージョン 26.3.312.0 以降では、 BillingClientParameters を使用して を構成し、BillingClient をインスタンス化します。これにより、初期化時に保留中の購入などの特定の機能を有効にできます。

// Set up initialization parameters
BillingClientParams params;
params.enable_pending_purchases = true;

// Instantiate the BillingClient with parameters
BillingClient billing_client(params);

26.3.312.0 より前

26.3.312.0 より前の SDK バージョンでは、BillingClient はデフォルト コンストラクタを使用したインスタンス化のみをサポートしています。これらのバージョンでは、高度な構成オプションは使用できません。。

BillingClient billing_client;

ステップ 2: 以前の購入とアプリ外で完了した購入をクエリする

アプリが起動したとき、またはフォアグラウンドに戻ったときに、購入をクエリします。これは、ゲーム外で行われた購入を検出したり、ユーザーが以前に行った購入へのアクセス権を付与したりするために必要です。

  1. BillingClient::QueryPurchases を使用して購入をクエリします。

  2. 購入を処理して続行します。

// Query for purchases when:
// - Application starts up
// - Application window re-enters the foreground
auto promise = std::make_shared<std::promise<QueryPurchasesResult>>();
billing_client.QueryPurchases([promise](QueryPurchasesResult result) {
   promise->set_value(std::move(result));
});

auto query_purchases_result = promise->get_future().get();
if (query_purchases_result.ok()) {
  auto purchases = query_purchases_result.value().product_purchase_details;
  // Process the purchases
} else {
  // Handle the error
}

ステップ 3: 購入可能なアイテムを表示する

購入可能なアイテムをクエリしてユーザーに表示する準備ができました。商品の詳細のクエリは、ローカライズされた商品情報を返すので、ユーザーにアイテムを表示する前の重要なステップとなります。

商品を販売する前に、ユーザーがまだその商品を所有していないことを確認します。ユーザーの購入履歴に消費型アイテムがまだある場合、ユーザーはそのアイテムを消費しない限り再購入できません。

  1. BillingClient::QueryProductDetails を使用して商品の詳細をクエリします。Google Play Console で登録した商品 ID を渡します。
  2. 商品の ローカライズされた名前と販売価格を含む ProductDetails をレンダリングします。
  3. 商品の offer_token への参照を保持します。これは、特典の購入フローを開始するために使用されます。
QueryProductDetailsParams params;
params.product_ids.push_back({"example_costmetic_1", ProductType::kTypeInApp});
params.product_ids.push_back({"example_costmetic_1", ProductType::kTypeInApp});
params.product_ids.push_back({"example_battle_pass", ProductType::kTypeInApp});

auto promise = std::make_shared<std::promise<QueryProductDetailsResult>>();
billing_client.QueryProductDetails(params, [promise](QueryProductDetailsResult result) {
   promise->set_value(std::move(result));
});

auto query_product_details_result = promise->get_future().get();
if (query_product_details_result.ok()) {
   auto product_details = query_product_details_result.value().product_details;
   // Display the available products and their offers to the user
} else {
   // Handle the error
}

ステップ 4: 購入フローを開始する

ユーザーが提示した商品を購入する意思を示したら、購入フローを開始する準備が整います。

  1. まず、 BillingClient::LaunchPurchaseFlow() を呼び出します。商品詳細をクエリしたときに取得した offer_tokenを渡します。
  2. 購入が完了すると、結果とともに継続関数が呼び出されます。
  3. 成功した場合、継続には ProductPurchaseDetailsが含まれます。購入を 処理して続行します。
LaunchPurchaseFlowParams params { product_offer.offer_token };

auto promise = std::make_shared<std::promise<LaunchPurchaseFlowResult>>();
billing_client.LaunchPurchaseFlow(params, [promise](LaunchPurchaseFlowResult result) {
   promise->set_value(std::move(result));
});
// The purchase flow has started and is now in progress.

auto launch_purchase_flow_result = promise->get_future().get();

// The purchase flow has now completed.
if (launch_purchase_flow_result.ok()) {
   auto purchase = launch_purchase_flow_result.value().product_purchase_details;
   // Process the purchase
} else if (launch_purchase_flow_result.code() == BillingError::kUserCanceled) {
   // Handle an error caused by the user canceling the purchase flow
} else {
   // Handle any other error codes
}

ステップ 5: 購入を処理する

バックエンド サーバーで処理する

バックエンド サーバーがあるゲームの場合は、 purchase_token をバックエンド サーバーに送信して処理を完了します。サーバーサイドの Play 請求サービス API を使用して、残りの処理を完了します。このサーバーサイド統合は、Play 請求サービスと統合された Android ゲームの場合と同じです。

void ProcessPurchasesWithServer(std::vector<ProductPurchaseDetails> purchases) {
   std::vector<std::string> purchase_tokens;
   for (const auto& purchase : purchases) {
      purchase_tokens.push_back(purchase.purchase_token);
   }

   // Send purchase tokens to backend server for processing
}

バックエンド サーバーなしで処理する

  1. ユーザーの支払いが保留になっていないことを確認するには、 ProductPurchaseDetails::purchase_statePurchaseState::kPurchaseStatePurchased であることを確認します。購入ステータスが保留中の場合は、購入した商品を受け取る前に追加の手順を完了する必要があることをユーザーに通知します。

  2. 購入した商品へのアクセス権をユーザーに付与し、ゲームの利用権ストレージを更新します。

  3. 消費不可の購入(一度しか購入できない商品)の場合は、 を使用して購入がすでに承認されているかどうかを確認します。 ProductPurchaseDetails::is_acknowledged

    1. 購入が承認されていない場合は、ユーザーに商品への利用権が付与されていることを Google に通知するには、 BillingClient::AcknowledgePurchase を呼び出します。
  4. 消費型購入(複数回購入できる商品)の場合は、 Google に、ユーザーに商品への利用権が付与されていることを BillingClient::ConsumePurchase を呼び出して通知します。

void ProcessPurchasesWithoutServer(std::vector<ProductPurchaseDetails> purchases) {
   std::vector<std::string> entitled_product_ids;
   for (const auto& purchase : purchases) {
      auto was_successful = ProcessPurchasePurchaseWithoutServer(purchase);
      if (was_successful) {
         entitled_product_ids.push_back(purchase.product_id);
      }
   }

   // Note that non-consumable products that were previously purchased may have
   // been refunded. These purchases will stop being returned by
   // `QueryPurchases()`. If your game has given a user access to one of these
   // products storage they should be revoked.
   //
   // ...
}

bool ProcessPurchasePurchaseWithoutServer(ProductPurchaseDetails purchase) {
   auto is_purchase_completed =
      purchase.purchase_state == PurchaseState::kPurchaseStatePurchased;
   if (!is_purchase_completed) {
      // Notify the user that they need to take additional steps to complete
      // this purchase.
      return false;
   }

   // Determine if the product ID is associated with a consumable product.
   auto is_consumable = IsConsumableProductId(purchase.product_id);
   if (is_consumable) {
      // Grant an entitlement to the product to the user.
      // ...
      // Then, notify Google by consuming the purchase.

      ConsumePurchaseParams params { purchase.purchase_token };
      auto promise = std::make_shared<std::promise<ConsumePurchaseResult>>();
      billing_client.ConsumePurchase(params, [promise](ConsumePurchaseResult result) {
         promise->set_value(std::move(result));
      });

      auto consume_purchase_result = promise->get_future().get();
      if (!consume_purchase_result.ok()) {
         // Examine the failure code & message for more details & notify user
         // of failure.
         // ...
         return false;
      }

      return true;
   }

   // Otherwise the product is assumed to be a non-consumable.

   // Grant an entitlement to the product to the user.
   // ...
   // Then, notify Google by acknowledging the purchase (if not already done).

   if (purchase.is_acknowledged) {
      return true;
   }

   AcknowledgePurchaseParams params { purchase.purchase_token };
   auto promise = std::make_shared<std::promise<AcknowledgePurchaseResult>>();
   billing_client.AcknowledgePurchase(params, [promise](AcknowledgePurchaseResult result) {
      promise->set_value(std::move(result));
   });

   auto acknowledge_purchase_result = promise->get_future().get();
   if (!acknowledge_purchase_result.ok()) {
      // Examine the failure code & message for more details & notify user
      // of failure.
      // ...
      return false;
   }

   return true;
}

クライアントサイドでの購入の確認

signatureProductPurchaseDetails から取得します。signature フィールドは、SHA1withRSA 署名アルゴリズムを使用して秘密鍵で署名されます。公開鍵を使用して次のように検証できます。

#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/sha.h>

#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

// Decodes a Base64 string into a vector of bytes using OpenSSL BIOs.
std::vector<unsigned char> base64_decode(const std::string& base64_string) {
    BIO *bio, *b64;
    b64 = BIO_new(BIO_f_base64());
    BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
    bio = BIO_new_mem_buf(base64_string.data(), base64_string.length());
    bio = BIO_push(b64, bio);

    std::vector<unsigned char> decoded_data;
    decoded_data.resize(base64_string.length());
    int length = BIO_read(bio, decoded_data.data(), decoded_data.size());
    if (length > 0) {
      decoded_data.resize(length);
    } else {
      decoded_data.clear();
    }
    BIO_free_all(bio);
    return decoded_data;
}

// Reads a PEM-encoded public key string and returns an EVP_PKEY object.
EVP_PKEY* createPublicKey(const std::string& publicKeyPem) {
  BIO* bio = BIO_new_mem_buf(publicKeyPem.data(), publicKeyPem.length());
  EVP_PKEY* pkey = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr);
  BIO_free(bio);
  return pkey;
}

// Verifies the RSA-SHA1 signature of given data using a public key.
bool verifySignature(const std::string& publicKeyPem,
                     const std::string& originalData,
                     const std::string& signature_b64) {
  std::vector<unsigned char> signature = base64_decode(signature_b64);
  EVP_PKEY* pkey = createPublicKey(publicKeyPem);
  if (!pkey) {
    std::cerr << "Error loading public key." << std::endl;
    ERR_print_errors_fp(stderr);
    return false;
  }

  EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
  if (!mdctx) {
    std::cerr << "EVP_MD_CTX_new failed." << std::endl;
    EVP_PKEY_free(pkey);
    return false;
  }

  if (EVP_DigestVerifyInit(mdctx, nullptr, EVP_sha1(), nullptr, pkey) <= 0 ||
      EVP_DigestVerifyUpdate(mdctx, originalData.c_str(),
                             originalData.length()) <= 0) {
    EVP_MD_CTX_free(mdctx);
    EVP_PKEY_free(pkey);
    std::cerr << "Error during EVP_DigestVerifyInit or EVP_DigestVerifyUpdate."
              << std::endl;
    return false;
  }

  int result = EVP_DigestVerifyFinal(
      mdctx, reinterpret_cast<const unsigned char*>(signature.data()),
      signature.size());

  EVP_MD_CTX_free(mdctx);
  EVP_PKEY_free(pkey);

  if (result == 0) {
    std::cerr << "Signature verification failed." << std::endl;
    return false;
  } else if (result != 1) {
    std::cerr << "Error during signature verification." << std::endl;
    ERR_print_errors_fp(stderr);
    return false;
  }

  return true;
}

ステップ 6: 統合をテストする

これで、Play 請求サービスとの統合をテストする準備が整いました。開発段階でテストするには、ライセンス テスター を利用することをおすすめします。ライセンス テスターは、購入に対して実際の課金が行われないテスト用支払い方法を利用できます。

ライセンス テスターの設定方法と、実施することをおすすめする一連の手動テスト については、Google Play Billing Library の統合を テストする方法に関するドキュメントをご覧ください