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

Latest commit

 

History

History
483 lines (356 loc) · 14.9 KB

File metadata and controls

483 lines (356 loc) · 14.9 KB

CLAUDE.md

Project Overview

React Native IAP - A high-performance in-app purchase library using Nitro Modules

Key Technologies

  • 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)

Package Manager

⚠️ IMPORTANT: This project uses Yarn 3 with workspaces

  • Workspace Structure: Only example is in the yarn workspace. example-expo is 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] or npx [package]

Project Structure

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 workspace

Auto-generated Files

  • src/types.ts is 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.ts rather than creating ad-hoc interfaces.

Development Commands

Building and Code Generation

# Generate Nitro bridge files (nitrogen)
yarn specs

# TypeScript type checking
yarn typecheck

# Linting
yarn lint
yarn lint --fix

# Clean build artifacts
yarn clean

Example App (React Native - Workspace)

# 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

Example-Expo (Independent Project)

# 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

iOS Setup

# 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 dependencies

VSCode Integration

The project includes VSCode launch configurations for easy development:

  1. Nitrogen + iOS Simulator: Generates Nitro files and runs on iOS simulator
  2. Nitrogen + iOS Device: Generates Nitro files and runs on physical iOS device
  3. Nitrogen + Android: Generates Nitro files and runs on Android

Access these from the Run and Debug panel (⌘⇧D) in VSCode.

Architecture Guidelines

Module Organization

  1. 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
  2. nitrogen/ - Auto-generated Files

    • Generated by bun run specs
    • Contains C++ bridge code and type definitions
    • DO NOT EDIT - These files are auto-generated
  3. 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

Coding Standards

Commit Message Convention

This repository follows the Angular Conventional Commits format:

<type>(<scope>): <subject>
  • type is one of: feat, fix, docs, style, refactor, perf, test, chore.
  • scope is optional but encouraged (e.g., hooks, android).
  • subject is imperative, lowercase, ≤ ~50 chars, and has no trailing period.
  • Wrap commit bodies at ~72 columns; include BREAKING CHANGE: / Closes #123 footers when needed.
  • Examples: feat(auth): add refresh token support, chore(ci): bump workflows.

Code Style

  • ESLint configuration for TypeScript
  • Prettier for code formatting
  • TypeScript with strict mode
  • Use type-only imports when importing types (import type)

Native Class Function Ordering Convention

When organizing native implementation classes (Swift/Kotlin), follow this strict ordering:

  1. Properties and Initialization - Class properties, init, deinit
  2. Public Cross-platform Methods - Methods without platform suffixes (e.g., initConnection, fetchProducts)
  3. Platform-specific Public Methods - Methods with IOS/Android suffixes (e.g., getStorefrontIOS, consumePurchaseAndroid)
  4. Event Listener Methods - Methods for managing event listeners
  5. 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() { }
}

CI Checks (Run Before Committing)

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

Hook API Semantics (useIAP)

  • Inside the useIAP hook, most methods return Promise<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: Use onPurchaseSuccess callback to receive purchase results, NOT the return value.
  • Defined exceptions in the hook that DO return values:
    • getActiveSubscriptions(subscriptionIds?) => Promise<ActiveSubscription[]> (also updates activeSubscriptions state)
    • 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({...}) returns Promise<RequestPurchaseResult | null> (though native returns empty array by design - actual results come through event listeners).

Common CI Fixes

  • TypeScript errors: Ensure all types are properly imported
  • Linting errors: Use bun run lint --fix to auto-fix formatting issues
  • Nitro generation: Run bun run specs after modifying .nitro.ts files

Platform-Specific Features

iOS (StoreKit 2)

  • Subscription management
  • Promotional offers
  • Family sharing
  • Refund requests
  • Transaction verification
  • Receipt validation

Android (Play Billing)

  • 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

Error Handling

Centralized Error Management

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 enum
  • isUserCancelledError() - Public helper - Check for user cancellation
  • getUserFriendlyErrorMessage() - Public helper - Get user-friendly error messages
  • ErrorCode enum (from types.ts) - Standardized error codes across platforms

Android & iOS (OpenIAP)

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 code
  • OpenIAPError.defaultMessage() - Gets default error messages
  • Error codes defined in OpenIAP package: dev.hyo.openiap.OpenIapError (Android) and OpenIAP framework (iOS)

Error Format

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;
}

Recommended Usage Pattern (useIAP Hook)

✅ 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);
    }
  },
});

Advanced: Root API Usage (Not Recommended for Most Users)

⚠️ Only use root API methods directly if you have specific advanced needs. Most developers should use 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.

Common Error Codes

All error codes are defined in the ErrorCode enum:

  • ErrorCode.UserCancelled - User cancelled the operation
  • ErrorCode.ItemUnavailable - Product not available in store
  • ErrorCode.NetworkError - Network connection issues
  • ErrorCode.ServiceError - Platform service issues
  • ErrorCode.DeveloperError - Invalid API usage
  • ErrorCode.NotPrepared - Service not initialized
  • ErrorCode.Unknown - Unexpected error
  • ErrorCode.SkuOfferMismatch - Android subscription offer mismatch

Development Guidelines

  • Always use yarn for package management (project uses Yarn 3 with workspaces)
  • Run yarn typecheck and yarn lint before committing
  • Regenerate Nitro files with yarn specs after modifying interfaces
  • Use Platform.OS checks for platform-specific code
  • Error Handling: Use useIAP hook - errors are automatically normalized to ErrorCode enum
  • Helper Functions: Use isUserCancelledError() and getUserFriendlyErrorMessage() for common error checks
  • Avoid Direct Parsing: Never call parseErrorStringToJsonObj() in user code - it's handled internally
  • Test error scenarios on both platforms

Troubleshooting

Common Issues

  1. Build failures after modifying .nitro.ts files

    • Run yarn specs to regenerate Nitro bridge files
  2. 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-expo is NOT in yarn workspace (independent project)
      • Only example is included in workspace structure
  3. iOS build errors

    cd example/ios
    bundle exec pod install
  4. Package installation issues

    • Run yarn install from the root directory
    • Clear yarn cache: yarn cache clean
    • Delete node_modules and reinstall: rm -rf node_modules example/node_modules && yarn install
  5. Metro bundler issues

    cd example
    yarn start --reset-cache