本机Android计费SDK

Catappult Android Billing(原生安卓计费)SDK 是一种实施 Catappult 计费的便捷解决方案。\ 它包括一个与 AppCoins Wallet(钱包)通信的计费客户端,允许您从 Catappult 获取产品并处理这些商品的购买。

概述

具有该 SDK 的应用中的计费流程如下:

  1. 与 Catappult Billing SDK 建立连接;
  2. 通过 Catappult Billing SDK 查询应用内产品;
  3. 最终用户想在您的应用上购买产品;
  4. 应用通过 Catappult Billing SDK 启动 AppCoins Wallet;
  5. AppCoins Wallet 处理付款并在完成后回调您的应用;
  6. 应用请求 Catappult Billing SDK 验证交易数据;
  7. 应用将产品提供给最终用户。
1121

接下来,在您的应用中实施该SDK。您的第一个目标,是实例化客户端并与其建立连接。 连接后,此计费客户端可用于获取 Catappult 中的注册产品、开始购买和处理购买。
因此,实施流程包括以下4个步骤:

  1. 与 Catappult Billing SDK 建立连接;
  2. 查询应用内产品;
  3. 启动购买流程;
  4. 处理购买并将该商品提供给用户。

📘

开发人员工具

Android Studio 插件:为了帮助实施我们的 Native Android Billing SDK,我们开发了一个 Android Studio 插件,以逐步指导您完成操作。 该插件可以在[此处]下载(https://plugins.jetbrains.com/plugin/19964-catappult-billing-integration),您可以在[此处](doc:android-studio-plugin)阅读更多关于此插件的信息。

示例实施:我们有一个示例实施供您参考,您可以参考此处了解更多详情。 请注意,其中并不涉及产品级别

**1. 与 Catappult Billing SDK 建立连接 **

在实例化并连接至 Catappult 之前,您需要在应用中添加依赖项和权限才能使用Catappult SDK。

依赖项和权限

在您的项目 build.gradle 中,请确保您拥有以下存储库:

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}

在您应用的 build.gradle 中,将 AppCoins 计费客户端添加到依赖项中。 如需获取最新版本,请访问以下链接:android-appcoins-billing

dependencies {
  implementation("io.catappult:android-appcoins-billing:0.9.0.0") //check the latest version in mvnrepository
	<...other dependencies..>
}

您需要在 AndroidManifest.xml 中添加两项权限:一个是在查询标签中添加一个包,以便 SDK 与 AppCoins 钱包通信交互;另一个是添加一个操作,以便 Web Payments(网络支付)与您的应用无缝集成。

<manifest>
  ...
  <queries>
    <!-- Required to work with Android 11 and above -->
    <package android:name="com.appcoins.wallet" />
    ...
  </queries>
  ...
  <uses-permission android:name="com.appcoins.BILLING" />
	<uses-permission android:name="android.permission.INTERNET" />
  ...
  <activity android:name="com.appcoins.sdk.billing.WebIapCommunicationActivity"
        android:exported="true">
  	<intent-filter>
      <action android:name="android.intent.action.VIEW"/>
      <category android:name="android.intent.category.DEFAULT"/>
      <category android:name="android.intent.category.BROWSABLE" />
    	<data android:scheme="web-iap-result" android:host="PACKAGE_OF_YOUR_APPLICATION"/>
  	</intent-filter>
  </activity>
  ...
</manifest>

启动服务连接

一旦权限和依赖项全部添加完毕,您需要初始化一个 AppcoinsBillingClient 实例。 这是用于与 Catappult Billing Library 通信的实例。 您在任何时候都应该只有一个活动实例,且其应该在应用的初始化中完成。
如需初始化计费客户端,必须使用 PurchasesUpdatedListener,而要启动连接,则必须使用 AppCoinsBillingStateListener。 本节将说明如何创建这两个必需的实例,以及如何实例化并连接 AppcoinsBillingClient。 PurchaseUpdatedListner 将在步骤4中展开说明。

AppCoinsBillingStateListener

这是适用于计费设置过程和状态的回调。 此侦听程序使用两种不同的方法:

名称定义
onBillingSetupFinished(responseCode)计费设置过程完成时调用此方法。
onBillingServiceDisconnected()计费连接丢失时调用此方法。

如果安装了钱包,服务将立即启动并调用计费状态侦听器。 否则,如果可能的话,付款将通过网页浏览器完成,如果不可能,系统将提示用户下载 Appcoins Wallet、安装 Appcoins Wallet 并设置新钱包。

