Skip to content

Commit 9336a44

Browse files
authored
feat(android): update to Google Play Billing Library v8.0.0 (#108)
- Update billing-ktx dependency from 7.0.0 to 8.0.0 - Fix Kotlin compatibility by setting JVM target to 17 - Update queryProductDetailsAsync to use QueryProductDetailsResult - Add PendingPurchasesParams for enablePendingPurchases - Add deprecation warning for getPurchaseHistories on Android - Update getPurchaseHistories to return empty array on Android - Add blog post documenting v8 changes and migration guide BREAKING CHANGE: getPurchaseHistories no longer returns purchase history on Android. Use getAvailablePurchases instead for active purchases.
1 parent 1c5f96c commit 9336a44

File tree

9 files changed

+321
-119
lines changed

9 files changed

+321
-119
lines changed

android/build.gradle

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,18 @@ android {
4141
lintOptions {
4242
abortOnError false
4343
}
44+
kotlinOptions {
45+
jvmTarget = "17"
46+
freeCompilerArgs += ["-Xskip-metadata-version-check"]
47+
}
48+
compileOptions {
49+
sourceCompatibility JavaVersion.VERSION_17
50+
targetCompatibility JavaVersion.VERSION_17
51+
}
4452
}
4553

4654
dependencies {
4755
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7"
48-
implementation "com.android.billingclient:billing-ktx:7.0.0"
56+
implementation "com.android.billingclient:billing-ktx:8.0.0"
4957
implementation "com.google.android.gms:play-services-base:18.1.0"
5058
}

android/src/main/java/expo/modules/iap/ExpoIapModule.kt

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import com.android.billingclient.api.BillingResult
1313
import com.android.billingclient.api.ConsumeParams
1414
import com.android.billingclient.api.GetBillingConfigParams
1515
import com.android.billingclient.api.ProductDetails
16+
import com.android.billingclient.api.QueryProductDetailsResult
1617
import com.android.billingclient.api.Purchase
17-
import com.android.billingclient.api.PurchaseHistoryRecord
1818
import com.android.billingclient.api.PurchasesUpdatedListener
1919
import com.android.billingclient.api.QueryProductDetailsParams
20-
import com.android.billingclient.api.QueryPurchaseHistoryParams
2120
import com.android.billingclient.api.QueryPurchasesParams
21+
import com.android.billingclient.api.PendingPurchasesParams
2222
import com.google.android.gms.common.ConnectionResult
2323
import com.google.android.gms.common.GoogleApiAvailability
2424
import expo.modules.kotlin.Promise
@@ -53,6 +53,21 @@ class ExpoIapModule :
5353
"responseCode" to responseCode,
5454
"debugMessage" to billingResult.debugMessage,
5555
)
56+
// Add sub-response code if available (v8.0.0+)
57+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
58+
try {
59+
val subResponseCode = billingResult.javaClass.getMethod("getSubResponseCode").invoke(billingResult) as? Int
60+
if (subResponseCode != null && subResponseCode != 0) {
61+
error["subResponseCode"] = subResponseCode
62+
// Check for specific sub-response codes
63+
if (subResponseCode == 1) { // PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS
64+
error["subResponseMessage"] = "Payment declined due to insufficient funds"
65+
}
66+
}
67+
} catch (e: Exception) {
68+
// Method doesn't exist in older versions, ignore
69+
}
70+
}
5671
val errorData = PlayUtils.getBillingResponseData(responseCode)
5772
error["code"] = errorData.code
5873
error["message"] = errorData.message
@@ -158,7 +173,7 @@ class ExpoIapModule :
158173
.setProductList(skuList)
159174
.build()
160175

161-
billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
176+
billingClient.queryProductDetailsAsync(params) { billingResult: BillingResult, productDetailsResult: QueryProductDetailsResult ->
162177
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
163178
promise.reject(
164179
IapErrorCode.E_QUERY_PRODUCT,
@@ -168,6 +183,8 @@ class ExpoIapModule :
168183
return@queryProductDetailsAsync
169184
}
170185

186+
val productDetailsList = productDetailsResult.productDetailsList ?: emptyList()
187+
171188
val items =
172189
productDetailsList.map { productDetails ->
173190
skus[productDetails.productId] = productDetails
@@ -269,45 +286,8 @@ class ExpoIapModule :
269286
}
270287
}
271288

