Skip to content
This repository was archived by the owner on Apr 26, 2026. It is now read-only.
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
29 changes: 23 additions & 6 deletions android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -501,9 +501,11 @@ class HybridRnIap : HybridRnIapSpec() {
val androidOptions = options?.android
initConnection(null).await()

val includeSuspended = androidOptions?.includeSuspended ?: false

RnIapLog.payload(
"getAvailablePurchases",
mapOf("type" to androidOptions?.type?.name)
mapOf("type" to androidOptions?.type?.name, "includeSuspended" to includeSuspended)
)

val typeName = androidOptions?.type?.name?.lowercase()
Expand All @@ -516,16 +518,23 @@ class HybridRnIap : HybridRnIapSpec() {
else -> null
}

// Create PurchaseOptions with includeSuspendedAndroid
val purchaseOptions = dev.hyo.openiap.PurchaseOptions(
includeSuspendedAndroid = includeSuspended
)

val result: List<OpenIapPurchase> = if (normalizedType != null) {
val typeEnum = parseProductQueryType(normalizedType)
RnIapLog.payload(
"getAvailablePurchases.native",
mapOf("type" to typeEnum.rawValue)
mapOf("type" to typeEnum.rawValue, "includeSuspended" to includeSuspended)
)
// Note: getAvailableItems doesn't accept PurchaseOptions
// includeSuspended only applies when fetching all types
openIap.getAvailableItems(typeEnum)
} else {
RnIapLog.payload("getAvailablePurchases.native", mapOf("type" to "all"))
openIap.getAvailablePurchases(null)
RnIapLog.payload("getAvailablePurchases.native", mapOf("type" to "all", "includeSuspended" to includeSuspended))
openIap.getAvailablePurchases(purchaseOptions)
}
Comment on lines 526 to 538

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The includeSuspended option will be ignored when a type is specified (e.g., 'subs' or 'inapp') because openIap.getAvailableItems(typeEnum) is called, which doesn't seem to accept the purchaseOptions that contain the includeSuspended flag. This will lead to inconsistent behavior where suspended purchases are only included when fetching all purchase types.

To fix this, you should use openIap.getAvailablePurchases(purchaseOptions) in both branches of the conditional and then filter the results by type if a type is provided. This ensures the includeSuspended option is always respected.

            val allPurchases = openIap.getAvailablePurchases(purchaseOptions)
            val result: List<OpenIapPurchase> = if (normalizedType != null) {
                val typeEnum = parseProductQueryType(normalizedType)
                RnIapLog.payload(
                    "getAvailablePurchases.native",
                    mapOf("type" to typeEnum.rawValue, "includeSuspended" to includeSuspended)
                )
                // Filter the purchases by type, as getAvailablePurchases fetches all types.
                allPurchases.filter { it.type.rawValue == normalizedType }
            } else {
                RnIapLog.payload("getAvailablePurchases.native", mapOf("type" to "all", "includeSuspended" to includeSuspended))
                allPurchases
            }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f986692.

Changes:

  • Now always uses openIap.getAvailablePurchases(purchaseOptions) to ensure includeSuspended is respected
  • Filters results by type after fetching when a type is specified

Thanks for catching this!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: Reverted in 2984378.

After investigation, the suggested fix doesn't work due to OpenIAP API limitations:

  1. getAvailableItems(type) doesn't accept PurchaseOptions parameter
  2. Purchase objects don't have a type property for filtering

The includeSuspended option can only work when fetching all purchase types (without type filter). This is a limitation of the upstream OpenIAP library that would need to be addressed there.

Added a comment in the code to clarify this limitation.

RnIapLog.result(
"getAvailablePurchases",
Expand Down Expand Up @@ -1045,6 +1054,13 @@ class HybridRnIap : HybridRnIapSpec() {
else -> null
}

// Extract productStatusAndroid (OpenIAP 1.3.14+, Billing Library 8.0+)
val productStatusAndroid = when (product) {
is ProductAndroid -> product.productStatusAndroid?.rawValue
is ProductSubscriptionAndroid -> product.productStatusAndroid?.rawValue
else -> null
}

// Serialize standardized cross-platform subscriptionOffers (OpenIAP 1.3.10+)
val standardizedSubsOffers = when (product) {
is ProductSubscriptionAndroid -> product.subscriptionOffers
Expand Down Expand Up @@ -1097,10 +1113,11 @@ class HybridRnIap : HybridRnIapSpec() {
subscriptionPeriodAndroid = subscriptionPeriodAndroid,
freeTrialPeriodAndroid = freeTrialPeriodAndroid,
subscriptionOfferDetailsAndroid = subscriptionOffersJson,
oneTimePurchaseOfferDetailsAndroid = oneTimeOffersNitro
oneTimePurchaseOfferDetailsAndroid = oneTimeOffersNitro,
productStatusAndroid = productStatusAndroid
)
}

// Purchase state is provided as enum value by OpenIAP

private fun convertToNitroPurchase(purchase: OpenIapPurchase): NitroPurchase {
Expand Down
141 changes: 141 additions & 0 deletions docs/blog/2026-01-19-release-14.7.3-billing-library-8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
---
slug: release-14.7.3-billing-library-8
title: 14.7.3 - Google Play Billing Library 8.0+ Features
authors: [hyochan]
tags: [release, openiap, android, ios, billing-library-8]
description: React Native IAP 14.7.3 adds support for Google Play Billing Library 8.0+ features including product status codes, suspended subscription handling, and iOS WWDC 2025 promotional offer improvements.
date: 2026-01-19
---

# React Native IAP 14.7.3

14.7.3 syncs with [OpenIAP v1.3.14](https://www.openiap.dev/docs/updates/notes#1314) bringing Google Play Billing Library 8.0+ features and iOS WWDC 2025 enhancements.

<!-- truncate -->

## New Features

### Product Status (Android 8.0+)

Products now include a `productStatusAndroid` field indicating fetch results:

```typescript
const products = await fetchProducts(['com.example.premium']);

products.forEach(product => {
switch (product.productStatusAndroid) {
case 'ok':
// Product fetched successfully
break;
case 'not-found':
// SKU doesn't exist in Play Console
break;
case 'no-offers-available':
// User not eligible for any offers
break;
}
});
```

Prior to Billing Library 8.0, products that couldn't be fetched were simply omitted. Now you can understand why a product wasn't available.

### Suspended Subscriptions (Android 8.1+)

New `includeSuspended` option in `getAvailablePurchases`:

```typescript
// Include suspended subscriptions in results
const purchases = await getAvailablePurchases({
android: {
includeSuspended: true,
},
});

purchases.forEach(purchase => {
if (purchase.isSuspendedAndroid) {
// Subscription is suspended due to payment issues
// DO NOT grant entitlements
// Direct user to subscription management
}
});
```

Suspended subscriptions have `isSuspendedAndroid: true` and should NOT be granted entitlements. Users should be directed to the subscription center to resolve payment issues.

### Win-Back Offers (iOS 18+)

New support for win-back offers to re-engage churned subscribers:

```typescript
import { requestSubscription } from 'react-native-iap';

await requestSubscription({
ios: {
sku: 'com.example.premium_monthly',
winBackOffer: {
offerId: 'win_back_50_percent',
},
},
});
```

### JWS Promotional Offers (iOS 15+, WWDC 2025)

New simplified JWS format for promotional offers, back-deployed to iOS 15:

```typescript
await requestSubscription({
ios: {
sku: 'com.example.premium_monthly',
promotionalOfferJWS: {
jws: 'eyJ...your-server-signed-jws...',
offerId: 'promo_summer_2025',
},
},
});
```

### Introductory Offer Eligibility Override (iOS 15+)

Override system-determined eligibility for introductory offers:

```typescript
await requestSubscription({
ios: {
sku: 'com.example.premium_monthly',
introductoryOfferEligibility: true, // Force eligibility
},
});
```

### Sub-Response Codes (Android 8.0+)

More granular error information via `SubResponseCodeAndroid`:

- `payment-declined-due-to-insufficient-funds` - Payment method has insufficient funds
- `user-ineligible` - User not eligible for the offer
- `no-applicable-sub-response-code` - No additional context available

## Type Updates

- `SubscriptionOfferTypeIOS` now includes `'win-back'` variant
- `RequestPurchaseIosProps.withOffer` clarified: only applies to subscriptions
- `BillingResultAndroid` now includes optional `subResponseCode`

## OpenIAP Versions

| Package | Version |
|---------|---------|
| openiap-gql | 1.3.14 |
| openiap-google | 1.3.25 |
| openiap-apple | 1.3.12 |

## Installation

```bash
npm install react-native-iap react-native-nitro-modules
```

For detailed changes, see the [OpenIAP Release Notes](https://www.openiap.dev/docs/updates/notes#1314).

Questions or feedback? [GitHub issues](https://github.com/hyochan/react-native-iap/issues).
26 changes: 17 additions & 9 deletions docs/docs/guides/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,21 +298,25 @@ const checkDeviceSupport = async () => {

#### 1. Network connectivity

Handle network errors gracefully:
Handle connection state gracefully:

```tsx
const {connectionError} = useIAP();
const {connected, initConnection} = useIAP({
onPurchaseError: (error) => {
// Connection errors are also reported here
console.error('Error:', error.message);
},
});

if (connectionError) {
if (!connected) {
return (
<View>
<Text>Store connection failed</Text>
<Text>{connectionError.message}</Text>
<Button
title="Retry"
onPress={() => {
// Implement retry logic
retryConnection();
// Retry connection
initConnection();
}}
/>
</View>
Expand Down Expand Up @@ -413,11 +417,15 @@ const {
### 3. Monitor connection state

```tsx
const {connected, connectionError} = useIAP();
const {connected} = useIAP({
onPurchaseError: (error) => {
console.error('Error occurred:', error);
},
});

useEffect(() => {
console.log('Connection state changed:', {connected, error: connectionError});
}, [connected, connectionError]);
console.log('Connection state changed:', {connected});
}, [connected]);
```

## Testing Strategies
Expand Down
65 changes: 65 additions & 0 deletions docs/static/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,18 @@ interface Purchase {
// Android specific
autoRenewingAndroid?: boolean;
packageNameAndroid?: string;
isSuspendedAndroid?: boolean; // v14.7.3+, Billing Library 8.1+
}
```

### ProductStatusAndroid (v14.7.3+, Billing Library 8.0+)

```tsx
type ProductStatusAndroid = 'ok' | 'not-found' | 'no-offers-available' | 'unknown';
```

Products now include `productStatusAndroid` field to explain why a product couldn't be fetched.

### ErrorCode

```tsx
Expand Down Expand Up @@ -281,6 +290,46 @@ await requestPurchase({
});
```

### iOS Subscription Offers (v14.7.3+)

```tsx
// Win-back offers (iOS 18+)
await requestPurchase({
request: {
apple: {
sku: 'monthly_sub',
winBackOffer: { offerId: 'win_back_50_percent' },
},
},
type: 'subs',
});

// JWS promotional offers (iOS 15+, WWDC 2025)
await requestPurchase({
request: {
apple: {
sku: 'monthly_sub',
promotionalOfferJWS: {
jws: 'eyJ...server-signed-jws...',
offerId: 'promo_offer_id',
},
},
},
type: 'subs',
});

// Override introductory offer eligibility (iOS 15+)
await requestPurchase({
request: {
apple: {
sku: 'monthly_sub',
introductoryOfferEligibility: true, // Force eligibility
},
},
type: 'subs',
});
```

### Restore Purchases

```tsx
Expand All @@ -295,6 +344,22 @@ const restore = async () => {
};
```

### Include Suspended Subscriptions (Android 8.1+)

```tsx
// By default, suspended subscriptions are excluded
// To include them:
const purchases = await getAvailablePurchases({
android: { includeSuspended: true }
});

// Check for suspended subscriptions
purchases.filter(p => p.isSuspendedAndroid).forEach(p => {
// DO NOT grant entitlements for suspended subscriptions
// Direct user to resolve payment issues
});
```

### Check Active Subscriptions

```tsx
Expand Down
9 changes: 8 additions & 1 deletion docs/versioned_docs/version-14.1/guides/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,14 @@ The connection can be in several states:
- **Error**: Connection failed

```tsx
const {connected, connectionError} = useIAP();
const [connectionError, setConnectionError] = useState<Error | null>(null);

const {connected} = useIAP({
onPurchaseError: (error) => {
// Handle connection and other errors
setConnectionError(error);
},
});

if (connectionError) {
return <ErrorView error={connectionError} />;
Expand Down
Loading