Play Billing (Java)

Suppose RecipeBox wants to sell a one-time "RecipeBox Pro" unlock that removes ads and adds unlimited recipe categories. If the app is distributed through Google Play, this isn't just a convenient way to handle it — it's a genuine Play Store policy requirement. Any app on Play that sells digital content or functionality consumed inside the app must use the Play Billing Library to process that payment. You cannot route it through your own payment gateway or a third-party checkout page for content used within the app itself.

Two Product Types

TypeDescriptionRecipeBox example
In-app product A one-time purchase. The user pays once and keeps the benefit forever. recipebox_pro_unlock — removes ads, unlocks unlimited categories.
Subscription A recurring charge on a billing cycle (monthly/yearly) until cancelled. recipebox_premium_packs — a monthly premium recipe pack subscription.

Setting Up Products in the Play Console

Before writing any Android code, the products themselves are configured outside Android Studio, in the Google Play Console: you create a product ID (e.g. recipebox_pro_unlock), set its price, and — for subscriptions — define the billing period and any free trial. The Android code below only ever refers to products by that ID; it never sets prices or billing periods itself.

Adding the Dependency

// app/build.gradle
dependencies {
    implementation 'com.android.billingclient:billing:6.2.1'
}

Connecting a BillingClient

// In RecipeBox's BillingManager.java
public class BillingManager {

    private final BillingClient billingClient;

    public BillingManager(Context context, PurchasesUpdatedListener listener) {
        billingClient = BillingClient.newBuilder(context)
                .setListener(listener)
                .enablePendingPurchases()
                .build();
    }

    public void connect() {
        billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(BillingResult result) {
                if (result.getResponseCode() == BillingResponseCode.OK) {
                    queryProducts();
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
                // Retry connection on the next user action
            }
        });
    }
}
Explanation:
  • enablePendingPurchases() is required so RecipeBox can handle purchases that need extra steps (e.g. cash payment methods) to complete.
  • The PurchasesUpdatedListener is where every purchase result — new or restored — eventually arrives.

Querying Available Products

private void queryProducts() {
    QueryProductDetailsParams.Product product =
            QueryProductDetailsParams.Product.newBuilder()
                    .setProductId("recipebox_pro_unlock")
                    .setProductType(BillingClient.ProductType.INAPP)
                    .build();

    QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
            .setProductList(Collections.singletonList(product))
            .build();

    billingClient.queryProductDetailsAsync(params, (result, productDetailsList) -> {
        if (!productDetailsList.isEmpty()) {
            launchPurchaseFlow(productDetailsList.get(0));
        }
    });
}

Launching the Purchase Flow

private void launchPurchaseFlow(ProductDetails productDetails) {
    BillingFlowParams.ProductDetailsParams detailsParams =
            BillingFlowParams.ProductDetailsParams.newBuilder()
                    .setProductDetails(productDetails)
                    .build();

    BillingFlowParams flowParams = BillingFlowParams.newBuilder()
            .setProductDetailsParamsList(Collections.singletonList(detailsParams))
            .build();

    billingClient.launchBillingFlow(activity, flowParams);
}
Explanation: launchBillingFlow() shows Google's own purchase confirmation UI over RecipeBox — the app never collects card details itself.

Handling the Result & Acknowledging the Purchase

Every purchase — whether it just completed or was restored from an earlier session — arrives in the PurchasesUpdatedListener passed to the BillingClient when it was built.

public class MainActivity extends AppCompatActivity
        implements PurchasesUpdatedListener {

    @Override
    public void onPurchasesUpdated(BillingResult result, List<Purchase> purchases) {
        if (result.getResponseCode() == BillingResponseCode.OK && purchases != null) {
            for (Purchase purchase : purchases) {
                handlePurchase(purchase);
            }
        }
    }

    private void handlePurchase(Purchase purchase) {
        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
            if (!purchase.isAcknowledged()) {
                AcknowledgePurchaseParams params = AcknowledgePurchaseParams.newBuilder()
                        .setPurchaseToken(purchase.getPurchaseToken())
                        .build();

                billingClient.acknowledgePurchase(params, ackResult -> {
                    // Now safe to unlock RecipeBox Pro features
                    unlockProFeatures();
                });
            }
        }
    }
}
Why acknowledgment matters: Google requires every purchase to be acknowledged — either via acknowledgePurchase() or by being consumed — within 3 days. If RecipeBox never acknowledges a purchase, Google treats it as abandoned and automatically refunds it to the user. This is a real, easy-to-miss failure mode: the payment succeeds, RecipeBox forgets to acknowledge it, and days later the user is refunded and Pro access silently disappears.

Server-Side Verification (Conceptual)

The flow above trusts the purchase object handed back on-device, which is fine for learning and for small apps. A production app selling something valuable is better served by sending the purchase token to your own backend and verifying it against the Google Play Developer API there. Server-side verification is harder to spoof than anything checked purely on the device, since a modified or rooted client can't fake a token that your server independently confirms with Google. RecipeBox doesn't need this for a simple Pro unlock, but it's worth knowing the option exists before scaling up a paid feature.

Billing setup checklist

Product configured in Play Console
BillingClient connected
Purchase acknowledged within 3 days
With monetization covered, RecipeBox's next feature needs the device's location — the Fused Location Provider lesson shows how to find nearby grocery stores.