Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implement Google Play Billing Library version 2

Google published a brand new version to handle the payments in Android but after searching quite a while I cannot find a single example or tutorial from someone who succeeded implementing it.

The documentation is very short and provides only a part of the necessary code: https://developer.android.com/google/play/billing/billing_library_overview

The only sample provided is made with Kotlin: https://github.com/android/play-billing-samples

Seems like they forgot about Java developers...

Does anyone know a tutorial online or succeeded to implement it? My current code is far from working to be published yet.

like image 302
Yoann Hercouet Avatar asked Jan 14 '20 14:01

Yoann Hercouet


4 Answers

Thanks @Webfreak, your answer for Kotlin guided to me to the right direction.

Here is how I implemented it for Java:

First add the 'billingclient' library to gradle :

implementation 'com.android.billingclient:billing:X.X.X'

And add the required permissions in the manifest file:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.android.vending.BILLING" />

The Activity must implements the following interfaces:

public class MainActivity extends AppCompatActivity implements
        ...
        PurchasesUpdatedListener,
        AcknowledgePurchaseResponseListener {

Then I initialize the billing client inside the onCreate method:

/** IN-APPS PURCHASE */
    private BillingClient mBillingClient;
    private long mLastPurchaseClickTime = 0;
    private List<String> mSkuList = new ArrayList<>();
    private List<SkuDetails> mSkuDetailsList = new ArrayList<>();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // AppPrefs is just a standalone class I used to get or set shared preferences easily
        mPrefs = AppPrefs.getInstance(this);


        // Rest of your code ...


        /** IN-APP PURCHASES */
        // Initialize the list of all the in-app product IDs I use for this app
        mSkuList.add(Parameters.UNIT_P1);// NoAdsPurchased
        mSkuList.add(Parameters.UNIT_P2);// CustomizationPurchased
        mSkuList.add(Parameters.UNIT_P3);// ChartsPurchased

        // Initialize the billing client
        setupBillingClient();

        // Apply the upgrades on my app according to the user's purchases
        applyUpgrades();
    }

The method setting up the billing client is here, along with the metyhod I use to retrieve the avaialble in-app products fro the app:

private void setupBillingClient() {
        mBillingClient = BillingClient
                .newBuilder(MainActivity.this)
                .enablePendingPurchases() // Useful for physical stores
                .setListener(MainActivity.this)
                .build();

        mBillingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(BillingResult billingResult) {
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    // Load the available products related to the app from Google Play
                    getAvailableProducts();

                    Purchase.PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP);// Or SkuType.SUBS if subscriptions

                    // Init all the purchases to false in the shared preferences (security prevention)
                    mPrefs.setNoAdsPurchased(false);
                    mPrefs.setCustomizationPurchased(false);
                    mPrefs.setChartsPurchased(false);

                    // Retrieve and loop all the purchases done by the user
                    // Update all the boolean related to the purchases done in the shared preferences
                    if (purchasesResult.getPurchasesList() != null) {
                        for (Purchase purchase : purchasesResult.getPurchasesList()) {
                            if (purchase.isAcknowledged()) {
                                Log.e(TAG, purchase.getSku());

                                switch (purchase.getSku()) {
                                    case Parameters.UNIT_P1:
                                        mPrefs.setNoAdsPurchased(true);
                                        break;
                                    case Parameters.UNIT_P2:
                                        mPrefs.setCustomizationPurchased(true);
                                        break;
                                    case Parameters.UNIT_P3:
                                        mPrefs.setChartsPurchased(true);
                                        break;
                                }
                            }
                        }
                    }
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
                // Try to restart the connection on the next request to
                // Google Play by calling the startConnection() method.
                // TODO Note: It's strongly recommended that you implement your own connection retry policy and override the onBillingServiceDisconnected() method. Make sure you maintain the BillingClient connection when executing any methods.
                Log.e(TAG, "onBillingServiceDisconnected");
            }
        });
    }

    private void getAvailableProducts() {
        if (mBillingClient.isReady()) {
            SkuDetailsParams params = SkuDetailsParams
                    .newBuilder()
                    .setSkusList(mSkuList)
                    .setType(BillingClient.SkuType.INAPP)
                    .build();

            mBillingClient.querySkuDetailsAsync(params, new SkuDetailsResponseListener() {
                @Override
                public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                        mSkuDetailsList = skuDetailsList;
                    }
                }
            });
        }
    }

