メインコンテンツまでスキップ

Android Billing SDK 集成

Aptoide Billing SDK 可让您的 Android 应用通过 AptoideBillingClient 销售应用内商品和订阅。本页介绍完整的集成流程:连接客户端、查询商品、发起购买流程,以及处理和消费购买。

您的第一个目标是实例化 AptoideBillingClient 并进行连接。连接完成后,即可使用此 Billing 客户端获取在 Aptoide Connect 中注册的商品、发起购买并进行处理。
因此,实现过程包含以下 4 个步骤:

  1. 设置与 Android Billing SDK 的连接;
  2. 查询应用内商品;
  3. 发起购买流程;
  4. 处理购买,将商品交付给用户并消费购买。
📘
开发者工具

Android Studio 插件:为帮助实现我们的原生 Android Billing SDK,我们开发了一个 Android Studio 插件,可逐步引导您完成集成。该插件可从此处下载,您可在此处了解有关此插件的更多信息。

实现示例:我们提供了一个可供参考的实现示例,请见此处。请注意,这并非生产级别示例。

1. 设置与 Android Billing SDK 的连接

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

依赖项和权限

请确保您项目的 build.gradle 中包含以下仓库:

allprojects {
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
}

在应用的 build.gradle 中,将 Android Billing SDK 添加到依赖项中。要获取最新版本,请查看以下链接:android-aptoide-billing

dependencies {
implementation("com.aptoide:android-aptoide-billing:1.+") //check the latest version in mvnrepository
<...other dependencies..>
}

启动服务连接

添加完所有依赖项后,您需要初始化一个 AptoideBillingClient 实例。这是用于与 Android Billing SDK 通信的实例。任何时候都应只有一个活动实例,并且应在应用初始化时完成此操作。
要初始化 Billing 客户端并启动连接,客户端初始化需要 PurchasesUpdatedListener,启动连接需要 AptoideBillingClientStateListener。本节介绍如何创建这两个必需的实例,以及如何实例化并连接 AptoideBillingClientPurchasesUpdatedListener 将在步骤 4 中详细说明。

如果已安装 Aptoide Wallet 应用,服务将立即启动并调用 Billing 状态监听器。否则,如果可行,将通过 WebView 完成支付;如果不可行,系统将提示用户下载 Aptoide Wallet、安装并设置新的钱包。

class MyApplication : Application() {
...
val aptoideBillingClientStateListener: AptoideBillingClientStateListener =
object : AptoideBillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode != BillingResponseCode.OK) {
Log.d(TAG, "Problem setting up in-app billing: ${billingResult.responseCode}")
return
}

Log.d(TAG, "Setup successful. Querying inventory.")
// Query in-app product details
queryInapps()
// Query subscriptions product details
querySubs()
// Check for pending purchases of Consumables
checkPendingConsumables()
// Check for pending and active Subscriptions
checkSubscriptions()
}

override fun onBillingServiceDisconnected() {
Log.d("Message: ", "Disconnected")
}
}
...
}

AptoideBillingClient

下面的示例展示了如何通过传入 AptoideBillingClientStateListenerPurchasesUpdatedListener 和公钥作为参数来构建并启动 Aptoide IAB。
点击此处从 Aptoide Connect 获取公钥。

class MyApplication : Application() {
...
private lateinit var billingClient: AptoideBillingClient
private val purchasesUpdatedListener
get()= PurchasesUpdatedListener { billingResult: BillingResult, purchases: List<Purchase> ->
//Defined in step 4
}
...
override fun onCreate() {
...
val publicKey = MY_KEY // Key obtained in Aptoide Connect's console

billingClient = AptoideBillingClient.newBuilder(this)
.setListener(purchasesUpdatedListener)
.setPublicKey(publicKey)
.build()

billingClient.startConnection(aptoideBillingClientStateListener)
...
}
...
}

当设置成功完成后,您应立即检查消耗型商品的待处理购买,以及活动或待处理的订阅。如果存在待处理的购买,您应验证其合法性,将商品交付给用户并消费这些购买。消费过程将在步骤 4 中说明。

消耗型商品

下面的示例展示了如何检查消耗型商品的待处理购买:

void fun checkPendingConsumables() {
billingClient.queryPurchasesAsync(
QueryPurchasesParams
.newBuilder()
.setProductType(ProductType.INAPP)
.build()
) { billingResult, purchases ->
// Validate the BillingResult then validate the Purchase in a server-to-server request
// follow this page /docs/iap-validators-server-to-server-check-client

// After validating, deliver the product to the User

// Lastly consumeAsync should be called to notify Aptoide Services of the Successfull delivery and allow the User to Purchase once again the Item
}
}

