Skip to content

Commit 61568a0

Browse files
committed
Upgrade to Google Billing v8
1 parent 73b7939 commit 61568a0

File tree

11 files changed

+104
-38
lines changed

11 files changed

+104
-38
lines changed

.idea/misc.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Google In-App Billing Library v7+ [![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21) [![JitCI](https://jitci.com/gh/moisoni97/google-inapp-billing/svg)](https://jitci.com/gh/moisoni97/google-inapp-billing) [![JitPack](https://jitpack.io/v/moisoni97/google-inapp-billing.svg)](https://jitpack.io/#moisoni97/google-inapp-billing)
1+
# Google In-App Billing Library v8+ [![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21) [![JitCI](https://jitci.com/gh/moisoni97/google-inapp-billing/svg)](https://jitci.com/gh/moisoni97/google-inapp-billing) [![JitPack](https://jitpack.io/v/moisoni97/google-inapp-billing.svg)](https://jitpack.io/#moisoni97/google-inapp-billing)
22
A simple implementation of the Android In-App Billing API.
33

44
It supports: in-app purchases (both consumable and non-consumable) and subscriptions with a base plan or multiple offers.
@@ -24,7 +24,7 @@ It is recommended to implement the `BillingConnector` instance in your MainActiv
2424

2525
This is necessary because sometimes (due to different reasons) the purchase is not instantly processed and will have a `PENDING` state. All `PENDING` state purchases cannot be `acknowledged` or `consumed` and **will be refunded** by Google after 3 days.
2626

27-
The library automatically handles acknowledgement and consumption, but for that, it needs the `BillingConnector` reference. It cannot happen in a background service. So if the `BillingConnector` is set in a remote activity that the user **rarely interacts with (or not at all)**, it will never receive the `Billing API callback` to acknowledge the new updated purchase status and the user will lose the purchase.
27+
The library automatically handles acknowledgement and consumption, but for that, it needs the `BillingConnector` reference. It cannot happen in a background service. So if the `BillingConnector` is set in a remote activity that the user **rarely interacts with (or not at all)**, it will never be instantiated to receive the `Billing API callback` to acknowledge the new updated purchase status and the user will lose the purchase.
2828

2929
The library provides `ACKNOWLEDGE_WARNING` and `CONSUME_WARNING` error callbacks to let you know that the purchase status is still `PENDING`. Here you can inform the user to wait or to come back a little bit later to receive the purchase.
3030

@@ -91,7 +91,7 @@ allprojects {
9191

9292
```gradle
9393
dependencies {
94-
implementation 'com.github.moisoni97:google-inapp-billing:1.1.6'
94+
implementation 'com.github.moisoni97:google-inapp-billing:1.1.7'
9595
}
9696
```
9797

@@ -172,6 +172,15 @@ billingConnector.setBillingEventListener(new BillingEventListener() {
172172
* */
173173
}
174174

175+
@Override
176+
public void onProductQueryError(@NonNull String productId, @NonNull BillingResponse response) {
177+
/*Callback after a specific product ID is not found*/
178+
179+
/*
180+
* This is useful for identifying configuration errors in the Play Console
181+
* */
182+
}
183+
175184
@Override
176185
public void onBillingError(@NonNull BillingConnector billingConnector, @NonNull BillingResponse response) {
177186
/*Callback after an error occurs*/

app/build.gradle

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ android {
1313
targetSdkVersion 36
1414

1515
versionCode 10
16-
versionName "1.1.6"
16+
versionName "1.1.7"
1717

1818
multiDexEnabled true
1919

@@ -34,11 +34,11 @@ android {
3434

3535
dependencies {
3636
testImplementation 'junit:junit:4.13.2'
37-
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
38-
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
37+
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
38+
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
3939

4040
implementation 'androidx.appcompat:appcompat:1.7.1'
41-
implementation 'com.google.android.material:material:1.12.0'
41+
implementation 'com.google.android.material:material:1.13.0'
4242
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
4343
implementation "org.jetbrains.kotlin:kotlin-stdlib"
4444
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22"))

app/src/main/java/games/moisoni/google_inapp_billing/JavaSampleActivity.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,13 @@ public void onPurchaseConsumed(@NonNull PurchaseInfo purchase) {
215215
//TODO - similarly check for other ids
216216
}
217217

218+
@Override
219+
public void onProductQueryError(@NonNull String productId, @NonNull BillingResponse response) {
220+
//TODO - do something
221+
Log.d("BillingConnector", "Product ID not found: " + productId);
222+
Toast.makeText(JavaSampleActivity.this, "Product ID not found: " + productId, Toast.LENGTH_SHORT).show();
223+
}
224+
218225
@Override
219226
public void onBillingError(@NonNull BillingConnector billingConnector, @NonNull BillingResponse response) {
220227
switch (response.getErrorType()) {

app/src/main/java/games/moisoni/google_inapp_billing/KotlinSampleActivity.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,19 @@ class KotlinSampleActivity : AppCompatActivity() {
243243
}
244244
}
245245

246+
override fun onProductQueryError(
247+
productId: String,
248+
response: BillingResponse
249+
) {
250+
//TODO - do something
251+
Log.d("BillingConnector", "Product ID not found: $productId")
252+
Toast.makeText(
253+
this@KotlinSampleActivity,
254+
"Product ID not found: $productId",
255+
Toast.LENGTH_SHORT
256+
).show()
257+
}
258+
246259
override fun onBillingError(
247260
billingConnector: BillingConnector,
248261
response: BillingResponse

app/src/main/java/games/moisoni/google_inapp_billing/RemoveAdsExampleActivity.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ public void onPurchaseConsumed(@NonNull PurchaseInfo purchase) {
120120

121121
}
122122

123+
@Override
124+
public void onProductQueryError(@NonNull String productId, @NonNull BillingResponse response) {
125+
126+
}
127+
123128
@Override
124129
public void onBillingError(@NonNull BillingConnector billingConnector, @NonNull BillingResponse response) {
125130
switch (response.getErrorType()) {

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ buildscript {
77
}
88

99
dependencies {
10-
classpath 'com.android.tools.build:gradle:8.12.1'
10+
classpath 'com.android.tools.build:gradle:8.13.0'
1111
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22"
1212

1313
// NOTE: Do not place your application dependencies here; they belong

google-iab/build.gradle

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,20 +47,20 @@ afterEvaluate {
4747
from components.release
4848
groupId = 'com.github.moisoni97'
4949
artifactId = 'google-inapp-billing'
50-
version = '1.1.6'
50+
version = '1.1.7'
5151
}
5252
}
5353
}
5454
}
5555

5656
dependencies {
5757
testImplementation 'junit:junit:4.13.2'
58-
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
59-
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
58+
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
59+
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
6060

6161
implementation 'androidx.appcompat:appcompat:1.7.1'
62-
implementation 'com.google.android.material:material:1.12.0'
62+
implementation 'com.google.android.material:material:1.13.0'
6363
implementation 'com.google.guava:guava:33.4.8-android'
6464

65-
implementation 'com.android.billingclient:billing:7.1.1'
65+
implementation 'com.android.billingclient:billing:8.0.0'
6666
}

google-iab/src/main/java/games/moisoni/google_iab/BillingConnector.java

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,11 @@
4646

4747
import java.util.ArrayList;
4848
import java.util.Collections;
49+
import java.util.HashMap;
4950
import java.util.HashSet;
5051
import java.util.Iterator;
5152
import java.util.List;
53+
import java.util.Map;
5254
import java.util.concurrent.atomic.AtomicLong;
5355

5456
import games.moisoni.google_iab.enums.ErrorType;
@@ -101,6 +103,7 @@ public class BillingConnector implements DefaultLifecycleObserver {
101103
private final Object purchasedProductsSync = new Object(); //object for thread safety
102104

103105
private int productDetailsQueriesPending;
106+
private int purchaseQueriesPending;
104107

105108
private boolean shouldAutoAcknowledge = false;
106109
private boolean shouldAutoConsume = false;
@@ -429,15 +432,32 @@ private void retryBillingClientConnection() {
429432
private void queryProductDetails(String productType, List<QueryProductDetailsParams.Product> productList) {
430433
QueryProductDetailsParams productDetailsParams = QueryProductDetailsParams.newBuilder().setProductList(productList).build();
431434

432-
billingClient.queryProductDetailsAsync(productDetailsParams, (billingResult, productDetailsList) -> {
435+
billingClient.queryProductDetailsAsync(productDetailsParams, (billingResult, productDetailsResult) -> {
433436
if (billingResult.getResponseCode() == OK) {
434-
if (productDetailsList.isEmpty()) {
435-
Log("Query Product Details: data not found. Make sure product ids are configured on Play Console");
437+
List<ProductDetails> productDetailsList = productDetailsResult.getProductDetailsList();
438+
439+
HashSet<String> foundProductIds = new HashSet<>();
440+
for (ProductDetails details : productDetailsList) {
441+
foundProductIds.add(details.getProductId());
442+
}
443+
444+
for (QueryProductDetailsParams.Product requestedProduct : productList) {
445+
String productId = requestedProduct.zza(); // .zza() gets the product ID string
446+
if (!foundProductIds.contains(productId)) {
447+
Log("Error: Product ID '" + productId + "' not found. " +
448+
"Make sure it is configured correctly in the Play Console");
449+
findUiHandler().post(() -> billingEventListener.onProductQueryError(productId, new BillingResponse(ErrorType.PRODUCT_ID_QUERY_FAILED,
450+
"Product ID '" + productId + "' not found", defaultResponseCode)
451+
));
452+
}
453+
}
436454

455+
if (productDetailsList.isEmpty()) {
456+
Log("Query Product Details: No valid products found. Make sure product ids are configured on Play Console");
437457
findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this, new BillingResponse(ErrorType.BILLING_ERROR,
438-
"No product found", defaultResponseCode)));
458+
"No products found", defaultResponseCode)));
439459
} else {
440-
Log("Query Product Details: data found");
460+
Log("Query Product Details: data found for " + productDetailsList.size() + " products");
441461

442462
List<ProductInfo> fetchedProductInfo = new ArrayList<>();
443463
for (ProductDetails productDetails : productDetailsList) {
@@ -459,7 +479,7 @@ private void queryProductDetails(String productType, List<QueryProductDetailsPar
459479
}
460480
}
461481
} else {
462-
Log("Query Product Details: failed");
482+
Log("Query Product Details: failed with response code: " + billingResult.getResponseCode());
463483
findUiHandler().post(() -> billingEventListener.onBillingError(BillingConnector.this,
464484
new BillingResponse(ErrorType.BILLING_ERROR, billingResult)));
465485
}
@@ -507,6 +527,11 @@ private boolean isProductIdConsumable(String productId) {
507527
*/
508528
private void fetchPurchasedProducts() {
509529
if (billingClient.isReady()) {
530+
purchaseQueriesPending = 1;
531+
if (isSubscriptionSupported() == SupportState.SUPPORTED) {
532+
purchaseQueriesPending++;
533+
}
534+
510535
billingClient.queryPurchasesAsync(
511536
QueryPurchasesParams.newBuilder().setProductType(INAPP).build(),
512537
(billingResult, purchases) -> {
@@ -588,20 +613,16 @@ private void processPurchases(ProductType productType, @NonNull List<Purchase> a
588613
}
589614
}
590615

591-
for (Purchase purchase : validPurchases) {
592-
List<String> purchasedProducts = purchase.getProducts();
593-
for (String purchaseProduct : purchasedProducts) {
594-
ProductInfo foundProductInfo = null;
595-
for (ProductInfo productInfo : fetchedProductInfoList) {
596-
if (productInfo.getProduct().equals(purchaseProduct)) {
597-
foundProductInfo = productInfo;
598-
break;
599-
}
600-
}
616+
Map<String, ProductInfo> productInfoMap = new HashMap<>();
617+
for (ProductInfo productInfo : fetchedProductInfoList) {
618+
productInfoMap.put(productInfo.getProduct(), productInfo);
619+
}
601620

621+
for (Purchase purchase : validPurchases) {
622+
for (String productId : purchase.getProducts()) {
623+
ProductInfo foundProductInfo = productInfoMap.get(productId);
602624
if (foundProductInfo != null) {
603-
ProductDetails productDetails = foundProductInfo.getProductDetails();
604-
PurchaseInfo purchaseInfo = new PurchaseInfo(generateProductInfo(productDetails), purchase);
625+
PurchaseInfo purchaseInfo = new PurchaseInfo(foundProductInfo, purchase);
605626
signatureValidPurchases.add(purchaseInfo);
606627
}
607628
}
@@ -615,14 +636,13 @@ private void processPurchases(ProductType productType, @NonNull List<Purchase> a
615636
while (iterator.hasNext()) {
616637
PurchaseInfo purchaseInfo = iterator.next();
617638
boolean isSubscription = purchaseInfo.getSkuProductType() == SkuProductType.SUBSCRIPTION;
618-
boolean isConsumable = purchaseInfo.getSkuProductType() == SkuProductType.CONSUMABLE;
619639

620640
if (productType == ProductType.SUBS && isSubscription) {
621641
iterator.remove();
622-
} else if (productType != ProductType.SUBS) {
623-
if (isConsumable || purchaseInfo.getSkuProductType() == SkuProductType.NON_CONSUMABLE) {
624-
iterator.remove();
625-
}
642+
} else if (productType == ProductType.INAPP && !isSubscription) {
643+
iterator.remove();
644+
} else if (productType == ProductType.COMBINED) {
645+
iterator.remove();
626646
}
627647
}
628648
}
@@ -633,7 +653,9 @@ private void processPurchases(ProductType productType, @NonNull List<Purchase> a
633653

634654
if (purchasedProductsFetched) {
635655
findUiHandler().post(() -> billingEventListener.onPurchasedProductsFetched(productType, signatureValidPurchases));
636-
fetchedPurchasedProducts = true;
656+
if (--purchaseQueriesPending == 0) {
657+
fetchedPurchasedProducts = true;
658+
}
637659
} else {
638660
findUiHandler().post(() -> billingEventListener.onProductsPurchased(signatureValidPurchases));
639661
}

google-iab/src/main/java/games/moisoni/google_iab/enums/ErrorType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ public enum ErrorType {
44
CLIENT_NOT_READY,
55
CLIENT_DISCONNECTED,
66
PRODUCT_NOT_EXIST,
7+
PRODUCT_ID_QUERY_FAILED,
78
CONSUME_ERROR,
89
CONSUME_WARNING,
910
ACKNOWLEDGE_ERROR,

0 commit comments

Comments
 (0)