When a purchase is done by the user (I allow purchases on several Fragments in my app), I call this function on the main Activity (using an interface):

@Override
    public void purchase(String sku) {
        // Mis-clicking prevention, using threshold of 3 seconds
        if (SystemClock.elapsedRealtime() - mLastPurchaseClickTime < 3000){
            Log.d(TAG, "Purchase click cancelled");
            return;
        }
        mLastPurchaseClickTime = SystemClock.elapsedRealtime();

        // Retrieve the SKU details
        for (SkuDetails skuDetails : mSkuDetailsList) {
            // Find the right SKU
            if (sku.equals(skuDetails.getSku())) {
                BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                        .setSkuDetails(skuDetails)
                        .build();
                mBillingClient.launchBillingFlow(MainActivity.this, flowParams);
                break;
            }
        }
    }

Here I implement the methods inherited:

@Override
    public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
            for (Purchase purchase : purchases) {
                handlePurchase(purchase);
            }
        } else {
            displayError(R.string.inapp_purchase_problem, billingResult.getResponseCode());
        }
    }

    private void handlePurchase(Purchase purchase) {
        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
            // Grant entitlement to the user.
            applyPurchase(purchase);

            // Acknowledge the purchase if it hasn't already been acknowledged.
            if (!purchase.isAcknowledged()) {
                AcknowledgePurchaseParams acknowledgePurchaseParams =
                        AcknowledgePurchaseParams.newBuilder()
                                .setPurchaseToken(purchase.getPurchaseToken())
                                .build();
                mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, MainActivity.this);
            }
        }
    }

    @Override
    public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
            displayError(R.string.inapp_purchase_success, billingResult.getResponseCode());
        }
    }

The method I added to acknowledge a purchase on my app:

private void applyPurchase(Purchase purchase) {

        switch (purchase.getSku()) {
            case Parameters.UNIT_P1:
                mPrefs.setNoAdsPurchased(true);
                break;
            case Parameters.UNIT_P2:
                mPrefs.setCustomizationPurchased(true);
                break;
            case Parameters.UNIT_P3:
                mPrefs.setChartsPurchased(true);
                break;
        }

        // I remove the ads right away if purchases
        if(mPrefs.getNoAdsPurchased()) {
            destroyAds();
        }
    }

This last method is used to apply all the upgrades/purchases on the app (with an example with the removal of the ads):

private void applyUpgrades() {
        // No ads
        if (mPrefs.getNoAdsPurchased()) {
            destroyAds();
        } else {
            loadAds();
        }

        if (mPrefs.getCustomizationPurchased()) {
            // Allow customization
            // ...
        }

        if (mPrefs.getChartsPurchased()) {
            // Allow charts visualization
            // ...
        }
    }

I guess this solution is not perfect yet but it is working, I will modify the code if I find improvements.

like image 109
Yoann Hercouet Avatar answered Oct 19 '22 19:10

Yoann Hercouet


Here is my implementation using billing 2.1.0 in Kotlin. You can easily convert it to Java if you see the whole picture (that's why I'm pasting you the entire activity).

class GoPremiumActivity : AppCompatActivity(), PurchasesUpdatedListener, AcknowledgePurchaseResponseListener {

