Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 41 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# flutter_inapp_purchase

<img src="https://flutter-inapp-purchase.hyo.dev/img/logo.png" width="200" alt="flutter_inapp_purchase logo" />
<div align="center">
<img src="https://flutter-inapp-purchase.hyo.dev/img/logo.png" width="200" alt="flutter_inapp_purchase logo" />

[![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)

A comprehensive Flutter plugin for implementing in-app purchases that conforms to the [Open IAP specification](https://openiap.dev)

[![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)
<a href="https://openiap.dev"><img src="https://openiap.dev/logo.png" alt="Open IAP" height="40" /></a>

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

## 📚 Documentation

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

```yaml
dependencies:
flutter_inapp_purchase: ^6.0.0-rc.3
flutter_inapp_purchase: ^6.0.0-rc.4
```

## 🔧 Quick Start

### Basic Usage

```dart
import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';

// Create instance
final iap = FlutterInappPurchase();

// Initialize connection
await FlutterInappPurchase.instance.initConnection();
await iap.initConnection();

// Get products
final products = await FlutterInappPurchase.instance.getProducts(['product_id']);
final products = await iap.getProducts(['product_id']);

// Request purchase
await FlutterInappPurchase.instance.requestPurchase(
await iap.requestPurchase(
RequestPurchase(
ios: RequestPurchaseIosProps(sku: 'product_id'),
android: RequestPurchaseAndroidProps(skus: ['product_id']),
Expand All @@ -38,6 +48,30 @@ await FlutterInappPurchase.instance.requestPurchase(
);
```

### Singleton Usage

For global state management or when you need a shared instance:

```dart
// Use singleton instance
final iap = FlutterInappPurchase.instance;
await iap.initConnection();

// The instance is shared across your app
final sameIap = FlutterInappPurchase.instance; // Same instance
```

### With Flutter Hooks

```dart
// useIAP hook automatically uses singleton
final iapState = useIAP();

// Access products, purchases, etc.
final products = iapState.products;
final currentPurchase = iapState.currentPurchase;
```

## Sponsors

💼 **[View Our Sponsors](https://openiap.dev/sponsors)**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,6 @@ class AmazonInappPurchasePlugin : MethodCallHandler {
"showInAppMessages" -> {
safeResult!!.success("in app messages not supported for amazon")
}
"consumeAllItems" -> {
// consumable is a separate type in amazon
safeResult!!.success("no-ops in amazon")
}
"getProducts",
"getSubscriptions" -> {
Log.d(TAG, call.method)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler,
}
fun setActivity(activity: Activity?) {
this.activity = activity
Log.d(TAG, "Activity set: ${if (activity != null) "not null" else "null"}")
}
fun setChannel(channel: MethodChannel?) {
this.channel = channel
Log.d(TAG, "Channel set: ${if (channel != null) "not null" else "null"}")
}
fun onDetachedFromActivity() {
endBillingClientConnection()
Expand Down Expand Up @@ -79,6 +81,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler,
return
}

Log.d(TAG, "Creating BillingClient with PurchasesUpdatedListener")
billingClient = BillingClient.newBuilder(context ?: return)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases(
Expand All @@ -87,6 +90,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler,
.build()
)
.build()
Log.d(TAG, "BillingClient created successfully")

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

when(call.method){
"showInAppMessages" -> showInAppMessages(safeChannel)
"consumeAllItems" -> consumeAllItems(safeChannel, call)
"getProducts" -> getProductsByType(BillingClient.ProductType.INAPP, call, safeChannel)
"getSubscriptions" -> getProductsByType(BillingClient.ProductType.SUBS, call, safeChannel)
"getAvailableItemsByType" -> getAvailableItemsByType(call, safeChannel)
Expand Down Expand Up @@ -212,53 +215,6 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler,
safeChannel.success("show in app messages ready")
}

private fun consumeAllItems(
safeChannel: MethodResultWrapper,
call: MethodCall
) {
try {
val array = ArrayList<String>()
val params = QueryPurchasesParams.newBuilder().apply { setProductType(BillingClient.ProductType.INAPP) }.build()
billingClient!!.queryPurchasesAsync(params)
{ billingResult, productDetailsList ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
if (productDetailsList.size == 0) {
safeChannel.error(
call.method,
"refreshItem",
"No purchases found"
)
return@queryPurchasesAsync
}

for (purchase in productDetailsList) {
val consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient!!.consumeAsync(consumeParams) { _, outToken ->
array.add(outToken)
if (productDetailsList.size == array.size) {
try {
safeChannel.success(array.toString())
return@consumeAsync
} catch (e: FlutterException) {
Log.e(TAG, e.message!!)
}
}
}
}
} else {
safeChannel.error(
call.method, "refreshItem",
"No results for query"
)
}
}

} catch (err: Error) {
safeChannel.error(call.method, err.message, "")
}
}

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

private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
Log.d(TAG, "PurchasesUpdatedListener triggered!")
Log.d(TAG, "BillingResult responseCode: ${billingResult.responseCode}")
Log.d(TAG, "Purchases: ${purchases?.size ?: 0} items")

try {
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
Log.d(TAG, "Purchase failed with response code: ${billingResult.responseCode}")
val json = JSONObject()
json.put("responseCode", billingResult.responseCode)
json.put("debugMessage", billingResult.debugMessage)
val errorData = BillingError.getErrorFromResponseData(billingResult.responseCode)
json.put("code", errorData.code)
json.put("message", errorData.message)
safeResult!!.invokeMethod("purchase-error", json.toString())
Log.d(TAG, "Sending purchase-error event to Flutter, channel is ${if (channel != null) "not null" else "null"}")
Log.d(TAG, "Error data: ${json.toString()}")
if (channel != null) {
channel!!.invokeMethod("purchase-error", json.toString())
Log.d(TAG, "Successfully sent purchase-error event")
} else {
Log.e(TAG, "Cannot send purchase-error event: channel is null!")
}
return@PurchasesUpdatedListener
}
if (purchases != null) {
Log.d(TAG, "Processing ${purchases.size} successful purchases")
for (purchase in purchases) {
Log.d(TAG, "Processing purchase: productId=${purchase.products[0]}, orderId=${purchase.orderId}")
val item = JSONObject()
item.put("productId", purchase.products[0])
item.put("transactionId", purchase.orderId)
Expand All @@ -662,7 +634,14 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler,
item.put("obfuscatedAccountIdAndroid", accountIdentifiers.obfuscatedAccountId)
item.put("obfuscatedProfileIdAndroid", accountIdentifiers.obfuscatedProfileId)
}
safeResult!!.invokeMethod("purchase-updated", item.toString())
Log.d(TAG, "Sending purchase-updated event to Flutter, channel is ${if (channel != null) "not null" else "null"}")
Log.d(TAG, "Purchase data: ${item.toString()}")
if (channel != null) {
channel!!.invokeMethod("purchase-updated", item.toString())
Log.d(TAG, "Successfully sent purchase-updated event")
} else {
Log.e(TAG, "Cannot send purchase-updated event: channel is null!")
}
return@PurchasesUpdatedListener
}
} else {
Expand All @@ -672,11 +651,18 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler,
val errorData = BillingError.getErrorFromResponseData(billingResult.responseCode)
json.put("code", errorData.code)
json.put("message", "purchases returns null.")
safeResult!!.invokeMethod("purchase-error", json.toString())
Log.d(TAG, "Sending purchase-error event to Flutter, channel is ${if (channel != null) "not null" else "null"}")
Log.d(TAG, "Error data: ${json.toString()}")
if (channel != null) {
channel!!.invokeMethod("purchase-error", json.toString())
Log.d(TAG, "Successfully sent purchase-error event")
} else {
Log.e(TAG, "Cannot send purchase-error event: channel is null!")
}
return@PurchasesUpdatedListener
}
} catch (je: JSONException) {
safeResult!!.invokeMethod("purchase-error", je.message)
channel?.invokeMethod("purchase-error", je.message)
return@PurchasesUpdatedListener
}
}
Expand Down
47 changes: 31 additions & 16 deletions docs/docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ Access iOS and Android specific features and capabilities.
- **iOS Features**: Offer code redemption, subscription management, StoreKit 2 support
- **Android Features**: Billing client state, pending purchases, deep links

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

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

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

