Native Android Billing SDK
如果您希望以中文查看此页面,请单击此处。
The Catappult Native Android Billing SDK is a simple solution to implement Catappult billing.
It consists of a Billing client that communicates with the AppCoins Wallet and allows you to get your products from Catappult and process the purchase of those items.
In Summary
The billing flow in your application with the SDK is as follows:
- Setup connection with Catappult Billing SDK;
- Query In-App Products from Catappult Billing SDK;
- End-user wants to buy a product on your app;
- Application launches the AppCoins Wallet via Catappult Billing SDK;
- The AppCoins Wallet handles the payment and, on completion, calls back your application;
- Application requests Catappult Billing SDK to validate transaction data;
- Application gives the product to the end-user.
Moving on to the implementation of the SDK in your application, your first goal is to instantiate the client and connect it. After the connection, this billing client can be used to get the products registered in Catappult, start a purchase, and process it.
So the implementation consists of 4 steps:
- Setup connection with Catappult Billing SDK;
- Query In-App Products;
- Launch the Purchase Flow;
- Process the purchase and give the item to the user.
Developer Tools
Android Studio plug-in: To help implement our Native Android Billing SDK, we have developed an Android Studio plug-in that will guide you step by step. The plug-in can be downloaded here and you can read more about this plug-in here.
Example implementation: We have an example implementation you can use for reference here. Note that this is not PRODUCTION GRADE.
1. Setup connection with Catappult Billing SDK
Before instantiating and connecting to Catappult, you need to add dependencies and permissions in your application to be able to use the Catappult SDK.
Dependencies and Permissions
In your project build.gradle
make sure you have the following repositories:
allprojects {
repositories {
google()
mavenCentral()
}
}
In your app's build.gradle
, add appcoins billing client to your dependencies. To get the latest version check the following link: android-appcoins-billing
dependencies {
implementation("io.catappult:android-appcoins-billing:0.9.+") //check the latest version in mvnrepository
<...other dependencies..>
}
In AndroidManifest.xml you need to add 2 permissions and a package in the queries tag so the SDK can communicate with the 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>
Starting the Service Connection
Once the permissions and dependencies are all added, you need to initialize an instance of AppcoinsBillingClient. This is the instance used to communicate with Catappult Billing Library. You should have only one active instance at any time and it should be done in the initialization of the application.
To initialize the billing client and start the connection, the PurchasesUpdatedListener is required for the client initialization and AppCoinsBillingStateListener is required to start the connection. This section explains how to create the 2 required instances and how to instantiate and connect the AppcoinsBillingClient. The PurchaseUpdatedListner will be expanded and explained in step 4.
AppCoinsBillingStateListener
This is a callback for the billing setup process and state. This listener uses two different methods:
Name | Definition |
---|---|
onBillingSetupFinished(responseCode) | This method is called when the billing setup process is complete. |
onBillingServiceDisconnected() | This method is called when the billing connection is lost. |
If the wallet is installed, the service will start immediately and the billing state listener will be called. Otherwise, if it is possible, the payments will be done via Web Browser, if not, the user will be prompted to download the Appcoins Wallet, install it, and setup a new 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 purchases of Consumables
checkPendingConsumables()
// Check for pending and active Subscriptions
checkSubscriptions()
// 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 purchases of Consumables
checkPendingConsumables();
// Check for pending and active Subscriptions
checkSubscriptions();
// 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
Below you can see an example of how to build and start Appcoins IAB by passing AppCoinsBillingStateListener, PurchasesUpdatedListener, and the public key as arguments.
To get the public key from catappult click here.
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);
...
}
...
}
When the setup finishes successfully you should immediately check for pending purchases of Consumables and the active or pending Subscriptions. If there are pending purchases you should consume them. The consumption will be explained in step 4.
Consumables
The example below shows how to check for pending Purchases of Consumables:
void fun checkPendingConsumables() {
val thread = Thread {
val purchasesResult = cab.queryPurchases(SkuType.inapp.toString())
val purchases = purchasesResult.purchases
// TODO: Consume the Purchase and give the Item to the User
}
thread.start()
}
private void checkPendingConsumables() {
Thread thread = new Thread(() -> {
PurchasesResult purchasesResult = cab.queryPurchases(SkuType.inapp.toString());
List<Purchase> purchases = purchasesResult.getPurchases();
// TODO: After you should consume the Purchase and give the Item to the User
});
thread.start();
}
Subscriptions
To verify the active/pending Subscriptions, use the queryPurchases
method. The result consists of Pending Subscriptions (to be consumed) and Active ones. To correctly remove from the User the Subscriptions that were expired, you should match the ones missing in the result received that are currently available to the User.
The example below shows how to check for Subscriptions:
void fun checkSubscriptions() {
val thread = Thread {
val subsResult = cab.queryPurchases(SkuType.subs.toString())
val subs = subsResult.purchases
// TODO: Consume the Subscriptions and give the Subscription to the User
// TODO: Remove Subscriptions from the User when not present in this list
}
thread.start()
}
private void checkSubscriptions() {
Thread thread = new Thread(() -> {
PurchaseResult subsResult = cab.queryPurchases(SkuType.subs.toString());
List<Purchase> subs = subsResult.getPurchases();
// TODO: Consume the Purchase and give the Subscription to the User
// TODO: Remove Subscriptions from the User when not present in this list
});
thread.start();
}
2. Query In-App Products
After starting the connection, you should query Catappult for the products available to buy in order to display them to the user with the correct pricing from the Catappult. This query includes not only the product's title but also the description, value, etc...
To query the products you can use the querySkuDetailsAsync which requires a SkuDetailsResponseListener to process Catappult's response.
SkuDetailsResponseListener
Name | Definition |
---|---|
onSkuDetailsResponse(responseCode, skuDetailsList) | This method receives the result of a query of SKU details and the list of SKU details that were queried |
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
}
}
...
}
After the listener is created, you can pass it to the querySkuDetailsAsync together with the parameters as shown below:
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. Launch the Purchase Flow
To start a purchase flow use the function lauchBillingFlow. This takes in an instance of BillingFlowParams which includes the SKU, the type of purchase (in-app purchase or in-app subscription) and data to be used by the developer. The following snippet shows a possible function to be associated with the "buy" button:
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(
sku,
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. Process the purchase and give the item to User
After the SDK processes and validates the purchase, it will notify you through the PurchasesUpdatedListener of the purchase data. This listener is the one registered in step 1 and contains the callback for when a purchase is updated, and in this callback is where you can get the details of the purchase and attribute the item to the user.
PurchasesUpdatedListener
Below is the definition of the PurchasesUpdatedListener and a sample snippet:
Name | Definition |
---|---|
onPurchasesUpdated(responseCode,listPurchases) | This method receives the notifications for purchases updates. |
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();
}
};
...
}
Consume a purchase
After the purchase is completed, it needs to be consumed. To consume a purchase, use the function consumeAsync. This function is shown in the PurchaseUpdatedListener snippet and requires a ConsumeResponseListener to handle Catappult's consumption response.
Note that, if you do not consume the purchase within 48 hours, it will automatically be refunded.
ConsumeResponseListener
The callback notifies the application when the item consumption operation ends.
Name | Definition |
---|---|
onConsumeResponse(responseCode,purchaseToken) | Callback that notifies if a consume operation has ended. |
Below you can find an example of the ConsumeResponseListener implementation.
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.");
}
};
...
}
FAQ
What is the target SDK level supported?
Currently, the target level of the Native Android SDK is 33.
What is the minimum SDK level supported?
Currently, the minimum supported level of the Native Android SDK is 19.
Are there any helpers to implement the SDK?
Yes, there is an Android Studio plug-in that will guide you step-by-step. The plug-in can be downloaded here.
How to link a user to a purchase?
If you need to link a purchase to a user, you can do it by passing the userId in the developer payload. The following example shows one example of a UserId passed to the purchase function.
startPurchase(sku, "user12345")
startPurchase(sku, "user12345");
You can retrieve this payload in the Purchase object. The following sample shows how to extract the payload in PurchasesUpdatedListener and do conditional processing:
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 {
...
}
};
How to test the purchase flow without setting ownership of the app?
For testing purposes, you can use the following data to test the billing of your application.
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.
How to implement the AppCoins Billing SDK using AARs or JARs?
To implement the SDK using AARs our JARs, make sure to follow the Android Developer Guide on AAR and JAR Libraries .
You can obtain the files in the official mavenRepository.
When adding to the gradle your files, don't forget to also add any dependency that is used by the AppCoins Billing SDK, and follow the same process for those dependencies. To get the dependencies, see the Compile Dependencies section (don't forget to use the ones of the version you are implementing).
Frequent issues
Some frequent issues that we have recognized are mostly avoidable by guaranteeing the following:
- Initialize the AppCoins Billing SDK in the
Application
class of your project instead of in aActivity
.- This is important because if the
Activity
is destroyed, there will be problems using the context from it;
- This is important because if the
- Items not available for a new purchase if it isn't consumed.
- It is important to consume the purchases, otherwise the transaction won't be acquired and the item won't be available for a new purchase;
- Show the prices for your products using the result from
querySkuDetailsAsync()
.- This is important to have matching prices for the users location in both your Application and the Wallet App;
- Make sure to avoid making calls in the
Main
thread.- This is important to follow so that the
Main
thread is not blocked with request made to the Wallet App or Backend callbacks;
- This is important to follow so that the
Updated 16 days ago