In this guide, we will build a small application with a subscription that works on Android with Google Play.
We will proceed in steps: setup, initialization, presentation and purchase.
First some setup.
Install NodeJS and Cordova
Setup your Cordova project
Prepare an Application on Google Play
Install the In-App Purchases plugin
Build a Release APK
Create a Product on Google Play
Upload a Release APK to Google Play
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.
Initialize the in-app purchase plugin
Handle the purchase events
Deliver our product
Secure the transactions
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.
Making sure we have a Cordova project that we can build for Android and/or iOS.
If it isn't already created:
$ cordova create CordovaProject cc.fovea.purchase.demo PurchaseNCCreating 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
$ cordova platform add android
Will output:
Using cordova-fetch for cordova-android@~7.1.1Adding 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 detectedStarting a Gradle Daemon (subsequent builds will be faster)[...]BUILD SUCCESSFUL in 1m 49sBuilt 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!
Make sure we have a Google Play application created and configured.
Open the Google Play Console.
Click "Create Application", fill in the required fields.
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.
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 "Developments tools" ⇒ "Services & APIs" section (on the left-side panel).
That is where you'll find this long Base64 string they call "Your license key for this application". 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.
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.
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.
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:
create an internal testing release
upload it
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.
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.
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.
Navigate to Settings > Account details.
In the License Testing section, add your tester's email addresses to Gmail accounts with testing access field.
Save your changes.
Testers can begin making purchases of your in-app products within 15 minutes.
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.
<!DOCTYPE html><html><head><meta http-equiv="Content-Security-Policy" content="default-src 'self' https://reeceipt-validator.fovea.cc 'unsafe-eval' 'unsafe-inline' gap:; style-src 'self' 'unsafe-inline'; media-src *"></head><body style="margin-top: 50px"><script type="text/javascript" src="cordova.js"></script><script type="text/javascript" src="js/index.js"></script></body></html>
Make sure to comment out Cordova template project's CSS.
We enabled the 'unsafe-inline'
Content-Security-Policy
by adding it to the default-src
section.
Since we'll be using Fovea.Billing, you also need to add your validation server to default-src, find your in the cordova setup documentation. On my case it's https://reeceipt-validation.fovea.cc
.
Let's now or edit the JavaScript file js/index.js
. Replace the content with the code below, which will setup a minimal application framework and render some HTML into the page based on the app state.
document.addEventListener('deviceready', onDeviceReady);function onDeviceReady() {const state = {};function setState(attr) {Object.assign(state, attr);render(state);}setState({error: '',status: 'Loading...',product1: {},product2: {},});// We will initialize the in-app purchase plugin here.function render() {const body = document.getElementsByTagName('body')[0];body.innerHTML = `<pre>${state.error}subscription: ${state.status}</pre>`;}}
Now let's initialize the in-app purchase plugin, where indicated in the onDeviceReady
function.
// We should first register all our products or we cannot use them in the app.store.register([{id: 'my_subscription1',type: store.PAID_SUBSCRIPTION,}, {id: 'my_subscription2',type: store.PAID_SUBSCRIPTION,}]);// Setup the receipt validator service.store.validator = '<<< YOUR_RECEIPT_VALIDATION_URL >>>';// Show errors for 10 seconds.store.error(function(error) {setState({ error: `ERROR ${error.code}: ${error.message}` });setTimeout(function() {setState({ error: `` });}, 10000);});// Later, we will add our events handlers here// Load informations about products and purchasesstore.refresh();}
Here's a little explanation:
We start by registering the product with ID my_subscription1
and my_subscription2
.
We declare the products as renewable subscriptions (store.PAID_SUBSCRIPTION
). ⇒ API Documentation.
We setup the link to the receipt validation server. If you're using Fovea.Billing, you'll find it here.
We setup an error handler. It will display the last error message for 10 seconds on top of the screen.
Finally, we perform the initial refresh()
of all product states. ⇒ API Documentation.
Whatever your setup is, you should make sure the initialization code is executed as soon as the javascript application starts. You have to be ready to handle IAP events as soon as possible.
Let's now display the subscription status, as it is provided by the native platform. Before the call to store.refresh()
we will add an handler for the update
event:
This hander is called whenever there's a change in our products' state.
// Called when any subscription product is updatedstore.when('subscription').updated(function() {const product1 = store.get('my_subscription1') || {};const product2 = store.get('my_subscription2') || {};let status = 'Please subscribe below';if (product1.owned || product2.owned)status = 'Subscribed';else if (product1.state === 'approved' || product2.state === 'approved')status = 'Processing...';setState({ product1, product2, status });});
Now we can display some information about our products from the render()
function:
function render() {const purchaseProduct1 = '';const purchaseProduct2 = '';const body = document.getElementsByTagName('body')[0];body.innerHTML = `<pre>${state.error}subscription: ${state.status}id: ${state.product1.id || ''}title: ${state.product1.title || ''}state: ${state.product1.state || ''}descr: ${state.product1.description || ''}price: ${state.product1.price || ''}expiry: ${state.product1.expiryDate || ''}</pre>${purchaseProduct1}<pre>id: ${state.product2.id || ''}title: ${state.product2.title || ''}descr: ${state.product2.description || ''}price: ${state.product2.price || ''}state: ${state.product2.state || ''}expiry: ${state.product2.expiryDate || ''}</pre>${purchaseProduct2}`;}
Whenever anything happens to our product, the interface will now be updated to reflect the current state.
Displaying your product information this way, i.e. exactly as loaded from the Store, is required by Apple and Google. Do otherwise and they might simply reject your application.
You can also disconnect the event handler with store.off()
so your subscription view is only updated when it's visible.
If you want a bit more background information about all of this, please check the introduction's displaying products section and the ⇒ API Documentation for full details about the fields found for a product.
So far so good, but what if we could actually initiate a purchase? To do so, we'll add a purchase button for both products. We already added placeholders for the purchase buttons, let's create them. Before displaying a purchase button, we need to make sure the user can actually purchase the item. It could be impossible for a few reasons: there's already a purchase in progress, the product is already owned, the feature is disabled for the user (Child Mode).
In our render()
function, we update the code that initialized purchaseProduct1
and purchaseProduct2
.
// button for product 1const purchaseProduct1 = state.product1.canPurchase? `<button onclick="store.order('my_subscription1')">Subscribe</button>` : '';// same for product 2const purchaseProduct2 = state.product2.canPurchase? `<button onclick="store.order('my_subscription2')">Subscribe</button>` : '';
The buy button should only be displayed when `product.canPurchase` is true. Otherwise, calling `store.order()` will generate an error.
We could make this a little nicer by changing the button labels to "Upgrade" or "Downgrade" when the other product is owned
, I will let this as an exercise to the reader.
Now, let's build and test!