分析 Play 结算服务中的商品购买流失情况

1. 简介

在此 Codelab 中,您将重点学习如何创建一次性商品、将应用与 Play 结算库 (PBL) 集成,以及分析购买交易中断的原因。

观众

本 Codelab 适用于正在使用 Play 结算库 (PBL) 或想要使用 PBL 通过一次性商品创收的 Android 应用开发者。

学习内容…

  • 如何在 Google Play 管理中心内创建一次性商品。
  • 如何将应用与 PBL 集成。
  • 如何在 PBL 中处理消耗型和非消耗型一次性商品购买交易。
  • 如何分析购买流程中的放弃情况。

所需条件…

2. 构建示例应用

此示例应用旨在成为一款功能齐全的 Android 应用,其源代码完整,可展示以下方面:

  • 将应用与 PBL 集成
  • 获取一次性商品
  • 启动一次性商品的购买流程
  • 导致以下结算响应的购买场景:
    • BILLING_UNAVAILABLE
    • USER_CANCELLED
    • OK
    • ITEM_ALREADY_OWNED

以下演示视频展示了示例应用在部署和运行后的外观和行为。

前提条件

在构建和部署示例应用之前,请执行以下操作:

构建

此构建步骤的目标是生成示例应用的已签名 Android App Bundle 文件。

如需生成 Android App Bundle,请执行以下步骤:

  1. 从 GitHub 下载示例应用
  2. 构建示例应用。在构建之前,请更改示例应用的软件包名称,然后进行构建。如果您的 Play 管理中心内有其他应用的软件包,请确保您为示例应用提供的软件包名称是唯一的。

    注意:构建示例应用只会创建一个可用于本地测试的 APK 文件。不过,运行应用不会提取商品和价格,因为您尚未在 Play 管理中心内配置商品,而这正是您在本 Codelab 中要进一步完成的操作。
  3. 生成已签名的 Android App Bundle。
    1. 生成上传密钥和密钥库
    2. 使用上传密钥为应用签名
    3. 配置 Play 应用签名功能

下一步是将 Android app bundle 上传到 Google Play 管理中心。

3. 在 Play 管理中心内创建一次性商品

如需在 Google Play 管理中心内创建一次性商品,您需要在 Play 管理中心内拥有一个应用。在 Play 管理中心内创建应用,然后上传之前创建的已签名 app bundle。

创建应用

如需创建应用,请执行以下操作:

  1. 使用您的开发者账号登录 Google Play 管理中心
  2. 点击创建应用。系统随即会打开创建应用页面。
  3. 输入应用名称,选择默认语言,以及其他与应用相关的详细信息。
  4. 点击创建应用。这会在 Google Play 管理中心内创建一个应用。

现在,您可以上传示例应用的已签名 app bundle。

上传已签名的 app bundle

  1. 将已签名的 app bundle 上传到 Google Play 管理中心的内部测试轨道。只有在上传后,您才能在 Play 管理中心内配置与创收相关的功能。
  2. 依次点击测试和发布 > 测试 > 内部版本 > 创建新的发布版本
  3. 输入版本名称,然后上传已签名的应用包文件。
  4. 点击下一步,然后点击保存并发布

现在,您可以创建一次性商品了。

创建一次性商品

如需创建一次性商品,请执行以下操作:

  1. Google Play 管理中心的左侧导航菜单中,依次前往借助 Play 变现 > 商品 > 一次性商品
  2. 点击创建一次性商品
  3. 输入以下商品详情:
    • 商品 ID:输入唯一的商品 ID。输入 one_time_product_01
    • (可选)标记:添加相关标记。
    • 名称:输入商品名称。例如 Product name
    • 说明:输入商品说明。例如 Product description
    • (可选)添加图标图片:上传可代表您产品的图标。
    注意:在此 Codelab 中,您可以跳过配置税费、法规遵从和计划部分。
  4. 点击下一步
  5. 添加购买选项并配置其地区供应情况。一次性商品需要至少一个购买选项,用于定义使用权的授予方式、价格和地区供应情况。在此 Codelab 中,我们将为商品添加标准的购买选项。在购买选项部分中,输入以下详细信息:
    • 购买选项 ID:输入购买选项 ID。例如 buy
    • 购买类型:选择购买
    • (可选)标记:添加特定于此购买选项的标记。
    • (可选)点击高级选项以配置高级选项。在此 Codelab 中,您可以跳过高级选项配置。
  6. 供应情况和定价部分中,依次点击设置价格 > 批量修改价格
  7. 选择国家 / 地区选项。此操作会选择所有地区。
  8. 点击继续。系统随即会打开一个用于输入价格的对话框。输入 10 美元,然后点击应用
  9. 点击保存,然后点击启用。系统随即会创建并启用购买选项。

