*

Store Integration – Google

In this tutorial, we’ll look at integrating the Google Play store with the brainCloud marketplace. By doing this, Google Android users will have the ability to validate purchased items from the Google Play store with brainCloud.

Note that apps still have to make the purchase directly with the Google Play Store. For more information on this process refer to the Google Play developer site.

Purchase Flow

brainCloud Portal Setup

Here are the steps to configure brainCloud for Google Play purchases.

Prerequisites

  • You should have set up your products in the brainCloud marketplace. Please refer to the Marketplace Configuration guide.
  • You should have created the Google Store In-App products as per the Google Developer documentation

Associate Google In-App Products with brainCloud Products

After setting up your products in the brainCloud marketplace, you’ll want to associate them with the Google product ids. In particular, we will be grabbing the product ids from the Google Play developer site and entering them into the appropriate brainCloud products.

  • Fill out all necessary settings information
  • Upload and roll-out your APK with billing permission to one of the releases and set countries and testers accordingly
  • Make sure the testing is active. Note: you can always upload a new updated app APK later
  • Select your App and go to the “In-App Products” section
  • Create a product from the In-app products section
  • Select your IAP and grab the Google Product ID

(for this tutorial we’ll use the product id 2012testproduct01 as an example of configuring at brainCloud)

  • Log into the brainCloud Portal
  • Go to the “Design | Marketplace | Products” page
  • Select the product you would like to associate with or create a new product by clicking Adding Product.
  • Click the “+ Edit Product” button
  • Find your Price (or create one) and click the edit icon
  • In the Edit Price dialog, click the + icon to add an entry for Google, enter your Google product id for this price – in our case 2012testproduct01.
  • At this point, you may want to select your price to be the default if it isn’t already selected. You can do this back on the Edit Product page.
  • You can now verify that your Product is coming back with the correct Google In-app Product id. Here is the output from the API Explorer “AppStore.GetSalesInventory” call using googlePlay as the storeId:

Google Certificates and Credentials

Once the products have been configured, you will need to add the appropriate Google credentials to brainCloud.

  • Navigate to the Design | Core App Infor | Application IDs page, click the Google icon and notice the following fields
  • The following sections will show you where to find the appropriate information for these three fields

Google Package Name

  • Log into the Google Play developer site
  • Select your application
  • Grab the package name from Dashboard

Google Service Account Email & p12 Certificate File

To validate purchases, brainCloud requires the usage of a Service Account in a Google Cloud project to communicate with the Google Play backend servers.

  • Create a project (or select an existing project). Note that the Google Service Account can be used across multiple Google Play apps. (by clicking the caret down icon, open the project list dialogue with select or create new project options)
  • Enable the Google Play Android Developer API for this project
  • Type the name of APIs or Services on the search bar field and select Google Play Android Developer API
  • Check Google Play Android Developer API is added on the list of APIs & Services
  • Navigate to the Home ->Credentials, click [+ CREATE CREDENTIALS], select Service account
  • Click CREATE to create a new Service Account
  • Set service account permissions and click CONTINUE (select a role and click continue as following then click DONE)
  • Click the ADD KEY drop-down button and select the Create new key option
  • From the pop-up dialog, select the P12 key type and click CREATE (a P12 key cert will be downloaded to your local computer’s default download folder.)
  • This should create a new Service Account and immediately download the p12 key file to your computer. If for some reason the browser did not download the p12 key file try Chrome.
  • Upload the p12 key file to the brainCloud portal.
  • Also, the Service Account should now show up on the Credentials page. Important: Grab the email address and save it to the brainCloud portal – Design | Core App Info | Application IDs, Google platform tab, Google Service Account Email field.

Linking the Google Console project to the Google Play app

For the Service Account to be active, you will need to link the console project to the Google Play app and give the Service Account permission to View Financial Reports.

Note you will need to be the account owner in order to complete these steps.

  • Select the created Google Cloud project from the above steps and link it to your Google Play Console account. Once done, you will find the linked project shows here:
  • Verify the created Service Account is linked to your Google Play Console account and listed here:
  • Click View access from the above image, it will navigate to the Users & permissions section
  • Select the App permissions tab and add your apps that you want to this service account to access by clicking the Add app
  • After your app, which has the in-app-products, is added to the accessible list of the service account. Switch to the Account permissions tab, and grant Financial data permission to this service account by checking the View financial data, orders, and cancellation survey responses checkbox.
  • You should now be able to validate your Google In-app product purchases with brainCloud.

Testing in-app-product purchases with an example app

  • After your testing app gets reviewed by Google Play, you can download and install it on your test device.
  • Login to brainCloud with any authentication methods
  • Launch the purchase flow from the app to buy the products
  • Buy the products and verify the purchases with brainCloud
  • Check transactions from the brainCloud portal

