expo-iap

repository·main·Indexed 43 days ago

https://github.com/hyochan/expo-iap

An in-app purchase module for Expo and React Native applications that follows the OpenIAP specification. It provides a unified, type-safe API for handling one-time purchases and recurring subscriptions across iOS and Android, featuring a TypeScript-first workflow and the useIAP hook.

Tokens
119.2K
Snippets
321
Records
430
Agent score
55%

What's inside expo-iap

  1. Understand the Example Implementations

    main

    The expo-iap-example repository is organized into two primary implementation patterns:

    🛒 Purchase Flow (/purchase-flow)

    Focuses on one-time in-app products. It demonstrates:

    • TypeScript-first approach for product purchases.
    • Platform-agnostic property access using type guards.
    • Clean error handling with proper types.

    🔄 Subscription Flow (/subscription-flow)

    Focuses on recurring subscription products. It demonstrates:

    • Subscription-specific pricing and period display.
    • Auto-renewal state management.
    • Platform-agnostic subscription handling.
  2. What is OpenIAP and how does it power expo-iap?

    main

    Expo IAP conforms to the OpenIAP specification, an open, vendor-neutral interoperability standard for in-app purchases. This specification ensures consistency across different platforms and libraries.

    Key components of the OpenIAP standard include:

    • Shared specification: Common types, error codes, and purchase flows.
    • Generated type-safe bindings: Swift, Kotlin, Dart, and GDScript derived from a single GraphQL schema.
    • Platform implementations: openiap-apple (StoreKit 2) and openiap-google (Play Billing 8.x).
    • Verification profiles: Standardized patterns for receipt validation and purchase verification.
  3. Use the Billing Programs API for Android (8.2.0+)

    main

    The Billing Programs API is the modern way to handle external billing programs in Google Play Billing Library 8.2.0+. It provides methods to check availability, launch external purchase flows, and retrieve reporting tokens for transactions made outside of Google Play.

    Important Requirements:

    • The external transaction token must be reported to the Google Play backend within 24 hours.
    • Requires server-side integration with the Google Play Developer API.
    • Failure to report tokens can result in refunds and account suspension.
    import {
      isBillingProgramAvailableAndroid,
      launchExternalLinkAndroid,
      createBillingProgramReportingDetailsAndroid,
    } from 'expo-iap';
    
    async function purchaseWithBillingProgram(productId: string) {
      // Step 1: Check availability
      const availability = await isBillingProgramAvailableAndroid('external-offer');
      if (!availability.isAvailable) {
        throw new Error('External offer program not available');
      }
    
      // Step 2: Launch external link
      await launchExternalLinkAndroid({
        billingProgram: 'external-offer',
        launchMode: 'launch-in-external-browser-or-app',
        linkType: 'link-to-digital-content-offer',
        linkUri: `https://your-payment-site.com/purchase/${productId}`,
      });
    
      // Step 3: After payment completes, get reporting token
      const details = await createBillingProgramReportingDetailsAndroid('external-offer');
    
      // Step 4: Report to Google (must be done within 24 hours)
      await reportToGooglePlayBackend(details.externalTransactionToken, productId);
    
      return {success: true, token: details.externalTransactionToken};
    }
  4. Leverage automatic TypeScript type inference

    main

    The expo-iap library is designed for a TypeScript-first workflow. Instead of manually casting results to platform-specific types (like ProductPurchaseIos), you can rely on automatic type inference by providing the correct configuration object to the API calls.

    When you pass the type and request parameters, TypeScript automatically determines the correct return type for the result.

    // ✅ NEW WAY - Automatic type inference
    const result = await requestPurchase({
      request: { sku: 'product.id' },
      type: 'in-app'
    });
    // TypeScript automatically knows the correct return type!
  5. Handle optional subscription fields in ProductIOS

    main

    The subscription field in ProductIOS is now optional because not all iOS products are subscriptions. When iterating through products, you must check for the existence of the subscription field before accessing its properties.

    // Check if product is a subscription
    if (product.subscription) {
      // Handle subscription-specific logic
      console.log('Period:', product.subscription.subscriptionPeriod);
    }
  6. Understand the event-driven nature of purchase requests

    main

    All request-based APIs (like requestPurchase) are event-driven, not promise-based.

    requestPurchase() does not resolve with a result. Instead, it triggers the native flow, and you must handle the outcome via:

    • onPurchaseSuccess / onPurchaseError (when using the useIAP hook)
    • purchaseUpdatedListener / purchaseErrorListener (for manual listeners)

    Why this design? It ensures robustness when the store delivers updates after app restarts or during edge timing cases (like StoreKit 2 event reordering). Avoid relying on await requestPurchase(...) for the final outcome.

  7. Name platform-specific functions with suffixes

    main

    Functions that only work on a single platform must include a platform suffix to avoid runtime errors on other platforms:

    • iOS-specific: Append IOS (uppercase) to the function name (e.g., validateReceiptIOS()).
    • Android-specific: Append Android to the function name (e.g., deepLinkToSubscriptionsAndroid()).

    Functions that handle platform differences internally (using Platform.select or the v2.7.0+ unified API) do not require suffixes.

    Type Naming

    • Platform-specific types: ProductIOS, ProductAndroid, PurchaseErrorIOS, PurchaseErrorAndroid.
    • Cross-platform types: Product, Purchase, PurchaseError.
    // ✅ Correct: Platform-Specific Functions
    export const validateReceiptIOS = async (
      props: ReceiptValidationProps,
    ): Promise<boolean> => {
      return ExpoIapModule.validateReceiptIOS(props.sku);
    };
    
    // ✅ Correct: Cross-Platform Functions
    export const fetchProducts = async (params: { skus: string[]; type?: 'in-app' | 'subs'; }): Promise<Product[]> => {
      return (
        Platform.select({
          ios: async () => { /* iOS implementation */ },
          android: async () => { /* Android implementation */ },
        })() || []
      );
    };
  8. Compare iOS and Android subscription behaviors

    main

    When implementing subscription logic, be aware of the following platform differences:

    FeatureiOSAndroid
    Subscription ChangeAutomatic (within same group)Manual with purchaseToken
    ParametersJust new skupurchaseTokenAndroid + replacementModeAndroid
    TimingOS-determinedSpecified via replacementModeAndroid
    Plan ChangesUse subscription groups with ranksUse base plans and offers
    Status CheckCheck expirationDateIOSCheck autoRenewingAndroid
    Cancellation DetectionUser manages in SettingsCheck autoRenewingAndroid === false
    ProrationHandled by App StoreConfigurable via replacementModeAndroid
  9. Requirements and limitations for iOS Alternative Billing

    main

    When implementing iOS external purchase links, adhere to the following constraints:

    • Minimum OS: iOS 16.0 or later.
    • URL Constraints: Must be HTTPS, no query parameters, max 1,000 characters.
    • Link Limits:
      • Music streaming apps: Up to 5 links per country (specifically for EU + Iceland, Norway).
      • Other apps: 1 link per country.
    • Validation: Since the app does not receive a purchase update via the standard IAP flow, you must validate purchases on your own backend server.
  10. How Onside detection works

    main

    On iOS 17.4+, the library uses MarketplaceKit to detect the app's distributor.

    • If installed from Onside: The library uses the Onside native module for all IAP calls.
    • If installed from Apple App Store: The library falls back to the standard Apple StoreKit module.

    Testing Note: To test Onside functionality, you must use a real iPhone with the Onside Store app installed.