    private lateinit var billingClient: BillingClient
    private val skuList = listOf(CStr.PRODUCT_ADS_REMOVE.value)
    private var skuDetails: SkuDetails? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.go_premium)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        setupBillingClient()

        purchaseButton.setOnClickListener {
            val flowParams = BillingFlowParams.newBuilder()
                    .setSkuDetails(skuDetails)
                    .build()
            billingClient.launchBillingFlow(this@GoPremiumActivity, flowParams)
        }
    }


    private fun setupBillingClient() {
        billingClient = BillingClient
                .newBuilder(this@GoPremiumActivity)
                .enablePendingPurchases()
                .setListener(this@GoPremiumActivity)
                .build()

        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult?) {
                if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK) {
                    getAvailableProducts()

                    val purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP)
                    val purchase = purchasesResult.purchasesList.firstOrNull { it.sku == CStr.PRODUCT_ADS_REMOVE.value}
                    if (purchase?.isAcknowledged == true) {
                        Global.prefs.adsRemovalPurchased = true
                        finish()
                    }
                } else {
                    showGeneralError()
                }
            }

            override fun onBillingServiceDisconnected() {
                /*DO NOTHING*/
            }
        })
    }

    fun getAvailableProducts() {
        if (billingClient.isReady) {
            val params = SkuDetailsParams
                    .newBuilder()
                    .setSkusList(skuList)
                    .setType(BillingClient.SkuType.INAPP)
                    .build()
            billingClient.querySkuDetailsAsync(params) { responseCode, skuDetailsList ->
                if (responseCode.responseCode == BillingClient.BillingResponseCode.OK) {
                    skuDetails = skuDetailsList.firstOrNull()
                    skuDetails?.let {
                        purchaseButton.text = String.format("BUY %s", it.price)
                        showSuccessOrError(success = true)
                    } ?: run {
                        showSuccessOrError(success = false)
                    }
                } else {
                    showGeneralError()
                }
            }
        } else {
            showGeneralError()
        }
    }

    override fun onPurchasesUpdated(billingResult: BillingResult?, purchases: MutableList<Purchase>?) {
        if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
            val purchase = purchases.firstOrNull { it.sku == CStr.PRODUCT_ADS_REMOVE.value}
            if (purchase?.purchaseState == Purchase.PurchaseState.PURCHASED) {
                if (!purchase.isAcknowledged) {
                    val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                            .setPurchaseToken(purchase.purchaseToken)
                            .build()
                    billingClient.acknowledgePurchase(acknowledgePurchaseParams, this@GoPremiumActivity)
                }
            }
        } else if (billingResult?.responseCode == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
            Global.prefs.adsRemovalPurchased = true
            finish()
        } else {
            Global.prefs.adsRemovalPurchased = false
            showSuccessOrError(success = true)
        }
    }

    override fun onAcknowledgePurchaseResponse(billingResult: BillingResult?) {
        if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK) {
            showThankYouDialog(this@GoPremiumActivity)
            Global.prefs.adsRemovalPurchased = true
        }
    }

    private fun showSuccessOrError(success: Boolean) {
        purchaseProgressBar.visibility = View.GONE
        if (success) {
            purchaseButton.visibility = View.VISIBLE
        } else {
            purchaseUnavailable.visibility = View.VISIBLE
        }
    }

    private fun showGeneralError() {
        purchaseProgressBar.visibility = View.GONE
        purchaseUnavailable.visibility = View.VISIBLE
    }

    companion object {
        fun newIntent(context: Context): Intent {
            return Intent(context, GoPremiumActivity::class.java)
        }
    }

    override fun onSupportNavigateUp(): Boolean {
        finish()
        return true
    }

    public override fun onDestroy() {
        super.onDestroy()
    }

    override fun onPause() {
        super.onPause()
        if (isFinishing) {
            finish()
        }
    }

    private fun showThankYouDialog(context: Context) {
        //Show dialog
    }
}

I can remove it if you specifically want it in Java.

like image 30
Webfreak Avatar answered Oct 19 '22 18:10

Webfreak


I'm begginer on Android Studio and i'm implementing the billing library 2.1.0. After a week of reading the android studio documentation and many tutorial about billing library I have made this java class, but i feel is not good enough, at least it do what it has to do. If you find any way to improve it, comment it. Thanks:

1.- Class Pago.java:

package com.example.billing;

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

import androidx.annotation.Nullable;

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.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
import com.example.R;

import static com.android.billingclient.api.BillingClient.BillingResponseCode.SERVICE_TIMEOUT;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.OK;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.USER_CANCELED;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.BILLING_UNAVAILABLE;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_UNAVAILABLE;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.ERROR;
import static com.android.billingclient.api.BillingClient.SkuType.INAPP;

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


public class Pagos implements PurchasesUpdatedListener, BillingClientStateListener, SkuDetailsResponseListener, ConsumeResponseListener {


    private BillingClient billingClient;
    private Context contextPago;
    private String skuId;
    private List<SkuDetails> misProductos;


    // Constructor de la clase Pagos
    public Pagos(Context context) {

        contextPago = context;

    }


    // Asigna el sku del producto que se quiere comprar
    public void comprar(String skuId) {

        this.skuId = skuId;
        configurarBillingClient();

    }


