From ad502c1065de1041fdba934bd0f239d9a8f77e91 Mon Sep 17 00:00:00 2001 From: Hyo Date: Mon, 19 Jan 2026 01:35:30 +0900 Subject: [PATCH 1/4] feat: sync with openiap v1.3.14 (billing library 8.0+ features) - Update openiap-versions.json (gql: 1.3.14, apple: 1.3.12, google: 1.3.25) - Regenerate TypeScript types from OpenIAP - Add productStatusAndroid to NitroProduct (8.0+) - Add includeSuspended option to getAvailablePurchases (8.1+) - Add new iOS types: PromotionalOfferJwsInputIOS, WinBackOfferInputIOS - Add RequestSubscriptionIosProps fields: introductoryOfferEligibility, promotionalOfferJWS, winBackOffer - Add SubResponseCodeAndroid for granular error info (8.0+) - Add BillingResultAndroid with subResponseCode - Add 'win-back' to SubscriptionOfferTypeIOS - Update llms.txt with new API docs - Add release blog post for v14.8.0 Co-Authored-By: Claude Opus 4.5 --- .../java/com/margelo/nitro/iap/HybridRnIap.kt | 27 +++- ...-01-19-release-14.8.0-billing-library-8.md | 141 ++++++++++++++++++ docs/static/llms.txt | 65 ++++++++ ios/HybridRnIap.swift | 13 ++ openiap-versions.json | 8 +- src/specs/RnIap.nitro.ts | 38 +++++ src/types.ts | 113 +++++++++++++- src/utils/type-bridge.ts | 2 + 8 files changed, 394 insertions(+), 13 deletions(-) create mode 100644 docs/blog/2026-01-19-release-14.8.0-billing-library-8.md 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..4e6723f7f 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,21 @@ 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) ) 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 +1052,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 +1111,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.8.0-billing-library-8.md b/docs/blog/2026-01-19-release-14.8.0-billing-library-8.md new file mode 100644 index 000000000..581032564 --- /dev/null +++ b/docs/blog/2026-01-19-release-14.8.0-billing-library-8.md @@ -0,0 +1,141 @@ +--- +slug: release-14.8.0-billing-library-8 +title: 14.8.0 - Google Play Billing Library 8.0+ Features +authors: [hyochan] +tags: [release, openiap, android, ios, billing-library-8] +description: React Native IAP 14.8.0 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.8.0 + +14.8.0 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/static/llms.txt b/docs/static/llms.txt index 6a587f8c3..1ab0db1fc 100644 --- a/docs/static/llms.txt +++ b/docs/static/llms.txt @@ -229,9 +229,18 @@ interface Purchase { // Android specific autoRenewingAndroid?: boolean; packageNameAndroid?: string; + isSuspendedAndroid?: boolean; // v14.8.0+, Billing Library 8.1+ } ``` +### ProductStatusAndroid (v14.8.0+, 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 @@ -281,6 +290,46 @@ await requestPurchase({ }); ``` +### iOS Subscription Offers (v14.8.0+) + +```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 @@ -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 diff --git a/ios/HybridRnIap.swift b/ios/HybridRnIap.swift index 5ddd14247..7cd2bc2d8 100644 --- a/ios/HybridRnIap.swift +++ b/ios/HybridRnIap.swift @@ -186,6 +186,19 @@ class HybridRnIap: HybridRnIapSpec { if let advancedCommerceData = iosRequest.advancedCommerceData { iosPayload["advancedCommerceData"] = advancedCommerceData } + // WWDC 2025 / iOS 18+ subscription offer fields + if let introductoryOfferEligibility = iosRequest.introductoryOfferEligibility { + iosPayload["introductoryOfferEligibility"] = introductoryOfferEligibility + } + if let promotionalOfferJWS = iosRequest.promotionalOfferJWS { + iosPayload["promotionalOfferJWS"] = [ + "jws": promotionalOfferJWS.jws, + "offerId": promotionalOfferJWS.offerId + ] + } + if let winBackOffer = iosRequest.winBackOffer { + iosPayload["winBackOffer"] = ["offerId": winBackOffer.offerId] + } let cachedType = await MainActor.run { self.productTypeBySku[iosRequest.sku] } let resolvedType = RnIapHelper.parseProductQueryType(cachedType) diff --git a/openiap-versions.json b/openiap-versions.json index 9339e7fdb..6f0b3c47b 100644 --- a/openiap-versions.json +++ b/openiap-versions.json @@ -1,6 +1,6 @@ { - "apple": "1.3.10", - "google": "1.3.23", - "gql": "1.3.12", - "docs": "1.3.7" + "apple": "1.3.12", + "google": "1.3.25", + "gql": "1.3.14", + "docs": "1.3.14" } diff --git a/src/specs/RnIap.nitro.ts b/src/specs/RnIap.nitro.ts index a53025451..97d485779 100644 --- a/src/specs/RnIap.nitro.ts +++ b/src/specs/RnIap.nitro.ts @@ -12,6 +12,7 @@ import type { ExternalPurchaseNoticeResultIOS, MutationFinishTransactionArgs, ProductCommon, + PromotionalOfferJwsInputIOS, PurchaseCommon, PurchaseOptions, VerifyPurchaseAppleOptions, @@ -24,6 +25,7 @@ import type { UserChoiceBillingDetails, PaymentModeIOS, SubscriptionProductReplacementParamsAndroid, + WinBackOfferInputIOS, } from '../types'; // Nitro-compatible enum types (Nitro doesn't support inline string unions from types.ts) @@ -118,6 +120,27 @@ export interface NitroRequestPurchaseIos { * @platform iOS */ advancedCommerceData?: RequestPurchaseIosProps['advancedCommerceData']; + /** + * Override introductory offer eligibility (iOS 15+, WWDC 2025). + * Set to true to indicate the user is eligible for introductory offer, + * or false to indicate they are not. When nil, the system determines eligibility. + * Back-deployed to iOS 15. + * @platform iOS + */ + introductoryOfferEligibility?: boolean | null; + /** + * JWS promotional offer (iOS 15+, WWDC 2025). + * New signature format using compact JWS string for promotional offers. + * Back-deployed to iOS 15. + * @platform iOS + */ + promotionalOfferJWS?: PromotionalOfferJwsInputIOS | null; + /** + * Win-back offer to apply (iOS 18+). + * Used to re-engage churned subscribers with a discount or free trial. + * @platform iOS + */ + winBackOffer?: WinBackOfferInputIOS | null; } export interface NitroRequestPurchaseAndroid { @@ -161,6 +184,13 @@ type NitroAvailablePurchasesAndroidType = 'inapp' | 'subs'; export interface NitroAvailablePurchasesAndroidOptions { type?: NitroAvailablePurchasesAndroidType; + /** + * Include suspended subscriptions in the result (Android 8.1+). + * Suspended subscriptions have isSuspendedAndroid=true and should NOT be granted entitlements. + * Users should be directed to the subscription center to resolve payment issues. + * Default: false (only active subscriptions are returned) + */ + includeSuspended?: boolean | null; } export interface NitroAvailablePurchasesOptions { @@ -543,6 +573,14 @@ export interface NitroProduct { freeTrialPeriodAndroid?: string | null; subscriptionOfferDetailsAndroid?: string | null; oneTimePurchaseOfferDetailsAndroid?: NitroOneTimePurchaseOfferDetail[] | null; + /** + * Product-level status code indicating fetch result (Android 8.0+) + * OK = product fetched successfully + * NOT_FOUND = SKU doesn't exist + * NO_OFFERS_AVAILABLE = user not eligible for any offers + * Available in Google Play Billing Library 8.0.0+ + */ + productStatusAndroid?: string | null; } // ╔══════════════════════════════════════════════════════════════════════════╗ diff --git a/src/types.ts b/src/types.ts index 82a881f2e..10a34a211 100644 --- a/src/types.ts +++ b/src/types.ts @@ -99,6 +99,22 @@ export interface BillingProgramReportingDetailsAndroid { externalTransactionToken: string; } +/** + * Extended billing result with sub-response code (Android) + * Available in Google Play Billing Library 8.0.0+ + */ +export interface BillingResultAndroid { + /** Debug message from the billing library */ + debugMessage?: (string | null); + /** The response code from the billing operation */ + responseCode: number; + /** + * Sub-response code for more granular error information (8.0+). + * Provides additional context when responseCode indicates an error. + */ + subResponseCode?: (SubResponseCodeAndroid | null); +} + export interface DeepLinkOptions { /** Android package name to target (required on Android) */ packageNameAndroid?: (string | null); @@ -525,7 +541,7 @@ export interface Mutation { * promoted products can be purchased directly via the standard purchase flow. * @deprecated Use promotedProductListenerIOS + requestPurchase instead */ - requestPurchaseOnPromotedProductIOS: boolean; + requestPurchaseOnPromotedProductIOS: Promise; /** Restore completed purchases across platforms */ restorePurchases: Promise; /** @@ -664,6 +680,14 @@ export interface ProductAndroid extends ProductCommon { oneTimePurchaseOfferDetailsAndroid?: (ProductAndroidOneTimePurchaseOfferDetail[] | null); platform: 'android'; price?: (number | null); + /** + * Product-level status code indicating fetch result (Android 8.0+) + * OK = product fetched successfully + * NOT_FOUND = SKU doesn't exist + * NO_OFFERS_AVAILABLE = user not eligible for any offers + * Available in Google Play Billing Library 8.0.0+ + */ + productStatusAndroid?: (ProductStatusAndroid | null); /** * @deprecated Use subscriptionOffers instead for cross-platform compatibility. * @deprecated Use subscriptionOffers instead @@ -769,6 +793,14 @@ export interface ProductRequest { type?: (ProductQueryType | null); } +/** + * Status code for individual products returned from queryProductDetailsAsync (Android) + * Prior to 8.0, products that couldn't be fetched were simply not returned. + * With 8.0+, these products are returned with a status code explaining why. + * Available in Google Play Billing Library 8.0.0+ + */ +export type ProductStatusAndroid = 'ok' | 'not-found' | 'no-offers-available' | 'unknown'; + export type ProductSubscription = ProductSubscriptionAndroid | ProductSubscriptionIOS; export interface ProductSubscriptionAndroid extends ProductCommon { @@ -794,6 +826,14 @@ export interface ProductSubscriptionAndroid extends ProductCommon { oneTimePurchaseOfferDetailsAndroid?: (ProductAndroidOneTimePurchaseOfferDetail[] | null); platform: 'android'; price?: (number | null); + /** + * Product-level status code indicating fetch result (Android 8.0+) + * OK = product fetched successfully + * NOT_FOUND = SKU doesn't exist + * NO_OFFERS_AVAILABLE = user not eligible for any offers + * Available in Google Play Billing Library 8.0.0+ + */ + productStatusAndroid?: (ProductStatusAndroid | null); /** * @deprecated Use subscriptionOffers instead for cross-platform compatibility. * @deprecated Use subscriptionOffers instead @@ -866,6 +906,23 @@ export type ProductType = 'in-app' | 'subs'; export type ProductTypeIOS = 'consumable' | 'non-consumable' | 'auto-renewable-subscription' | 'non-renewing-subscription'; +/** + * JWS promotional offer input for iOS 15+ (StoreKit 2, WWDC 2025). + * New signature format using compact JWS string for promotional offers. + * This provides a simpler alternative to the legacy signature-based promotional offers. + * Back-deployed to iOS 15. + */ +export interface PromotionalOfferJwsInputIOS { + /** + * Compact JWS string signed by your server. + * The JWS should contain the promotional offer signature data. + * Format: header.payload.signature (base64url encoded) + */ + jws: string; + /** The promotional offer identifier from App Store Connect */ + offerId: string; +} + export type Purchase = PurchaseAndroid | PurchaseIOS; export interface PurchaseAndroid extends PurchaseCommon { @@ -980,6 +1037,13 @@ export interface PurchaseOfferIOS { export interface PurchaseOptions { /** Also emit results through the iOS event listeners */ alsoPublishToEventListenerIOS?: (boolean | null); + /** + * Include suspended subscriptions in the result (Android 8.1+). + * Suspended subscriptions have isSuspendedAndroid=true and should NOT be granted entitlements. + * Users should be directed to the subscription center to resolve payment issues. + * Default: false (only active subscriptions are returned) + */ + includeSuspendedAndroid?: (boolean | null); /** Limit to currently active items on iOS */ onlyIncludeActiveItemsIOS?: (boolean | null); } @@ -1156,7 +1220,10 @@ export interface RequestPurchaseIosProps { quantity?: (number | null); /** Product SKU */ sku: string; - /** Discount offer to apply */ + /** + * Promotional offer to apply (subscriptions only, ignored for one-time purchases). + * iOS only supports promotional offers for auto-renewable subscriptions. + */ withOffer?: (DiscountOfferInputIOS | null); } @@ -1238,8 +1305,32 @@ export interface RequestSubscriptionIosProps { advancedCommerceData?: (string | null); andDangerouslyFinishTransactionAutomatically?: (boolean | null); appAccountToken?: (string | null); + /** + * Override introductory offer eligibility (iOS 15+, WWDC 2025). + * Set to true to indicate the user is eligible for introductory offer, + * or false to indicate they are not. When nil, the system determines eligibility. + * Back-deployed to iOS 15. + */ + introductoryOfferEligibility?: (boolean | null); + /** + * JWS promotional offer (iOS 15+, WWDC 2025). + * New signature format using compact JWS string for promotional offers. + * Back-deployed to iOS 15. + */ + promotionalOfferJWS?: (PromotionalOfferJwsInputIOS | null); quantity?: (number | null); sku: string; + /** + * Win-back offer to apply (iOS 18+) + * Used to re-engage churned subscribers with a discount or free trial. + * The offer is available when the customer is eligible and can be discovered + * via StoreKit Message (automatic) or subscription offer APIs. + */ + winBackOffer?: (WinBackOfferInputIOS | null); + /** + * Promotional offer to apply for subscription purchases. + * Requires server-signed offer with nonce, timestamp, keyId, and signature. + */ withOffer?: (DiscountOfferInputIOS | null); } @@ -1295,6 +1386,12 @@ export interface RequestVerifyPurchaseWithIapkitResult { store: IapStore; } +/** + * Sub-response codes for more granular purchase error information (Android) + * Available in Google Play Billing Library 8.0.0+ + */ +export type SubResponseCodeAndroid = 'no-applicable-sub-response-code' | 'payment-declined-due-to-insufficient-funds' | 'user-ineligible'; + export interface Subscription { /** * Fires when a user selects developer billing in the External Payments flow (Android only) @@ -1415,7 +1512,7 @@ export interface SubscriptionOfferIOS { type: SubscriptionOfferTypeIOS; } -export type SubscriptionOfferTypeIOS = 'introductory' | 'promotional'; +export type SubscriptionOfferTypeIOS = 'introductory' | 'promotional' | 'win-back'; /** Subscription period value combining unit and count. */ export interface SubscriptionPeriod { @@ -1615,6 +1712,16 @@ export interface VerifyPurchaseWithProviderResult { export type VoidResult = void; +/** + * Win-back offer input for iOS 18+ (StoreKit 2) + * Win-back offers are used to re-engage churned subscribers. + * The offer is automatically presented via StoreKit Message when eligible, + * or can be applied programmatically during purchase. + */ +export interface WinBackOfferInputIOS { + /** The win-back offer ID from App Store Connect */ + offerId: string; +} // -- Query helper types (auto-generated) export type QueryArgsMap = { canPresentExternalPurchaseNoticeIOS: never; diff --git a/src/utils/type-bridge.ts b/src/utils/type-bridge.ts index 54452c776..d17a2a42d 100644 --- a/src/utils/type-bridge.ts +++ b/src/utils/type-bridge.ts @@ -312,6 +312,8 @@ export function convertNitroProductToProduct( subscriptionOfferDetailsAndroid: parseSubscriptionOffers( nitroProduct.subscriptionOfferDetailsAndroid, ), + // Product status (Billing Library 8.0+, OpenIAP 1.3.14+) + productStatusAndroid: nitroProduct.productStatusAndroid ?? null, }; // Parse standardized subscriptionOffers (cross-platform, OpenIAP 1.3.10+) From f986692b5abec6c7d7ad8d6c4cb59257f913c12e Mon Sep 17 00:00:00 2001 From: Hyo Date: Mon, 19 Jan 2026 03:22:53 +0900 Subject: [PATCH 2/4] fix(android): respect includeSuspended when type is specified - Fix getAvailablePurchases to always use purchaseOptions with includeSuspended, then filter by type instead of using getAvailableItems which ignores the option - Update docs to remove non-existent connectionError from useIAP examples and use correct API (connected state + onPurchaseError) Co-Authored-By: Claude Opus 4.5 --- .../java/com/margelo/nitro/iap/HybridRnIap.kt | 7 +++-- docs/docs/guides/troubleshooting.md | 26 +++++++++++------ .../version-14.1/guides/lifecycle.md | 9 +++++- .../version-14.1/guides/troubleshooting.md | 29 +++++++++++-------- .../version-14.2/guides/lifecycle.md | 9 +++++- .../version-14.2/guides/troubleshooting.md | 29 +++++++++++-------- .../version-14.3/guides/lifecycle.md | 9 +++++- .../version-14.3/guides/troubleshooting.md | 29 +++++++++++-------- .../version-14.4/guides/troubleshooting.md | 26 +++++++++++------ .../version-14.5/guides/troubleshooting.md | 26 +++++++++++------ .../version-14.6/guides/troubleshooting.md | 26 +++++++++++------ 11 files changed, 148 insertions(+), 77 deletions(-) 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 4e6723f7f..8d900cd4e 100644 --- a/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +++ b/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt @@ -523,16 +523,19 @@ class HybridRnIap : HybridRnIapSpec() { includeSuspendedAndroid = includeSuspended ) + // Always use getAvailablePurchases with purchaseOptions to respect includeSuspended + // Then filter by type if specified + val allPurchases = openIap.getAvailablePurchases(purchaseOptions) val result: List = if (normalizedType != null) { val typeEnum = parseProductQueryType(normalizedType) RnIapLog.payload( "getAvailablePurchases.native", mapOf("type" to typeEnum.rawValue, "includeSuspended" to includeSuspended) ) - openIap.getAvailableItems(typeEnum) + allPurchases.filter { it.type.rawValue == typeEnum.rawValue } } else { RnIapLog.payload("getAvailablePurchases.native", mapOf("type" to "all", "includeSuspended" to includeSuspended)) - openIap.getAvailablePurchases(purchaseOptions) + allPurchases } RnIapLog.result( "getAvailablePurchases", 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}