Confirming the purchase Client Side

After making a purchase client-side, GooglePlay will return a receipt with an orderId, productId, and purchaseToken that is used by the VerifyPurchase API call.

Code Snippets of Integrating the Google Play Billing Library into your Android app – Android Studio

  • when billing clients starting a connection
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
    // [start querySku]
    if (billingResult.getResponseCode() ==  BillingClient.BillingResponseCode.OK) {
        // The BillingClient is ready. You can query purchases here.
        List<String> skuList = new ArrayList<>();
        skuList.add("2012testproduct01");
        skuList.add("2012testproduct02");
        Log.e(TAG, "buy product...billingResult2");
        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
        params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
        billingClient.querySkuDetailsAsync(params.build(),
            new SkuDetailsResponseListener() {
                @Override
                public void onSkuDetailsResponse(BillingResult billingResult,
                                                  List<SkuDetails> skuDetailsList) {
                    // Process the result.
                    if (billingResult == null) {
                        Log.wtf(TAG, "onSkuDetailsResponse: null BillingResult");
                        return;
                    }
                    int responseCode = billingResult.getResponseCode();
                    String debugMessage = billingResult.getDebugMessage();
                    switch (responseCode) {
                        case BillingClient.BillingResponseCode.OK:
                            Log.i(TAG, "onSkuDetailsResponse: " + responseCode + " " + debugMessage);
                            if (skuDetailsList == null) {
                                Log.w(TAG, "onSkuDetailsResponse: null SkuDetails list");
                                skusWithSkuDetails.postValue(Collections.<String, SkuDetails>emptyMap());
                            } else {
                                Map<String, SkuDetails> newSkusDetailList = new HashMap<String, SkuDetails>();
                                for (SkuDetails skuDetails : skuDetailsList) {                                   newSkusDetailList.put(skuDetails.getSku(), skuDetails);
                                    BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
                                            .setSkuDetails(skuDetails)
                                            .build();
                                    int responseCodeF = billingClient.launchBillingFlow(MainActivity.this, billingFlowParams).getResponseCode();
                                }                               skusWithSkuDetails.postValue(newSkusDetailList);
                                Log.i(TAG, "onSkuDetailsResponse: count " + newSkusDetailList.size());
                            }
                            break;
                        case BillingClient.BillingResponseCode.SERVICE_DISCONNECTED:
                        case BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE:
                        case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE:
                        case BillingClient.BillingResponseCode.ITEM_UNAVAILABLE:
                        case BillingClient.BillingResponseCode.DEVELOPER_ERROR:
                        case BillingClient.BillingResponseCode.ERROR:
                            Log.e(TAG, "onSkuDetailsResponse: " + responseCode + " " + debugMessage);
                            break;
                        case BillingClient.BillingResponseCode.USER_CANCELED:
                            Log.i(TAG, "onSkuDetailsResponse: " + responseCode + " " + debugMessage);
                            break;
                        // These response codes are not expected.
                        case BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED:
                        case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
                        case BillingClient.BillingResponseCode.ITEM_NOT_OWNED:
                        default:
                            Log.wtf(TAG, "onSkuDetailsResponse: " + responseCode + " " + debugMessage);
                    }
                }
            });
    }
    // [Start query purchase]
    if (!billingClient.isReady()) {
        Log.e(TAG, "queryPurchases: BillingClient is not ready");
    }
    Log.d(TAG, "queryPurchases: INAPP");
    Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
    if (result == null) {
        Log.i(TAG, "queryPurchases: null purchase result");
        processPurchases(null);
    } else {
        if (result.getPurchasesList() == null) {
            Log.i(TAG, "queryPurchases: null purchase list");
            processPurchases(null);
        } else {
            processPurchases(result.getPurchasesList());
        }
    }
}
  • Verify purchases with brainCloud inside onPurchasesUpdated() method
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
    if (billingResult == null) {
        Log.wtf(TAG, "onPurchasesUpdated: null BillingResult");
        return;
    }
    int responseCode = billingResult.getResponseCode();
    String debugMessage = billingResult.getDebugMessage();
    Log.d(TAG, "onPurchasesUpdated: $responseCode $debugMessage");
    switch (responseCode) {
        case BillingClient.BillingResponseCode.OK:
            if (purchases == null) {
                Log.d(TAG, "onPurchasesUpdated: null purchase list");
                processPurchases(null);
            }else{
                processPurchases(purchases);
                //bc purchase verify
                for (Purchase purchase : purchases) {
                    String storeId = "googlePlay";
                    JSONObject receiptData = new JSONObject();
                    String productId = purchase.getSku();
                    String orderId = purchase.getOrderId();
                    String token = purchase.getPurchaseToken();
                    String developerPayload = purchase.getDeveloperPayload();
                    try {
                        receiptData.put("productId", productId);
                        receiptData.put("orderId", orderId);
                        receiptData.put("token", token);
                        receiptData.put("developerPayload", developerPayload);
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                    String receiptDataS = receiptData.toString();
                    //verify purchase from brainCloud                 _bc.GetWrapper().getAppStoreService().verifyPurchase(storeId, receiptDataS, theCallback);
                }
            }
            break;
        case BillingClient.BillingResponseCode.USER_CANCELED:
            Log.i(TAG, "onPurchasesUpdated: User canceled the purchase");
            break;
        case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
            Log.i(TAG, "onPurchasesUpdated: The user already owns this item");
            break;
    }
}

