Skip to content

Commit 6a38465

Browse files
authored
Fix connection lifecycle and undefined method issues (#518)
## Summary This PR fixes critical issues with IAP connection lifecycle management and undefined method errors in the Flutter InApp Purchase plugin. ### Changes 1. **Fix undefined method error in getAvailablePurchases** - Replace direct method calls with proper channel invocations - Fix getAvailableItemsByType calls for Android platform - Use extractPurchased helper to process results correctly 2. **Fix connection check in subscription flow screen** - Change connection result comparison from string to boolean - Match the pattern used in purchase flow screen - Resolves "Not connected" issue in subscription flow 3. **Add proper connection lifecycle management** - Add endConnection calls to dispose methods in all screens - Ensure connections are properly closed when leaving screens - Prevent connection issues when re-entering screens 4. **Remove hook and provider implementations** - Completely remove use_iap hook implementation - Remove IapProvider from the example app - Simplify API to use direct FlutterInappPurchase.instance calls - Remove flutter_hooks dependency from example app and main package 5. **Update to version 6.0.0-rc.4** - Add new errors and events modules for better error handling - Update example app with improved screen structure - Clean up deprecated methods and imports - Restructure documentation to align with simplified API ## Breaking Changes - Removed useIap hook - use FlutterInappPurchase.instance directly instead - Removed IapProvider - no longer needed with simplified API - Removed flutter_hooks dependency ## Test Plan - [x] Test purchase flow screen - connections properly initialized and closed - [x] Test subscription flow screen - connections properly initialized and closed - [x] Test available purchases screen - connections properly initialized and closed - [x] Verify no connection issues when navigating between screens - [x] Confirm purchases and subscriptions work correctly on both iOS and Android - [x] Verify all screens work without hooks/provider ## Related Issues Fixes connection lifecycle issues reported in the example app where screens would show "Not connected" after navigating back and forth.
1 parent d8c8b8b commit 6a38465

38 files changed

+4121
-6742
lines changed

README.md

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
# flutter_inapp_purchase
22

3-
<img src="https://flutter-inapp-purchase.hyo.dev/img/logo.png" width="200" alt="flutter_inapp_purchase logo" />
3+
<div align="center">
4+
<img src="https://flutter-inapp-purchase.hyo.dev/img/logo.png" width="200" alt="flutter_inapp_purchase logo" />
5+
6+
[![Pub Version](https://img.shields.io/pub/v/flutter_inapp_purchase.svg?style=flat-square)](https://pub.dartlang.org/packages/flutter_inapp_purchase) [![Flutter CI](https://github.com/hyochan/flutter_inapp_purchase/actions/workflows/ci.yml/badge.svg)](https://github.com/hyochan/flutter_inapp_purchase/actions/workflows/ci.yml) [![Coverage Status](https://codecov.io/gh/hyochan/flutter_inapp_purchase/branch/main/graph/badge.svg?token=WXBlKvRB2G)](https://codecov.io/gh/hyochan/flutter_inapp_purchase) ![License](https://img.shields.io/badge/license-MIT-blue.svg)
7+
8+
A comprehensive Flutter plugin for implementing in-app purchases that conforms to the [Open IAP specification](https://openiap.dev)
49

5-
[![Pub Version](https://img.shields.io/pub/v/flutter_inapp_purchase.svg?style=flat-square)](https://pub.dartlang.org/packages/flutter_inapp_purchase) [![Flutter CI](https://github.com/hyochan/flutter_inapp_purchase/actions/workflows/ci.yml/badge.svg)](https://github.com/hyochan/flutter_inapp_purchase/actions/workflows/ci.yml) [![Coverage Status](https://codecov.io/gh/hyochan/flutter_inapp_purchase/branch/main/graph/badge.svg?token=WXBlKvRB2G)](https://codecov.io/gh/hyochan/flutter_inapp_purchase) ![License](https://img.shields.io/badge/license-MIT-blue.svg)
10+
<a href="https://openiap.dev"><img src="https://openiap.dev/logo.png" alt="Open IAP" height="40" /></a>
611

7-
A comprehensive Flutter plugin for implementing in-app purchases that conforms to the [Open IAP specification](https://openiap.dev)
12+
</div>
813

914
## 📚 Documentation
1015

@@ -14,22 +19,27 @@ A comprehensive Flutter plugin for implementing in-app purchases that conforms t
1419

1520
```yaml
1621
dependencies:
17-
flutter_inapp_purchase: ^6.0.0-rc.3
22+
flutter_inapp_purchase: ^6.0.0-rc.4
1823
```
1924
2025
## 🔧 Quick Start
2126
27+
### Basic Usage
28+
2229
```dart
2330
import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
2431

32+
// Create instance
33+
final iap = FlutterInappPurchase();
34+
2535
// Initialize connection
26-
await FlutterInappPurchase.instance.initConnection();
36+
await iap.initConnection();
2737

2838
// Get products
29-
final products = await FlutterInappPurchase.instance.getProducts(['product_id']);
39+
final products = await iap.getProducts(['product_id']);
3040

3141
// Request purchase
32-
await FlutterInappPurchase.instance.requestPurchase(
42+
await iap.requestPurchase(
3343
RequestPurchase(
3444
ios: RequestPurchaseIosProps(sku: 'product_id'),
3545
android: RequestPurchaseAndroidProps(skus: ['product_id']),
@@ -38,6 +48,30 @@ await FlutterInappPurchase.instance.requestPurchase(
3848
);
3949
```
4050

51+
### Singleton Usage
52+
53+
For global state management or when you need a shared instance:
54+
55+
```dart
56+
// Use singleton instance
57+
final iap = FlutterInappPurchase.instance;
58+
await iap.initConnection();
59+
60+
// The instance is shared across your app
61+
final sameIap = FlutterInappPurchase.instance; // Same instance
62+
```
63+
64+
### With Flutter Hooks
65+
66+
```dart
67+
// useIAP hook automatically uses singleton
68+
final iapState = useIAP();
69+
70+
// Access products, purchases, etc.
71+
final products = iapState.products;
72+
final currentPurchase = iapState.currentPurchase;
73+
```
74+
4175
## Sponsors
4276

4377
💼 **[View Our Sponsors](https://openiap.dev/sponsors)**

android/src/main/kotlin/dev/hyo/flutterinapppurchase/AmazonInappPurchasePlugin.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,6 @@ class AmazonInappPurchasePlugin : MethodCallHandler {
6363
"showInAppMessages" -> {
6464
safeResult!!.success("in app messages not supported for amazon")
6565
}
66-
"consumeAllItems" -> {
67-
// consumable is a separate type in amazon
68-
safeResult!!.success("no-ops in amazon")
69-
}
7066
"getProducts",
7167
"getSubscriptions" -> {
7268
Log.d(TAG, call.method)

android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt

Lines changed: 38 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler,
3434
}
3535
fun setActivity(activity: Activity?) {
3636
this.activity = activity
37+
Log.d(TAG, "Activity set: ${if (activity != null) "not null" else "null"}")
3738
}
3839
fun setChannel(channel: MethodChannel?) {
3940
this.channel = channel
41+
Log.d(TAG, "Channel set: ${if (channel != null) "not null" else "null"}")
4042
}
4143
fun onDetachedFromActivity() {
4244
endBillingClientConnection()
@@ -79,6 +81,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler,
7981
return
8082
}
8183

84+
Log.d(TAG, "Creating BillingClient with PurchasesUpdatedListener")
8285
billingClient = BillingClient.newBuilder(context ?: return)
8386
.setListener(purchasesUpdatedListener)
8487
.enablePendingPurchases(
@@ -87,6 +90,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler,
8790
.build()
8891
)
8992
.build()
93+
Log.d(TAG, "BillingClient created successfully")
9094

9195
billingClient?.startConnection(object : BillingClientStateListener {
9296
private var alreadyFinished = false
@@ -162,7 +166,6 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler,
162166

163167
when(call.method){
164168
"showInAppMessages" -> showInAppMessages(safeChannel)
165-
"consumeAllItems" -> consumeAllItems(safeChannel, call)
166169
"getProducts" -> getProductsByType(BillingClient.ProductType.INAPP, call, safeChannel)
167170
"getSubscriptions" -> getProductsByType(BillingClient.ProductType.SUBS, call, safeChannel)
168171
"getAvailableItemsByType" -> getAvailableItemsByType(call, safeChannel)
@@ -212,53 +215,6 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler,
212215
safeChannel.success("show in app messages ready")
213216
}
214217

215-
private fun consumeAllItems(
216-
safeChannel: MethodResultWrapper,
217-
call: MethodCall
218-
) {
219-
try {
220-
val array = ArrayList<String>()
221-
val params = QueryPurchasesParams.newBuilder().apply { setProductType(BillingClient.ProductType.INAPP) }.build()
222-
billingClient!!.queryPurchasesAsync(params)
223-
{ billingResult, productDetailsList ->
224-
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
225-
if (productDetailsList.size == 0) {
226-
safeChannel.error(
227-
call.method,
228-
"refreshItem",
229-
"No purchases found"
230-
)
231-
return@queryPurchasesAsync
232-
}
233-
234-
for (purchase in productDetailsList) {
235-
val consumeParams = ConsumeParams.newBuilder()
236-
.setPurchaseToken(purchase.purchaseToken)
237-
.build()
238-
billingClient!!.consumeAsync(consumeParams) { _, outToken ->
239-
array.add(outToken)
240-
if (productDetailsList.size == array.size) {
241-
try {
242-
safeChannel.success(array.toString())
243-
return@consumeAsync
244-
} catch (e: FlutterException) {
245-
Log.e(TAG, e.message!!)
246-
}
247-
}
248-
}
249-
}
250-
} else {
251-
safeChannel.error(
252-
call.method, "refreshItem",
253-
"No results for query"
254-
)
255-
}
256-
}
257-
258-
} catch (err: Error) {
259-
safeChannel.error(call.method, err.message, "")
260-
}
261-
}
262218

263219
private fun getAvailableItemsByType(
264220
call: MethodCall,
@@ -618,7 +574,9 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler,
618574
val errorData = BillingError.getErrorFromResponseData(responseCode.responseCode)
619575
safeChannel.error(TAG, errorData.code, "Failed to launch billing flow: ${errorData.message}")
620576
} else {
577+
// Return success immediately - purchase result will come via purchasesUpdatedListener
621578
safeChannel.success("Billing flow launched successfully")
579+
Log.d(TAG, "Billing flow launched, purchase result will be sent via purchase-updated event")
622580
}
623581
} else {
624582
Log.e(TAG, "Activity is null, cannot launch billing flow")
@@ -631,19 +589,33 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler,
631589
}
632590

633591
private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
592+
Log.d(TAG, "PurchasesUpdatedListener triggered!")
593+
Log.d(TAG, "BillingResult responseCode: ${billingResult.responseCode}")
594+
Log.d(TAG, "Purchases: ${purchases?.size ?: 0} items")
595+
634596
try {
635597
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
598+
Log.d(TAG, "Purchase failed with response code: ${billingResult.responseCode}")
636599
val json = JSONObject()
637600
json.put("responseCode", billingResult.responseCode)
638601
json.put("debugMessage", billingResult.debugMessage)
639602
val errorData = BillingError.getErrorFromResponseData(billingResult.responseCode)
640603
json.put("code", errorData.code)
641604
json.put("message", errorData.message)
642-
safeResult!!.invokeMethod("purchase-error", json.toString())
605+
Log.d(TAG, "Sending purchase-error event to Flutter, channel is ${if (channel != null) "not null" else "null"}")
606+
Log.d(TAG, "Error data: ${json.toString()}")
607+
if (channel != null) {
608+
channel!!.invokeMethod("purchase-error", json.toString())
609+
Log.d(TAG, "Successfully sent purchase-error event")
610+
} else {
611+
Log.e(TAG, "Cannot send purchase-error event: channel is null!")
612+
}
643613
return@PurchasesUpdatedListener
644614
}
645615
if (purchases != null) {
616+
Log.d(TAG, "Processing ${purchases.size} successful purchases")
646617
for (purchase in purchases) {
618+
Log.d(TAG, "Processing purchase: productId=${purchase.products[0]}, orderId=${purchase.orderId}")
647619
val item = JSONObject()
648620
item.put("productId", purchase.products[0])
649621
item.put("transactionId", purchase.orderId)
@@ -662,7 +634,14 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler,
662634
item.put("obfuscatedAccountIdAndroid", accountIdentifiers.obfuscatedAccountId)
663635
item.put("obfuscatedProfileIdAndroid", accountIdentifiers.obfuscatedProfileId)
664636
}
665-
safeResult!!.invokeMethod("purchase-updated", item.toString())
637+
Log.d(TAG, "Sending purchase-updated event to Flutter, channel is ${if (channel != null) "not null" else "null"}")
638+
Log.d(TAG, "Purchase data: ${item.toString()}")
639+
if (channel != null) {
640+
channel!!.invokeMethod("purchase-updated", item.toString())
641+
Log.d(TAG, "Successfully sent purchase-updated event")
642+
} else {
643+
Log.e(TAG, "Cannot send purchase-updated event: channel is null!")
644+
}
666645
return@PurchasesUpdatedListener
667646
}
668647
} else {
@@ -672,11 +651,18 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler,
672651
val errorData = BillingError.getErrorFromResponseData(billingResult.responseCode)
673652
json.put("code", errorData.code)
674653
json.put("message", "purchases returns null.")
675-
safeResult!!.invokeMethod("purchase-error", json.toString())
654+
Log.d(TAG, "Sending purchase-error event to Flutter, channel is ${if (channel != null) "not null" else "null"}")
655+
Log.d(TAG, "Error data: ${json.toString()}")
656+
if (channel != null) {
657+
channel!!.invokeMethod("purchase-error", json.toString())
658+
Log.d(TAG, "Successfully sent purchase-error event")
659+
} else {
660+
Log.e(TAG, "Cannot send purchase-error event: channel is null!")
661+
}
676662
return@PurchasesUpdatedListener
677663
}
678664
} catch (je: JSONException) {
679-
safeResult!!.invokeMethod("purchase-error", je.message)
665+
channel?.invokeMethod("purchase-error", je.message)
680666
return@PurchasesUpdatedListener
681667
}
682668
}

docs/docs/api/index.md

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ Access iOS and Android specific features and capabilities.
2727
- **iOS Features**: Offer code redemption, subscription management, StoreKit 2 support
2828
- **Android Features**: Billing client state, pending purchases, deep links
2929

30-
### 🎧 Event Listeners
30+
### 🎧 Event Listeners (Open IAP Spec)
3131
Real-time streams for monitoring purchase events and connection states.
3232

33-
- **Purchase Events**: Success, errors, and state changes
33+
- **purchaseUpdatedListener**: Stream for successful purchase updates
34+
- **purchaseErrorListener**: Stream for purchase errors
3435
- **Connection Events**: Store connection status updates
3536

3637
### 🔧 Types & Enums
@@ -42,37 +43,51 @@ Comprehensive type definitions for type-safe development.
4243

4344
## Quick Start
4445

46+
### Instance Management
47+
48+
flutter_inapp_purchase provides flexible instance management:
49+
50+
```dart
51+
// Option 1: Create your own instance (recommended for most cases)
52+
final iap = FlutterInappPurchase();
53+
54+
// Option 2: Use singleton for global state management
55+
final iap = FlutterInappPurchase.instance;
56+
57+
// Option 3: Use with IapProvider (recommended for Flutter apps)
58+
final iapProvider = IapProvider.of(context);
59+
```
60+
61+
### Basic Implementation
62+
4563
```dart
4664
import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
4765
4866
class PurchaseManager {
49-
StreamSubscription<PurchasedItem?>? _purchaseSubscription;
50-
StreamSubscription<PurchaseResult?>? _errorSubscription;
67+
final FlutterInappPurchase iap = FlutterInappPurchase();
68+
StreamSubscription<Purchase>? _purchaseSubscription;
69+
StreamSubscription<PurchaseError>? _errorSubscription;
5170
5271
Future<void> initializePurchases() async {
5372
// Initialize connection
54-
await FlutterInappPurchase.instance.initConnection();
73+
await iap.initConnection();
5574
56-
// Set up listeners
57-
_purchaseSubscription = FlutterInappPurchase.purchaseUpdated.listen(
75+
// Set up listeners (Open IAP spec)
76+
_purchaseSubscription = iap.purchaseUpdatedListener.listen(
5877
(purchase) {
59-
if (purchase != null) {
60-
handlePurchaseSuccess(purchase);
61-
}
78+
handlePurchaseSuccess(purchase);
6279
},
6380
);
6481
65-
_errorSubscription = FlutterInappPurchase.purchaseError.listen(
82+
_errorSubscription = iap.purchaseErrorListener.listen(
6683
(error) {
67-
if (error != null) {
68-
handlePurchaseError(error);
69-
}
84+
handlePurchaseError(error);
7085
},
7186
);
7287
}
7388
7489
Future<void> makePurchase(String productId) async {
75-
await FlutterInappPurchase.instance.requestPurchase(
90+
await iap.requestPurchase(
7691
request: RequestPurchase(
7792
ios: RequestPurchaseIOS(sku: productId, quantity: 1),
7893
android: RequestPurchaseAndroid(skus: [productId]),
@@ -83,7 +98,7 @@ class PurchaseManager {
8398
}
8499
```
85100

86-
## TypeScript Support
101+
## Dart Type Safety
87102

88103
flutter_inapp_purchase provides full type safety with comprehensive type definitions for all methods, parameters, and return values.
89104

0 commit comments

Comments
 (0)