在此 Codelab 中,请创建 3 个额外的一次性商品,并使用以下商品 ID:

  • consumable_product_01
  • consumable_product_02
  • consumable_product_03

示例应用已配置为使用这些商品 ID。您可以提供不同的商品 ID,在这种情况下,您必须修改示例应用,以使用您提供的商品 ID。

Google Play 管理中心内打开示例应用,然后依次前往通过 Play 创收 > 商品 > 一次性商品。然后点击创建一次性商品,并重复第 3 步到第 9 步。

一次性商品创建视频

以下示例视频展示了之前介绍的一次性商品创建步骤。

4. 与 PBL 集成

现在,我们将了解如何将应用与 Play 结算库 (PBL) 集成。本部分介绍了集成的高级步骤,并为每个步骤提供了代码段。您可以将这些代码段作为实现实际集成的指南。

如需将应用与 PBL 集成,请执行以下步骤:

  1. 向示例应用添加 Play 结算库依赖项。
    dependencies {
    val billing_version = "8.0.0"
    
    implementation("com.android.billingclient:billing-ktx:$billing_version")
    }
    
  2. 初始化 BillingClient。BillingClient 是位于应用中并与 Play 结算库通信的客户端 SDK。以下代码段展示了如何初始化结算客户端。
    protected BillingClient createBillingClient() {
    return BillingClient.newBuilder(activity)
        .setListener(purchasesUpdatedListener)
        .enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
        .enableAutoServiceReconnection()
        .build();
    }
    
  3. 连接到 Google Play。以下代码段展示了如何连接到 Google Play。
    public void startBillingConnection(ImmutableList<Product> productList) {
    Log.i(TAG, "Product list sent: " + productList);
    Log.i(TAG, "Starting connection");
    billingClient.startConnection(
        new BillingClientStateListener() {
          @Override
          public void onBillingSetupFinished(BillingResult billingResult) {
            if (billingResult.getResponseCode() == BillingResponseCode.OK) {
              // Query product details to get the product details list.
              queryProductDetails(productList);
            } else {
              // BillingClient.enableAutoServiceReconnection() will retry the connection on
              // transient errors automatically.
              // We don't need to retry on terminal errors (e.g., BILLING_UNAVAILABLE,
              // DEVELOPER_ERROR).
              Log.e(TAG, "Billing connection failed: " + billingResult.getDebugMessage());
              Log.e(TAG, "Billing response code: " + billingResult.getResponseCode());
            }
          }
    
          @Override
          public void onBillingServiceDisconnected() {
            Log.e(TAG, "Billing Service connection lost.");
          }
        });
    }
    
  4. 获取一次性商品详情。将应用与 PBL 集成后,您必须将一次性商品详情提取到应用中。以下代码段展示了如何在应用中提取一次性商品详情。
    private void queryProductDetails(ImmutableList<Product> productList) {
    Log.i(TAG, "Querying products for: " + productList);
    QueryProductDetailsParams queryProductDetailsParams =
        QueryProductDetailsParams.newBuilder().setProductList(productList).build();
    billingClient.queryProductDetailsAsync(
        queryProductDetailsParams,
        new ProductDetailsResponseListener() {
          @Override
          public void onProductDetailsResponse(
              BillingResult billingResult, QueryProductDetailsResult productDetailsResponse) {
            // check billingResult
            Log.i(TAG, "Billing result after querying: " + billingResult.getResponseCode());
            // process returned productDetailsList
            Log.i(
                TAG,
                "Print unfetched products: " + productDetailsResponse.getUnfetchedProductList());
            setupProductDetailsMap(productDetailsResponse.getProductDetailsList());
            billingServiceClientListener.onProductDetailsFetched(productDetailsMap);
          }
        });
    }
    
    提取 ProductDetails,会返回类似于如下所示的响应:
    {
        "productId": "consumable_product_01",
        "type": "inapp",
        "title": "Shadow Coat (Yolo's Realm | Play Samples)",
        "name": "Shadow Coat",
        "description": "A sleek, obsidian coat for stealth and ambushes",
        "skuDetailsToken": "<---skuDetailsToken--->",
        "oneTimePurchaseOfferDetails": {},
        "oneTimePurchaseOfferDetailsList": [
            {
                "priceAmountMicros": 1990000,
                "priceCurrencyCode": "USD",
                "formattedPrice": "$1.99",
                "offerIdToken": "<--offerIdToken-->",
                "purchaseOptionId": "buy",
                "offerTags": []
            }
        ]
    },
    {
        "productId": "consumable_product_02",
        "type": "inapp",
        "title": "Emperor Den (Yolo's Realm | Play Samples)",
        "name": "Emperor Den",
        "description": "A fair lair glowing with molten rock and embers",
        "skuDetailsToken": "<---skuDetailsToken--->",
        "oneTimePurchaseOfferDetails": {},
        "oneTimePurchaseOfferDetailsList": [
            {
                "priceAmountMicros": 2990000,
                "priceCurrencyCode": "USD",
                "formattedPrice": "$2.99",
                "offerIdToken": "<--offerIdToken-->",
                "purchaseOptionId": "buy",
                "offerTags": []
            }
        ]
    }
    
  5. 启动结算流程。
    public void launchBillingFlow(String productId) {
    ProductDetails productDetails = productDetailsMap.get(productId);
    if (productDetails == null) {
      Log.e(
          TAG, "Cannot launch billing flow: ProductDetails not found for productId: " + productId);
      billingServiceClientListener.onBillingResponse(
          BillingResponseCode.ITEM_UNAVAILABLE,
          BillingResult.newBuilder().setResponseCode(BillingResponseCode.ITEM_UNAVAILABLE).build());
      return;
    }
    ImmutableList<ProductDetailsParams> productDetailsParamsList =
        ImmutableList.of(
            ProductDetailsParams.newBuilder().setProductDetails(productDetails).build());
    
    BillingFlowParams billingFlowParams =
        BillingFlowParams.newBuilder()
            .setProductDetailsParamsList(productDetailsParamsList)
            .build();
    
    billingClient.launchBillingFlow(activity, billingFlowParams);
    }
    
  6. 检测和处理购买交易。在此步骤中,您需要执行以下操作:
    1. 验证购买交易
    2. 向用户授予使用权
    3. 通知用户
    4. 向 Google 通知购买流程
    其中,步骤 a、b 和 c 应在后端完成,因此不在本 Codelab 的范围内。以下代码段展示了如何针对消耗型一次性商品通知 Google:
    private void handlePurchase(Purchase purchase) {
    // Step 1: Send the purchase to your secure backend to verify the purchase following
    // https://developer.android.com/google/play/billing/security#verify
    
    // Step 2: Update your entitlement storage with the purchase. If purchase is
    // in PENDING state then ensure the entitlement is marked as pending and the
    // user does not receive benefits yet. It is recommended that this step is
    // done on your secure backend and can combine in the API call to your
    // backend in step 1.
    
    // Step 3: Notify the user using appropriate messaging.
    if (purchase.getPurchaseState() == PurchaseState.PURCHASED) {
      for (String product : purchase.getProducts()) {
        Log.d(TAG, product + " purchased successfully! ");
      }
    }
    
    // Step 4: Notify Google the purchase was processed.
    // For one-time products, acknowledge the purchase.
    // This sample app (client-only) uses billingClient.acknowledgePurchase().
    // For consumable one-time products, consume the purchase
    // This sample app (client-only) uses billingClient.consumeAsync()
    // If you have a secure backend, you must acknowledge purchases on your server using the
    // server-side API.
    // See https://developer.android.com/google/play/billing/security#acknowledge
    if (purchase.getPurchaseState() == PurchaseState.PURCHASED && !purchase.isAcknowledged()) {
    
      if (shouldConsume(purchase)) {
        ConsumeParams consumeParams =
            ConsumeParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build();
        billingClient.consumeAsync(consumeParams, consumeResponseListener);
    
      } else {
        AcknowledgePurchaseParams acknowledgePurchaseParams =
            AcknowledgePurchaseParams.newBuilder()
                .setPurchaseToken(purchase.getPurchaseToken())
                .build();
        billingClient.acknowledgePurchase(
            acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
      }
     }
    }
    

5. 分析购买流程中的放弃情况

到目前为止,在此 Codelab 中,Play 结算服务响应主要侧重于有限的几种情形,例如 USER_CANCELLEDBILLING_UNAVAILABLEOKITEM_ALREADY_OWNED 响应。不过,Play 结算可以返回 13 种不同的响应代码,这些代码可能会因各种实际因素而触发。

本部分详细说明了 USER_CANCELLEDBILLING_UNAVAILABLE 错误响应的原因,并建议了您可以采取的可能纠正措施。

USER_CANCELED 响应错误代码

此响应代码表示用户在完成购买交易之前放弃了购买流程界面。

可能的原因

您可以采取哪些措施?

  • 可能表明用户意向较低,且对价格敏感。
  • 购买交易正在等待处理或付款被拒。

BILLING_UNAVAILABLE 响应错误代码

此响应代码表示,由于用户的付款服务提供商或其所选的付款方式存在问题,因此无法完成购买交易。例如,用户的信用卡已过期,或者用户位于不受支持的国家/地区。此代码并不表示 Play 结算系统本身存在错误。

可能的原因

您可以采取哪些措施?

  • 用户设备上的 Play 商店应用已过期。
  • 用户所在的国家/地区不支持 Play。
  • 用户是企业用户,其企业管理员已禁止用户进行购买。
  • Google Play 无法通过用户的付款方式扣款。例如,用户的信用卡可能已过期。
  • 监控系统问题和特定区域的趋势
  • 请考虑迁移到 PBL 8,因为它支持更精细的 PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS 子响应代码。如果您收到此响应代码,请考虑通知用户付款失败,或建议用户使用其他付款方式。
  • 此响应代码专为重试而设计,可让您实现合适的重试策略。
    在这种情况下,自动重试不太可能有帮助。但是,如果用户解决了导致问题的情况,手动重试会有所帮助。例如,如果用户将其 Play 商店版本更新为受支持的版本,则手动重试初始操作可以解决问题。

    如果用户没有处于会话状态时收到此响应代码,那么重试可能没有意义。如果您因购买流程收到 `BILLING_UNAVAILABLE` 响应,很可能是因为用户在购买过程中收到了 Google Play 的反馈,并且可能知道错误所在。在这种情况下,您可以显示一条错误消息,说明出现了问题,并提供一个“重试”按钮,以便用户在解决问题后进行手动重试。

针对响应错误代码的重试策略

针对 Play 结算库 (PBL) 中的可恢复错误的有效重试策略因上下文而异,例如用户在会话期间的互动(如购买期间)与后台操作(如在应用恢复时查询购买交易)。实现这些策略非常重要,因为某些 BillingResponseCode 值表示可以通过重试解决的临时问题,而其他值表示永久性问题,不需要重试。

对于用户在会话中遇到的错误,建议采用简单的重试策略,并设置最大尝试次数,以尽可能减少对用户体验的干扰。相反,对于不需要立即执行的后台操作(例如确认新购买交易),建议采用指数退避算法

如需详细了解特定响应代码及其对应的建议重试策略,请参阅处理 BillingResult 响应代码

6. 后续步骤

参考文档

7. 恭喜!

恭喜!您已成功在 Google Play 管理中心内创建新的一次性商品、测试结算响应代码,并分析了购买流程中的用户流失情况。

调查问卷

我们非常重视您对此 Codelab 的反馈。请考虑花几分钟时间填写我们的调查问卷。