订阅

要验证活动/待处理的订阅,请使用 queryPurchasesAsync 方法。结果由待处理订阅(待消费)和活动订阅组成。为了正确地从用户处移除已过期的订阅,您应将所收到结果中缺失但当前用户仍可用的订阅进行比对。

下面的示例展示了如何检查订阅:

void fun checkSubscriptions() {
billingClient.queryPurchasesAsync(
QueryPurchasesParams
.newBuilder()
.setProductType(ProductType.SUBS)
.build()
) { billingResult, purchases ->
// Validate the BillingResult then validate the Subscription in a server-to-server request
// follow this page /docs/iap-validators-server-to-server-check-client

// After validating, deliver the product to the User

// Lastly consumeAsync should be called to make the Subscription active, there will be no issue in consuming more than once
// Remove Subscriptions from the User when not present in this list
}
}

📘
注意

要向用户实时提供其订阅状态的信息,请使用 RTDN

2. 查询应用内商品

启动连接后,您应向 Aptoide Connect 查询可供购买的商品,以便使用来自 Aptoide Connect 的正确价格向用户展示这些商品。此查询不仅包含商品的标题,还包含描述、价格等信息。
要查询商品,您可以使用 queryProductDetailsAsync,它需要一个 ProductDetailsResponseListener 来处理 Aptoide Connect 的响应。

创建监听器后,您可以将其连同参数一起传递给 queryProductDetailsAsync,如下所示:

private fun queryInapps() {
val queryProductDetailsParams =
QueryProductDetailsParams.newBuilder()
.setProductList(
immutableListOf(
Product.newBuilder()
.setProductId("your_product_id")
.setProductType(ProductType.INAPP)
.build()
)
)
.build()

billingClient.queryProductDetailsAsync(
queryProductDetailsParams
) { billingResult, productDetailsResult ->
// Validate the BillingResult and then Process the ProductDetails result
if (billingResult.responseCode == BillingResponseCode.OK) {
for (productDetails in productDetailsResult.productDetailsList) {
// Process here the successfully fetched product details
}

for (unfetchedProduct in productDetailsResult.unfetchedProductList) {
// Process here the unfetched products
}
}
}
}

private fun querySubs() {
val queryProductDetailsParams =
QueryProductDetailsParams.newBuilder()
.setProductList(
immutableListOf(
Product.newBuilder()
.setProductId("your_product_id")
.setProductType(ProductType.SUBS)
.build()
)
)
.build()

billingClient.queryProductDetailsAsync(
queryProductDetailsParams
) { billingResult, productDetailsResult ->
// Validate the BillingResult and then Process the ProductDetails result
if (billingResult.responseCode == BillingResponseCode.OK) {
for (productDetails in productDetailsResult.productDetailsList) {
// Process here the successfully fetched product details
}

for (unfetchedProduct in productDetailsResult.unfetchedProductList) {
// Process here the unfetched products
}
}
}
}
private void queryInapps() {
QueryProductDetailsParams queryProductDetailsParams =
QueryProductDetailsParams.newBuilder()
.setProductList(
List.of(
QueryProductDetailsParams.Product.newBuilder()
.setProductId("your_product_id")
.setProductType(ProductType.INAPP)
.build())
)
.build();

billingClient.queryProductDetailsAsync(
queryProductDetailsParams,
(billingResult, productDetailsList) -> {
// Validate the BillingResult and then Process the ProductDetails result
if (billingResult.getResponseCode() == AptoideBillingClient.BillingResponseCode.OK) {
for (ProductDetails productDetails : productDetailsResult.getProductDetailsList()) {
// Process here the successfully fetched product details
}

for (UnfetchedProduct unfetchedProduct : productDetailsResult.getUnfetchedProductList()) {
// Process here the unfetched products
}
}
}
);
}

private void querySubs() {
QueryProductDetailsParams queryProductDetailsParams =
QueryProductDetailsParams.newBuilder()
.setProductList(
List.of(
QueryProductDetailsParams.Product.newBuilder()
.setProductId("your_product_id")
.setProductType(ProductType.SUBS)
.build())
)
.build();

billingClient.queryProductDetailsAsync(
queryProductDetailsParams,
(billingResult, productDetailsList) -> {
// Validate the BillingResult and then Process the ProductDetails result
if (billingResult.getResponseCode() == AptoideBillingClient.BillingResponseCode.OK) {
for (ProductDetails productDetails : productDetailsResult.getProductDetailsList()) {
// Process here the successfully fetched product details
}

for (UnfetchedProduct unfetchedProduct : productDetailsResult.getUnfetchedProductList()) {
// Process here the unfetched products
}
}
}
);
}

