Native Android Billing SDK

This guide assumes that you have already Google Play billing integrated into your app. It will help you migrate from Google Play to the Catappult Billing service. Catappult billing service is implemented on the "AppCoins Wallet". The AppCoins Wallet is widely available in different app stores and exposes an Android Service which your app should bind to. Once bound to AppCoinsWallet service, your app can start communicating over IPC using an AIDL interface.

Google Play IAB to AppCoins IAB Migration Guide

Requirements

1. Catappult Account

The migration to AppCoins IAB requires your registration to Catappult.

2. Available App in Catappult's Backoffice

In order to retrieve your public key you need to upload an app to your Catappult account.

3. Catappult Public Key

Similarly to Google Play IAB, AppCoins IAB exposes a public key. You should use AppCoins IAB public key to verify your purchases. It works exactly like Google Play IAB key, thus, you will only need to replace make the replacement.

To retrieve the Catappult public key from Catappult's back office you should go to the section "My Apps" and choose the "API Keys" button.

11211121

This image illustrates where the public key can be retrieved from the back office.

After retrieving the public key from Catappult's back office, you need to change your code, replacing your previous Google Play public key by our public key.

//Example key, use your own
//Don't forget to copy the complete key

String base64EncodedPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzZZoGzIM+TlYDyGWl78g37ChRtrUWCOTnF8Sy4zwC2vqQhI/QETkYM/jMruY3pLvdDOcdWm9xoZLJq5exxqQyhrZbSoziXQ9bIOJwNSjA/WIJnAewAk8QGPhvjNFy0wflCqLDWflrt04vbvvNsdh9UpgceIdinzqE0MhF94HJr1Ovb5JEMktDgU89SarCsx90a4Psk6cdapulGjdpL35dBRkRkM74sfNmSy8Oan9FaI36+z4h88MgTrELHnQ0XTlS32flvCK7nhICV+bzcY89wPRPN4rkqjS4F5nngwAnaz5VMV0Zy5SZBZntLS1QIA641FxJJHAikM8ZYz3hkTn0QIDAQAB";

Integration

1. On build.gradle

First of all, find the build.gradle of your application in your project directory.

622622

Example of build.gradle in our sample project appcoins-iab-sample.

After you located the build.gradle file, add the following code:

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

Our native android SDK uses one main library:

To retrieve the latest version from the previous links you should look at the bottom of the page for "Choose dependency snippet:" label and choose the option "Gradle". Copy the value from the code snippet and add it to the "dependencies" tag of your build.gradle. Check the example below.

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

2. Permissions

You app requires permissions to perform with the AppCoins IAB. The permission is declared in the AndroidManifest.xml file of your app. Since Google Play IAB already declares a permission with name com.android.vending.BILLING, rename it to com.appcoins.BILLING.

Google Play IAB

<uses-permission android:name="com.android.vending.BILLING" />

AppCoins IAB

<uses-permission android:name="com.appcoins.BILLING" />
<uses-permission android:name="android.permission.INTERNET" />

3. Queries

To work with android 11 and above the app also needs to permit queries to the appcoins wallet. The queries are declared in the AndroidManifest.xml

<manifest>
  ...
    <queries>
        <package android:name="com.appcoins.wallet" />
        ...
  </queries>
  ...
</manifest>

4. Starting the Service Connection

The developer needs to call "startConnection" on the billing sdk by acreating two different listeners:

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, the user will be prompted to download the Appcoins Wallet, install it and setup a new wallet.

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();
      // Qury subscriptions sku details
      querySubs();
      Log.d(TAG, "Setup successful. Querying inventory.");
    }

    @Override public void onBillingServiceDisconnected() {
      Log.d("Message: ", "Disconnected");
    }
  };
  ...
}
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()
        // Qury subscriptions sku details
        querySubs()
        Log.d(TAG, "Setup successful. Querying inventory.")
      }

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

PurchasesUpdatedListener

Callback for purchase updates and can be triggered, for example, when the user purchases something in the app. This interface has one method:

Name

Definition

onPurchasesUpdated(responseCode,listPurchases)

This method receives the notifications for purchases updates.

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.
          // You might also want to check the purchase type to prevent consumption of subscription
          // consumeResponseListener is explained in point 6
          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();
      }
    };
  ...
}
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 consumePurchase may be called 
            // to allow the user to purchase the item again.
            // You might also want to check the purchase type to prevent consumption of subscription
            // consumeResponseListener is explained in point 6
            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()
      }
    }
  ...
}

Bellow you can see an example of how to start build and start Appcoins IAB passing AppCoinsBillingStateListener , PurchasesUpdatedListener has arguments.

class MainActivity extends Activity {
  ...
  protected void onCreate(Bundle savedInstanceState) {
    ...
    String base64EncodedPublicKey = MY_KEY
    cab = CatapultBillingAppCoinsFactory.BuildAppcoinsBilling(
      this,
      base64EncodedPublicKey,
      purchasesUpdatedListener
    );
    cab.startConnection(appCoinsBillingStateListener);
    ...
  }
  ...
}
class MainActivity : Activity() {
  ...
    override fun onCreate(savedInstanceState: Bundle ?) {
        ...
        val base64EncodedPublicKey = MY_KEY
        cab = CatapultBillingAppCoinsFactory.BuildAppcoinsBilling(
            this,
            base64EncodedPublicKey,
            purchasesUpdatedListener
        )
        cab.startConnection(appCoinsBillingStateListener)
        ...
    }
  ...
}

When the setup finishes successfully you should immediately check for pending and/or owned purchases.
The example below shows how to check pending or owned purchases:

private void checkPurchases() {
    PurchasesResult purchasesResult = cab.queryPurchases(SkuType.inapp.toString());
    List<Purchase> purchases = purchasesResult.getPurchases();
}
void fun checkPurchases() {
  val purchasesResult = cab.queryPurchases(SkuType.inapp.toString())
  val purchases = purchasesResult.purchases
}

5. Query Sku Details

In order to have access inside the app to the SKU prices, title, description, etc... you can use the billing functionality to query the Sku Details, first, you need the listener triggered after a call to querySkuDetailsAsync.

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

Here is an example of callSkuDetails called in the AppcoinsBillingStateListener:

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

6. Making a purchase

To make a purchase you need to add the following code to the "buy" button function:

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

Afterwards in the onActivityResult you need to pass the result of the activity that contained the purchase flow to the AppcoinsBillingClient so that the result of the purchase can be processed and verified with your Catappult public key.
The following code snippet shows how you can implement it:

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

7. Consume a purchase

To receive the response of consuming a purchase you need to create the following listener:

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, find an example of the ConsumeResponseListener implementation.

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

User Purchase Validation

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 with the userId on the PurchaseUpdatedListener onPurchaseUpdated by looping through the list of purchases you can extract the payload as shown below.

for (Purchase purchase: purchases) {
  String developerPayload = purchase.getDeveloperPayload();
  if (developerPayload.equals("user12345")) {
    ...
  }
}
for (purchase in purchases) {
    val developerPayload = purchase.developerPayload
    if (developerPayload == "user12345") {
        ...
    }
}

Testing

For testing purposes (before submitting to Catappult) you can use the following data to test the billing of you 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 by clicking here to get your hands on an already working sample.

Known issues