272-
AsyncFunction("getPurchaseHistoryByType") { type: String, promise: Promise ->
273-
ensureConnection(promise) { billingClient ->
274-
billingClient.queryPurchaseHistoryAsync(
275-
QueryPurchaseHistoryParams
276-
.newBuilder()
277-
.setProductType(
278-
if (type == "subs") BillingClient.ProductType.SUBS else BillingClient.ProductType.INAPP,
279-
).build(),
280-
) { billingResult: BillingResult, purchaseHistoryRecordList: List<PurchaseHistoryRecord>? ->
281-
282-
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
283-
PlayUtils.rejectPromiseWithBillingError(
284-
promise,
285-
billingResult.responseCode,
286-
)
287-
return@queryPurchaseHistoryAsync
288-
}
289-
290-
Log.d(TAG, purchaseHistoryRecordList.toString())
291-
val items = mutableListOf<Map<String, Any?>>()
292-
purchaseHistoryRecordList?.forEach { purchase ->
293-
val item =
294-
mutableMapOf<String, Any?>(
295-
"id" to purchase.products.firstOrNull() as Any?,
296-
"ids" to purchase.products,
297-
"transactionDate" to purchase.purchaseTime.toDouble(),
298-
"transactionReceipt" to purchase.originalJson,
299-
"purchaseTokenAndroid" to purchase.purchaseToken,
300-
"dataAndroid" to purchase.originalJson,
301-
"signatureAndroid" to purchase.signature,
302-
"developerPayload" to purchase.developerPayload,
303-
"platform" to "android",
304-
)
305-
items.add(item)
306-
}
307-
promise.resolve(items)
308-
}
309-
}
310-
}
289+
// getPurchaseHistoryByType removed in Google Play Billing Library v8
290+
// Use getAvailableItemsByType instead to get active purchases
311291

312292
AsyncFunction("buyItemByType") { params: Map<String, Any?>, promise: Promise ->
313293
val type = params["type"] as String
@@ -425,7 +405,23 @@ class ExpoIapModule :
425405

426406
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
427407
val errorData = PlayUtils.getBillingResponseData(billingResult.responseCode)
428-
promise.reject(errorData.code, billingResult.debugMessage, null)
408+
var errorMessage = billingResult.debugMessage ?: errorData.message
409+
410+
// Check for sub-response codes (v8.0.0+)
411+
try {
412+
val subResponseCode = billingResult.javaClass.getMethod("getSubResponseCode").invoke(billingResult) as? Int
413+
if (subResponseCode != null && subResponseCode != 0) {
414+
if (subResponseCode == 1) { // PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS
415+
errorMessage = "$errorMessage (Payment declined due to insufficient funds)"
416+
} else {
417+
errorMessage = "$errorMessage (Sub-response code: $subResponseCode)"
418+
}
419+
}
420+
} catch (e: Exception) {
421+
// Method doesn't exist in older versions, ignore
422+
}
423+
424+
promise.reject(errorData.code, errorMessage, null)
429425
return@ensureConnection
430426
}
431427
}
@@ -560,7 +556,12 @@ class ExpoIapModule :
560556
BillingClient
561557
.newBuilder(context)
562558
.setListener(this)
563-
.enablePendingPurchases()
559+
.enablePendingPurchases(
560+
PendingPurchasesParams.newBuilder()
561+
.enableOneTimeProducts()
562+
.build()
563+
)
564+
.enableAutoServiceReconnection()
564565
.build()
565566

