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,