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
4 changes: 2 additions & 2 deletions NitroIap.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ Pod::Spec.new do |s|
s.dependency 'React-jsi'
s.dependency 'React-callinvoker'
# OpenIAP Apple for StoreKit 2 integration
# Use ~> to allow patch updates (e.g., 1.2.30 -> 1.2.31, but not 1.3.0)
s.dependency 'openiap', "~> #{apple_version}"
# Exact version match for consistent builds
s.dependency 'openiap', "#{apple_version}"

install_modules_dependencies(s)
end
116 changes: 113 additions & 3 deletions android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,34 @@ class HybridRnIap : HybridRnIapSpec() {
}
}
}


override fun hasActiveSubscriptions(subscriptionIds: Array<String>?): Promise<Boolean> {
return Promise.async {
initConnection(null).await()

RnIapLog.payload(
"hasActiveSubscriptions",
mapOf("subscriptionIds" to (subscriptionIds?.toList() ?: "all"))
)

try {
val hasActive = openIap.hasActiveSubscriptions(subscriptionIds?.toList())
RnIapLog.result("hasActiveSubscriptions", hasActive)
hasActive
} catch (e: Exception) {
RnIapLog.failure("hasActiveSubscriptions", e)
val error = OpenIAPError.ServiceUnavailable
throw OpenIapException(
toErrorJson(
error = error,
debugMessage = e.message,
messageOverride = "Failed to check active subscriptions: ${e.message}"
)
)
}
}
}

// Transaction management methods (Unified)
override fun finishTransaction(params: NitroFinishTransactionParams): Promise<Variant_Boolean_NitroPurchaseResult> {
return Promise.async {
Expand Down Expand Up @@ -1078,7 +1105,7 @@ class HybridRnIap : HybridRnIapSpec() {
)

Variant_NitroReceiptValidationResultIOS_NitroReceiptValidationResultAndroid.Second(result)

} catch (e: Exception) {
val debugMessage = e.message
val error = OpenIAPError.InvalidReceipt
Expand All @@ -1092,7 +1119,60 @@ class HybridRnIap : HybridRnIapSpec() {
}
}
}


override fun verifyPurchaseWithProvider(params: NitroVerifyPurchaseWithProviderProps): Promise<NitroVerifyPurchaseWithProviderResult> {
return Promise.async {
try {
// Convert Nitro enum to string (e.g., IAPKIT -> "iapkit")
val providerString = params.provider.name.lowercase()
RnIapLog.payload("verifyPurchaseWithProvider", mapOf("provider" to providerString))

// Build the props map for OpenIAP - use string value for provider
val propsMap = mutableMapOf<String, Any?>("provider" to providerString)
params.iapkit?.let { iapkit ->
val iapkitMap = mutableMapOf<String, Any?>()
iapkit.apiKey?.let { iapkitMap["apiKey"] = it }
iapkit.google?.let { google ->
iapkitMap["google"] = mapOf("purchaseToken" to google.purchaseToken)
}
iapkit.apple?.let { apple ->
iapkitMap["apple"] = mapOf("jws" to apple.jws)
}
propsMap["iapkit"] = iapkitMap
}

val props = dev.hyo.openiap.VerifyPurchaseWithProviderProps.fromJson(propsMap)
val result = openIap.verifyPurchaseWithProvider(props)

RnIapLog.result("verifyPurchaseWithProvider", mapOf("provider" to result.provider, "iapkitCount" to result.iapkit.size))

// Convert result to Nitro types
val nitroIapkitResults = result.iapkit.map { item ->
NitroVerifyPurchaseWithIapkitResult(
isValid = item.isValid,
state = mapIapkitPurchaseState(item.state.name),
store = mapIapkitStore(item.store.name)
)
}.toTypedArray()

NitroVerifyPurchaseWithProviderResult(
iapkit = nitroIapkitResults,
provider = mapPurchaseVerificationProvider(result.provider.name)
)
} catch (e: Exception) {
RnIapLog.failure("verifyPurchaseWithProvider", e)
val error = OpenIAPError.VerificationFailed
throw OpenIapException(
toErrorJson(
error = error,
debugMessage = e.message,
messageOverride = "Verification failed: ${e.message ?: "unknown reason"}"
)
)
}
}
}

