Skip to content
This repository was archived by the owner on Apr 26, 2026. It is now read-only.

Commit b36d26e

Browse files
authored
feat: add verifyPurchaseWithProvider for IAPKit verification (#3100)
- Add verifyPurchaseWithProvider function to verify purchases with external providers (IAPKit) - Add hasActiveSubscriptions helper function - Update types: rename ReceiptValidation* to VerifyPurchase* for consistency with OpenIAP - Add verification method selection UI to example screens (PurchaseFlow, SubscriptionFlow) - Configure react-native-dotenv for IAPKIT_API_KEY environment variable - Fix iOS: use stringValue instead of rawValue for Nitro enum serialization - Fix Android: convert Nitro enum to lowercase string for OpenIAP compatibility - Add enum mapping helper functions for Android (mapIapkitPurchaseState, mapIapkitStore, mapPurchaseVerificationProvider) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added provider-based purchase verification and a verifyPurchase API. * Added hasActiveSubscriptions check. * Example app: verification method selector (ignore/local/IAPKit) and hook support. * Exposed verification methods in the public IAP hook and types. * **Documentation** * Added example environment config and usage notes for IAPKit API key. * **Chores** * Updated build tooling for environment loading and bumped native dependency versions. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 709e60c commit b36d26e

22 files changed

Lines changed: 1440 additions & 130 deletions

File tree

NitroIap.podspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ Pod::Spec.new do |s|
4242
s.dependency 'React-jsi'
4343
s.dependency 'React-callinvoker'
4444
# OpenIAP Apple for StoreKit 2 integration
45-
# Use ~> to allow patch updates (e.g., 1.2.30 -> 1.2.31, but not 1.3.0)
46-
s.dependency 'openiap', "~> #{apple_version}"
45+
# Exact version match for consistent builds
46+
s.dependency 'openiap', "#{apple_version}"
4747

4848
install_modules_dependencies(s)
4949
end

android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,34 @@ class HybridRnIap : HybridRnIapSpec() {
549549
}
550550
}
551551
}
552-
552+
553+
override fun hasActiveSubscriptions(subscriptionIds: Array<String>?): Promise<Boolean> {
554+
return Promise.async {
555+
initConnection(null).await()
556+
557+
RnIapLog.payload(
558+
"hasActiveSubscriptions",
559+
mapOf("subscriptionIds" to (subscriptionIds?.toList() ?: "all"))
560+
)
561+
562+
try {
563+
val hasActive = openIap.hasActiveSubscriptions(subscriptionIds?.toList())
564+
RnIapLog.result("hasActiveSubscriptions", hasActive)
565+
hasActive
566+
} catch (e: Exception) {
567+
RnIapLog.failure("hasActiveSubscriptions", e)
568+
val error = OpenIAPError.ServiceUnavailable
569+
throw OpenIapException(
570+
toErrorJson(
571+
error = error,
572+
debugMessage = e.message,
573+
messageOverride = "Failed to check active subscriptions: ${e.message}"
574+
)
575+
)
576+
}
577+
}
578+
}
579+
553580
// Transaction management methods (Unified)
554581
override fun finishTransaction(params: NitroFinishTransactionParams): Promise<Variant_Boolean_NitroPurchaseResult> {
555582
return Promise.async {
@@ -1078,7 +1105,7 @@ class HybridRnIap : HybridRnIapSpec() {
10781105
)
10791106

10801107
Variant_NitroReceiptValidationResultIOS_NitroReceiptValidationResultAndroid.Second(result)
1081-
1108+
10821109
} catch (e: Exception) {
10831110
val debugMessage = e.message
10841111
val error = OpenIAPError.InvalidReceipt
@@ -1092,7 +1119,60 @@ class HybridRnIap : HybridRnIapSpec() {
10921119
}
10931120
}
10941121
}
1095-
1122+
1123+
override fun verifyPurchaseWithProvider(params: NitroVerifyPurchaseWithProviderProps): Promise<NitroVerifyPurchaseWithProviderResult> {
1124+
return Promise.async {
1125+
try {
1126+
// Convert Nitro enum to string (e.g., IAPKIT -> "iapkit")
1127+
val providerString = params.provider.name.lowercase()
1128+
RnIapLog.payload("verifyPurchaseWithProvider", mapOf("provider" to providerString))
1129+
1130+
// Build the props map for OpenIAP - use string value for provider
1131+
val propsMap = mutableMapOf<String, Any?>("provider" to providerString)
1132+
params.iapkit?.let { iapkit ->
1133+
val iapkitMap = mutableMapOf<String, Any?>()
1134+
iapkit.apiKey?.let { iapkitMap["apiKey"] = it }
1135+
iapkit.google?.let { google ->
1136+
iapkitMap["google"] = mapOf("purchaseToken" to google.purchaseToken)
1137+
}
1138+
iapkit.apple?.let { apple ->
1139+
iapkitMap["apple"] = mapOf("jws" to apple.jws)
1140+
}
1141+
propsMap["iapkit"] = iapkitMap
1142+
}
1143+
1144+
val props = dev.hyo.openiap.VerifyPurchaseWithProviderProps.fromJson(propsMap)
1145+
val result = openIap.verifyPurchaseWithProvider(props)
1146+
1147+
RnIapLog.result("verifyPurchaseWithProvider", mapOf("provider" to result.provider, "iapkitCount" to result.iapkit.size))
1148+
1149+
// Convert result to Nitro types
1150+
val nitroIapkitResults = result.iapkit.map { item ->
1151+
NitroVerifyPurchaseWithIapkitResult(
1152+
isValid = item.isValid,
1153+
state = mapIapkitPurchaseState(item.state.name),
1154+
store = mapIapkitStore(item.store.name)
1155+
)
1156+
}.toTypedArray()
1157+
1158+
NitroVerifyPurchaseWithProviderResult(
1159+
iapkit = nitroIapkitResults,
1160+
provider = mapPurchaseVerificationProvider(result.provider.name)
1161+
)
1162+
} catch (e: Exception) {
1163+
RnIapLog.failure("verifyPurchaseWithProvider", e)
1164+
val error = OpenIAPError.VerificationFailed
1165+
throw OpenIapException(
1166+
toErrorJson(
1167+
error = error,
1168+
debugMessage = e.message,
1169+
messageOverride = "Verification failed: ${e.message ?: "unknown reason"}"
1170+
)
1171+
)
1172+
}
1173+
}
1174+
}
1175+
10961176
// iOS-specific methods - Not applicable on Android, return appropriate defaults
10971177
override fun subscriptionStatusIOS(sku: String): Promise<Array<NitroSubscriptionStatus>?> {
10981178
return Promise.async {
@@ -1324,6 +1404,36 @@ class HybridRnIap : HybridRnIapSpec() {
13241404
}
13251405
}
13261406

1407+
// Helper functions to map OpenIAP enum values to Nitro enum values
1408+
private fun mapIapkitPurchaseState(stateName: String): IapkitPurchaseState {
1409+
return when (stateName.uppercase()) {
1410+
"ENTITLED" -> IapkitPurchaseState.ENTITLED
1411+
"PENDING_ACKNOWLEDGMENT", "PENDING-ACKNOWLEDGMENT" -> IapkitPurchaseState.PENDING_ACKNOWLEDGMENT
1412+
"PENDING" -> IapkitPurchaseState.PENDING
1413+
"CANCELED" -> IapkitPurchaseState.CANCELED
1414+
"EXPIRED" -> IapkitPurchaseState.EXPIRED
1415+
"READY_TO_CONSUME", "READY-TO-CONSUME" -> IapkitPurchaseState.READY_TO_CONSUME
1416+
"CONSUMED" -> IapkitPurchaseState.CONSUMED
1417+
"INAUTHENTIC" -> IapkitPurchaseState.INAUTHENTIC
1418+
else -> IapkitPurchaseState.UNKNOWN
1419+
}
1420+
}
1421+
1422+
private fun mapIapkitStore(storeName: String): IapkitStore {
1423+
return when (storeName.uppercase()) {
1424+
"APPLE" -> IapkitStore.APPLE
1425+
"GOOGLE" -> IapkitStore.GOOGLE
1426+
else -> IapkitStore.GOOGLE // Default to Google on Android
1427+
}
1428+
}
1429+
1430+
private fun mapPurchaseVerificationProvider(providerName: String): PurchaseVerificationProvider {
1431+
return when (providerName.uppercase()) {
1432+
"IAPKIT" -> PurchaseVerificationProvider.IAPKIT
1433+
else -> PurchaseVerificationProvider.NONE
1434+
}
1435+
}
1436+
13271437
private fun toErrorResult(
13281438
error: OpenIAPError,
13291439
productId: String? = null,

docs/docs/api/methods/core-methods.md

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,16 @@ Note: react-native-iap aligns with the OpenIAP API surface. For canonical cross-
2020

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

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

3334
## initConnection()
3435

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

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

493+
## verifyPurchaseWithProvider()
494+
495+
Verifies purchases using external verification services like IAPKit. This provides additional validation and security beyond local device verification.
496+
497+
```tsx
498+
import {verifyPurchaseWithProvider} from 'react-native-iap';
499+
500+
const verifyWithIAPKit = async (purchase: Purchase) => {
501+
try {
502+
const result = await verifyPurchaseWithProvider({
503+
provider: 'iapkit',
504+
iapkit: {
505+
apiKey: 'your-iapkit-api-key',
506+
environment: 'production', // or 'sandbox'
507+
apple: {
508+
jws: purchase.purchaseToken, // iOS JWS token
509+
},
510+
google: {
511+
purchaseToken: purchase.purchaseToken, // Android purchase token
512+
packageName: 'com.your.app',
513+
productId: purchase.productId,
514+
},
515+
},
516+
});
517+
518+
for (const item of result.iapkit) {
519+
console.log('Is Valid:', item.isValid);
520+
console.log('State:', item.state); // 'entitled', 'expired', 'canceled', etc.
521+
console.log('Store:', item.store); // 'apple' or 'google'
522+
}
523+
524+
return result;
525+
} catch (error) {
526+
console.error('Verification failed:', error);
527+
}
528+
};
529+
```
530+
531+
**Parameters:**
532+
533+
- `params` (object):
534+
- `provider` ('iapkit'): The verification provider to use
535+
- `iapkit?` (object): IAPKit-specific configuration
536+
- `apiKey` (string): Your IAPKit API key
537+
- `environment?` ('production' | 'sandbox'): Environment for verification
538+
- `apple?` (object): iOS verification data
539+
- `jws` (string): The JWS token from the purchase
540+
- `google?` (object): Android verification data
541+
- `purchaseToken` (string): The purchase token
542+
- `packageName` (string): Your app's package name
543+
- `productId` (string): The product ID
544+
545+
**Returns:** `Promise<VerifyPurchaseWithProviderResult>`
546+
547+
```typescript
548+
interface VerifyPurchaseWithProviderResult {
549+
provider: 'iapkit';
550+
iapkit: Array<{
551+
isValid: boolean;
552+
state: IapkitPurchaseState;
553+
store: 'apple' | 'google';
554+
}>;
555+
}
556+
557+
type IapkitPurchaseState =
558+
| 'pending'
559+
| 'unknown'
560+
| 'entitled'
561+
| 'pending-acknowledgment'
562+
| 'canceled'
563+
| 'expired'
564+
| 'ready-to-consume'
565+
| 'consumed'
566+
| 'inauthentic';
567+
```
568+
569+
**Platform Behavior:**
570+
571+
- **iOS**: Sends the JWS (JSON Web Signature) token to IAPKit for server-side verification
572+
- **Android**: Sends the purchase token along with package name and product ID for verification
573+
- **Both**: Returns the verification state and validity from IAPKit's servers
574+
575+
**Use Cases:**
576+
577+
- Server-side receipt validation without maintaining your own validation infrastructure
578+
- Cross-platform purchase verification with a unified API
579+
- Enhanced security through external verification services
580+
581+
> **Note:** You need an IAPKit API key to use this feature. Visit [iapkit.com](https://iapkit.com) to get started.
582+
492583
## Purchase Interface
493584

494585
```tsx

example/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# IAPKit Configuration
2+
# Get your API key from https://iapkit.com
3+
IAPKIT_API_KEY=your_iapkit_api_key_here

example/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,8 @@ yarn-error.log
7373
!.yarn/releases
7474
!.yarn/sdks
7575
!.yarn/versions
76+
77+
# Environment variables
78+
.env
79+
.env.local
80+
.env*.local

example/babel.config.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
11
module.exports = {
22
presets: ['module:@react-native/babel-preset'],
3+
plugins: [
4+
[
5+
'module:react-native-dotenv',
6+
{
7+
moduleName: '@env',
8+
path: '.env',
9+
blocklist: null,
10+
allowlist: null,
11+
safe: false,
12+
allowUndefined: true,
13+
},
14+
],
15+
],
316
};

example/ios/Podfile.lock

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ PODS:
88
- hermes-engine (0.81.1):
99
- hermes-engine/Pre-built (= 0.81.1)
1010
- hermes-engine/Pre-built (0.81.1)
11-
- NitroIap (14.4.41):
11+
- NitroIap (14.4.46):
1212
- boost
1313
- DoubleConversion
1414
- fast_float
1515
- fmt
1616
- glog
1717
- hermes-engine
1818
- NitroModules
19-
- openiap (~> 1.2.36)
19+
- openiap (~> 1.2.41)
2020
- RCT-Folly
2121
- RCT-Folly/Fabric
2222
- RCTRequired
@@ -68,7 +68,7 @@ PODS:
6868
- ReactCommon/turbomodule/core
6969
- SocketRocket
7070
- Yoga
71-
- openiap (1.2.36)
71+
- openiap (1.2.41)
7272
- RCT-Folly (2024.11.18.00):
7373
- boost
7474
- DoubleConversion
@@ -2501,7 +2501,7 @@ DEPENDENCIES:
25012501
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
25022502
- NitroIap (from `../..`)
25032503
- NitroModules (from `../node_modules/react-native-nitro-modules`)
2504-
- openiap (from `https://github.com/hyodotdev/openiap.git`, tag `apple-v1.2.36`)
2504+
- openiap (from `https://github.com/hyodotdev/openiap.git`, tag `apple-v1.2.41`)
25052505
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
25062506
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
25072507
- RCTRequired (from `../node_modules/react-native/Libraries/Required`)
@@ -2598,7 +2598,7 @@ EXTERNAL SOURCES:
25982598
:path: "../node_modules/react-native-nitro-modules"
25992599
openiap:
26002600
:git: https://github.com/hyodotdev/openiap.git
2601-
:tag: apple-v1.2.36
2601+
:tag: apple-v1.2.41
26022602
RCT-Folly:
26032603
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
26042604
RCTDeprecation:
@@ -2737,7 +2737,7 @@ EXTERNAL SOURCES:
27372737
CHECKOUT OPTIONS:
27382738
openiap:
27392739
:git: https://github.com/hyodotdev/openiap.git
2740-
:tag: apple-v1.2.36
2740+
:tag: apple-v1.2.41
27412741

27422742
SPEC CHECKSUMS:
27432743
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
@@ -2747,9 +2747,9 @@ SPEC CHECKSUMS:
27472747
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
27482748
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
27492749
hermes-engine: 4f8246b1f6d79f625e0d99472d1f3a71da4d28ca
2750-
NitroIap: bddc337e61a1e171f4ee5fe818fb2a95827c31a5
2750+
NitroIap: 7df9a60b52c7af7e9360c6e2822c968bba4aff83
27512751
NitroModules: 0ba3a58906a86566ea83abc016f8692374c19761
2752-
openiap: 16d75978e3610d8e9f334ed0c690dbf484ecefdc
2752+
openiap: d7041da00adbd23638590a393106f63556e49501
27532753
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
27542754
RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077
27552755
RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a

example/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@react-navigation/native-stack": "^7.3.25",
1818
"react": "19.1.0",
1919
"react-native": "0.81.1",
20+
"react-native-dotenv": "^3.4.11",
2021
"react-native-nitro-modules": "^0.31.1",
2122
"react-native-safe-area-context": "^5.6.1",
2223
"react-native-screens": "^4.15.4"

0 commit comments

Comments
 (0)