    // Configura el Billing Client para iniciar la conexión con Google Play Console
    private void configurarBillingClient() {

        //  1. Configura el Billing Client
        billingClient = BillingClient.newBuilder(contextPago)
                .enablePendingPurchases()
                .setListener(this)
                .build();

        // 2. Inicia la conexión y asigna los Listener
        billingClient.startConnection(this);

    }


    @Override
    // Evento salta al llamar billingClient.startConnection()
    public void onBillingSetupFinished(BillingResult billingResult) {

        // Busca compras en el Servidor de Google y las marca como consumidas
        consumeCompras();

        // Verifica que la versión de Play Store sea compatible con INAPP
        if (!billingClient.isReady()) {
            String mensaje = contextPago.getString(R.string.PAGOS_MENSAJE_VERSIÓN_NO_COMPATIBLE);
            Toast.makeText(contextPago, mensaje, Toast.LENGTH_LONG).show();
            return;
        }

        // Verifica que la versión de Play Store sea compatible con Suscripciones
        // if (billingClient.isFeatureSupported(SUBSCRIPTIONS).getResponseCode() != OK) {
        //     String mensaje = contextPago.getString(R.string.PAGOS_MENSAJE_VERSIÓN_NO_COMPATIBLE);
        //     Toast.makeText(contextPago, mensaje, Toast.LENGTH_LONG).show();
        //     return; //GooglePlayNoSoportaComprasDeSuscripciones
        // }

        // Verifica que la Configuración se haya hecho bien, sino muestra mensaje de error
        if (verificaResponseCode(billingResult.getResponseCode()) == OK) {
            consultaProductos();
        }

    }


    // Asigna los elemento que se consultarán a Google y los envía con querySkuDetailsAsync
    private void consultaProductos() {

        // Inicializa constantes
        String ITEM_SKU_1 = "android.test.item_unavailable";
        String ITEM_SKU_2 = "android.test.canceled";
        String ITEM_SKU_3 = "android.test.purchased";
        String ITEM_SKU_4 = "donar";
        String ITEM_SKU_5 = "prueba.1";

        // Agrega los productos que se consultarán a Google
        List<String> skuList = new ArrayList<>();
        skuList.add(ITEM_SKU_1);
        skuList.add(ITEM_SKU_2);
        skuList.add(ITEM_SKU_3);
        skuList.add(ITEM_SKU_4);
        skuList.add(ITEM_SKU_5);
        // TODO Cambiar el ingreso manual de items por una consulta a servidor propio de backend seguro.

        SkuDetailsParams.Builder skuDetailsParams = SkuDetailsParams
                .newBuilder()
                .setSkusList(skuList)
                .setType(INAPP);

        // Envía consulta a Google y devuelve el listado de productos mediante onSkuDetailsResponse
        billingClient.querySkuDetailsAsync(skuDetailsParams.build(), this);

    }


    @Override
    // Evento salta cuando Google envía los detalles de los Productos en Venta
    public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {

        if (verificaResponseCode(billingResult.getResponseCode()) == OK) {
            if (skuDetailsList != null) {
                misProductos = skuDetailsList;
                muestraDialogoCompra();
            } else {
                String mensaje = contextPago.getString(R.string.PAGOS_MENSAJE_NO_SKUDETAILSLIST);
                Toast.makeText(contextPago, mensaje, Toast.LENGTH_LONG).show();
            }
        }

    }


    // Lanza el dialogo de compra de Google
    private void muestraDialogoCompra() {

        BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                .setSkuDetails(getSkuIdDetails())
                .build();
        billingClient.launchBillingFlow((Activity) contextPago, flowParams);

    }


    // Obtiene el Producto que se comprará según el Sku ingresado mediante comprar(sku);
    private SkuDetails getSkuIdDetails() {

        if (misProductos == null) return null;
        for (SkuDetails skuProducto : misProductos) {
            if (skuId.equals(skuProducto.getSku())) return skuProducto;
        }
        return null;

    }


