Non Consumable on Android

In this guide, we will build a small application with a non-consumable product that works on Android.

We will proceed in steps: setup, initialization, presentation and purchase.

First some setup.

  1. Install NodeJS and Cordova

  2. Setup your Cordova project

  3. Prepare an Application on Google Play

  4. Install the In-App Purchases plugin

  5. Build a Release APK

  6. Create a Product on Google Play

  7. Upload a Release APK to Google Play

  8. Prepare Test Accounts

Of couse you can skip the first few steps if you already have a working application you want to integrate the code into.

Once we have a Cordova application with IAP support enabled and everything is in place on Google Play, we will get into some coding.

  1. Initialize the in-app purchase plugin

  2. Handle the purchase events

  3. Deliver our product

  4. Secure the transactions

Setup

1. Install Dependencies

Needless to say, make sure you have the tools installed on your machine. Developing from a mac is generally recommended for doing iOS development, it's way easier. If you only plan on doing Android, then everything will work.

During the writing of this guide, I've been using the following environment:

  • NodeJS v10.12.0

  • Cordova v8.1.2

  • macOS 10.14.1

I'm not saying it won't work with different version. If you start fresh, it might be a good idea to use an up-to-date environment.

2. Create Cordova Project

Making sure we have a Cordova project that we can build for Android and/or iOS.

Create the project

If it isn't already created:

$ cordova create CordovaProject cc.fovea.purchase.demo PurchaseNC
Creating a new cordova project.

For details about what those parameters are:

$ cordova help create

Note, feel free to pick a different project ID and name. Remember whatever values you put in here.

