I'm in the process of implementing in app billing for Android and have got to the point where I can retrieve a list of products from the store. And can activate the Google purchase dialog via calling the launchBillingFlow() method. The documentation indicates that once this has been called, the onPurchasesUpdated is then called with the result. However this isn't happening for me. The logging confirms that the purchase is requested (from within my method: startPurchaseFlow()). My onPurchasesUpdated() is also called when the activity first runs and provides a OK result (0) to confirm connection set up.
But why isn't it being called after launchBillingFlow()?
Class that holds purchase mechanics:
public class BillingManager implements PurchasesUpdatedListener {
private final BillingClient mBillingClient; // Billing client used to interface with Google Play
private final Store mActivity; // Referenced in constructor
// Structure to hold the details of SKUs returned from querying store
private static final HashMap<String, List<String>> SKUS;
static
{
SKUS = new HashMap<>();
SKUS.put(BillingClient.SkuType.INAPP, Arrays.asList("com.identifier.unlock")); // Strings for in app permanent products
}
public List<String> getSkus(@BillingClient.SkuType String type) {
return SKUS.get(type);
}
// Constructor
public BillingManager(Store activity) {
mActivity = activity;
mBillingClient = BillingClient.newBuilder(mActivity).setListener(this).build(); // Initialise billing client and set listener
mBillingClient.startConnection(new BillingClientStateListener() { // Start connection via billing client
@Override
public void onBillingSetupFinished(@BillingClient.BillingResponse int billingResponse) { // Actions to complete when connection is set up
if (billingResponse == BillingClient.BillingResponse.OK) {
Log.i("dev", "onBillingSetupFinished() response: " + billingResponse);
mActivity.getProducts();
} else {
Log.w("dev", "onBillingSetupFinished() error code: " + billingResponse);
}
}
@Override
public void onBillingServiceDisconnected() { // Called when the connection is disconnected
Log.w("dev", "onBillingServiceDisconnected()");
}
});
}
// Receives callbacks on updates regarding future purchases
@Override
public void onPurchasesUpdated(@BillingClient.BillingResponse int responseCode,
List<Purchase> purchases) {
Log.d(TAG, "onPurchasesUpdated() response: " + responseCode);
if (responseCode == 0 && !purchases.isEmpty()) {
String purchaseToken;
for (Purchase element : purchases) {
purchaseToken = element.getPurchaseToken();
mBillingClient.consumeAsync(purchaseToken, null); // Test to 'undo' the purchase TEST
}
}
}
// Used to query store and get details of products args include products to query including type and list of SKUs and a listener for response
public void querySkuDetailsAsync(@BillingClient.SkuType final String itemType,
final List<String> skuList, final SkuDetailsResponseListener listener) {
// Create a SkuDetailsParams instance containing args
SkuDetailsParams skuDetailsParams = SkuDetailsParams.newBuilder()
.setSkusList(skuList).setType(itemType).build();
//Query the billing client using the SkuDetailsParams object as an arg
mBillingClient.querySkuDetailsAsync(skuDetailsParams,
new SkuDetailsResponseListener() {
// Override the response to use the listener provided originally in args
@Override
public void onSkuDetailsResponse(int responseCode,
List<SkuDetails> skuDetailsList) {
listener.onSkuDetailsResponse(responseCode, skuDetailsList);
}
});
}
// Start purchase flow with retry option
public void startPurchaseFlow(final String skuId, final String billingType) {
Log.i("dev", "Starting purchaseflow...");
// Specify a runnable to start when connection to Billing client is established
Runnable executeOnConnectedService = new Runnable() {
@Override
public void run() {
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setType(billingType)
.setSku(skuId)
.build();
mBillingClient.launchBillingFlow(mActivity, billingFlowParams);
Log.i("dev", "Just called launchBillingFlow..." + skuId);
}
};
// If Billing client was disconnected, we retry 1 time
// and if success, execute the query
startServiceConnectionIfNeeded(executeOnConnectedService);
}
// Starts connection with reconnect try
private void startServiceConnectionIfNeeded(final Runnable executeOnSuccess) {
if (mBillingClient.isReady()) {
if (executeOnSuccess != null) {
executeOnSuccess.run();
}
} else {
mBillingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@BillingClient.BillingResponse int billingResponse) {
if (billingResponse == BillingClient.BillingResponse.OK) {
Log.i(TAG, "onBillingSetupFinished() response: " + billingResponse);
if (executeOnSuccess != null) {
executeOnSuccess.run();
}
} else {
Log.w(TAG, "onBillingSetupFinished() error code: " + billingResponse);
}
}
@Override
public void onBillingServiceDisconnected() {
Log.w(TAG, "onBillingServiceDisconnected()");
}
});
}
}
} // End of class
Class that implements interface and initiates request for purchases and displays product information:
public class Store extends AppCompatActivity {
SharedPreferences prefs; // used to access and update the pro value
BillingManager billingManager; // Used to process purchases
// Following are used to store local details about unlock product from the play store
String productSku = "Loading"; // Holds SKU details
String productBillingType = "Loading";
String productTitle = "Loading"; // Will be used to display product title in the store activity
String productPrice = "Loading"; // Used to display product price
String productDescription = "Loading"; // Used to display the product description
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_store);
// Set up toolbar
Toolbar myToolbar = (Toolbar) findViewById(R.id.my_toolbar);
setSupportActionBar(myToolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
// Create billing manager instance
billingManager = new BillingManager(this);
// Set up the shared preferences variable
prefs = this.getSharedPreferences(
"com.identifier", Context.MODE_PRIVATE); // Initiate the preferences
// set up buttons
final Button btnBuy = findViewById(R.id.btnBuy);
btnBuy.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
billingManager.startPurchaseFlow(/*productSku*/ "android.test.purchased", productBillingType); // Amended for TEST
}
});
final Button btnPro = findViewById(R.id.btnPro);
btnPro.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
getProducts();
}
});
getProducts();
updateDisplay();
} // End of onCreate
// Used to unlock the app
public void unlock() {
Log.d("dev", "in unlock(), about to set to true");
prefs.edit().putBoolean("pro", true).apply();
MainActivity.pro = true;
}
// Go back if back/home pressed
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
// Respond to the action bar's Up/Home button
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
// Used to request details of products from the store from this class
public void getProducts() {
List<String> inAppSkus = billingManager.getSkus(BillingClient.SkuType.INAPP); // Create local list of Skus for query
billingManager.querySkuDetailsAsync(BillingClient.SkuType.INAPP, inAppSkus, new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(int responseCode, List<SkuDetails> skuDetailsList) {
if (responseCode == BillingClient.BillingResponse.OK && skuDetailsList != null) {
for (SkuDetails details : skuDetailsList) {
productSku = details.getSku();
productTitle = details.getTitle();
productDescription = details.getDescription();
productPrice = details.getPrice();
productBillingType = details.getType();
}
updateDisplay();
}
}
});
}
// Helper method to update the display with strings
private void updateDisplay() {
final TextView titleText = findViewById(R.id.txtTitle);
final TextView descriptionText = findViewById(R.id.txtDescription);
final TextView priceText = findViewById(R.id.txtPrice);
titleText.setText(productTitle);
descriptionText.setText(productDescription);
priceText.setText(productPrice);
}
}
Ok, so this (replacing the onPurchasesUpdated method above) is now working/responding as expected. Why, I don't know, but it is.
@Override
public void onPurchasesUpdated(@BillingClient.BillingResponse int responseCode,
List<Purchase> purchases) {
if (responseCode == BillingClient.BillingResponse.OK
&& purchases != null) {
for (Purchase purchase : purchases) {
Log.d(TAG, "onPurchasesUpdated() response: " + responseCode);
Log.i("dev", "successful purchase...");
String purchasedSku = purchase.getSku();
Log.i("dev", "Purchased SKU: " + purchasedSku);
String purchaseToken = purchase.getPurchaseToken();
mBillingClient.consumeAsync(purchaseToken, null); // Test to 'undo' the purchase TEST
mActivity.unlock();
}
} else if (responseCode == BillingClient.BillingResponse.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
Log.d(TAG, "onPurchasesUpdated() response: User cancelled" + responseCode);
} else {
// Handle any other error codes.
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With