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
10 changes: 9 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,13 @@
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"cSpell.words": ["couroutines", "hyochan", "openiap", "Podfile", "skus"]
"cSpell.words": [
"couroutines",
"hyochan",
"iapkit",
"martie",
"openiap",
"Podfile",
"skus"
]
}
71 changes: 0 additions & 71 deletions PR_SUMMARY.md

This file was deleted.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<img src="https://hyochan.github.io/react-native-iap/img/icon.png" alt="React Native IAP Logo" width="150" />

[![Version](http://img.shields.io/npm/v/react-native-iap.svg?style=flat-square)](https://npmjs.org/package/react-native-iap)
[![Next Version](https://img.shields.io/npm/v/react-native-iap/next.svg?style=flat-square&label=next)](https://npmjs.org/package/react-native-iap)
[![Download](http://img.shields.io/npm/dm/react-native-iap.svg?style=flat-square)](https://npmjs.org/package/react-native-iap)
[![Backers and Sponsors](https://img.shields.io/opencollective/all/react-native-iap.svg)](https://opencollective.com/react-native-iap)
[![CI - Test](https://github.com/hyochan/react-native-iap/actions/workflows/ci-test.yml/badge.svg)](https://github.com/hyochan/react-native-iap/actions/workflows/ci-test.yml)
Expand Down
108 changes: 72 additions & 36 deletions android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import dev.hyo.openiap.RequestPurchaseResultPurchase
import dev.hyo.openiap.RequestPurchaseResultPurchases
import dev.hyo.openiap.RequestSubscriptionAndroidProps
import dev.hyo.openiap.RequestSubscriptionPropsByPlatforms
import dev.hyo.openiap.VerifyPurchaseGoogleOptions
import dev.hyo.openiap.VerifyPurchaseProps
import dev.hyo.openiap.VerifyPurchaseResultAndroid
import dev.hyo.openiap.InitConnectionConfig as OpenIapInitConnectionConfig
import dev.hyo.openiap.listener.OpenIapPurchaseErrorListener
import dev.hyo.openiap.listener.OpenIapPurchaseUpdateListener
Expand Down Expand Up @@ -1119,51 +1122,84 @@ class HybridRnIap : HybridRnIapSpec() {
}
}

// Receipt validation
// Receipt validation - calls OpenIAP's verifyPurchase
override fun validateReceipt(params: NitroReceiptValidationParams): Promise<Variant_NitroReceiptValidationResultIOS_NitroReceiptValidationResultAndroid> {
return Promise.async {
try {
// For Android, we need the androidOptions to be provided
val androidOptions = params.androidOptions
?: throw OpenIapException(toErrorJson(OpenIAPError.DeveloperError))

// Android receipt validation would typically involve server-side validation
// using Google Play Developer API. Here we provide a simplified implementation
// that demonstrates the expected structure.

// In a real implementation, you would make an HTTP request to Google Play API
// using the androidOptions.accessToken, androidOptions.packageName, etc.

// For now, we'll return a mock successful validation result
// This should be replaced with actual Google Play Developer API calls
val currentTime = System.currentTimeMillis()

// For Android, we need the google options to be provided (new platform-specific structure)
val nitroGoogleOptions = params.google
?: throw OpenIapException(toErrorJson(OpenIAPError.DeveloperError, debugMessage = "Missing required parameter: google options"))

// Validate required google fields
val validations = mapOf(
"google.sku" to nitroGoogleOptions.sku,
"google.accessToken" to nitroGoogleOptions.accessToken,
"google.packageName" to nitroGoogleOptions.packageName,
"google.purchaseToken" to nitroGoogleOptions.purchaseToken
)
for ((name, value) in validations) {
if (value.isEmpty()) {
throw OpenIapException(toErrorJson(OpenIAPError.DeveloperError, debugMessage = "Missing or empty required parameter: $name"))
}
}

RnIapLog.payload("validateReceipt", mapOf(
"sku" to nitroGoogleOptions.sku,
"packageName" to nitroGoogleOptions.packageName,
"isSub" to nitroGoogleOptions.isSub
))

// Create OpenIAP VerifyPurchaseGoogleOptions
val googleOptions = VerifyPurchaseGoogleOptions(
sku = nitroGoogleOptions.sku,
accessToken = nitroGoogleOptions.accessToken,
packageName = nitroGoogleOptions.packageName,
purchaseToken = nitroGoogleOptions.purchaseToken,
isSub = nitroGoogleOptions.isSub
)

// Create OpenIAP VerifyPurchaseProps
val props = VerifyPurchaseProps(google = googleOptions)

// Call OpenIAP's verifyPurchase - this makes the actual Google Play API call
val verifyResult = openIap.verifyPurchase(props)
RnIapLog.result("validateReceipt", verifyResult.toString())

// Cast to Android result type (on Android, verifyPurchase returns VerifyPurchaseResultAndroid)
val androidResult = verifyResult as? VerifyPurchaseResultAndroid
?: throw OpenIapException(toErrorJson(OpenIAPError.InvalidPurchaseVerification, debugMessage = "Unexpected result type from verifyPurchase"))

// Convert OpenIAP result to Nitro result
val result = NitroReceiptValidationResultAndroid(
autoRenewing = androidOptions.isSub ?: false,
betaProduct = false,
cancelDate = null,
cancelReason = "",
deferredDate = null,
deferredSku = null,
freeTrialEndDate = 0.0,
gracePeriodEndDate = 0.0,
parentProductId = params.sku,
productId = params.sku,
productType = if (androidOptions.isSub == true) "subs" else "inapp",
purchaseDate = currentTime.toDouble(),
quantity = 1.0,
receiptId = androidOptions.productToken,
renewalDate = if (androidOptions.isSub == true) (currentTime + (30L * 24 * 60 * 60 * 1000)).toDouble() else 0.0, // 30 days from now if subscription
term = if (androidOptions.isSub == true) "P1M" else "", // P1M = 1 month
termSku = params.sku,
testTransaction = false
autoRenewing = androidResult.autoRenewing,
betaProduct = androidResult.betaProduct,
cancelDate = androidResult.cancelDate,
cancelReason = androidResult.cancelReason,
deferredDate = androidResult.deferredDate,
deferredSku = androidResult.deferredSku,
freeTrialEndDate = androidResult.freeTrialEndDate,
gracePeriodEndDate = androidResult.gracePeriodEndDate,
parentProductId = androidResult.parentProductId,
productId = androidResult.productId,
productType = androidResult.productType,
purchaseDate = androidResult.purchaseDate,
quantity = androidResult.quantity.toDouble(),
receiptId = androidResult.receiptId,
renewalDate = androidResult.renewalDate,
term = androidResult.term,
termSku = androidResult.termSku,
testTransaction = androidResult.testTransaction
)

Variant_NitroReceiptValidationResultIOS_NitroReceiptValidationResultAndroid.Second(result)

} catch (e: OpenIapException) {
RnIapLog.failure("validateReceipt", e)
throw e
} catch (e: Exception) {
RnIapLog.failure("validateReceipt", e)
val debugMessage = e.message
val error = OpenIAPError.InvalidReceipt
val error = OpenIAPError.InvalidPurchaseVerification
throw OpenIapException(
toErrorJson(
error = error,
Expand Down
116 changes: 105 additions & 11 deletions docs/blog/2025-12-11-release-14.6.0-billing-programs.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ React Native IAP 14.6.0 brings the **new Billing Programs API** from Google Play

### Billing Programs API (Android 8.2.0+)

Google Play Billing Library 8.2.0 introduces the new **Billing Programs API** that replaces the deprecated alternative billing APIs. This API supports:
[Google Play Billing Library 8.2.0](https://developer.android.com/google/play/billing/release-notes#8-2-0), released on December 9, 2025, introduces the new **Billing Programs API** that replaces the deprecated alternative billing APIs. This API supports:

- **External Content Links** - For eligible apps to link to external content
- **External Offers** - For eligible apps to offer purchases outside Google Play
Expand Down Expand Up @@ -81,7 +81,59 @@ Also adds the new **`KeepExisting`** replacement mode from Billing Library 8.1.0

For subscriptions, we've added the `isSuspendedAndroid` field from Billing Library 8.1.0+. This indicates whether a subscription is currently suspended (e.g., due to payment issues during a grace period).

## Breaking Change
## Breaking Changes

### `verifyPurchase` / `validateReceipt` API Restructured

The `verifyPurchase` and `validateReceipt` APIs have been refactored to use platform-specific options. The `sku` field has been moved from the root level into each platform's options object, and `androidOptions` has been replaced with `google`.

**Before (14.5.0):**

```typescript
// iOS
await verifyPurchase({sku: 'premium_monthly'});

// Android
await verifyPurchase({
sku: 'premium_monthly',
androidOptions: {
packageName: 'com.example.app',
productToken: 'token...',
accessToken: 'oauth_token...',
isSub: true,
},
});
```

**After (14.6.0):**

```typescript
// Provide all platform options - library handles platform detection internally
await verifyPurchase({
apple: {sku: 'premium_monthly'},
google: {
sku: 'premium_monthly',
packageName: 'com.example.app',
purchaseToken: 'token...', // renamed from productToken
accessToken: 'oauth_token...',
isSub: true,
},
// Optional: Meta Quest support
horizon: {
sku: '50_gems',
userId: '123456789',
accessToken: 'OC|app_id|app_secret',
},
});
```

**Key Changes:**

- `sku` moved from root level → inside each platform options (`apple.sku`, `google.sku`, `horizon.sku`)
- `androidOptions` renamed → `google`
- `productToken` renamed → `purchaseToken` (in google options)
- New `horizon` options added for Meta Quest verification
- No need for `Platform.OS` checks - provide all options and let the library handle it

### `oneTimePurchaseOfferDetailsAndroid` is now an Array

Expand Down Expand Up @@ -168,24 +220,66 @@ interface ProductAndroidOneTimePurchaseOfferDetail {
}
```

## OpenIAP Updates
## New Types

### VerifyPurchase Platform-Specific Types

```typescript
interface VerifyPurchaseAppleOptions {
sku: string; // Product SKU to validate
}

interface VerifyPurchaseGoogleOptions {
sku: string; // Product SKU to validate
accessToken: string; // OAuth access token from your backend
packageName: string; // Android package name
purchaseToken: string; // Purchase token from the purchase
isSub?: boolean; // Whether this is a subscription
}

interface VerifyPurchaseHorizonOptions {
sku: string; // Product SKU to validate
userId: string; // Meta Horizon user ID
accessToken: string; // Meta access token (OC|app_id|app_secret)
}

- **openiap-google**: [1.3.10 → 1.3.12](https://github.com/hyodotdev/openiap/compare/google-v1.3.10...google-v1.3.12)
- **openiap-gql**: [1.3.0 → 1.3.2](https://github.com/hyodotdev/openiap/compare/1.3.0...1.3.2)
interface VerifyPurchaseProps {
apple?: VerifyPurchaseAppleOptions;
google?: VerifyPurchaseGoogleOptions;
horizon?: VerifyPurchaseHorizonOptions;
}
```

## Deprecated Methods

The following methods are deprecated in favor of the new Billing Programs API:

| Deprecated Method | Replacement | Documentation |
|-------------------|-------------|---------------|
| [`checkAlternativeBillingAvailabilityAndroid()`](/docs/14.5/api/methods/core-methods#checkalternativebillingavailabilityandroid) | [`isBillingProgramAvailableAndroid()`](/docs/api/methods/core-methods#isbillingprogramavailableandroid) | Check billing program availability |
| [`showAlternativeBillingDialogAndroid()`](/docs/14.5/api/methods/core-methods#showalternativebillingdialogandroid) | [`launchExternalLinkAndroid()`](/docs/api/methods/core-methods#launchexternallinkandroid) | Launch external billing flow |
| [`createAlternativeBillingTokenAndroid()`](/docs/14.5/api/methods/core-methods#createalternativebillingtokenandroid) | [`createBillingProgramReportingDetailsAndroid()`](/docs/api/methods/core-methods#createbillingprogramreportingdetailsandroid) | Get external transaction token |
| `replacementModeAndroid` parameter | `subscriptionProductReplacementParams` | Per-product replacement configuration |

These deprecated methods will continue to work but will be removed in a future major version. We recommend migrating to the new Billing Programs API.

## OpenIAP Updates

## Early Access Installation
- **openiap-apple**: [1.3.0 → 1.3.2](https://github.com/hyodotdev/openiap/compare/apple-v1.3.0...apple-v1.3.2)
- **openiap-google**: [1.3.12 → 1.3.14](https://github.com/hyodotdev/openiap/compare/google-v1.3.12...google-v1.3.14)
- **openiap-gql**: [1.3.2 → 1.3.4](https://github.com/hyodotdev/openiap/compare/gql-v1.3.2...gql-v1.3.4)

Version 14.6 is currently available as a preview release. Install with the `@next` tag:
## Installation

```bash
npm install react-native-iap@next react-native-nitro-modules
npm install react-native-iap react-native-nitro-modules
# or
yarn add react-native-iap@next react-native-nitro-modules
yarn add react-native-iap react-native-nitro-modules
# or
bun add react-native-iap@next react-native-nitro-modules
bun add react-native-iap react-native-nitro-modules
```

Check out the [14.6 (Next) documentation](/docs/next/intro) for details on all upcoming features.
Check out the [documentation](/docs/intro) for details on all features.

## References

Expand Down
Loading