Let's head into our cordova project's directory (should match whatever we used in the previous step.

$ cd CordovaProject

Add Android platform

$ cordova platform add android

Will output:

    Using cordova-fetch for cordova-android@~7.1.1
    Adding android project...
    [...]
    Saving android@~7.1.1 into config.xml file ...

Let's check if that builds.

$ cordova build android

Which outputs:

    Android Studio project detected
    Starting a Gradle Daemon (subsequent builds will be faster)
    [...]
    BUILD SUCCESSFUL in 1m 49s
    Built the following apk(s):
    __EDITED__/platforms/android/app/build/outputs/apk/debug/app-debug.apk

Alright, seems like we have no problems with our Android build chain. If you do have problems, fixing it is out of scope from this guide but it's required!

3. Create Google Play Application

Make sure we have a Google Play application created and configured.

Create the App

Need more help? I recommend you check Google's own documentation. It's well detailed, easy to follow and probably the most up-to-date resource you can find.

Retrieve the Billing Key

We need to inform the plugin of our app's BILLING_KEY. That piece of information can be found on the Google Play Publisher Console.

So head again to your Google Play Console.

In the "All Applications" menu, go to the application you want to setup. In my case "Cordova Purchase Demo".

From there, find the "Monetization setup" section (on the left-side panel).

Under Licensing, you'll find this long Base64-encoded string (an RSA public key). Keep it around for later reference. That's your Billing Key.

The Billing Key will be required to install the plugin on Android and setup receipt validation.

4. Install Cordova Purchase Plugin

To install the plugin, we will use the usual cordova plugin add command. There is little subtleties on Android.

When you need Android support, you need to setup your BILLING_KEY.

cordova plugin add cordova-plugin-purchase --variable BILLING_KEY="<BILLING_KEY>"

You can find that piece of information on the Google Play Publisher Console, as explained here.

Now let's try to build.

cordova build android

Successful build?

[...]
BUILD SUCCESSFUL in 2s

All good! Seems like we can build an app with support for the Billing API.

Let's now prepare a release APK.

5. Android Release APK

To generate a release build, I generally use the following script: android-release.sh

The script calls cordova build android --release with the correct command line arguments. It requires you have generated a keystore file for your application already.

If you haven't generated a keystore file for your application yet, you can use the following command line:

keytool -genkey -v -keystore android-release.keystore -alias release \
-keyalg RSA -keysize 2048 -validity 10000

I'll ask you a few questions. The only tricky one is "Do you wan't to use the same password for the alias?", the answer is yes. Please note that the above command defines the keystore's alias as release, you can use any value, but just remember the value you chose.

Keep the android-release.keystore file in a safe place, backup it everywhere you can! Don't loose it, don't loose the password. You won't EVER be able to update your app on Google Play without it!

Then build.

$ export KEYSTORE_ALIAS=release
$ export KEYSTORE_PASSWORD=my_password
$ ./android-release.sh

Replace $KEYSTORE_ALIAS and $KEYSTORE_PASSWORD with whatever match your those from your keystore file...

The output should end with a line like this:

Build is ready:

<SOME_PATH>/android-release-20181015-1145.apk

There you go, this is your first release APK.

6. Upload to Google Play

Once you have built your release APK, you need to upload it to Google Play in order to be able to test In-App Purchases. In-App Purchase is not enabled in "debug build". In order to test in-app purchase, your APK needs to be signed with your release signing key. In order for Google to know your release signing key for this application, you need to upload a release APK:

  • Signed with this key.

  • Have the BILLING permission enabled

    • it is done when you add the plugin to your project, so make sure you didn't skip this step.

Google already provides detailed resource on how to upload a release build. What we want here is to:

  1. create an internal testing release

  2. upload it

  3. publish it (privately probably).

Once you went over those steps, you can test your app with in-app purchase enabled without uploading to Google Play each time, but you need to sign the APK with the same "release" signing key.

Note that it might up to 24 hours for your IAP to work after you uploaded the first release APK.

6. Create In-App Products

There is still a bit more preparatory work: we need to setup our in-app product.

Back in the "Google Play Console", open the "Store presence" ⇒ "In-app products" section.

If you haven't yet uploaded an APK, it'll warn you that you need to upload a release APK.

Once this is done, you can create a product. Google offers 2 kinds of products:

  • Managed Products

  • Subscriptions

The latest is for auto-renewing subscriptions, in all other cases, you should a "Managed Product".

  • Click the CREATE button.

  • Fill in all the required information (title, description, prices).

  • Make sure the Status is ACTIVE.

  • SAVE

And we're done!

There's might be some delay between creating a product on the Google Play Console and seeing it in your app. If your product doesn't show up after 24h, then you should start to worry.

8. Prepare Test Accounts

To test your Google Play Billing implementation with actual in-app purchases, you must use a test account. By default, the only test account registered is the one that's associated with your developer account. You can register additional test accounts by using the Google Play Console.

  1. Navigate to Settings > Account details.

  2. In the License Testing section, add your tester's email addresses to Gmail accounts with testing access field.

  3. Save your changes.

Testers can begin making purchases of your in-app products within 15 minutes.

Coding

Initialization

Assuming you're starting from a blank project, we'll add the minimal amount of HTML for the purpose of this tutorial. Let's replace the <body> from the www/index.html file with the below.

<body>
  <div class="app">
    <p id="locked">FEATURE LOCKED</p>
    <div id="nonconsumable1-purchase">Please wait...</div>
  </div>
  <script type="text/javascript" src="cordova.js"></script>
  <script type="text/javascript" src="js/index.js"></script>
</body>

Let's also make sure to comment out Cordova template project's CSS.

You also need to enable the 'unsafe-inline' Content-Security-Policy by adding it to the default-src section:

<meta http-equiv="Content-Security-Policy"
      content="default-src 'self' 'unsafe-inline' [...]" />

You can download the full index.html file here.

We will now create a new JavaScript file and load it from the HTML. The code below will initialize the plugin.

document.addEventListener('deviceready', initStore);

function initStore() {

    if (!window.store) {
        console.log('Store not available');
        return;
    }

    store.register({
        id:    'nonconsumable1',
        type:   store.NON_CONSUMABLE
    });

    store.error(function(error) {
        console.log('ERROR ' + error.code + ': ' + error.message);
    });
    
    // ... MORE HERE SOON

    store.refresh();
}

Here's a little explanation:

Lines 5-8, we check if the plugin is loaded.

Lines 10-13, we register the product with ID nonconsumable1. We declare it as a non-consumable (store.NON_CONSUMABLE). ⇒ API Documentation.

Lines 15-17, we setup an error handler. It just logs errors to the console.

Line 21, we perform the initial refresh() of all products states. ⇒ API Documentation.

Whatever your setup is, you should make sure this runs as soon as the javascript application starts. You have to be ready to handle IAP events as soon as possible.

Presentation

For the sake of this tutorial's simplicity, let's store the state of the feature (locked or unlocked) in localStorage: window.localStorage.unlocked

When the app starts, we'll refresh the #locked html element:

function refreshLockedUI() {
    document.getElementById('locked').textContent =
        'Feature ' + window.localStorage.unlocked === 'YES' ? 'UNLOCKED! \o/' : 'locked :('
}

document.addEventListener('deviceready', refreshLockedUI);

This part was easy,. Now for a bit more challenge, let's display the title, description and price of the in-app product for purchasing the feature, nonconsumable1 that we registered in the initialization code above.

We'll add a little more at initStore() function, line 20.

store.when('nonconsumable1').updated(refreshProductUI);

Then define the refreshProductUI() function at the bottom of the file.

function refreshProductUI(product) {
    const info = product.loaded
        ? `title: ${product.title}<br/>` +
          `desc: ${product.description}<br/>` +
          `price: ${product.price}<br/>`
        : 'Retrieving info...';
    const button = product.canPurchase
         ? '<button onclick="purchaseNonConsumable1()">Buy Now!</button>'
         : '';
    const el = document.getElementById('nonconsumable1-purchase');
    el.htmlContent = info + button;
}

Lines 2, check if the product has been loaded.

Lines 3-5, retrieve and display product informations.

Lines 7-8, add the "Buy Now!" button if product can be purchased.

If you want a bit more background information about this, please check the Displaying Products section and the ⇒ API Documentation for full details about the fields found for a product.

Let's build and test that!

Testing

In-App Purchase on Android will only work on release builds, i.e. builds that are signed with the same certificate that the one you're using for APKs you upload on Google Play.

We went over this already, in the Android Release APK section. This is how we're building the release build:

./android-release.sh

Feel free to use your own method to create a release APK.

Install the generated APK on a real device (important!):

adb install -r path/to/android-release.apk

IAP do not work in the emulator!

If needed, you can them access the logs with adb, like this:

adb logcat "chromium:I *:E"

The android-release.sh script has a shortcut that will build, install and start adb logcat:

./android-release.sh run

You can now make purchase with on of your test accounts.

A test account can purchase an item in your product list only if the item is published.

Purchase

Now that we have our purchase button, let's implement the purchaseNonConsumable1 button.

function purchaseNonConsumable1() {
    store.order('nonconsumable1');
}

Can it be easier than that? Well, not so fast! The code as it is won't do much with this order request. To process the purchase we have to implement the various steps of the purchase flow.

I already introduced the purchase flow in the introduction, check the Purchase process section if you need a refresher. The official documentation provides more details. ⇒ API Documentation

So the first thing that will happen is that the canPurchase state of the product will change to false. But remember, we added this in the previous step:

store.when('nonconsumable1 updated', refreshProductUI);

So we're covered. The UI will be refreshed when canPurchase changes.

When the user is done with the native interface (enter his/her password and confirm), you'll receive the approved event, let's handle it by adding the below to the initStore() function, before the call to store.refresh().

store.when('nonconsumable1 approved', function(p) {
    p.verify();
});
store.when('nonconsumable1 verified', finishPurchase);

Then we will add the finishPurchase function at the end of our JavaScript file.

function finishPurchase(p) {
    window.localStorage.unlocked = "YES";
    refreshLockedUI();
    p.finish();
}

This is a good enough implementation, but let's go one more step and setup a receipt validator.

For this tutorial, we will use Fovea's own service which is free while in development:

  1. Head to https://billing.fovea.cc/ and create an Account.

  2. Setup your project's package name and billing key, Save.

  3. Go to the Cordova Setup page to copy the line store.validator = "<something>".

Copy this line inside the initStore() function, anywhere before the initial store.refresh(). Also add the recommended Content-Security-Policy to your index.html as mentioned in the documentation.

Alright, we're done with coding! Let's try the whole thing now. We will repeat the steps from the Testing section above:

./android-release.sh run

Here we go. Let's try to purchase from the app, make sure you are using one of the test account.

The full source for this tutorial is available here: https://gist.github.com/j3k0/372440b2e9e250f318e669bb94947003

Last updated