// iOS-specific methods - Not applicable on Android, return appropriate defaults
override fun subscriptionStatusIOS(sku: String): Promise<Array<NitroSubscriptionStatus>?> {
return Promise.async {
Expand Down Expand Up @@ -1324,6 +1404,36 @@ class HybridRnIap : HybridRnIapSpec() {
}
}

// Helper functions to map OpenIAP enum values to Nitro enum values
private fun mapIapkitPurchaseState(stateName: String): IapkitPurchaseState {
return when (stateName.uppercase()) {
"ENTITLED" -> IapkitPurchaseState.ENTITLED
"PENDING_ACKNOWLEDGMENT", "PENDING-ACKNOWLEDGMENT" -> IapkitPurchaseState.PENDING_ACKNOWLEDGMENT
"PENDING" -> IapkitPurchaseState.PENDING
"CANCELED" -> IapkitPurchaseState.CANCELED
"EXPIRED" -> IapkitPurchaseState.EXPIRED
"READY_TO_CONSUME", "READY-TO-CONSUME" -> IapkitPurchaseState.READY_TO_CONSUME
"CONSUMED" -> IapkitPurchaseState.CONSUMED
"INAUTHENTIC" -> IapkitPurchaseState.INAUTHENTIC
else -> IapkitPurchaseState.UNKNOWN
}
}

private fun mapIapkitStore(storeName: String): IapkitStore {
return when (storeName.uppercase()) {
"APPLE" -> IapkitStore.APPLE
"GOOGLE" -> IapkitStore.GOOGLE
else -> IapkitStore.GOOGLE // Default to Google on Android
}
}

private fun mapPurchaseVerificationProvider(providerName: String): PurchaseVerificationProvider {
return when (providerName.uppercase()) {
"IAPKIT" -> PurchaseVerificationProvider.IAPKIT
else -> PurchaseVerificationProvider.NONE
}
}

private fun toErrorResult(
error: OpenIAPError,
productId: String? = null,
Expand Down
109 changes: 100 additions & 9 deletions docs/docs/api/methods/core-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@ Note: react-native-iap aligns with the OpenIAP API surface. For canonical cross-

These cross‑platform methods work on both iOS and Android. For StoreKit/Play‑specific helpers, see the Platform‑specific APIs section below.

- `initConnection()` — Initialize the store connection
- `endConnection()` — End the store connection and cleanup
- `fetchProducts()` — Fetch product and subscription metadata
- `requestPurchase()` — Start a purchase for products or subscriptions
- `finishTransaction()` — Complete a transaction after validation
- `getAvailablePurchases()` — Restore non‑consumables and subscriptions
- `deepLinkToSubscriptions()` — Open native subscription management UI
- `getStorefront()` — Get current storefront country code
- `hasActiveSubscriptions()` — Check if user has active subscriptions
- [`initConnection()`](#initconnection) — Initialize the store connection
- [`endConnection()`](#endconnection) — End the store connection and cleanup
- [`fetchProducts()`](#fetchproducts) — Fetch product and subscription metadata
- [`requestPurchase()`](#requestpurchase) — Start a purchase for products or subscriptions
- [`finishTransaction()`](#finishtransaction) — Complete a transaction after validation
- [`getAvailablePurchases()`](#getavailablepurchases) — Restore non‑consumables and subscriptions
- [`deepLinkToSubscriptions()`](#deeplinktosubscriptions) — Open native subscription management UI
- [`getStorefront()`](#getstorefront) — Get current storefront country code
- [`hasActiveSubscriptions()`](#hasactivesubscriptions) — Check if user has active subscriptions
- [`verifyPurchaseWithProvider()`](#verifypurchasewithprovider) — Verify purchases with external providers (e.g., IAPKit)

## initConnection()

Expand Down Expand Up @@ -489,6 +490,96 @@ const checkIfUserHasSubscription = async () => {

**Returns:** `Promise<boolean>` - Returns true if user has at least one active subscription

## verifyPurchaseWithProvider()

Verifies purchases using external verification services like IAPKit. This provides additional validation and security beyond local device verification.

```tsx
import {verifyPurchaseWithProvider} from 'react-native-iap';

const verifyWithIAPKit = async (purchase: Purchase) => {
try {
const result = await verifyPurchaseWithProvider({
provider: 'iapkit',
iapkit: {
apiKey: 'your-iapkit-api-key',
environment: 'production', // or 'sandbox'
apple: {
jws: purchase.purchaseToken, // iOS JWS token
},
google: {
purchaseToken: purchase.purchaseToken, // Android purchase token
packageName: 'com.your.app',
productId: purchase.productId,
},
},
});

for (const item of result.iapkit) {
console.log('Is Valid:', item.isValid);
console.log('State:', item.state); // 'entitled', 'expired', 'canceled', etc.
console.log('Store:', item.store); // 'apple' or 'google'
}

return result;
} catch (error) {
console.error('Verification failed:', error);
}
};
```

**Parameters:**

- `params` (object):
- `provider` ('iapkit'): The verification provider to use
- `iapkit?` (object): IAPKit-specific configuration
- `apiKey` (string): Your IAPKit API key
- `environment?` ('production' | 'sandbox'): Environment for verification
- `apple?` (object): iOS verification data
- `jws` (string): The JWS token from the purchase
- `google?` (object): Android verification data
- `purchaseToken` (string): The purchase token
- `packageName` (string): Your app's package name
- `productId` (string): The product ID

**Returns:** `Promise<VerifyPurchaseWithProviderResult>`

```typescript
interface VerifyPurchaseWithProviderResult {
provider: 'iapkit';
iapkit: Array<{
isValid: boolean;
state: IapkitPurchaseState;
store: 'apple' | 'google';
}>;
}

type IapkitPurchaseState =
| 'pending'
| 'unknown'
| 'entitled'
| 'pending-acknowledgment'
| 'canceled'
| 'expired'
| 'ready-to-consume'
| 'consumed'
| 'inauthentic';
```

**Platform Behavior:**

- **iOS**: Sends the JWS (JSON Web Signature) token to IAPKit for server-side verification
- **Android**: Sends the purchase token along with package name and product ID for verification
- **Both**: Returns the verification state and validity from IAPKit's servers

**Use Cases:**

- Server-side receipt validation without maintaining your own validation infrastructure
- Cross-platform purchase verification with a unified API
- Enhanced security through external verification services

> **Note:** You need an IAPKit API key to use this feature. Visit [iapkit.com](https://iapkit.com) to get started.

## Purchase Interface

```tsx
Expand Down
3 changes: 3 additions & 0 deletions example/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# IAPKit Configuration
# Get your API key from https://iapkit.com
IAPKIT_API_KEY=your_iapkit_api_key_here
5 changes: 5 additions & 0 deletions example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,8 @@ yarn-error.log
!.yarn/releases
!.yarn/sdks
!.yarn/versions

# Environment variables
.env
.env.local
.env*.local
13 changes: 13 additions & 0 deletions example/babel.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
[
'module:react-native-dotenv',
{
moduleName: '@env',
path: '.env',
blocklist: null,
allowlist: null,
safe: false,
allowUndefined: true,
},
],
],
};
16 changes: 8 additions & 8 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ PODS:
- hermes-engine (0.81.1):
- hermes-engine/Pre-built (= 0.81.1)
- hermes-engine/Pre-built (0.81.1)
- NitroIap (14.4.41):
- NitroIap (14.4.46):
- boost
- DoubleConversion
- fast_float
- fmt
- glog
- hermes-engine
- NitroModules
- openiap (~> 1.2.36)
- openiap (~> 1.2.41)
- RCT-Folly
- RCT-Folly/Fabric
- RCTRequired
Expand Down Expand Up @@ -68,7 +68,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- openiap (1.2.36)
- openiap (1.2.41)
- RCT-Folly (2024.11.18.00):
- boost
- DoubleConversion
Expand Down Expand Up @@ -2501,7 +2501,7 @@ DEPENDENCIES:
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- NitroIap (from `../..`)
- NitroModules (from `../node_modules/react-native-nitro-modules`)
- openiap (from `https://github.com/hyodotdev/openiap.git`, tag `apple-v1.2.36`)
- openiap (from `https://github.com/hyodotdev/openiap.git`, tag `apple-v1.2.41`)
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
- RCTRequired (from `../node_modules/react-native/Libraries/Required`)
Expand Down Expand Up @@ -2598,7 +2598,7 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-nitro-modules"
openiap:
:git: https://github.com/hyodotdev/openiap.git
:tag: apple-v1.2.36
:tag: apple-v1.2.41
RCT-Folly:
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
RCTDeprecation:
Expand Down Expand Up @@ -2737,7 +2737,7 @@ EXTERNAL SOURCES:
CHECKOUT OPTIONS:
openiap:
:git: https://github.com/hyodotdev/openiap.git
:tag: apple-v1.2.36
:tag: apple-v1.2.41

SPEC CHECKSUMS:
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
Expand All @@ -2747,9 +2747,9 @@ SPEC CHECKSUMS:
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: 4f8246b1f6d79f625e0d99472d1f3a71da4d28ca
NitroIap: bddc337e61a1e171f4ee5fe818fb2a95827c31a5
NitroIap: 7df9a60b52c7af7e9360c6e2822c968bba4aff83
NitroModules: 0ba3a58906a86566ea83abc016f8692374c19761
openiap: 16d75978e3610d8e9f334ed0c690dbf484ecefdc
openiap: d7041da00adbd23638590a393106f63556e49501
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077
RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a
Expand Down
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@react-navigation/native-stack": "^7.3.25",
"react": "19.1.0",
"react-native": "0.81.1",
"react-native-dotenv": "^3.4.11",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"react-native-nitro-modules": "^0.31.1",
"react-native-safe-area-context": "^5.6.1",
"react-native-screens": "^4.15.4"
Expand Down
Loading