class MyApplication : Application() {
  ...
    val appCoinsBillingStateListener: AppCoinsBillingStateListener =
    object : AppCoinsBillingStateListener {
      override fun onBillingSetupFinished(responseCode: Int) {
        if (responseCode != ResponseCode.OK.value) {
          Log.d(TAG, "Problem setting up in-app billing: $responseCode")
          return
        }
        
        // Check for pending and/or owned purchases
        checkPurchases()
        // Query in-app sku details
        queryInapps()
        // Query subscriptions sku details
        querySubs()
        Log.d(TAG, "Setup successful. Querying inventory.")
      }

      override fun onBillingServiceDisconnected() {
        Log.d("Message: ", "Disconnected")
      }
    }
  ...
}
class MyApplication extends Application {
  ...
    AppCoinsBillingStateListener appCoinsBillingStateListener = new AppCoinsBillingStateListener() {
    @Override public void onBillingSetupFinished(int responseCode) {
      if (responseCode != ResponseCode.OK.getValue()) {
        Log.d(TAG, "Problem setting up in-app billing: " + responseCode);
        return;
      }
      
      // Check for pending and/or owned purchases
      checkPurchases();
      // Query in-app sku details
      queryInapps();
      // Query subscriptions sku details
      querySubs();
      Log.d(TAG, "Setup successful. Querying inventory.");
    }

    @Override public void onBillingServiceDisconnected() {
      Log.d("Message: ", "Disconnected");
    }
  };
  ...
}

AppcoinsBillingClient