处理商品详情结果

queryProductDetailsAsync 方法返回一个 QueryProductDetailsResult 对象。此结果分为两个不同的列表:

  • ProductDetails 列表:包含成功获取的商品的元数据(例如价格、描述)。使用它来填充您的 UI。
  • UnfetchedProduct 列表:包含获取失败的商品。对这些对象调用方法以检索具体的失败状态码。
❗️
价格展示的强制要求

要通过应用审核,您必须使用 ProductDetails 对象中返回的本地化价格元数据来填充您的 UI。Aptoide 要求您应用中显示的价格与实际结账价格完全一致。

这一点至关重要,因为您应用 UI 与 Aptoide 购买界面之间的任何差异都会造成用户不信任,并导致交易放弃

未使用 API 提供的结果将导致应用被拒绝

3. 发起购买流程

要启动购买流程,请使用 launchBillingFlow 函数。它接收一个 BillingFlowParams 实例,该实例包含一个 ProductDetails 对象,可通过调用 queryProductDetailsAsync 方法获取。下面的代码片段展示了一个可与"购买"按钮关联的函数示例:

fun startPayment(
context: Context,
productDetails: ProductDetails,
obfuscatedAccountId: String?,
developerPayload: String?
) {
// Only allow the user to make Purchases in case the billing service is already setup
if (!billingClient.isReady) {
Log.d(TAG, "Billing service is not ready yet to make purchases.")
return
}

// Verify if the purchase of Subscription type you are making is a Free Trial or not
val isFreeTrial = isFreeTrialSubscription(productDetails, obfuscatedAccountId)

val productDetailsParamsList = listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
// Use the ProductDetails obtained via queryProductDetailsAsync() method
.setProductDetails(productDetails)
.build()
)

val billingFlowParams =
BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList)
.apply {
obfuscatedAccountId?.let {
setObfuscatedAccountId(it)
}
developerPayload?.let {
setDeveloperPayload(it)
}
setFreeTrial(shouldStartFreeTrial)
}.build()

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

// Verify if the purchase of Subscription type you are making is a Free Trial or not
Boolean isFreeTrial = isFreeTrialSubscription(productDetails, obfuscatedAccountId);

List<BillingFlowParams.ProductDetailsParams> productDetailsParamsList = List.of(
BillingFlowParams.ProductDetailsParams.newBuilder()
// Use the ProductDetails obtained via queryProductDetailsAsync() method
.setProductDetails(productDetails)
.build()
);

BillingFlowParams billingFlowParams =
BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList)
.setObfuscatedAccountId(obfuscatedAccountId)
.setDeveloperPayload(developerPayload)
.setFreeTrial(isFreeTrial)
.build();

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

使用 obfuscatedAccountId 参数有助于防止 Billing 系统的欺诈和滥用。此标识符使我们能够将 Billing 活动准确关联到您应用中的特定用户,从而增强安全性和可靠性。如果没有用户账户逻辑,此参数可以为 null它不会在 Purchase 结果中返回。

如果您需要将购买与自己应用的购买验证逻辑相关联,请使用 developerPayload 来存储有关该购买的信息。

发起免费试用

要为用户启用免费试用,请将 BillingFlowParams 类中的 freeTrial 参数设置为 true。这将根据所提供的 obfuscatedAccountId 发起免费试用支付。

注意: 发起免费试用支付时,obfuscatedAccountId必填项

如果用户已经消费过该订阅的免费试用,则任何后续的订阅尝试都将自动回退到常规支付流程。这可确保流畅的支付体验,并允许您默认将 freeTrial 安全地设置为 true 而不会引发错误。

private fun isFreeTrialSubscription(
productDetails: ProductDetails,
obfuscatedAccountId: String?
): Boolean {
// First verify if the Free Trial feature and Obfucasted Account Id parameter are available
if (billingClient.isFeatureSupported(FeatureType.FREE_TRIALS) != 0) {
return false
}

if (billingClient.isFeatureSupported(FeatureType.OBFUSCATED_ACCOUNT_ID) != 0) {
return false
}

// Verify if the Product Type is a Subscription
if (!productDetails.productType == ProductType.SUBS) {
return false
}

// Apply your internal App Logic to verify if the User should receive a Free Trial or not
/* Example:
return obfuscatedAccountId == "123" && productDetails.productId == "trial_dice"
*/

return false
}
private boolean isFreeTrialSubscription(
ProductDetails productDetails,
String obfuscatedAccountId
) {
// First verify if the features Free Trial and Obfucasted Account Id are available
if (billingClient.isFeatureSupported(FeatureType.FREE_TRIALS) != 0) {
return false;
}

if (billingClient.isFeatureSupported(FeatureType.OBFUSCATED_ACCOUNT_ID) != 0) {
return false;
}

// Verify if the Product Type is a Subscription
if (!productDetails.getProductType().equals(ProductType.SUBS)) {
return false;
}

// Apply your internal App Logic to verify if the User should receive a Free Trial or not
/* Example:
return obfuscatedAccountId.equals("123") && productDetails.getProductId().equals("trial_dice")
*/

return false;
}