566567
billingClientCache?.startConnection(
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
---
2+
slug: google-play-billing-v8
3+
title: Google Play Billing Library v8.0.0 Support
4+
authors: [hyochan]
5+
tags: [release, android, google-play-billing]
6+
---
7+
8+
# Google Play Billing Library v8.0.0 Support
9+
10+
We've updated expo-iap to support Google Play Billing Library v8.0.0, which includes several important changes and improvements for Android in-app purchases.
11+
12+
## Key Changes
13+
14+
### 1. Updated Dependencies
15+
16+
The Android module now uses the latest Google Play Billing Library:
17+
18+
```gradle
19+
implementation "com.android.billingclient:billing-ktx:8.0.0"
20+
```
21+
22+
### 2. Removed Deprecated Methods
23+
24+
**getPurchaseHistory is no longer available on Android**
25+
26+
Google Play Billing Library v8 has removed the `queryPurchaseHistoryAsync()` method. The `getPurchaseHistories()` function will now return an empty array on Android with a console warning:
27+
28+
```typescript
29+
// Before v8
30+
const history = await getPurchaseHistories(); // Returns purchase history
31+
32+
// After v8
33+
const history = await getPurchaseHistories(); // Returns [] on Android with warning
34+
// Use getAvailablePurchases() instead for active purchases
35+
```
36+
37+
### 3. Updated API Signatures
38+
39+
The `queryProductDetailsAsync` method now uses `ProductDetailsResult`:
40+
41+
```kotlin
42+
billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsResult ->
43+
val productDetailsList = productDetailsResult.productDetailsList ?: emptyList()
44+
// Handle products
45+
}
46+
```
47+
48+
### 4. New PendingPurchasesParams
49+
50+
The `enablePendingPurchases()` method now requires a `PendingPurchasesParams` parameter:
51+
52+
```kotlin
53+
.enablePendingPurchases(
54+
PendingPurchasesParams.newBuilder()
55+
.enableOneTimeProducts()
56+
.build()
57+
)
58+
```
59+
60+
### 5. Automatic Service Reconnection
61+
62+
The library now includes automatic service reconnection support with `enableAutoServiceReconnection()`, improving reliability when the billing service disconnects unexpectedly:
63+
64+
```kotlin
65+
.enableAutoServiceReconnection()
66+
```
67+
68+
### 6. Sub-Response Codes
69+
70+
The library now provides more detailed error information through sub-response codes. For example, when a payment fails due to insufficient funds:
71+
72+
```javascript
73+
// Error object now includes sub-response codes
74+
{
75+
responseCode: 6, // ERROR
76+
debugMessage: "Error processing purchase",
77+
subResponseCode: 1, // PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS
78+
subResponseMessage: "Payment declined due to insufficient funds"
79+
}
80+
```
81+
82+
This helps provide better user feedback for specific payment failures.
83+
84+
## Migration Guide
85+
86+
### Breaking Changes Summary
87+
88+
1. **`getPurchaseHistory()` removed** - Use `getAvailablePurchases()` instead
89+
2. **`querySkuDetailsAsync()` removed** - Already migrated to `queryProductDetailsAsync()`
90+
3. **`enablePendingPurchases()` signature changed** - Now requires `PendingPurchasesParams`
91+
4. **`queryPurchasesAsync(skuType)` removed** - Use `queryPurchasesAsync(QueryPurchasesParams)` instead
92+
93+
### For getPurchaseHistory Users
94+
95+
If you're using `getPurchaseHistory()` or `getPurchaseHistories()` on Android:
96+
97+
```typescript
98+
// Old approach
99+
const history = await getPurchaseHistories();
100+
101+
// New approach - use getAvailablePurchases for active purchases
102+
const activePurchases = await getAvailablePurchases();
103+
```
104+
105+
### No Other Breaking Changes
106+
107+
All other APIs remain compatible. The library handles the v8 changes internally, so most apps won't need any code changes beyond the purchase history migration.
108+
109+
## Benefits
110+
111+
- **Future-Proof**: Compliance with Google Play's latest billing requirements (deadline: August 31, 2025)
112+
- **Improved Reliability**: Automatic service reconnection reduces connection-related errors
113+
- **Better API**: More structured response objects with QueryProductDetailsResult
114+
- **Enhanced Features**: Support for new features and improvements
115+
116+
## Upgrading
117+
118+
To upgrade to version 2.7.0:
119+
120+
```bash
121+
npm install expo-iap@2.7.0
122+
# or
123+
yarn add expo-iap@2.7.0
124+
# or
125+
bun add expo-iap@2.7.0
126+
```
127+
128+
## Requirements
129+
130+
- Android Gradle Plugin 4.0 or higher
131+
- Kotlin 1.6 or higher
132+
- JVM target 17 (automatically configured)
133+
134+
## What's New in Google Play Billing Library v8.0.0
135+
136+
### New Features
137+
- **Multiple purchase options for one-time products** - More flexibility in product offerings
138+
- **Sub-response codes** - Better error details (e.g., insufficient funds)
139+
- **Automatic reconnection** - Simplified connection management
140+
- **Improved queryProductDetailsAsync** - Now returns unfetched products with status codes
141+
142+
### Terminology Changes
143+
- "In-app items" → "One-time products"
144+
145+
### Removed Methods
146+
- `queryPurchaseHistory()` - Removed completely
147+
- `querySkuDetailsAsync()` - Use `queryProductDetailsAsync()`
148+
- `enablePendingPurchases()` without params - Use with `PendingPurchasesParams`
149+
- `queryPurchasesAsync(skuType)` - Use `QueryPurchasesParams` version
150+
151+
For complete details, see the [official Android release notes](https://developer.android.com/google/play/billing/release-notes).
152+
153+
## Feedback
154+
155+
If you encounter any issues with this update, please [open an issue](https://github.com/hyochan/expo-iap/issues) on our GitHub repository.
156+
157+
Happy coding! 🚀

0 commit comments

Comments
 (0)