以下的示例展示了如何通过将 AppCoinsBillingStateListener、PurchasesUpdatedListener 和公钥当作参数传递来构建和启动 Appcoins IAB。
[如需从Catappult获取公钥,请点击此处。]
(<https://docs.catappult.io/docs/billing-integration#public-key>)

class MyApplication : Application() {
  ...
    private lateinit var cab: AppcoinsBillingClient
    private val purchasesUpdatedListener =
    PurchasesUpdatedListener { responseCode: Int, purchases: List<Purchase> -> {
    //Defined in step 4
    }}
  ...
    override fun onCreate() {
        ...
        val base64EncodedPublicKey = MY_KEY // Key obtained in Catappult's console
        cab = CatapultBillingAppCoinsFactory.BuildAppcoinsBilling(
            this,
            base64EncodedPublicKey,
            purchasesUpdatedListener
        )
        cab.startConnection(appCoinsBillingStateListener)
        ...
    }
  ...
}
class MyApplication extends Application {
  ...
  private AppcoinsBillingClient cab;
  PurchasesUpdatedListener purchaseUpdatedListener = (responseCode, purchases) -> {
  // Defined in step 4
  };
  ..
  protected void onCreate() {
    ...
    String base64EncodedPublicKey = MY_KEY // Key obtained in Catappult's console
    cab = CatapultBillingAppCoinsFactory.BuildAppcoinsBilling(
      this,
      base64EncodedPublicKey,
      purchasesUpdatedListener
    );
    cab.startConnection(appCoinsBillingStateListener);
    ...
  }
  ...
}

成功完成设置后,您应该立即检查是否存在待处理和/或已拥有的订阅。 如果有待处理的购买,您应该进行消费。 消费将在步骤4中予以说明。
下面的示例展示了如何查看待处理或已拥有的购买:

void fun checkPurchases() {
  val thread = Thread {
    val purchasesResult = cab.queryPurchases(SkuType.inapp.toString())
    val purchases = purchasesResult.purchases
    // queryPurchases of subscriptions will always return active and to consume subscription
    val subsResult = cab.queryPurchases(SkuType.subs.toString())
    val subs = subsResult.purchases
  }
  thread.start()
}
private void checkPurchases() {
  Thread thread = new Thread(() -> {
    PurchasesResult purchasesResult = cab.queryPurchases(SkuType.inapp.toString());
    List<Purchase> purchases = purchasesResult.getPurchases();

    // queryPurchases of subscriptions will always return active and to consume subscription
    PurchaseResult subsResult = cab.queryPurchases(SkuType.subs.toString());
    List<Purchase> subs = subsResult.getPurchases();
  });
  thread.start();
}

2. 查询应用内产品

启动连接后,您可以向 Catappult 查询可购买的产品,以便将其显示给用户。 此查询不仅包括产品的标题,还包括描述、价值等...
如需查询产品,您可以使用querySkuDetailsAsync,它需要SkuDetailsResponseListener来处理Catappult的响应。

SkuDetailsResponseListener

名称定义
onSkuDetailsResponse(responseCode, skuDetailsList)该方法接收 SKU 详情的查询结果和 SKU 查询详情的列表
class MainActivity : Activity() {
    ...
    val skuDetailsResponseListener = SkuDetailsResponseListener {responseCode, skuDetailsList ->
        Log.d(TAG, "Received skus $responseCode $skuDetailsList")
        for (sku in skuDetailsList) {
            Log.d(TAG, "sku details: $sku")
            // You should add these details to a list in order to update 
            // UI or use it in any other way
        }
    }
    ...
}
class MainActivity extends Activity {
    ...
    SkuDetailsResponseListener skuDetailsResponseListener = (responseCode, skuDetailsList) -> {
        Log.d(TAG, "Received skus " + responseCode)
        for (SkuDetails sku: skuDetailsList) {
            Log.d(TAG, "sku details: " + sku)
            // You should add these details to a list in order to update 
            // UI or use it in any other way
        }
    }
    ...
}

创建侦听程序后,您可以将其与参数一起传递给querySkuDetailsAsync,如下所示:

private fun queryInapps() {
    cab.querySkuDetailsAsync(
        SkuDetailsParams().apply{
            itemType = SkuType.inapp.toString()
            moreItemSkus = mutableListOf<String>() // Fill with the skus of items
        },
        skuDetailsResponseListener
    )
}

private fun querySubs() {
    cab.querySkuDetailsAsync(
        SkuDetailsParams().apply{
            itemType = SkuType.subs.toString()
            moreItemSkus = mutableListOf<String>() // Fill with the skus of subscriptions
        },
        skuDetailsResponseListener
    )
}
private void queryInapps() {
    List<String> inapps = ArrayList<String>();
    // Fill the inapps with the skus of items

    SkuDetailsParams skuDetailsParams = SkuDetailsParams();
    skuDetailsParams.setItemType(SkuType.inapp.toString());
    skuDetailsParams.setMoreItemSkus(inapps);
    cab.querySkuDetailsAsync(skuDetailsParams, skuDetailsResponseListener);
}

private void querySubs() {
    List<String> subs = ArrayList<String>();
    // Fill the subs with the skus of subscriptions

    SkuDetailsParams skuDetailsParams = SkuDetailsParams();
    skuDetailsParams.setItemType(SkuType.subs.toString());
    skuDetailsParams.setMoreItemSkus(subs);
    cab.querySkuDetailsAsync(skuDetailsParams, skuDetailsResponseListener);
}

3. 启动购买流程

如需启动购买流程,请使用函数lauchBillingFlow。 这将引入一个BillingFlowParams实例,其中包括SKU、购买类型(应用内购买或应用内订阅)以及开发人员要使用的数据。 以下代码片段显示了与“购买”按钮相关联的可能函数:

private fun startPurchase(sku: String, developerPayload: String) {
    // Only allow the user to make Purchases in case the billing service is already setup
    if (!cab.isReady) {
        Log.d(TAG, "Billing service is not ready yet to make purchases.")
        return
    }
    
    Log.d(TAG, "Launching purchase flow.")
    // Your sku type, can also be SkuType.subs.toString()
    val skuType = SkuType.inapp.toString()
    val billingFlowParams = BillingFlowParams(
        sku,
        skuType,
        null, // Deprecated parameter orderReference
        developerPayload,
        "BDS"
    )

    val activity: Activity = this
    val thread = Thread {
        val responseCode = cab.launchBillingFlow(activity, billingFlowParams)
        runOnUiThread {
            if (responseCode != ResponseCode.OK.value) {
                val builder =
                    AlertDialog.Builder(this)
                builder.setMessage("Error purchasing with response code : $responseCode")
                builder.setNeutralButton("OK", null)
                Log.d(TAG, "Error purchasing with response code : $responseCode")
                builder.create().show()
            }
        }
    }
    thread.start()
}
private void startPurchase(String sku, String developerPayload) {
    // Only allow the user to make Purchases in case the billing service is already setup
    if (!cab.isReady()) {
        Log.d(TAG, "Billing service is not ready yet to make purchases.");
        return;
    }

    Log.d(TAG, "Launching purchase flow.");
    // Your sku type, can also be SkuType.subs.toString()
    String skuType = SkuType.inapp.toString();
    BillingFlowParams billingFlowParams =
        new BillingFlowParams(
            Skus.YOUR_SKU_ID,
            skuType,
            null, // Deprecated parameter orderReference
            developerPayload,
            "BDS"
        );

    final Activity activity = this;
    Thread thread = new Thread(() -> {
      final int responseCode = cab.launchBillingFlow(activity, billingFlowParams);
      runOnUiThread(() -> {
        if (responseCode != ResponseCode.OK.getValue()) {
          AlertDialog.Builder builder = new AlertDialog.Builder(this);
          builder.setMessage("Error purchasing with response code : " + responseCode);
          builder.setNeutralButton("OK", null);
          Log.d(TAG, "Error purchasing with response code : " + responseCode);
          builder.create().show();
        }
      });
    });
    thread.start();
}

4. 处理购买并将该商品提供给用户

在该SDK处理并验证购买后,它会通过PurchasesUpdatedListener将购买数据通知给您。 此侦听程序正是在第1步中注册的侦听程序,包含购买更新时的回调,您可以在该回调中获取购买的详细信息并将商品归于用户。

PurchasesUpdatedListener
以下是PurchasesUpdatedListener的定义和示例片段:

名称定义
onPurchasesUpdated(responseCode,listPurchases)此方法接收购买更新的通知。
class MyApplication : Application() {
  ...
  private var purchasesUpdatedListener =
    PurchasesUpdatedListener { responseCode: Int, purchases: List<Purchase> ->
      if (responseCode == ResponseCode.OK.value) {
        for (purchase in purchases) {
            token = purchase.token

            // After validating and attributing the product, consumePurchase should be called 
            // to allow the user to purchase the item again and change the purchase's state.
            // Also consume subscriptions to make them active, there will be no issue in consuming more than once
            cab.consumeAsync(token, consumeResponseListener);
        }
      } else {
        AlertDialog.Builder(this).setMessage(
          String.format(
            Locale.ENGLISH, "response code: %d -> %s", responseCode,
            ResponseCode.values()[responseCode].name
          )
        )
        .setPositiveButton(android.R.string.ok) { dialog, which -> dialog.dismiss() }
        .create()
        .show()
      }
    }
  ...
}
class MyApplication extends Application {
  ...
    PurchasesUpdatedListener purchaseUpdatedListener = (responseCode, purchases) -> {
      if (responseCode == ResponseCode.OK.getValue()) {
        for (Purchase purchase : purchases) {
          token = purchase.getToken();
          
          // After validating and attributing consumePurchase may be called 
          // to allow the user to purchase the item again and change the purchase's state.
          // Also consume subscriptions to make them active, there will be no issue in consuming more than once
          cab.consumeAsync(token, consumeResponseListener);
        }
      } else {
        new AlertDialog.Builder(this).setMessage(
            String.format(Locale.ENGLISH, "response code: %d -> %s", responseCode,
                ResponseCode.values()[responseCode].name()))
            .setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
            .create()
            .show();
      }
    };
  ...
}

消费购买

购买完成后,需要消费购买。 要消费购买,请使用函数consumeAsync。 此函数显示在PurchaseUpdatedListener片段中,需要ConsumeResponseListener来处理Catappult的消费响应。

请注意,如果您没有在48小时内消费购买,它将自动退款。

ConsumeResponseListener

当商品消费操作结束时,回调将通知应用。

名称定义
onConsumeResponse(responseCode,purchaseToken)通知消费操作是否结束的回调。

您可以在下方找到 ConsumeResponseListener 实施的示例。

class MyApplication : Application() {
  ...
    val consumeResponseListener = ConsumeResponseListener {responseCode, purchaseToken ->
        Log.d(TAG, "Consumption finished. Purchase: $purchaseToken, result: $responseCode")
        if (responseCode == ResponseCode.OK.value) {

            Log.d(TAG, "Consumption successful. Provisioning.");
            //Your SKU logic goes here
        } else {
            complain("Error while consuming token: $purchaseToken");
        }
        Log.d(TAG, "End consumption flow.");
    }
  ...
}
class MyApplication extends Application {
  ...
    ConsumeResponseListener consumeResponseListener = new ConsumeResponseListener() {
        @Override public void onConsumeResponse(int responseCode, String purchaseToken) {
            Log.d(TAG, "Consumption finished. Purchase: " + purchaseToken + ", result: " + responseCode);

            if (responseCode == ResponseCode.OK.getValue()) {
                Log.d(TAG, "Consumption successful. Provisioning.");
                //Your SKU logic goes here
            } else {
                complain("Error while consuming token: " + purchaseToken);
            }
            Log.d(TAG, "End consumption flow.");
        }
    };
  ...
}

常见问题解答

支持的目标SDK级别是多少?

目前,Native Android SDK 的目标级别是33。


支持的最低SDK级别是多少?

目前,Native Android SDK 的最低支持级别是19。


是否存在任何帮助程序来实施该SDK?
是的,目前存在一个 AndroidStudio 插件可以指导您逐步完成实施。 您可以在此处完成该插件的下载。


如何将用户链接到购买?
如果您需要将购买链接到用户,您可以通过在开发人员负载中传递userId来实现。 以下展示了已传递到购买函数的UserId的示例。

startPurchase(sku, "user12345")
startPurchase(sku, "user12345");

您可以在Purchase对象中检索此负载。 以下示例展示了如何提取PurchasesUpdatedListener中的负载并进行条件处理:

private var purchasesUpdatedListener =
    PurchasesUpdatedListener { responseCode: Int, purchases: List<Purchase> ->
      if (responseCode == ResponseCode.OK.value) {
        for (purchase in purchases) {
          	token = purchase.token
            val developerPayload = purchase.developerPayload
            if (developerPayload == "user12345") {
            	...
            }
          	...
        }
      } else {
      	...
      }
    }
PurchasesUpdatedListener purchaseUpdatedListener = (responseCode, purchases) -> {
  if (responseCode == ResponseCode.OK.getValue()) {
    for (Purchase purchase : purchases) {
      token = purchase.getToken();
      String developerPayload = purchase.getDeveloperPayload();
      if (developerPayload.equals("user12345")) {
        ...
      }
      ...
    }
  } else {
    ...
  }
};

如何在未设置应用所有权的情况下测试购买流程?
出于测试目的,您可以使用以下数据来测试应用计费。

applicationId:

com.appcoins.sample

IAB_KEY:

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyEt94j9rt0UvpkZ2jPMZZ16yUrBOtjpIQCWi/
F3HN0+iwSAeEJyDw7xIKfNTEc0msm+m6ud1kJpLK3oCsK61syZ8bYQlNZkUxTaWNof1nMnbw3Xu5nuY
MuowmzDqNMWg5jNooy6oxwIgVcdvbyGi5RIlxqbo2vSAwpbAAZE2HbUrysKhLME7IOrdRR8MQbSbKE
y/9MtfKz0uZCJGi9h+dQb0b69H7Yo+/BN/ayBSJzOPlaqmiHK5lZsnZhK+ixpB883fr+PgSczU7qGoktqoe
6Fs+nhk9bLElljCs5ZIl9/NmOSteipkbplhqLY7KwapDmhrtBgrTetmnW9PU/eCWQIDAQAB

You can also get a version of Google's Trivial Drive with our billing implementation here and get your hands on an already working sample.


如何使用 AAR 或 JAR 实施 AppCoins 计费 SDK?

如需使用 AAR 和 JAR 实施 SDK,请确保遵循 安卓开发人员指南:AAR 和 JAR 库

您可以在官方的mavenRepository上获取相关文件。

在向 gradle 添加文件时,不要忘记添加 AppCoins Billing SDK 使用的任何依赖项,并对这些依赖项遵循相同的过程。 如需获取依赖项,请参阅编译依赖项部分(不要忘记使用您正在实施的版本)。


常出现的问题

我们发现的一些常出现的问题大多可以通过完成以下重要事项来避免发生:

  • 在您的项目的“应用(Application)”类文件内初始化AppCoins Billing SDK,而不是在一个“操作界面(Activity)”内。
    • 这一点很重要,因为如果“操作界面(Activity)”被销毁,使用其包含的内容就会出现问题;
  • 如果新购买的物品未被消费,则该物品不可用。
    • 这一点很重要:务必进行消费购买。否则的话,交易信息将无法获取,且商品将无法用于新的购买;
  • 使用querySkuDetailsAsync()的结果显示产品的价格。
    • 这一点很重要,因为这样的话,即可在您的应用和钱包应用 (Wallet App) 中为用户位置提供匹配的价格;
  • 确保避免在 Main 线程中进行调用。
    • 这一点很重要,因为这样的话,Main线程就不会被钱包应用或后端回调的请求阻塞;