Google Play IAP verification using Cloud Functions
If you ever wanted to monetize your app on Google Play you are bound to use Google Play IAP-(In app purchases).
Google conveniently provides Google Play Billing Library to make implementing billing into your app easy. While the documentation shows how to implement billing library on Android, it also mentions how developer has to implement their own purchase verification (Verification on a server is the preferred way, verification can also be performed on device as well). This article will cover the Google Play Billing Library verification with Firebase Cloud Functions (which will act as a secured server).
Firebase Cloud Functions are serverless, meaning you don’t have to manage a server instance yourself - Firebase will take care of it automatically. It also has a free tier that is enough for most people (pricing information).
If you want to learn more about Google Play Billing Library implementation on Android, check out this codelab . You can also take a look at the samples provided by Google here.
Requirements
-
Firebase/Google Cloud Project (Free tier is enough to get started)
Setting up
In order to access the Google Developer APIs in Cloud Functions we need to first create a service account in Google API console and then link it to Google Play Developer account and provide necessary access to it. Later we can use the googleapis from Cloud Function to get information about the purchases.
Google Play Console Setup :
-
Go to Settings > Developer Account > API access. Make sure you have the necessary permissions to access all the settings in Google Play Console and Firebase project is linked to app in Google Play Console.
-
In that API access > Service Accounts select Create Service Account
- This would open a dialog as shown above. To create a service account you have to navigate to Google API Console as provided in the first step of the dialog.
- Click on Create Service Account. You will be now asked to enter service account name, ID and description. Once you filled those click Create.
- Second step is optional but you can give role access to this service account.
- In third step select you will have option to create a key. Click on create key and select JSON option. A JSON file will be downloaded with
project_id
,private_key
,private_key_id
and other information.
-
Once you created the service account and downloaded the JSON file, Go back to Google Play Console and click on DONE in the dialog. Created service account should automatically be displayed in the service accounts list.
-
Now click on Grant Access for the service account created. That should automatically take you to Users & permissions and open the following dialog.
-
Set the Role to Finance, this allows the service account to get information related to purchases and subscriptions. You can select other roles (Administrator, Release Manager, Product Lead, Customer Service, Read Only) that makes use of Google Play Developer API in future. Finally click on add user.
-
Do note that some permissions may disable the access provided by finance role which is required for getting the purchases/subscriptions information.
This concludes Google Play Console setup
Cloud Functions :
Now for the fun part, I will be using Firebase Cloud Functions and Typescript (You can set it up using Google Cloud Functions as well). This example shows verification for subscription type only, but the process is similar for one time purchased products as well.
-
We will be using Google Play Developer API
-
Purchases.products
- Reference -
Purchases.subscriptions
- Reference
So ideally what we want to do is, Cloud function will receive purchase_token
, sku_id
and package_name
as input data. We will then query the realtime database or firestore to see any other user is using this purchase token. If no one has that, we will make the get call to Purchases.products
or Purchases.subscriptions
and get information related to that purchase like orderId, start and expiration time for subscriptions, etc.
If the purchase token is valid, we will receive a successful response. Once we receive a successful response, we will assign this particular purchase token to the user who called the cloud function. If the response fails, we will send back a failure response to app and notify user.
::P.S. You can avoid saving subscription/purchase information in realtime database or firestore. This is just to avoid subscription sharing between two or more users.::
Enough talk, show me the code 😑:
In the example below I will not be showing how to query user purchases from database, since it varies depending on how you architecture it. But you can refer the Firebase docs to learn on how to retrieve, save and build queries for realtime database and firestore. You can follow this documentation on how to write cloud functions using typescript.
- First install googleapis dependency.
npm i googleapis
- Then open/create index.ts
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import * as key from './service-account-key.json'; // JSON key file
import { google } from 'googleapis';
import { CallableContext } from 'firebase-functions/lib/providers/https';
admin.initializeApp();
const authClient = new google.auth.JWT({
email: key.client_email,
key: key.private_key,
scopes: ["https://www.googleapis.com/auth/androidpublisher"]
});
const playDeveloperApiClient = google.androidpublisher({
version: 'v3',
auth: authClient
});
export const verifySubscription = functions.https.onCall(async (data, context: CallableContext) => {
const skuId: string = data.sku_id;
const purchaseToken: string = data.purchase_token;
const packageName: string = data.package_name;
try {
await authClient.authorize();
const subscription = await playDeveloperApiClient.purchases.subscriptions.get({
packageName: packageName,
subscriptionId: skuId,
token: purchaseToken
});
if (subscription.status === 200) {
// Subscription response is successful. subscription.data will return the subscription information.
return {
status: 200,
message: "Subscription verification successfuly!"
}
}
} catch (error) {
// Logging error for debugging
console.log(error)
}
// This message is returned when there is no successful response from the subscription/purchase get call
return {
status: 500,
message: "Failed to verify subscription, Try again!"
}
});
Umm, Just copy and paste the code?
Alright, let me explain what the above code block does. So for interacting with Google Play Developer API we need an auth scope, This is where the service account we created is used. We are importing the service account key (JSON file) we downloaded. We are also importing googleapis
, firebase-admin
, firebase-functions
.
After that we are creating the authClient
and playDeveloperApiClient
. In authClient
we are using the email and key from the service account key file. We are setting the scope to androidpublisher
since we want to access the Google Play Developer APIs.
const authClient = new google.auth.JWT({
email: key.client_email,
key: key.private_key,
scopes: ["https://www.googleapis.com/auth/androidpublisher"]
});
const playDeveloperApiClient = google.androidpublisher({
version: 'v3',
auth: authClient
});
Now that we have our authClient and playDeveloperApiClient setup. let’s move to creating the star of the show: Cloud Functions 🎉, There are many variants but we will be using the callable kind to call the functions directly from the app.
export const verifySubscription = functions.https.onCall(async (data, context: CallableContext) => {});
functions.https.onCall
represents that Cloud Function is called directly from the app. (You can check Firebase Cloud Functions documentation for more information on triggers you can use)
Now let’s get the input parameters we passed to Cloud Functions from the app (Don’t worry, I will show you how to call Cloud Functions from the app later in the article 😉). In order to make the API call for getting subscription or purchase information we need sku_id
, purchase_token
, package_name
.
const skuId: string = data.sku_id;
const purchaseToken: string = data.purchase_token;
const packageName: string = data.package_name;
Once we have the input parameters all that left is make the API call to playDeveloperApiClient
to get subscription or purchase information.
try {
await authClient.authorize();
const subscription = await playDeveloperApiClient.purchases.subscriptions.get({
packageName: packageName,
subscriptionId: skuId,
token: purchaseToken
});
if (subscription.status === 200) {
// Subscription response is successful. subscription.data will return the subscription information.
return {
status: 200,
message: "Subscription verification successfuly!"
}
}
} catch (error) {
// Logging error for debugging
console.log(error)
}
// This message is returned when there is no successful response from the subscription/purchase get call
return {
status: 500,
message: "Failed to verify subscription, Try again!"
}
We pass in the input parameters into playDeveloperApiClient.purchases.subscriptions.get({})
and wait for result. Once result is obtained, the next step is performed, which is checking for the subscription.status === 200
. This essentially means a successful response is received. Above example just checks for successful response from playDeveloperApiClient
and returns a success or failure message.
subscription
object also has a data object (subscription.data
) with all the information you need about subscription like orderId
, startTimeMillis
, expiryTimeMillis
etc. If it’s been cancelled recently, it would also contain userCancellationTimeMillis
, cancelReason
etc.
Keep in mind the response from Purchases.products
and Purchases.subscriptions
varies, so make sure you check the documentations. If you want to check one time purchases, make sure you verify purchase state. Refer to above given Google Play Developer API links for more information on responses of Products and Subscriptions.
Since playDeveloperApiClient
will throw some exceptions, we are wrapping it with try/catch. If a subscription is cancelled for long or is invalid, it will throw “expired too long“ or “invalid exceptions”. I couldn’t find a list of all exceptions thrown by the client, which is why I used try/catch. (If you know list of exceptions thrown by playDeveloperApiClient
please let me know)
Here is the code for calling the Cloud Functions directly from the app. First make sure you have necessary Firebase dependencies, check Firebase docs for those.
val firebaseFunctions = FirebaseFunctions.getInstance()
private fun verifySubscription(purchase: PurchaseInfo) {
// Place check to see if skuId and purchaseToken are not empty?
val data = mapOf(
"sku_id" to purchase.skuId,
"purchase_token" to purchase.purchaseToken,
"package_name" to packageName, // Provide your app package name
)
// Passing the name of cloud function in getHttpsCallable()
firebaseFunctions.getHttpsCallable("verifySubscription").call(data).continueWith {
// Make sure you provide proper checks, don't do it like this 😂
try {
// Converting the string response in json using Gson
val subJson = Gson().toJson(it.result?.data)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
While the example above improves the reliability from fake purchase responses, I would suggest saving purchase information to database and cross checking with other users to avoid duplicate subscriptions.
Summary:
Let’s recap what we did, We first created a service account using Google API Console and download the key (JSON file), then we granted finance level access to the service account from Google Play Developer Console.
Then we create a Firebase Cloud Function in typescript, use the download key file to create an auth client and play developer api client. We use those to make get calls for products and subscriptions information. Based on the purchase state or subscriptions response we send back a success or failure response to app.
I am using a similar approach in production for my Memoire app. If you have any more queries or suggestions on improving the code, feel free to let me know.