In-App purchasing demo | Qt 6.4

- Qt 6.4
- In-App purchasing demo
- Qt 6.4.1 Reference Documentation

# In-App purchasing demo

     // Copyright (C) 2021 The Qt Company Ltd.
     // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

     package org.qtproject.qt.android.purchasing;

     import java.util.ArrayList;
     import java.util.List;

     import android.app.Activity;
     import android.content.Context;
     import android.util.Log;

     import com.android.billingclient.api.AcknowledgePurchaseParams;
     import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
     import com.android.billingclient.api.BillingClient;
     import com.android.billingclient.api.BillingClientStateListener;
     import com.android.billingclient.api.BillingFlowParams;
     import com.android.billingclient.api.BillingResult;
     import com.android.billingclient.api.ConsumeParams;
     import com.android.billingclient.api.ConsumeResponseListener;
     import com.android.billingclient.api.Purchase;
     import com.android.billingclient.api.Purchase.PurchaseState;
     import com.android.billingclient.api.PurchasesResponseListener;
     import com.android.billingclient.api.PurchasesUpdatedListener;
     import com.android.billingclient.api.SkuDetails;
     import com.android.billingclient.api.SkuDetailsParams;
     import com.android.billingclient.api.SkuDetailsResponseListener;

     /***********************************************************************
      ** More info: https://developer.android.com/google/play/billing
      ** Add Dependencies below to build.gradle file:

     dependencies {
         def billing_version = "4.0.0"
         implementation "com.android.billingclient:billing:$billing_version"
     }

     ***********************************************************************/

     public class InAppPurchase implements PurchasesUpdatedListener
     {
         private Context m_context = null;
         private long m_nativePointer;
         private String m_publicKey = null;
         private int purchaseRequestCode;

         private BillingClient billingClient;

         public static final int RESULT_OK = BillingClient.BillingResponseCode.OK;
         public static final int RESULT_USER_CANCELED = BillingClient.BillingResponseCode.USER_CANCELED;
         public static final String TYPE_INAPP = BillingClient.SkuType.INAPP;
         public static final String TAG = "InAppPurchase";

         // Should be in sync with InAppTransaction::FailureReason
         public static final int FAILUREREASON_NOFAILURE    = 0;
         public static final int FAILUREREASON_USERCANCELED = 1;
         public static final int FAILUREREASON_ERROR        = 2;

         public InAppPurchase(Context context, long nativePointer)
         {
             m_context = context;
             m_nativePointer = nativePointer;
         }

         public void initializeConnection(){
             billingClient = BillingClient.newBuilder(m_context)
                     .enablePendingPurchases()
                     .setListener(this)
                     .build();
             billingClient.startConnection(new BillingClientStateListener() {
                 @Override
                 public void onBillingSetupFinished(BillingResult billingResult) {
                     if (billingResult.getResponseCode() == RESULT_OK) {
                         purchasedProductsQueried(m_nativePointer);
                     }
                 }

                 @Override
                 public void onBillingServiceDisconnected() {
                     Log.w(TAG, "Billing service disconnected");
                 }
             });
         }

         @Override
         public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {

             int responseCode = billingResult.getResponseCode();

             if (purchases == null) {
                 purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, "Data missing from result");
                 return;
             }

             if (billingResult.getResponseCode() == RESULT_OK) {
                 handlePurchase(purchases);
             } else if (responseCode == RESULT_USER_CANCELED) {
                 purchaseFailed(purchaseRequestCode, FAILUREREASON_USERCANCELED, "");
             } else {
                 String errorString = getErrorString(responseCode);
                 purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, errorString);
             }
         }

         //Get list of purchases from onPurchasesUpdated
         private void handlePurchase(List<Purchase> purchases) {

             for (Purchase purchase : purchases) {
                 try {
                     if (m_publicKey != null && !Security.verifyPurchase(m_publicKey, purchase.getOriginalJson(), purchase.getSignature())) {
                         purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, "Signature could not be verified");
                         return;
                     }
                     int purchaseState = purchase.getPurchaseState();
                     if (purchaseState != PurchaseState.PURCHASED) {
                         purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, "Unexpected purchase state in result");
                         return;
                     }
                 } catch (Exception e) {
                     e.printStackTrace();
                     purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, e.getMessage());
                 }
                 purchaseSucceeded(purchaseRequestCode, purchase.getSignature(), purchase.getOriginalJson(), purchase.getPurchaseToken(), purchase.getOrderId(), purchase.getPurchaseTime());
             }
         }

         public void queryDetails(final String[] productIds) {

             int index = 0;
             while (index < productIds.length) {
                 List<String> productIdList = new ArrayList<>();
                 for (int i = index; i < Math.min(index + 20, productIds.length); ++i) {
                     productIdList.add(productIds[i]);
                 }
                 index += productIdList.size();

                 SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
                 params.setSkusList(productIdList).setType(TYPE_INAPP);
                 billingClient.querySkuDetailsAsync(params.build(),
                         new SkuDetailsResponseListener() {
                             @Override
                             public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
                                 int responseCode = billingResult.getResponseCode();

                                 if (responseCode != RESULT_OK) {
                                     Log.e(TAG, "queryDetails: Couldn't retrieve sku details.");
                                     return;
                                 }
                                 if (skuDetailsList == null) {
                                     Log.e(TAG, "queryDetails: No details list in response.");
                                     return;
                                 }

                                 for (SkuDetails skuDetails : skuDetailsList) {
                                     try {
                                         String queriedProductId = skuDetails.getSku();
                                         String queriedPrice = skuDetails.getPrice();
                                         String queriedTitle = skuDetails.getTitle();
                                         String queriedDescription = skuDetails.getDescription();
                                         registerProduct(m_nativePointer,
                                                 queriedProductId,
                                                 queriedPrice,
                                                 queriedTitle,
                                                 queriedDescription);
                                     } catch (Exception e) {
                                         e.printStackTrace();
                                     }
                                 }
                             }
                         });

                 queryPurchasedProducts(productIdList);
             }
         }

         //Launch Google purchasing screen
         public void launchBillingFlow(String identifier, int requestCode){

             purchaseRequestCode = requestCode;
             List<String> skuList = new ArrayList<>();
             skuList.add(identifier);
             SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
             params.setSkusList(skuList).setType(TYPE_INAPP);
             billingClient.querySkuDetailsAsync(params.build(),
                     new SkuDetailsResponseListener() {
                         @Override
                         public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {

                             if (billingResult.getResponseCode() != RESULT_OK) {
                                 Log.e(TAG, "Unable to launch Google Play purchase screen");
                                 String errorString = getErrorString(requestCode);
                                 purchaseFailed(requestCode, FAILUREREASON_ERROR, errorString);
                                 return;
                             }
                             else if (skuDetailsList == null){
                                 purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, "Data missing from result");
                                 return;
                             }

                             BillingFlowParams purchaseParams = BillingFlowParams.newBuilder()
                                     .setSkuDetails(skuDetailsList.get(0))
                                     .build();

                             //Results will be delivered to onPurchasesUpdated
                             billingClient.launchBillingFlow((Activity) m_context, purchaseParams);
                         }
                     });
         }

         public void consumePurchase(String purchaseToken){

             ConsumeResponseListener listener = new ConsumeResponseListener() {
                 @Override
                 public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
                     if (billingResult.getResponseCode() != RESULT_OK) {
                         Log.e(TAG, "Unable to consume purchase. Response code: " + billingResult.getResponseCode());
                     }
                 }
             };
             ConsumeParams consumeParams =
                     ConsumeParams.newBuilder()
                             .setPurchaseToken(purchaseToken)
                             .build();
             billingClient.consumeAsync(consumeParams, listener);
         }

         public void acknowledgeUnlockablePurchase(String purchaseToken){

             AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                     .setPurchaseToken(purchaseToken)
                     .build();

             AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = new AcknowledgePurchaseResponseListener() {
                 @Override
                 public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
                     if (billingResult.getResponseCode() != RESULT_OK){
                         Log.e(TAG, "Unable to acknowledge purchase. Response code: " + billingResult.getResponseCode());
                     }
                 }
             };
             billingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
         }

         public void queryPurchasedProducts(List<String> productIdList) {

             billingClient.queryPurchasesAsync(TYPE_INAPP, new PurchasesResponseListener() {
                 @Override
                 public void onQueryPurchasesResponse(BillingResult billingResult, List<Purchase> list) {
                     for (Purchase purchase : list) {

                         if (productIdList.contains(purchase.getSkus().get(0))) {
                             registerPurchased(m_nativePointer,
                                     purchase.getSkus().get(0),
                                     purchase.getSignature(),
                                     purchase.getOriginalJson(),
                                     purchase.getPurchaseToken(),
                                     purchase.getDeveloperPayload(),
                                     purchase.getPurchaseTime());
                         }
                     }
                 }
             });
         }

         private String getErrorString(int responseCode){
             String errorString;
             switch (responseCode) {
                 case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE: errorString = "Billing unavailable"; break;
                 case BillingClient.BillingResponseCode.ITEM_UNAVAILABLE: errorString = "Item unavailable"; break;
                 case BillingClient.BillingResponseCode.DEVELOPER_ERROR: errorString = "Developer error"; break;
                 case BillingClient.BillingResponseCode.ERROR: errorString = "Fatal error occurred"; break;
                 case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED: errorString = "Item already owned"; break;
                 case BillingClient.BillingResponseCode.ITEM_NOT_OWNED: errorString = "Item not owned"; break;
                 default: errorString = "Unknown billing error " + responseCode; break;
             };
             return errorString;
         }

         public void setPublicKey(String publicKey)
         {
             m_publicKey = publicKey;
         }

         private void purchaseFailed(int requestCode, int failureReason, String errorString)
         {
             purchaseFailed(m_nativePointer, requestCode, failureReason, errorString);
         }

         private void purchaseSucceeded(int requestCode,
                                        String signature,
                                        String purchaseData,
                                        String purchaseToken,
                                        String orderId,
                                        long timestamp)
         {
             purchaseSucceeded(m_nativePointer, requestCode, signature, purchaseData, purchaseToken, orderId, timestamp);
         }

         private native static void queryFailed(long nativePointer, String productId);
         private native static void purchasedProductsQueried(long nativePointer);
         private native static void registerProduct(long nativePointer,
                                                    String productId,
                                                    String price,
                                                    String title,
                                                    String description);
         private native static void purchaseFailed(long nativePointer,
                                                   int requestCode,
                                                   int failureReason,
                                                   String errorString);
         private native static void purchaseSucceeded(long nativePointer,
                                                      int requestCode,
                                                      String signature,
                                                      String data,
                                                      String purchaseToken,
                                                      String orderId,
                                                      long timestamp);
         private native static void registerPurchased(long nativePointer,
                                                      String identifier,
                                                      String signature,
                                                      String data,
                                                      String purchaseToken,
                                                      String orderId,
                                                      long timestamp);
     }