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); }