Code Snippets of Integrating the Google Play Billing Library into your Android app – Unity

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using System.Threading.Tasks;
using UnityEngine.Purchasing;
using BrainCloud.JsonFx.Json;
public class BrainCloudInterface : MonoBehaviour, IStoreListener //needed for unity iap
{
    //these are simply references to the unity specific canvas system
    Text status;
    string statusText;
    string email;
    string authCode;
    string idToken;
    //purchase
    string productId;
    string orderId;
    string purchaseToken;
    string developerPayload;
    //Google info 
    Dictionary<string, object> wrapper;
    string store;
    string payload;
    Dictionary<string, object> gpDetails;
    string gpJson;
    string gpSig;
    Dictionary<string, object> gpJsonDict;
    //for purchasing
    private static IStoreController m_StoreController;          // The Unity Purchasing system.
    private static IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.
    public static string kProductIDConsumable = "bc_test_orb";
    // Use this for initialization
    void Start()
    {
        //allow the people who sign in to change profiles. 
        BCConfig._bc.SetAlwaysAllowProfileSwitch(true);
        BCConfig._bc.Client.EnableLogging(true);
        //unity's ugly way to look for gameobjects
        status = GameObject.Find("Status").GetComponent<Text>();
        // If we haven't set up the Unity Purchasing reference
        if (m_StoreController == null)
        {
            // Begin to configure our connection to Purchasing
            InitializePurchasing();
        }
    }
    void Update()
    {
        status.text = statusText;
    }
    public void OnAuthEmail()
    {
        BCConfig._bc.AuthenticateEmailPassword("ryan.daniel.ruth@gmail.com", "password", true, OnSuccess_AuthenticateEmail, OnError_AuthenticateEmail);
    }
    public void OnSuccess_AuthenticateEmail(string responseData, object cbObject)
    {
        statusText = "Logged into braincloud!\n" + responseData;
    }
    public void OnError_AuthenticateEmail(int statusCode, int reasonCode, string statusMessage, object cbObject)
    {
        statusText = "Failed to Login to braincloud...\n" + statusMessage + "\n" + reasonCode;
    }
    //purchasing 
    public void InitializePurchasing()
    {
        // If we have already connected to Purchasing ...
        if (IsInitialized())
        {
            // ... we are done here.
            return;
        }
        // Create a builder, first passing in a suite of Unity provided stores.
        var configurationBuilder = ConfigurationBuilder.Instance(Google.Play.Billing.GooglePlayStoreModule.Instance()); //For Google purchasing specifically
        //var configurationBuilder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
        // Add a product to sell / restore by way of its identifier, associating the general identifier
        // with its store-specific identifiers.
        configurationBuilder.AddProduct(kProductIDConsumable, ProductType.Consumable);
        // Kick off the remainder of the set-up with an asynchrounous call, passing the configuration 
        // and this class' instance. Expect a response either in OnInitialized or OnInitializeFailed.
        UnityPurchasing.Initialize(this, configurationBuilder);
    }
    private bool IsInitialized()
    {
        // Only say we are initialized if both the Purchasing references are set.
        return m_StoreController != null && m_StoreExtensionProvider != null;
    }
    public void OnGooglePurchase()
    {
        // Buy the consumable product using its general identifier. Expect a response either 
        // through ProcessPurchase or OnPurchaseFailed asynchronously.
        BuyProductID(kProductIDConsumable);
    }
    public void OnVerifyPurchase()
    {
        gpJsonDict = (Dictionary<string, object>)MiniJson.JsonDecode(gpJson);
        Dictionary<string, object> receiptData = new Dictionary<string, object>();
        receiptData.Add("productId", gpJsonDict["productId"]);
        receiptData.Add("orderId", gpJsonDict["orderId"]);
        receiptData.Add("token", gpJsonDict["purchaseToken"]);
        //Developer payload is not supported
        //Google Play deprecated developer payload and is replacing it with alternatives that are more meaningful and contextual. 
        receiptData.Add("developerPayload", ""); //So pass in empty string for developer payload.
        string receiptDataString = JsonWriter.Serialize(receiptData);
        BCConfig._bc.AppStoreService.VerifyPurchase("googlePlay", receiptDataString, OnSuccess_VerifyPurchase, OnError_VerifyPurchase);
    }
    public void OnSuccess_VerifyPurchase(string responseData, object cbObject)
    {
        statusText = "Verified Purchase!\n" + responseData;
    }
    public void OnError_VerifyPurchase(int statusCode, int reasonCode, string statusMessage, object cbObject)
    {
        statusText = "Failed to Verify Purchase...\n" + statusMessage + "\n" + reasonCode;
    }
    void BuyProductID(string productId)
    {
        // If Purchasing has been initialized ...
        if (IsInitialized())
        {
            // ... look up the Product reference with the general product identifier and the Purchasing 
            // system's products collection.
            Product product = m_StoreController.products.WithID(productId);
            // If the look up found a product for this device's store and that product is ready to be sold ... 
            if (product != null && product.availableToPurchase)
            {
                Debug.Log(string.Format("Purchasing product asychronously: '{0}'", product.definition.id));
                // ... buy the product. Expect a response either through ProcessPurchase or OnPurchaseFailed 
                // asynchronously.
                m_StoreController.InitiatePurchase(product);
            }
            // Otherwise ...
            else
            {
                // ... report the product look-up failure situation  
                Debug.Log("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
            }
        }
        // Otherwise ...
        else
        {
            // ... report the fact Purchasing has not succeeded initializing yet. Consider waiting longer or 
            // retrying initiailization.
            Debug.Log("BuyProductID FAIL. Not initialized.");
        }
    }
    public void OnShowGoogleStats()
    {
        statusText = "STORE: " + store +"\nPAYLOAD: " + payload + "\nJSON: " + gpJson + "\nSIGNATURE: " + gpSig;
    }
    public void OnShowJSONStats()
    {
        gpJsonDict = (Dictionary<string, object>)MiniJson.JsonDecode(gpJson);
        statusText = "PRODUCTID: " + gpJsonDict["productId"] + "\n:ORDERID " + gpJsonDict["orderId"] + "\nTOKEN: " + gpJsonDict["purchaseToken"];
    }
    //  
    // --- IStoreListener callbacks
    //
    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        // Purchasing has succeeded initializing. Collect our Purchasing references.
        statusText = "OnInitialized: Google Store PASS";
        //Debug.Log("OnInitialized: PASS");
        // Overall Purchasing system, configured with products for this application.
        m_StoreController = controller;
        // Store specific subsystem, for accessing device-specific store features.
        m_StoreExtensionProvider = extensions;
    }
    public void OnInitializeFailed(InitializationFailureReason error)
    {
        // Purchasing set-up has not succeeded. Check error for reason. Consider sharing this reason with the user.
        statusText = "OnInitializeFailed InitializationFailureReason:" + error;
        //statusText = "blah bala";
        //Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);
    }
    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
    {
        // A consumable product has been purchased by this user.
        if (String.Equals(args.purchasedProduct.definition.id, kProductIDConsumable, StringComparison.Ordinal))
        {
            //Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
            statusText = "ProcessPurchase: PASS. Product: " + args.purchasedProduct.definition.id;
        }
        else
        {
            //Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id));
            statusText = "ProcessPurchase: FAIL. Unrecognized product: " + args.purchasedProduct.definition.id;
        }
        wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(args.purchasedProduct.receipt);
        store = (string)wrapper["Store"];
        payload = (string)wrapper["Payload"];
        gpDetails = (Dictionary<string, object>)MiniJson.JsonDecode(payload);
        gpJson = (string)gpDetails["json"];
        gpSig = (string)gpDetails["signature"];
        // Return a flag indicating whether this product has completely been received, or if the application needs 
        // to be reminded of this purchase at next app launch. Use PurchaseProcessingResult.Pending when still 
        // saving purchased products to the cloud, and when that save is delayed. 
        return PurchaseProcessingResult.Complete;
    }
    public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    {
        // A product purchase attempt did not succeed. Check failureReason for more detail. Consider sharing 
        // this reason with the user to guide their troubleshooting actions.
        //Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
        statusText = "OnPurchaseFailed: FAIL. Product: " + product.definition.storeSpecificId + ", PurchaseFailureReason: " + failureReason;
    }
}

Here is the repository of the above Unity example.