4. 处理购买、将商品交付给用户并消费购买

SDK 处理并验证购买后,将通过 PurchasesUpdatedListener 向您通知购买数据。此监听器即步骤 1 中注册的监听器,包含购买更新时的回调,您可在此回调中获取购买详情并将商品归属给用户。

下面是 PurchasesUpdatedListener 集成的示例代码片段:

class MyApplication : Application() {
...
private var purchasesUpdatedListener =
PurchasesUpdatedListener { billingResult: BillingResult, purchases: List<Purchase> ->
if (billingResult.responseCode == BillingResponseCode.OK) {
for (purchase in purchases) {
val purchaseToken = purchase.purchaseToken

// Validate the Purchase in a server-to-server request
// follow this page /docs/iap-validators-server-to-server-check-client

// After validating, deliver the product to the User

// Lastly consumeAsync 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
billingClient.consumeAsync(
ConsumeParams.newBuilder()
.setPurchaseToken(purchaseToken)
.build(),
consumeResponseListener
)
}
} else {
Log.e(TAG, "Error on receiving the Purchase with response code : ${billingResult.responseCode}")
}
}
...
}
class MyApplication extends Application {
...
PurchasesUpdatedListener purchaseUpdatedListener = (billingResult, purchases) -> {
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
for (Purchase purchase : purchases) {
String purchaseToken = purchase.getPurchaseToken();

// Validate the Purchase in a server-to-server request
// follow this page /docs/iap-validators-server-to-server-check-client

// After validating, deliver the product to the User

// Lastly consumeAsync 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

billingClient.consumeAsync(
ConsumeParams.newBuilder()
.setPurchaseToken(purchaseToken)
.build(),
consumeResponseListener
);
}
} else {
Log.e(TAG, "Error on receiving the Purchase with response code :" + billingResult.getResponseCode());
}
};
...
}

验证购买

为确保购买的合法性并防止欺诈,您的应用应始终通过服务器到服务器的请求来验证购买。要进行此验证,请遵循应用内购买验证页面中的指南。只有在成功验证购买后,您才应继续将商品交付给用户。

消费购买

购买成功验证商品也成功交付给用户后,需要消费该购买。要消费购买,请使用我们的 Consume API。对于订阅,请使用我们的 Acknowledge API。这对于在后端/服务器端处理商品交付逻辑的应用非常有用。

请注意,如果您未在 48 小时内消费购买,该购买将被自动退款。

如果您的应用仅基于客户端逻辑,则应使用 Aptoide Billing SDK 中的 consumeAsync 函数。此函数在 PurchasesUpdatedListener 代码片段中有所展示,它需要一个 ConsumeResponseListener 来处理 Aptoide Connect 的消费响应。

下面是 ConsumeResponseListener 实现的示例:

class MyApplication : Application() {
...
val consumeResponseListener = ConsumeResponseListener {billingResult, purchaseToken ->
Log.d(TAG, "Consumption finished. Purchase: $purchaseToken, result: ${billingResult.responseCode}")
if (billingResult.responseCode == BillingResponseCode.OK) {
Log.d(TAG, "Consumption successful. Provisioning.")
} else {
Log.e(TAG, "Error while consuming token: $purchaseToken")
}
Log.d(TAG, "End consumption flow.")
}
...
}
class MyApplication extends Application {
...
ConsumeResponseListener consumeResponseListener = new ConsumeResponseListener() {
@Override public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
Log.d(TAG, "Consumption finished. Purchase: " + purchaseToken + ", result: " + billingResult.getResponseCode());

if (billingResult.getResponseCode() == BillingResponseCode.OK) {
Log.d(TAG, "Consumption successful. Provisioning.");
} else {
Log.e(TAG, "Error while consuming token: " + purchaseToken);
}
Log.d(TAG, "End consumption flow.");
}
};
...
}

常见问题

支持的目标 SDK 级别是什么?

