Implement Google Play Billing Library version 2

前端 未结 4 884
傲寒
傲寒 2021-02-03 14:48

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 im

相关标签:
4条回答
  • 2021-02-03 15:13

    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.

    0 讨论(0)
  • Here is a sample app for Google Play Billing version 2 in Java:

    Classy Taxi in Java

    0 讨论(0)
  • 2021-02-03 15:17

    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.

    0 讨论(0)
  • 2021-02-03 15:18

    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.

    0 讨论(0)
提交回复
热议问题