## Quick Start

### Instance Management

flutter_inapp_purchase provides flexible instance management:

```dart
// Option 1: Create your own instance (recommended for most cases)
final iap = FlutterInappPurchase();

// Option 2: Use singleton for global state management
final iap = FlutterInappPurchase.instance;

// Option 3: Use with IapProvider (recommended for Flutter apps)
final iapProvider = IapProvider.of(context);
```

### Basic Implementation

```dart
import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';

class PurchaseManager {
StreamSubscription<PurchasedItem?>? _purchaseSubscription;
StreamSubscription<PurchaseResult?>? _errorSubscription;
final FlutterInappPurchase iap = FlutterInappPurchase();
StreamSubscription<Purchase>? _purchaseSubscription;
StreamSubscription<PurchaseError>? _errorSubscription;

Future<void> initializePurchases() async {
// Initialize connection
await FlutterInappPurchase.instance.initConnection();
await iap.initConnection();

// Set up listeners
_purchaseSubscription = FlutterInappPurchase.purchaseUpdated.listen(
// Set up listeners (Open IAP spec)
_purchaseSubscription = iap.purchaseUpdatedListener.listen(
(purchase) {
if (purchase != null) {
handlePurchaseSuccess(purchase);
}
handlePurchaseSuccess(purchase);
},
);

_errorSubscription = FlutterInappPurchase.purchaseError.listen(
_errorSubscription = iap.purchaseErrorListener.listen(
(error) {
if (error != null) {
handlePurchaseError(error);
}
handlePurchaseError(error);
},
);
}

Future<void> makePurchase(String productId) async {
await FlutterInappPurchase.instance.requestPurchase(
await iap.requestPurchase(
request: RequestPurchase(
ios: RequestPurchaseIOS(sku: productId, quantity: 1),
android: RequestPurchaseAndroid(skus: [productId]),
Expand All @@ -83,7 +98,7 @@ class PurchaseManager {
}
```

## TypeScript Support
## Dart Type Safety

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

Expand Down
Loading