React Native IAP - A high-performance in-app purchase library using Nitro Modules
- React Native
- Nitro Modules (Native bridge)
- TypeScript
- Swift (iOS with StoreKit 2)
- Kotlin (Android with Google Play Billing)
- Yarn 3 (Package manager with workspace support)
- Workspace Structure: Only
exampleis in the yarn workspace.example-expois an independent project - Install dependencies:
yarn install(installs for library and example only) - Add packages to library:
yarn add [package] - Add packages to example:
yarn workspace rn-iap-example add [package] - Add packages to example-expo:
cd example-expo && bun add [package](independent) - Run scripts:
yarn [script] - Execute packages:
yarn dlx [package]ornpx [package]
src/
├── index.tsx # Main exports and API
├── specs/
│ └── RnIap.nitro.ts # Nitro interface definitions (native bridge)
└── types/
└── *.ts # TypeScript type definitions
ios/
├── HybridIAP.swift # iOS native implementation (StoreKit 2)
└── *.swift # Other iOS implementation files
android/
└── src/main/java/com/margelo/nitro/iap/
└── *.kt # Android native implementation (Play Billing)
nitrogen/
└── generated/ # Auto-generated Nitro bridge files (DO NOT EDIT)
example/ # React Native example app (workspace)
├── ios/
├── android/
└── package.json
example-expo/ # Independent Expo example app (NOT in workspace)
├── app/
├── scripts/
└── package.json # Uses bun, independent from yarn workspacesrc/types.tsis generated; never edit manually. Update the underlying schema/spec and rerun the generators instead.- When declaring API params/results in JS/TS modules, import the canonical types from
src/types.tsrather than creating ad-hoc interfaces.
# Generate Nitro bridge files (nitrogen)
yarn specs
# TypeScript type checking
yarn typecheck
# Linting
yarn lint
yarn lint --fix
# Clean build artifacts
yarn clean# No need to navigate to example directory or install separately
# Dependencies are managed via workspaces
# iOS
cd example && yarn ios
cd example && yarn ios --device # For physical device
# Android
cd example && yarn android
# Start Metro bundler
cd example && yarn start
# Or from root:
yarn workspace rn-iap-example ios
yarn workspace rn-iap-example android
yarn workspace rn-iap-example start# Independent project - requires separate setup
# Uses bun and expo setup script
# Initial setup (copies lib files and builds)
cd example-expo && bun setup
# iOS
cd example-expo && bun ios
cd example-expo && bun ios --device # For physical device
# Android
cd example-expo && bun android
# Start Metro bundler
cd example-expo && bun start# For example (workspace)
cd example/ios
bundle install # Install Ruby dependencies
bundle exec pod install # Install iOS dependencies
# For example-expo (independent)
cd example-expo/ios
pod install # iOS dependenciesThe project includes VSCode launch configurations for easy development:
- Nitrogen + iOS Simulator: Generates Nitro files and runs on iOS simulator
- Nitrogen + iOS Device: Generates Nitro files and runs on physical iOS device
- Nitrogen + Android: Generates Nitro files and runs on Android
Access these from the Run and Debug panel (⌘⇧D) in VSCode.
-
src/specs/RnIap.nitro.ts - Native Bridge Interface
- Contains the Nitro interface definition that bridges to native code
- Includes ALL native method declarations
- This is the contract between TypeScript and native implementations
-
nitrogen/ - Auto-generated Files
- Generated by
bun run specs - Contains C++ bridge code and type definitions
- DO NOT EDIT - These files are auto-generated
- Generated by
-
Native Implementation
- iOS:
ios/HybridIAP.swift- StoreKit 2 implementation - Android:
android/.../Iap.kt- Play Billing implementation with auto-reconnection - Connection Management: Uses Google Play Billing v8.0.0+ automatic service reconnection
- iOS:
This repository follows the Angular Conventional Commits format:
<type>(<scope>): <subject>typeis one of:feat,fix,docs,style,refactor,perf,test,chore.scopeis optional but encouraged (e.g.,hooks,android).subjectis imperative, lowercase, ≤ ~50 chars, and has no trailing period.- Wrap commit bodies at ~72 columns; include
BREAKING CHANGE:/Closes #123footers when needed. - Examples:
feat(auth): add refresh token support,chore(ci): bump workflows.
- ESLint configuration for TypeScript
- Prettier for code formatting
- TypeScript with strict mode
- Use type-only imports when importing types (
import type)
When organizing native implementation classes (Swift/Kotlin), follow this strict ordering:
- Properties and Initialization - Class properties, init, deinit
- Public Cross-platform Methods - Methods without platform suffixes (e.g.,
initConnection,fetchProducts) - Platform-specific Public Methods - Methods with IOS/Android suffixes (e.g.,
getStorefrontIOS,consumePurchaseAndroid) - Event Listener Methods - Methods for managing event listeners
- Private Helper Methods - All private implementation details
Example structure:
class HybridRnIap {
// MARK: - Properties
private var isInitialized: Bool = false
// MARK: - Initialization
override init() { }
// MARK: - Public Methods (Cross-platform)
func initConnection() { }
func fetchProducts() { }
// MARK: - iOS-specific Public Methods
func getStorefrontIOS() { }
func clearTransactionIOS() { }
// MARK: - Event Listener Methods
func addPurchaseUpdatedListener() { }
// MARK: - Private Helper Methods
private func ensureConnection() { }
private func convertToNitroProduct() { }
}IMPORTANT: Always run these checks locally before committing to avoid CI failures:
# Install dependencies
yarn install
# TypeScript Check
yarn typecheck
# Linting
yarn lint --fix
# Generate Nitro files if specs changed
yarn specs
# Run all checks in sequence
yarn install && yarn typecheck && yarn lint --fix- Inside the
useIAPhook, most methods returnPromise<void>and update internal state. Do not design examples that expect returned data from these methods.- Examples:
fetchProducts,requestPurchase,getAvailablePurchases. - After calling, consume state from the hook:
products,subscriptions,availablePurchases, etc. - For
requestPurchase: UseonPurchaseSuccesscallback to receive purchase results, NOT the return value.
- Examples:
- Defined exceptions in the hook that DO return values:
getActiveSubscriptions(subscriptionIds?) => Promise<ActiveSubscription[]>(also updatesactiveSubscriptionsstate)hasActiveSubscriptions(subscriptionIds?) => Promise<boolean>
- The root (index) API is value-returning and can be awaited to receive data directly. Use root API when not using React state.
- Example:
const result = await requestPurchase({...})returnsPromise<RequestPurchaseResult | null>(though native returns empty array by design - actual results come through event listeners).
- Example:
- TypeScript errors: Ensure all types are properly imported
- Linting errors: Use
bun run lint --fixto auto-fix formatting issues - Nitro generation: Run
bun run specsafter modifying.nitro.tsfiles
- Subscription management
- Promotional offers
- Family sharing
- Refund requests
- Transaction verification
- Receipt validation
- Multiple SKU purchases
- Subscription offers
- Obfuscated account/profile IDs
- Purchase acknowledgment
- Product consumption
- Automatic Service Reconnection - Uses
enableAutoServiceReconnection()to handle disconnections automatically - Simplified Connection Handling - Only checks if billing client exists (not connection state), auto-reconnection handles the rest
The project uses a centralized error handling approach across all platforms:
TypeScript (src/utils/error.ts + src/types.ts)
parseErrorStringToJsonObj()- Internal utility - Parses native error strings (used by library internals)normalizeErrorCodeFromNative()- Internal utility - Converts native error codes to ErrorCode enumisUserCancelledError()- Public helper - Check for user cancellationgetUserFriendlyErrorMessage()- Public helper - Get user-friendly error messagesErrorCodeenum (from types.ts) - Standardized error codes across platforms
Both platforms use the OpenIAP library's error handling:
- OpenIAPError - Unified error types across Android and iOS
OpenIAPError.toCode()- Converts error to kebab-case string codeOpenIAPError.defaultMessage()- Gets default error messages- Error codes defined in OpenIAP package:
dev.hyo.openiap.OpenIapError(Android) and OpenIAP framework (iOS)
Native Error Codes:
Both Android and iOS now use OpenIAP's unified error codes (kebab-case format):
- Examples:
user-cancelled,item-unavailable,network-error,developer-error - See OpenIAP Error Handling for complete list
TypeScript Error Format:
All errors in TypeScript are automatically normalized to ErrorCode enum values:
interface PurchaseError {
code: ErrorCode; // Normalized enum value
message: string;
productId?: string;
responseCode?: number;
debugMessage?: string;
}✅ The useIAP hook automatically normalizes all errors - this is the recommended approach:
import {
useIAP,
ErrorCode,
isUserCancelledError,
getUserFriendlyErrorMessage,
} from 'react-native-iap';
const {requestPurchase} = useIAP({
onPurchaseError: (error) => {
// error is already a PurchaseError object with normalized ErrorCode
// No need to call parseErrorStringToJsonObj()!
// Option 1: Direct error code comparison
if (error.code === ErrorCode.UserCancelled) {
console.log('User cancelled purchase');
return;
}
// Option 2: Use helper function
if (isUserCancelledError(error)) {
return;
}
// Option 3: Get user-friendly message
const friendlyMessage = getUserFriendlyErrorMessage(error);
Alert.alert('Purchase Failed', friendlyMessage);
// Option 4: Handle specific errors
switch (error.code) {
case ErrorCode.NetworkError:
showRetryDialog();
break;
case ErrorCode.ItemUnavailable:
showProductUnavailableMessage();
break;
default:
console.error('Purchase failed:', error.message);
}
},
});useIAP hook.
When using root API methods with event listeners, errors come through purchaseErrorListener:
import {
purchaseErrorListener,
isUserCancelledError,
getUserFriendlyErrorMessage,
ErrorCode,
} from 'react-native-iap';
// Errors from purchaseErrorListener are already normalized
const errorSubscription = purchaseErrorListener((error) => {
// error is already a PurchaseError object
// No need to parse!
if (isUserCancelledError(error)) {
return;
}
const friendlyMessage = getUserFriendlyErrorMessage(error);
console.error('Purchase failed:', friendlyMessage);
});
// Remember to clean up
errorSubscription.remove();Note: parseErrorStringToJsonObj() is an internal utility used by the library to convert native error strings. As an end-user, you should never need to call it directly - errors are already parsed for you.
All error codes are defined in the ErrorCode enum:
ErrorCode.UserCancelled- User cancelled the operationErrorCode.ItemUnavailable- Product not available in storeErrorCode.NetworkError- Network connection issuesErrorCode.ServiceError- Platform service issuesErrorCode.DeveloperError- Invalid API usageErrorCode.NotPrepared- Service not initializedErrorCode.Unknown- Unexpected errorErrorCode.SkuOfferMismatch- Android subscription offer mismatch
- Always use
yarnfor package management (project uses Yarn 3 with workspaces) - Run
yarn typecheckandyarn lintbefore committing - Regenerate Nitro files with
yarn specsafter modifying interfaces - Use Platform.OS checks for platform-specific code
- Error Handling: Use
useIAPhook - errors are automatically normalized toErrorCodeenum - Helper Functions: Use
isUserCancelledError()andgetUserFriendlyErrorMessage()for common error checks - Avoid Direct Parsing: Never call
parseErrorStringToJsonObj()in user code - it's handled internally - Test error scenarios on both platforms
-
Build failures after modifying .nitro.ts files
- Run
yarn specsto regenerate Nitro bridge files
- Run
-
React Duplication Instance Issues
⚠️ - Problem: "Cannot read properties of null" or "useState of null" errors
- Cause: Multiple React instances loaded due to workspace setup
- Solution: Metro resolver alias configuration already applied in
example/metro.config.js
// example/metro.config.js uses modern alias approach: resolver: { alias: { 'react-native': path.resolve(__dirname, 'node_modules/react-native'), 'react': path.resolve(__dirname, 'node_modules/react'), 'react-native-iap': path.resolve(__dirname, '..'), } }
- Additional Notes:
- Resolutions are configured in root
package.json(workspace level) example-expois NOT in yarn workspace (independent project)- Only
exampleis included in workspace structure
- Resolutions are configured in root
-
iOS build errors
cd example/ios bundle exec pod install
-
Package installation issues
- Run
yarn installfrom the root directory - Clear yarn cache:
yarn cache clean - Delete node_modules and reinstall:
rm -rf node_modules example/node_modules && yarn install
- Run
-
Metro bundler issues
cd example yarn start --reset-cache