将 Google Play 原生 PC 结算库集成到您的应用中

使用 Play 结算服务销售数字商品,让您的游戏创收。该 SDK 提供了一些 API,用于展示可供购买的商品、启动购买流程和处理购买交易。对这些结算 API 的调用是使用在 Google Play Games 客户端内启动游戏的 Google 账号执行的,不需要任何额外的登录步骤。

如果您已与 Android Play 结算库集成,那么这些 Play 结算 API 应该看起来很熟悉。与 Play 结算的任何服务器端集成都可以供 PC 版游戏重复使用,因为它们在 Android 和 PC 上是相同的。

前提条件

第 1 步:查询之前的购买交易和在应用外完成的购买交易

当应用启动或重新进入前台时,查询购买交易。这是检测游戏外发生的购买交易或解锁用户之前购买的商品的访问权限所必需的。

  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
}

第 2 步:显示可供购买的商品

您就可以查询可售的商品并将其展示给用户了。在将商品展示给用户之前,查询商品详情是非常重要的一步,因为查询会返回本地化的商品信息。

在提供待售商品之前,检查用户是否尚未拥有该商品。如果用户的消耗型商品仍在他们的购买记录中,您必须先消耗掉该商品,然后他们才能再次购买。

  1. 使用 BillingClient::QueryProductDetails 查询商品详情。传入您在 Google Play 管理中心内注册的商品 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
}

第 3 步:启动购买流程

当用户表现出购买您向其展示的商品的意图时,您就可以启动购买流程了。

  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
}

第 4 步:处理购买交易

通过后端服务器进行处理

对于具有后端服务器的游戏,请通过将 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_state 是否为 PurchaseState::kPurchaseStatePurchased,确保用户的付款未处于待处理状态。如果购买状态为待处理,请通知用户他们需要完成其他步骤才能收到购买的产品。

  2. 授予用户对所购商品的访问权限,并更新游戏的授权存储空间。

  3. 对于非消耗型购买交易(只能购买一次的商品),请使用 ProductPurchaseDetails::is_acknowledged 检查购买交易是否已确认。

    1. 如果购买交易尚未确认,请通过调用 BillingClient::AcknowledgePurchase 通知 Google 用户已获得相应商品的授权。
  4. 对于消耗型购买交易(可多次购买的商品),请通过调用 BillingClient::ConsumePurchase 通知 Google 用户已获得相应商品的使用权。

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

客户端购买验证

您将从 ProductPurchaseDetails 中获取 signaturesignature 字段使用您的私钥和 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;
}

第 5 步:测试集成

现在,您可以测试与 Play 结算服务的集成情况了。为了在开发阶段进行测试,我们建议您利用许可测试人员。许可测试人员可以使用测试付款方式,该方式不会针对购买交易收取真实费用。

如需了解如何设置许可测试人员以及我们建议执行的一系列手动测试,请参阅有关如何测试 Google Play 结算库集成的文档。