目前,原生 Android SDK 的目标级别为 33。开发者可以使用更高的 API 级别而不会出现问题,因为 Android 保持对 API 级别 33 的向后兼容性。


支持的最低 SDK 级别是什么?

目前,原生 Android SDK 支持的最低级别为 21。


如何从 OSP 迁移到 SDK 集成?

要从 OSP 迁移到 SDK 集成,请遵循此指南,了解主要差异所需操作


如何从旧版 AppCoins Billing SDK 迁移到这个最新的 Aptoide Billing SDK?

要从旧版 AppCoins Billing SDK 迁移到新的 Aptoide Billing SDK,请遵循此指南,了解主要差异所需的代码更改


有没有帮助实现 SDK 的辅助工具?
有,有一个 Android Studio 插件可逐步引导您完成。该插件可从此处下载。


如何将用户与购买关联?
如果您需要将购买与用户关联,可以通过在 obfuscatedAccountId 参数中传入 userId 来实现。下面的示例展示了传递给购买函数的一个 UserId 示例。

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

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

private val purchasesUpdatedListener
get()= PurchasesUpdatedListener { billingResult: BillingResult, purchases: List<Purchase> ->
if (billingResult.responseCode == BillingResponseCode.OK) {
for (purchase in purchases) {
val obfuscatedAccountId = purchase.accountIdentifiers?.obfuscatedAccountId
if (obfuscatedAccountId != null && obfuscatedAccountId == "user12345") {
...
}
...
}
} else {
...
}
}
PurchasesUpdatedListener purchaseUpdatedListener = (billingResult, purchases) -> {
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
for (Purchase purchase : purchases) {
String obfuscatedAccountId = purchase.getAccountIdentifiers().getObfuscatedAccountId();
if (obfuscatedAccountId != null && obfuscatedAccountId.equals("user12345")) {
...
}
...
}
} else {
...
}
};

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

applicationId:

com.appcoins.sample

IAB_KEY:

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

您也可以从此处获取嵌入了我们 Billing 实现的 Google Trivial Drive 版本,亲自体验一个已可运行的示例。


如何使用 AAR 或 JAR 实现 Android Billing SDK?

要使用 AAR 或 JAR 实现 SDK,请务必遵循 AAR 和 JAR 库的 Android 开发者指南

您可以从官方 mavenRepository 获取这些文件。

将文件添加到 gradle 时,请不要忘记同时添加 Android Billing SDK 所使用的任何依赖项,并对这些依赖项遵循相同的流程。要获取依赖项,请参阅 Compile Dependencies 部分(请不要忘记使用您所实现版本对应的依赖项)。


我正尝试从 Google Play Billing SDK 迁移到 Aptoide Billing SDK,有没有额外的文档可以帮助完成此过程?

有!如果您正尝试从 Google Play Billing SDK 迁移到我们的 Aptoide Billing SDK,可参阅这个文档页面,其中包含成功完成迁移的最重要步骤。


我仍在使用旧版 AppCoins Billing SDK,在哪里可以找到旧版文档?

如果您仍在使用旧版 AppCoins Billing SDK,强烈建议您过渡到新的 Aptoide Billing SDK,以便在功能和安全补丁方面保持最新。尽管如此,旧版文档仍可在此处获取。


我在调用 SDK 时收到 1429 TOO_MANY_REQUESTS 响应码,这是为什么?

此错误是因为您已达到 SDK 的速率限制。为确保系统稳定性,我们可能会限制短时间内可发出的请求数量。如果您的应用发送的调用过多过快——通常是由于意外的循环或高频轮询——系统将临时阻止后续请求以防止过载。

要解决此问题,请检查您的代码,确保 SDK 调用的触发频率不超过必要范围。如果您需要重试失败的请求,我们建议使用指数退避,即在每次后续重试之间等待稍长的时间。这能让系统有时间重置您的限制,并确保您的应用恢复到正常状态。


常见问题

我们识别出的一些常见问题大多可通过确保以下几点来避免:

  • 在项目的 Application 类中初始化 Android Billing SDK,而不是在 Activity 中。
    • 这一点很重要,因为如果 Activity 被销毁,使用其上下文将会出现问题;
  • 如果商品未被消费,将无法再次购买。
    • 消费购买很重要,否则交易将不会被确认,商品也无法再次购买;
  • 使用 queryProductDetailsAsync() 的结果来显示您商品的价格。
    • 这一点很重要,可使您的应用与 Wallet 应用中针对用户所在位置的价格保持一致;
  • 请务必避免在 Main 线程中进行调用。
    • 遵循这一点很重要,可避免 Main 线程因向 Wallet 应用发出的请求或后端回调而被阻塞;