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:

  1. Setup connection with Catappult Billing SDK;
  2. Query In-App Products from Catappult Billing SDK;
  3. End-user wants to buy a product on your app;
  4. Application launches the AppCoins Wallet via Catappult Billing SDK;
  5. The AppCoins Wallet handles the payment and, on completion, calls back your application;
  6. Application requests Catappult Billing SDK to validate transaction data;
  7. Application gives the product to the end-user.
1121

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:

  1. Setup connection with Catappult Billing SDK;
  2. Query In-App Products;
  3. Launch the Purchase Flow;
  4. 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:

NameDefinition
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

NameDefinition
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:

NameDefinition
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.

NameDefinition
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 a Activity.
    • This is important because if the Activity is destroyed, there will be problems using the context from it;
  • 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;