Native Android Billing SDK

如果您希望以中文查看此页面,请单击此处

The Catappult 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 Appcoins Wallet;
  4. Process the purchase and give the item to the user.

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 {
    <...other dependencies..>
    implementation 'io.catappult:android-appcoins-billing:0.6.7.0'
}

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.
To initialize the billing client and start the connection, the PurchasesUpdatedListener is required for the client initialization and AppCoinsBillingStateListeneris 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, the user will be prompted to download the Appcoins Wallet, install it, and setup a new 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

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

When the setup finishes successfully you should immediately check for pending and/or owned subscriptions. If there are pending purchases you should consume them. The consumption will be explained in step 4.
The example below shows how to check pending or owned purchases:

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. Query In-App Products

After starting the connection, you can query Catappult for the products available to buy in order to display them to the user. 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 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
        }
    }
    ...
}

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 Appcoins Wallet

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

In order for the SDK to receive the purchase data, you need to add a function call to Catappult's Billing SDK's onActivityResult. With this, the SDK will process and validate the purchase and notify you through PurchasesUpdatedListener.
The following code snippet shows how you can implement it:

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. Process the purchase and give item to User

After the Appcoins Wallet processes the purchase, it will return to your app the resulting data in the intent. With the changes made to onActivityResult in the previous step, the SDK will be notified with the data and it will validate the purchase info with your public key.
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 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();
      }
    };
  ...
}

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.

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 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.");
        }
    };
  ...
}

FAQ

Are there any helpers to implement the SDK?
Yes, there is an AndroidStudio plugin that will guide you step-by-step. Catappult Billing Integration

641

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 a certified 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 the version of Google's Trivial Drive with our billing implementation here to get your hands on an already working sample.