本机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. 应用将产品提供给最终用户。
1940

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

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

1. 与Catappult Billing SDK建立连接

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

依赖项和权限

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

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

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

dependencies {
	<...other dependencies..>
	implementation 'io.catappult:android-appcoins-billing:0.6.7.0'
}

AndroidManifest.xml中,您需要在查询标签中添加2个权限和1个程序包,以便该SDK可以与AppCoins Wallet通信。

<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" />
	...
</manifest>

启动服务连接

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

AppCoinsBillingStateListener

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

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

如果安装了钱包,服务将立即启动并调用计费状态侦听器。 否则,系统将提示用户下载Appcoins Wallet、安装Appcoins Wallet并设置新钱包。

class MainActivity : Activity() {
  ...
    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 MainActivity extends Activity {
  ...
    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获取公钥,请单击此处。

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

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

void fun checkPurchases() {
  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
}
private void checkPurchases() {
    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();
}

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 can 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 can 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. 启动Appcoins Wallet

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

private fun startPurchase(sku: String, developerPayload: String) {
    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,
        "orderId=" + System.currentTimeMillis(),
        developerPayload,
        "BDS"
    )

    if (!cab.isReady) {
        cab.startConnection(appCoinsBillingStateListener)
    }

    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) {
    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,
            "orderId=" +System.currentTimeMillis(),
            developerPayload,
            "BDS"
        );
    
    //Make sure that the billing service is ready
    if (!cab.isReady()) {
      startConnection();
    }

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

为了让SDK接收购买数据,您需要添加一个函数调用到Catappult Billing SDK的onActivityResult。 这样一来,该SDK将处理并验证购买,然后通过PurchasesUpdatedListener通知您。
以下代码片段展示了如何实施该操作:

class MainActivity : Activity() {
  ...
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        cab.onActivityResult(requestCode, resultCode, data)
    }
  ...
}
class MainActivity extends Activity {
  ...
    @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        cab.onActivityResult(requestCode, resultCode, data);
    }
  ...
}

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

在Appcoins Wallet处理购买后,它会将在该意图下所得的数据返回到您的应用。 在通过上一步骤对onActivityResult进行更改后,该SDK将收到包含数据的通知,然后使用您的公钥验证购买信息。
在该SDK处理并验证购买后,它会通过PurchasesUpdatedListener将购买数据通知给您。 此侦听程序正是在第1步中注册的侦听程序,包含购买更新时的回调,您可以在该回调中获取购买的详细信息并将商品归于用户。

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

NameDefinition
onPurchasesUpdated(responseCode,listPurchases)This method receives the notifications for purchases updates.
class MainActivity : Activity() {
  ...
  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 MainActivity extends Activity {
  ...
    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的消费响应。

ConsumeResponseListener

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

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

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

class MainActivity : Activity() {
  ...
    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 MainActivity extends Activity {
  ...
    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?
是,有个AndroidStudio插件可以指导您逐步完成实施。Catappult计费集成

641

如何将用户链接到购买?
如果您需要将购买链接到用户,您可以通过在开发人员负载中传递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

您还可以在此处获取我们已实施计费的 Google Trivial Drive 版本,以获得已经生效的示例。