diff --git a/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt b/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt index 2dc3f8ccf..537e41c0f 100644 --- a/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +++ b/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt @@ -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() @@ -516,16 +518,23 @@ class HybridRnIap : HybridRnIapSpec() { else -> null } + // Create PurchaseOptions with includeSuspendedAndroid + val purchaseOptions = dev.hyo.openiap.PurchaseOptions( + includeSuspendedAndroid = includeSuspended + ) + val result: List = 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) } RnIapLog.result( "getAvailablePurchases", @@ -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 @@ -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 { diff --git a/docs/blog/2026-01-19-release-14.7.3-billing-library-8.md b/docs/blog/2026-01-19-release-14.7.3-billing-library-8.md new file mode 100644 index 000000000..2ae361852 --- /dev/null +++ b/docs/blog/2026-01-19-release-14.7.3-billing-library-8.md @@ -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. + + + +## 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). diff --git a/docs/docs/guides/troubleshooting.md b/docs/docs/guides/troubleshooting.md index edc8d948f..f9b812fa5 100644 --- a/docs/docs/guides/troubleshooting.md +++ b/docs/docs/guides/troubleshooting.md @@ -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 ( Store connection failed - {connectionError.message}