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

1. 简介

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

注意:如需成功完成此 Codelab,您需要能够使用一次性商品的多种购买选项和优惠功能。此功能属于抢先体验计划 (EAP)。EAP 中的产品和功能“按原样”提供,可能只能获得有限的支持。如要使用 EAP 功能,请填写一次性商品 EAP 意向调查表提交申请。不过,如果您只是想了解如何使用 Play 结算服务响应代码分析购买流失情况,请直接前往此 Codelab 的分析购买流失情况部分。

观众群

此 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. 输入版本名称,然后上传已签名的 App Bundle 文件。
  4. 点击下一步 ,然后点击保存并发布

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

创建一次性商品

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

  1. Google Play 管理中心 内,从左侧导航菜单中依次前往 Monetize with Play > Products > One-time products
  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 的目的,请使用以下商品 ID 创建 3 个额外的一次性商品:

  • consumable_product_01
  • consumable_product_02
  • consumable_product_03

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

Google Play 管理中心 内打开示例应用,然后依次前往 Monetize with Play > Products > One-time products 。然后点击创建一次性商品 ,并重复执行第 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 的反馈,并且可能知道错误所在。在这种情况下,您可以显示一条出错提示,说明出现了问题,并提供一个 `Try again` 按钮,以便用户在解决问题后进行手动重试。

响应错误代码的重试策略

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

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

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

6. 后续步骤

参考文档

7. 恭喜!

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

调查问卷

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