    @Override
    // Evento salta cuando se finaliza el Proceso de compra
    public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> list) {

        if (verificaResponseCode(billingResult.getResponseCode()) == OK) {
            // Validar compra con consulta a Google para evitar ingeniería inversa de hackers
            if (validaCompra()) {
                // Compra confirmada
                Log.i("Pagos", "Compra encontrada en servidor");
            } else {
                // Compra no encontrada: Mensaje de error - Revocar privilegios
                Log.i("Pagos", "Compra no encontrada posible hacker");
            }
            consumeCompras();
        }

    }


    // Valida la compra y Devuelve True si encuentra la compra del usuario en el Servidor de Google
    private boolean validaCompra() {

        List<Purchase> purchasesList = billingClient.queryPurchases(INAPP).getPurchasesList();
        if (purchasesList != null && !purchasesList.isEmpty()) {
            for (Purchase purchase : purchasesList) {
                if (purchase.getSku().equals(skuId)) {
                    return true;
                }
            }
        }
        return false;

    }


    // Busca compras en el Servidor de Google y las marca como consumidas
    private void consumeCompras() {

        Purchase.PurchasesResult queryPurchases = billingClient.queryPurchases(INAPP);
        if (queryPurchases.getResponseCode() == OK) {
            List<Purchase> purchasesList = queryPurchases.getPurchasesList();
            if (purchasesList != null && !purchasesList.isEmpty()) {
                for (Purchase purchase : purchasesList) {
                    ConsumeParams params = ConsumeParams.newBuilder()
                            .setPurchaseToken(purchase.getPurchaseToken())
                            .build();
                    billingClient.consumeAsync(params, this);
                }
            }
        }

    }


    @Override
    // Evento salta cuando se ha consumido un producto, Si responseCode = 0, ya se puede volver a comprar
    public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
        if (billingResult.getResponseCode() == OK) {
            Log.i("Pagos", "Token de Compra: " + purchaseToken + " consumida");
        } else {
            Log.i("Pagos", "Error al consumir compra, responseCode: " + billingResult.getResponseCode());
        }
    }


    @Override
    // Evento salta cuando se pierde la conexión durante una compra
    public void onBillingServiceDisconnected() {
        billingClient.startConnection(this);
    }


    // Verifica que el estado del responseCode sea OK, si no muestra mensaje de Error
    private int verificaResponseCode(int responseCode) {

        if (responseCode == OK) return OK;
        if (responseCode == USER_CANCELED) return USER_CANCELED;

        String mensaje = "";
        switch (responseCode) {
            case SERVICE_TIMEOUT:
                mensaje = contextPago.getString(R.string.PAGOS_MENSAJE_SERVICE_TIMEOUT);
                break;
            case BILLING_UNAVAILABLE:
                mensaje = contextPago.getString(R.string.PAGOS_MENSAJE_BILLING_UNAVAILABLE);
                break;
            case ITEM_UNAVAILABLE:
                mensaje = contextPago.getString(R.string.PAGOS_MENSAJE_ITEM_UNAVAILABLE);
                break;
            case ERROR:
                mensaje = contextPago.getString(R.string.PAGOS_MENSAJE_ERROR);
                break;
            default:
                mensaje = contextPago.getString(R.string.PAGOS_MENSAJE_ERROR) + " código: " + responseCode;
                break;
        }
        Toast.makeText(contextPago, mensaje, Toast.LENGTH_LONG).show();
        return responseCode;

    }


}

3.- Manifest

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.android.vending.BILLING" />

4.- build.gradle

// Google Play Billing Library
implementation 'com.android.billingclient:billing:2.1.0'

5.- Usage, place this code wherever you want to show the billing component:

private final String SKU_UNAVAILABLE = "android.test.item_unavailable";
private final String SKU_CANCELED = "android.test.canceled";
private final String SKU_PURCHASED = "android.test.purchased";
private final String SKU_DONAR = "donar";


private void donar() {
    Pagos pagos = new Pagos(this);
    pagos.comprar(SKU_DONAR);
    cargandoDialogoCompra(true);
}

You can change SKU_DONAR, to SKU_UNAVAILABLE, SKU_CANCELED, SKU_PURCHASED because these are items for testing purposes and as i read its not neccesary to add them to play console

6.- Google Play console

Presencia en Google Play Store -> Productos integrados en la aplicación -> Productos administrados:

Donación (donar) PEN 9.99

That's all, please improve my code, thanks to you all.

like image 33
Angel Avatar answered Oct 19 '22 19:10

Angel


Here is a sample app for Google Play Billing version 2 in Java:

Classy Taxi in Java

like image 23
Dyadee Avatar answered Oct 19 '22 20:10

Dyadee