From 58b7bca3f936d47b140b6830b463d84c88f949bb Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 9 Dec 2025 05:09:55 +0900 Subject: [PATCH 1/3] feat: migrate to openiap 1.3.0 with IapStore and store field - Update openiap versions: apple 1.3.0, google 1.3.10, gql 1.3.0 - Add IapStore type ('unknown' | 'apple' | 'google' | 'horizon') - Add store field to Purchase (platform field deprecated) - Add apple/google fields to RequestPurchasePropsByPlatforms (ios/android deprecated) - Change verifyPurchaseWithProvider iapkit from array to object - Add errors field to VerifyPurchaseWithProviderResult - Create versioned docs for 14.4, update current to 14.5 - Update documentation with correct types and examples --- .../java/com/margelo/nitro/iap/HybridRnIap.kt | 29 +- docs/docs/api/methods/core-methods.md | 14 +- docs/docs/api/types.md | 16 +- docs/docs/examples/purchase-flow.md | 6 +- docs/docs/intro.md | 4 +- docs/docusaurus.config.ts | 6 +- .../version-14.4/api/error-codes.md | 215 +++ .../version-14.4/api/error-handling.md | 82 ++ docs/versioned_docs/version-14.4/api/index.md | 54 + .../version-14.4/api/methods/_category_.json | 4 + .../version-14.4/api/methods/core-methods.md | 1219 +++++++++++++++++ .../version-14.4/api/methods/listeners.md | 519 +++++++ docs/versioned_docs/version-14.4/api/types.md | 195 +++ .../version-14.4/api/use-iap.md | 639 +++++++++ .../version-14.4/examples/_category_.json | 8 + .../examples/alternative-billing.md | 237 ++++ .../examples/available-purchases.md | 91 ++ .../version-14.4/examples/offer-code.md | 71 + .../version-14.4/examples/purchase-flow.md | 155 +++ .../examples/subscription-flow.md | 154 +++ .../getting-started/installation.md | 153 +++ .../getting-started/setup-android.md | 238 ++++ .../getting-started/setup-horizon.md | 140 ++ .../version-14.4/getting-started/setup-ios.md | 183 +++ .../version-14.4/guides/_category_.json | 4 + .../guides/alternative-billing.md | 429 ++++++ .../version-14.4/guides/error-handling.md | 223 +++ .../version-14.4/guides/expo-plugin.md | 60 + .../versioned_docs/version-14.4/guides/faq.md | 756 ++++++++++ .../version-14.4/guides/lifecycle.md | 212 +++ .../guides/offer-code-redemption.md | 151 ++ .../version-14.4/guides/purchases.md | 145 ++ .../guides/subscription-offers.md | 478 +++++++ .../guides/subscription-validation.md | 197 +++ .../version-14.4/guides/support.md | 29 + .../version-14.4/guides/troubleshooting.md | 494 +++++++ .../version-14.4/installation.md | 153 +++ docs/versioned_docs/version-14.4/intro.md | 153 +++ docs/versioned_docs/version-14.4/sponsors.md | 11 + .../version-14.4-sidebars.json | 75 + docs/versions.json | 2 +- example/ios/Podfile.lock | 134 +- example/screens/PurchaseFlow.tsx | 28 +- example/screens/SubscriptionFlow.tsx | 28 +- ios/HybridRnIap.swift | 20 +- ios/RnIapHelper.swift | 8 + openiap-versions.json | 6 +- src/__tests__/hooks/useIAP.test.ts | 1 + src/__tests__/index.test.ts | 85 +- src/__tests__/utils/type-bridge.test.ts | 2 + src/index.ts | 13 +- src/specs/RnIap.nitro.ts | 20 +- src/types.ts | 51 +- src/utils/type-bridge.ts | 22 + 54 files changed, 8251 insertions(+), 171 deletions(-) create mode 100644 docs/versioned_docs/version-14.4/api/error-codes.md create mode 100644 docs/versioned_docs/version-14.4/api/error-handling.md create mode 100644 docs/versioned_docs/version-14.4/api/index.md create mode 100644 docs/versioned_docs/version-14.4/api/methods/_category_.json create mode 100644 docs/versioned_docs/version-14.4/api/methods/core-methods.md create mode 100644 docs/versioned_docs/version-14.4/api/methods/listeners.md create mode 100644 docs/versioned_docs/version-14.4/api/types.md create mode 100644 docs/versioned_docs/version-14.4/api/use-iap.md create mode 100644 docs/versioned_docs/version-14.4/examples/_category_.json create mode 100644 docs/versioned_docs/version-14.4/examples/alternative-billing.md create mode 100644 docs/versioned_docs/version-14.4/examples/available-purchases.md create mode 100644 docs/versioned_docs/version-14.4/examples/offer-code.md create mode 100644 docs/versioned_docs/version-14.4/examples/purchase-flow.md create mode 100644 docs/versioned_docs/version-14.4/examples/subscription-flow.md create mode 100644 docs/versioned_docs/version-14.4/getting-started/installation.md create mode 100644 docs/versioned_docs/version-14.4/getting-started/setup-android.md create mode 100644 docs/versioned_docs/version-14.4/getting-started/setup-horizon.md create mode 100644 docs/versioned_docs/version-14.4/getting-started/setup-ios.md create mode 100644 docs/versioned_docs/version-14.4/guides/_category_.json create mode 100644 docs/versioned_docs/version-14.4/guides/alternative-billing.md create mode 100644 docs/versioned_docs/version-14.4/guides/error-handling.md create mode 100644 docs/versioned_docs/version-14.4/guides/expo-plugin.md create mode 100644 docs/versioned_docs/version-14.4/guides/faq.md create mode 100644 docs/versioned_docs/version-14.4/guides/lifecycle.md create mode 100644 docs/versioned_docs/version-14.4/guides/offer-code-redemption.md create mode 100644 docs/versioned_docs/version-14.4/guides/purchases.md create mode 100644 docs/versioned_docs/version-14.4/guides/subscription-offers.md create mode 100644 docs/versioned_docs/version-14.4/guides/subscription-validation.md create mode 100644 docs/versioned_docs/version-14.4/guides/support.md create mode 100644 docs/versioned_docs/version-14.4/guides/troubleshooting.md create mode 100644 docs/versioned_docs/version-14.4/installation.md create mode 100644 docs/versioned_docs/version-14.4/intro.md create mode 100644 docs/versioned_docs/version-14.4/sponsors.md create mode 100644 docs/versioned_sidebars/version-14.4-sidebars.json diff --git a/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt b/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt index 3c5040ff0..39d303361 100644 --- a/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +++ b/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt @@ -922,6 +922,7 @@ class HybridRnIap : HybridRnIapSpec() { transactionDate = purchase.transactionDate, purchaseToken = purchase.purchaseToken, platform = IapPlatform.ANDROID, + store = mapIapStore(purchase.store), quantity = purchase.quantity.toDouble(), purchaseState = mapPurchaseState(purchase.purchaseState), isAutoRenewing = purchase.isAutoRenewing, @@ -970,7 +971,16 @@ class HybridRnIap : HybridRnIapSpec() { dev.hyo.openiap.PurchaseState.Unknown -> PurchaseState.UNKNOWN } } - + + private fun mapIapStore(store: dev.hyo.openiap.IapStore): IapStore { + return when (store) { + dev.hyo.openiap.IapStore.Apple -> IapStore.APPLE + dev.hyo.openiap.IapStore.Google -> IapStore.GOOGLE + dev.hyo.openiap.IapStore.Horizon -> IapStore.HORIZON + dev.hyo.openiap.IapStore.Unknown -> IapStore.UNKNOWN + } + } + // Billing error messages handled by OpenIAP // iOS-specific method - not supported on Android @@ -1144,19 +1154,28 @@ class HybridRnIap : HybridRnIapSpec() { 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)) + RnIapLog.result("verifyPurchaseWithProvider", mapOf("provider" to result.provider, "hasIapkit" to (result.iapkit != null))) // Convert result to Nitro types - val nitroIapkitResults = result.iapkit.map { item -> + val nitroIapkitResult = result.iapkit?.let { item -> NitroVerifyPurchaseWithIapkitResult( isValid = item.isValid, state = mapIapkitPurchaseState(item.state.name), store = mapIapkitStore(item.store.name) ) - }.toTypedArray() + } + + // Convert errors if present + val nitroErrors = result.errors?.map { error -> + NitroVerifyPurchaseWithProviderError( + code = error.code, + message = error.message + ) + }?.toTypedArray() NitroVerifyPurchaseWithProviderResult( - iapkit = nitroIapkitResults, + iapkit = nitroIapkitResult, + errors = nitroErrors, provider = mapPurchaseVerificationProvider(result.provider.name) ) } catch (e: Exception) { diff --git a/docs/docs/api/methods/core-methods.md b/docs/docs/api/methods/core-methods.md index 5d6901de4..b6cd5667d 100644 --- a/docs/docs/api/methods/core-methods.md +++ b/docs/docs/api/methods/core-methods.md @@ -547,11 +547,17 @@ const verifyWithIAPKit = async (purchase: Purchase) => { ```typescript interface VerifyPurchaseWithProviderResult { provider: 'iapkit'; - iapkit: Array<{ + iapkit?: { isValid: boolean; state: IapkitPurchaseState; - store: 'apple' | 'google'; - }>; + store: IapStore; + } | null; + errors?: VerifyPurchaseWithProviderError[] | null; +} + +interface VerifyPurchaseWithProviderError { + code?: string | null; + message: string; } type IapkitPurchaseState = @@ -564,6 +570,8 @@ type IapkitPurchaseState = | 'ready-to-consume' | 'consumed' | 'inauthentic'; + +type IapStore = 'unknown' | 'apple' | 'google' | 'horizon'; ``` **Platform Behavior:** diff --git a/docs/docs/api/types.md b/docs/docs/api/types.md index c49456eb7..c6c76c92d 100644 --- a/docs/docs/api/types.md +++ b/docs/docs/api/types.md @@ -133,13 +133,25 @@ The request types have been harmonised to match the schema definitions. ```ts export interface RequestPurchasePropsByPlatforms { - android?: RequestPurchaseAndroidProps | null; + /** Apple-specific purchase parameters */ + apple?: RequestPurchaseIosProps | null; + /** Google-specific purchase parameters */ + google?: RequestPurchaseAndroidProps | null; + /** @deprecated Use apple instead */ ios?: RequestPurchaseIosProps | null; + /** @deprecated Use google instead */ + android?: RequestPurchaseAndroidProps | null; } export interface RequestSubscriptionPropsByPlatforms { - android?: RequestSubscriptionAndroidProps | null; + /** Apple-specific subscription parameters */ + apple?: RequestSubscriptionIosProps | null; + /** Google-specific subscription parameters */ + google?: RequestSubscriptionAndroidProps | null; + /** @deprecated Use apple instead */ ios?: RequestSubscriptionIosProps | null; + /** @deprecated Use google instead */ + android?: RequestSubscriptionAndroidProps | null; } export type MutationRequestPurchaseArgs = diff --git a/docs/docs/examples/purchase-flow.md b/docs/docs/examples/purchase-flow.md index 864db13ed..70a8e2f24 100644 --- a/docs/docs/examples/purchase-flow.md +++ b/docs/docs/examples/purchase-flow.md @@ -116,10 +116,10 @@ const {finishTransaction} = useIAP({ IAPKit returns a standardized response: ```typescript -interface IapkitVerificationResult { +interface RequestVerifyPurchaseWithIapkitResult { isValid: boolean; state: IapkitPurchaseState; - store: 'apple' | 'google'; + store: IapStore; } type IapkitPurchaseState = @@ -132,6 +132,8 @@ type IapkitPurchaseState = | 'consumed' | 'unknown' | 'inauthentic'; + +type IapStore = 'unknown' | 'apple' | 'google' | 'horizon'; ``` ### Verification Methods diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 32036a9e1..8dcd2785e 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -87,8 +87,8 @@ useEffect(() => { // Purchase (platform-specific) await requestPurchase({ request: { - ios: {sku: 'product_id'}, - android: {skus: ['product_id']}, + apple: {sku: 'product_id'}, + google: {skus: ['product_id']}, }, type: 'in-app', }); diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 645c5f6ff..e0f704f13 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -51,9 +51,13 @@ const config: Config = { exclude: ['**/node_modules/**'], versions: { current: { - label: '14.4 (Current)', + label: '14.5 (Current)', path: '', }, + '14.4': { + label: '14.4', + path: '14.4', + }, '14.3': { label: '14.3', path: '14.3', diff --git a/docs/versioned_docs/version-14.4/api/error-codes.md b/docs/versioned_docs/version-14.4/api/error-codes.md new file mode 100644 index 000000000..d2cd6c7c4 --- /dev/null +++ b/docs/versioned_docs/version-14.4/api/error-codes.md @@ -0,0 +1,215 @@ +--- +sidebar_position: 2 +--- + +import GreatFrontEndTopFixed from "@site/src/uis/GreatFrontEndTopFixed"; + +# Error Codes + + + +React Native IAP provides a centralized error handling system with platform-specific error code mapping. This ensures consistent error handling across iOS and Android platforms. + +## Error Code Enum + +### ErrorCode + +The `ErrorCode` enum provides standardized error codes that map to platform-specific errors: + +```tsx +import {ErrorCode} from 'react-native-iap'; + +export enum ErrorCode { + Unknown = 'unknown', + UserCancelled = 'user-cancelled', + UserError = 'user-error', + ItemUnavailable = 'item-unavailable', + RemoteError = 'remote-error', + NetworkError = 'network-error', + ServiceError = 'service-error', + ReceiptFailed = 'receipt-failed', + ReceiptFinished = 'receipt-finished', + ReceiptFinishedFailed = 'receipt-finished-failed', + NotPrepared = 'not-prepared', + NotEnded = 'not-ended', + AlreadyOwned = 'already-owned', + DeveloperError = 'developer-error', + BillingResponseJsonParseError = 'billing-response-json-parse-error', + DeferredPayment = 'deferred-payment', + Interrupted = 'interrupted', + IapNotAvailable = 'iap-not-available', + PurchaseError = 'purchase-error', + SyncError = 'sync-error', + TransactionValidationFailed = 'transaction-validation-failed', + ActivityUnavailable = 'activity-unavailable', + AlreadyPrepared = 'already-prepared', + Pending = 'pending', + ConnectionClosed = 'connection-closed', + InitConnection = 'init-connection', + ServiceDisconnected = 'service-disconnected', + QueryProduct = 'query-product', + SkuNotFound = 'sku-not-found', + SkuOfferMismatch = 'sku-offer-mismatch', + ItemNotOwned = 'item-not-owned', + BillingUnavailable = 'billing-unavailable', + FeatureNotSupported = 'feature-not-supported', + EmptySkuList = 'empty-sku-list', +} +``` + +## PurchaseError + +A custom error class for purchase-related errors. + +```tsx +export interface PurchaseErrorProps { + message: string; + responseCode?: number; + debugMessage?: string; + code?: ErrorCode; + productId?: string; + platform?: IapPlatform; +} + +export type PurchaseError = Error & PurchaseErrorProps; +``` + +### createPurchaseError + +Creates a `PurchaseError` instance. + +```tsx +export const createPurchaseError = ( + props: PurchaseErrorProps, +): PurchaseError => { + // ... +}; +``` + +### createPurchaseErrorFromPlatform + +Creates a `PurchaseError` from platform-specific error data. + +```tsx +export const createPurchaseErrorFromPlatform = ( + errorData: PurchaseErrorProps, + platform: IapPlatform, +): PurchaseError => { + // ... +}; +``` + +## ErrorCodeUtils + +Utility functions for error code mapping and validation. + +### getNativeErrorCode + +Gets the native error code for the current platform: + +```tsx +ErrorCodeUtils.getNativeErrorCode(errorCode: ErrorCode): string +``` + +### fromPlatformCode + +Maps platform-specific error code to standardized ErrorCode: + +```tsx +ErrorCodeUtils.fromPlatformCode( + platformCode: string | number, + platform?: IapPlatform, +): ErrorCode +``` + +### toPlatformCode + +Maps ErrorCode to platform-specific code: + +```tsx +ErrorCodeUtils.toPlatformCode( + errorCode: ErrorCode, + platform?: IapPlatform, +): string | number +``` + +### isValidForPlatform + +Checks if error code is valid for the specified platform: + +```tsx +ErrorCodeUtils.isValidForPlatform( + errorCode: ErrorCode, + platform: IapPlatform, +): boolean +``` + +## Error Helper Functions + +These functions help interpret error objects. + +### isUserCancelledError + +Returns `true` if the error is a user cancellation error. + +```tsx +export function isUserCancelledError(error: unknown): boolean; +``` + +### isNetworkError + +Returns `true` if the error is a network-related error. + +```tsx +export function isNetworkError(error: unknown): boolean; +``` + +### isRecoverableError + +Returns `true` if the error is a recoverable error. + +```tsx +export function isRecoverableError(error: unknown): boolean; +``` + +### getUserFriendlyErrorMessage + +Returns a user-friendly error message for a given error. + +```tsx +export function getUserFriendlyErrorMessage(error: ErrorLike): string; +``` + +## User-Friendly Error Messages + +The `getUserFriendlyErrorMessage` function provides localized and user-friendly messages for common errors. + +| ErrorCode | Message | +| --- | --- | +| `UserCancelled` | 'Purchase cancelled' | +| `NetworkError` | 'Network connection error. Please check your internet connection and try again.' | +| `ReceiptFinished` | 'Receipt already finished' | +| `ServiceDisconnected` | 'Billing service disconnected. Please try again.' | +| `BillingUnavailable` | 'Billing is unavailable on this device or account.' | +| `ItemUnavailable` | 'This item is not available for purchase' | +| `ItemNotOwned` | "You don't own this item" | +| `AlreadyOwned` | 'You already own this item' | +| `SkuNotFound` | 'Requested product could not be found' | +| `SkuOfferMismatch` | 'Selected offer does not match the SKU' | +| `DeferredPayment` | 'Payment is pending approval' | +| `NotPrepared` | 'In-app purchase is not ready. Please try again later.' | +| `ServiceError` | 'Store service error. Please try again later.' | +| `FeatureNotSupported` | 'This feature is not supported on this device.' | +| `TransactionValidationFailed` | 'Transaction could not be verified' | +| `ReceiptFailed` | 'Receipt processing failed' | +| `EmptySkuList` | 'No product IDs provided' | +| `InitConnection` | 'Failed to initialize billing connection' | +| `QueryProduct` | 'Failed to query products. Please try again later.' | +| _default_ | 'An unexpected error occurred' | + +## See Also + +- [Error Handling Guide](../guides/error-handling) +- [useIAP Hook](./use-iap) +- [Types Reference](./types) +- [Troubleshooting](../guides/troubleshooting) diff --git a/docs/versioned_docs/version-14.4/api/error-handling.md b/docs/versioned_docs/version-14.4/api/error-handling.md new file mode 100644 index 000000000..ad847fdcc --- /dev/null +++ b/docs/versioned_docs/version-14.4/api/error-handling.md @@ -0,0 +1,82 @@ +--- +title: Error Handling +sidebar_label: Error Handling +--- + +import GreatFrontEndTopFixed from "@site/src/uis/GreatFrontEndTopFixed"; + +# Error Handling + + + +All methods can throw errors that should be handled appropriately. Use the `PurchaseError` type and helper functions for consistent, cross‑platform error information. + +```ts +import { + PurchaseError, + requestPurchase, + isUserCancelledError, + isNetworkError, + getUserFriendlyErrorMessage, + ErrorCode, +} from 'react-native-iap'; + +try { + await requestPurchase({request: {sku: 'product_id'}}); +} catch (error) { + // Check for user cancellation + if (isUserCancelledError(error)) { + console.log('User cancelled purchase'); + return; + } + + // Check for network errors + if (isNetworkError(error)) { + console.log('Network error, please try again'); + return; + } + + // Get user-friendly error message + const friendlyMessage = getUserFriendlyErrorMessage(error); + console.error('Purchase failed:', friendlyMessage); + + // Or access error details directly + if (error instanceof PurchaseError) { + console.error(`Error code: ${error.code}, Message: ${error.message}`); + } +} +``` + +## Error Types + +### PurchaseError + +The `PurchaseError` interface extends the standard `Error` class with additional purchase-specific properties: + +```ts +interface PurchaseError extends Error { + code?: ErrorCode; + responseCode?: number; + debugMessage?: string; + productId?: string; + platform?: IapPlatform; +} +``` + +### ErrorCode Enum + +Use the `ErrorCode` enum for type-safe error code comparisons: + +```ts +import {ErrorCode} from 'react-native-iap'; + +if (error instanceof PurchaseError && error.code === ErrorCode.UserCancelled) { + // Handle user cancellation +} +``` + +## Error Codes + +For the complete list of error codes and their meanings, see the reference below. + +- [Error Codes](./error-codes) diff --git a/docs/versioned_docs/version-14.4/api/index.md b/docs/versioned_docs/version-14.4/api/index.md new file mode 100644 index 000000000..be6ff5287 --- /dev/null +++ b/docs/versioned_docs/version-14.4/api/index.md @@ -0,0 +1,54 @@ +import GreatFrontEndTopFixed from "@site/src/uis/GreatFrontEndTopFixed"; + +# API Reference + + + +Welcome to the React Native IAP API documentation. Here you'll find comprehensive guides and references for all the features and functionality available in React Native IAP. + +Note: react-native-iap follows the OpenIAP API model for consistency across platforms and SDKs. For the canonical API surface, see OpenIAP APIs: + +- [OpenIAP APIs](https://www.openiap.dev/docs/apis) + +## Available APIs + +### 🎣 [useIAP Hook](./use-iap) + +The main React hook for handling in-app purchases in your application. + +- Purchase products and subscriptions +- Restore previous purchases +- Handle purchase states and loading +- Complete transactions + +### ⚠️ [Error Codes](./error-codes) + +Comprehensive list of all error codes that can be returned by React Native IAP. + +- Centralized error management +- Platform-specific error mappings +- Troubleshooting guidelines +- Error handling best practices + +## Quick Start + +```javascript +import {useIAP} from 'react-native-iap'; + +function MyComponent() { + const {products, purchaseProduct, restorePurchases, isLoading, error} = + useIAP(); + + // Your component logic here +} +``` + +## TypeScript Support + +React Native IAP is built with TypeScript and provides full type safety for all APIs. All types are automatically exported when you install the package. + +## Need Help? + +- Check our [Getting Started Guide](/docs/intro) +- Visit our [GitHub repository](https://github.com/hyochan/react-native-iap) +- Read our [Blog](/blog) for latest updates diff --git a/docs/versioned_docs/version-14.4/api/methods/_category_.json b/docs/versioned_docs/version-14.4/api/methods/_category_.json new file mode 100644 index 000000000..eabe83d35 --- /dev/null +++ b/docs/versioned_docs/version-14.4/api/methods/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Methods", + "position": 2 +} diff --git a/docs/versioned_docs/version-14.4/api/methods/core-methods.md b/docs/versioned_docs/version-14.4/api/methods/core-methods.md new file mode 100644 index 000000000..5d6901de4 --- /dev/null +++ b/docs/versioned_docs/version-14.4/api/methods/core-methods.md @@ -0,0 +1,1219 @@ +--- +title: Core Methods +sidebar_label: Core Methods +sidebar_position: 1 +--- + +import GreatFrontEndTopFixed from "@site/src/uis/GreatFrontEndTopFixed"; + +# Core Methods + + + +This section covers the core methods available in react-native-iap for managing in-app purchases. + +Note: react-native-iap aligns with the OpenIAP API surface. For canonical cross-SDK API docs, see: + +- [OpenIAP APIs](https://www.openiap.dev/docs/apis) + +## Unified APIs + +These cross‑platform methods work on both iOS and Android. For StoreKit/Play‑specific helpers, see the Platform‑specific APIs section below. + +- [`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() + +Initializes the connection to the store. This method must be called before any other store operations. + +```tsx +import {initConnection} from 'react-native-iap'; + +const initialize = async () => { + try { + await initConnection(); + console.log('Store connection initialized'); + } catch (error) { + console.error('Failed to initialize connection:', error); + } +}; +``` + +**Returns:** `Promise` + +**Note:** When using the `useIAP` hook, connection is automatically managed. + +## endConnection() + +Ends the connection to the store and cleans up resources. + +```tsx +import {endConnection} from 'react-native-iap'; + +const cleanup = async () => { + try { + await endConnection(); + console.log('Store connection ended'); + } catch (error) { + console.error('Failed to end connection:', error); + } +}; +``` + +**Returns:** `Promise` + +**Note:** When using the `useIAP` hook, connection cleanup is automatic. + +## fetchProducts() + +Fetches product or subscription information from the store. + +```tsx +import {fetchProducts} from 'react-native-iap'; + +// Fetch in-app products +const loadProducts = async () => { + try { + const products = await fetchProducts({ + skus: ['com.example.product1', 'com.example.product2'], + type: 'in-app', + }); + + console.log('Products:', products); + return products; + } catch (error) { + console.error('Failed to fetch products:', error); + } +}; + +// Fetch subscriptions +const loadSubscriptions = async () => { + try { + const subscriptions = await fetchProducts({ + skus: ['com.example.premium_monthly', 'com.example.premium_yearly'], + type: 'subs', + }); + + console.log('Subscriptions:', subscriptions); + return subscriptions; + } catch (error) { + console.error('Failed to fetch subscriptions:', error); + } +}; +``` + +**Parameters:** + +- `params` (object): + - `skus` (string[]): Array of product or subscription IDs to fetch + - `type` ('in-app' | 'subs'): Product type - 'in-app' for products, 'subs' for subscriptions + +**Returns:** `Promise` + +[**Product Type Overview**](../types.md#product-types) + +## requestPurchase() + +Initiates a purchase request for products or subscriptions. + +> **📖 Reference:** [OpenIAP Request APIs](https://www.openiap.dev/docs/apis#request-apis) **⚠️ Platform Differences:** +> +> - **iOS**: Can only purchase one product at a time (uses `sku: string`) +> - **Android**: Can purchase multiple products at once (uses `skus: string[]`) +> +> This exists because the iOS App Store processes purchases individually, while Google Play supports batch purchases. +> +> **📝 Note:** Both consumable and non-consumable in-app products use the same `requestPurchase()` API with `type: 'in-app'`. The platform handles the consumable/non-consumable behavior automatically based on your store configuration. + +### Recommended usage with useIAP hook (Recommended) + +```tsx +import {useIAP} from 'react-native-iap'; + +const ProductPurchaseComponent = () => { + const {requestPurchase, products} = useIAP({ + onPurchaseSuccess: (purchase) => { + console.log('Purchase successful:', purchase); + // Grant user access to purchased content + unlockProduct(purchase.productId); + }, + onPurchaseError: (error) => { + console.error('Purchase failed:', error); + // Handle purchase error (user cancelled, network error, etc.) + }, + }); + + const buyProduct = (productId: string) => { + requestPurchase({ + request: { + ios: { + sku: productId, + quantity: 1, + }, + android: { + skus: [productId], + }, + }, + type: 'in-app', + }); + // No need to await - result handled through callbacks above + }; + + return ( + // Your component JSX + ); +}; +``` + +### Direct API usage (Advanced) + +```tsx +import {requestPurchase} from 'react-native-iap'; + +// Product purchase (consumable and non-consumable) +const buyProduct = (productId: string) => { + requestPurchase({ + request: { + ios: { + sku: productId, + quantity: 1, + }, + android: { + skus: [productId], + }, + }, + type: 'in-app', + }); + // Purchase result is handled via purchaseUpdatedListener/purchaseErrorListener or useIAP hook callbacks (onPurchaseSuccess, onPurchaseError) + // See: docs/api/methods/listeners#purchaseupdatedlistener and https://hyochan.github.io/expo-iap/docs/api/methods/listeners#purchaseerrorlistener +}; + +// Subscription purchase +const buySubscription = (subscriptionId: string, subscription?: any) => { + requestPurchase({ + request: { + ios: { + sku: subscriptionId, + appAccountToken: 'user-123', + }, + android: { + skus: [subscriptionId], + subscriptionOffers: + subscription?.subscriptionOfferDetails?.map((offer) => ({ + sku: subscriptionId, + offerToken: offer.offerToken, + })) || [], + }, + }, + type: 'subs', + }); + // Purchase result is handled via purchaseUpdatedListener/purchaseErrorListener or useIAP hook callbacks (onPurchaseSuccess, onPurchaseError) + // See: docs/api/methods/listeners#purchaseupdatedlistener and https://hyochan.github.io/expo-iap/docs/api/methods/listeners#purchaseerrorlistener +}; +``` + +### Detailed Platform Examples + +#### iOS Only + +```tsx +await requestPurchase({ + request: { + sku: productId, + quantity: 1, + appAccountToken: 'user-account-token', + }, + type: 'in-app', +}); +``` + +#### Android Only + +```tsx +await requestPurchase({ + request: { + skus: [productId], + obfuscatedAccountIdAndroid: 'user-account-id', + obfuscatedProfileIdAndroid: 'user-profile-id', + }, + type: 'in-app', +}); +``` + +**Parameters:** + +- `params` (object): + - `request` (object): Purchase request configuration + - **iOS**: `sku` (string) - Product ID to purchase + - **Android**: `skus` (string[]) - Array of product IDs to purchase + - **Cross-platform**: Include both `sku` and `skus` for compatibility + - `quantity?` (number, iOS only): Purchase quantity + - `appAccountToken?` (string, iOS only): User identifier for receipt validation + - `obfuscatedAccountIdAndroid?` (string, Android only): Obfuscated account ID + - `obfuscatedProfileIdAndroid?` (string, Android only): Obfuscated profile ID + - `isOfferPersonalized?` (boolean, Android only): Whether offer is personalized + - `type?` ('in-app' | 'subs'): Purchase type, defaults to 'in-app' + +**Returns:** `Promise` + +**Note:** The actual purchase result is delivered through purchase listeners or the `useIAP` hook callbacks, not as a return value. + +#### Important Subscription Properties + +For subscription status checks after a purchase or when listing entitlements: + +- iOS: Check `expirationDateIOS` to determine if the subscription is still active +- Android: Check `autoRenewingAndroid` to see if auto‑renewal has been canceled + +## finishTransaction() + +Completes a purchase transaction. Must be called after successful receipt validation. + +```tsx +import {finishTransaction} from 'react-native-iap'; + +const completePurchase = async (purchase) => { + try { + // Validate receipt on your server first + const isValid = await validateReceiptOnServer(purchase); + + if (isValid) { + // Grant purchase to user + await grantPurchaseToUser(purchase); + + // Finish the transaction + await finishTransaction({ + purchase, + isConsumable: true, // Set to true for consumable products + }); + + console.log('Transaction completed'); + } + } catch (error) { + console.error('Failed to finish transaction:', error); + } +}; +``` + +**Parameters:** + +- `params` (object): + - `purchase` (Purchase): The purchase object to finish + - `isConsumable?` (boolean): Whether the product is consumable (Android) + +**Returns:** `Promise` + +## getAvailablePurchases() + +Retrieves available purchases for restoration (non-consumable products and subscriptions). + +```tsx +import {getAvailablePurchases} from 'react-native-iap'; + +const restorePurchases = async () => { + try { + const purchases = await getAvailablePurchases(); + + for (const purchase of purchases) { + // Validate and restore each purchase + const isValid = await validateReceiptOnServer(purchase); + if (isValid) { + await grantPurchaseToUser(purchase); + } + } + + console.log('Purchases restored'); + } catch (error) { + console.error('Failed to restore purchases:', error); + } +}; +``` + +**Parameters:** + +- `options?` (iOS only): + - `alsoPublishToEventListenerIOS?`: boolean + - `onlyIncludeActiveItemsIOS?`: boolean + +**Returns:** `Promise` + +**Platform behaviour:** + +- **iOS** – the library forwards the optional flags through StoreKit 2. `onlyIncludeActiveItemsIOS` defaults to `true`, so the result only contains entitlements that are still active. Setting `alsoPublishToEventListenerIOS` mirrors the data through the purchase event listeners for apps that subscribe directly to those callbacks. +- **Android** – Google Play separates `inapp` (one‑time) and `subs` purchases. The library internally calls the billing client twice—once for each type—and merges the results before running validation. No additional options are required; both product classes are returned together. + +## deepLinkToSubscriptions() + +Opens the platform-specific subscription management UI. + +```tsx +import {deepLinkToSubscriptions} from 'react-native-iap'; + +const openSubscriptionSettings = () => { + try { + deepLinkToSubscriptions({skuAndroid: 'your_subscription_sku'}); + } catch (error) { + console.error('Failed to open subscription settings:', error); + } +}; +``` + +**Returns:** `Promise` + +## getStorefront() + +Return the storefront in ISO 3166-1 alpha-2 or ISO 3166-1 alpha-3 format. Works on both iOS and Android—on other platforms it returns an empty string. + +```tsx +import {getStorefront} from 'react-native-iap'; + +const storeFront = await getStorefront(); +``` + +**Returns:** `Promise` + +## getActiveSubscriptions() + +Retrieves all active subscriptions with detailed status information. This method follows the OpenIAP specification for cross-platform subscription management. + +```tsx +import {getActiveSubscriptions} from 'react-native-iap'; + +const checkSubscriptions = async () => { + try { + // Get all active subscriptions + const allActiveSubscriptions = await getActiveSubscriptions(); + + // Or filter by specific subscription IDs + const specificSubscriptions = await getActiveSubscriptions([ + 'premium_monthly', + 'premium_yearly', + ]); + + for (const subscription of allActiveSubscriptions) { + console.log('Product ID:', subscription.productId); + console.log('Is Active:', subscription.isActive); + + if (Platform.OS === 'ios') { + console.log('Expiration Date:', subscription.expirationDateIOS); + console.log( + 'Days until expiration:', + subscription.daysUntilExpirationIOS, + ); + console.log('Environment:', subscription.environmentIOS); + } else if (Platform.OS === 'android') { + console.log('Auto Renewing:', subscription.autoRenewingAndroid); + } + + console.log('Will expire soon:', subscription.willExpireSoon); + } + } catch (error) { + console.error('Failed to get active subscriptions:', error); + } +}; +``` + +**Parameters:** + +- `subscriptionIds?` (string[]): Optional array of subscription product IDs to filter. If not provided, returns all active subscriptions. + +**Returns:** `Promise` + +**ActiveSubscription Interface:** + +```typescript +interface ActiveSubscription { + productId: string; + isActive: boolean; + transactionId: string; + transactionDate: number; // epoch milliseconds + expirationDateIOS?: number | null; // epoch milliseconds + daysUntilExpirationIOS?: number | null; + willExpireSoon?: boolean | null; + environmentIOS?: string | null; // "Sandbox" | "Production" + autoRenewingAndroid?: boolean | null; + purchaseToken?: string | null; // JWS (iOS) or purchaseToken (Android) +} +``` + +> Optional properties may be `undefined` or `null` when the store does not provide the value (for example, `expirationDateIOS` is only present for auto-renewing products). + +**Platform Behavior:** + +- **iOS**: Derived from StoreKit 2 entitlements. `expirationDateIOS`, `daysUntilExpirationIOS`, and `environmentIOS` come directly from the latest validated transaction. +- **Android**: Derived from Google Play Billing purchases. `autoRenewingAndroid` mirrors the Play auto-renew flag and `purchaseToken` forwards the token you need for server-side validation. +- **Shared**: `transactionId` and `transactionDate` correspond to the most recent entitlement event on either platform. + +## hasActiveSubscriptions() + +Checks if the user has any active subscriptions. This is a convenience method that returns a boolean result. + +```tsx +import {hasActiveSubscriptions} from 'react-native-iap'; + +const checkIfUserHasSubscription = async () => { + try { + // Check if user has any active subscriptions + const hasAny = await hasActiveSubscriptions(); + + // Or check for specific subscriptions + const hasPremium = await hasActiveSubscriptions([ + 'premium_monthly', + 'premium_yearly', + ]); + + if (hasAny) { + console.log('User has active subscriptions'); + } + + if (hasPremium) { + console.log('User has premium subscription'); + } + } catch (error) { + console.error('Failed to check subscription status:', error); + } +}; +``` + +**Parameters:** + +- `subscriptionIds?` (string[]): Optional array of subscription product IDs to check. If not provided, checks all subscriptions. + +**Returns:** `Promise` - 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` + +```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 +interface Purchase { + id: string; // Transaction identifier + productId: string; + transactionDate: number; + purchaseToken?: string; // Unified token (iOS JWS or Android token) + + // iOS-specific properties + originalTransactionDateIOS?: number; + originalTransactionIdentifierIOS?: string; + expirationDateIOS?: number; + environmentIOS?: 'Production' | 'Sandbox'; + + // Android-specific properties + dataAndroid?: string; + signatureAndroid?: string; + purchaseStateAndroid?: number; + isAcknowledgedAndroid?: boolean; + packageNameAndroid?: string; + developerPayloadAndroid?: string; + obfuscatedAccountIdAndroid?: string; + obfuscatedProfileIdAndroid?: string; + autoRenewingAndroid?: boolean; +} +``` + +## Platform-specific APIs + +### iOS Specific + +The following iOS‑only helpers expose StoreKit and App Store specific capabilities. Most day‑to‑day flows are covered by the cross‑platform Core Methods above; use these only when you need iOS features. + +### clearTransactionIOS() + +Clears all pending transactions from the iOS payment queue. Useful if your app previously crashed or missed finishing transactions. + +```ts +import {clearTransactionIOS, getPendingTransactionsIOS} from 'react-native-iap'; + +// Inspect then clear +const pending = await getPendingTransactionsIOS(); +if (pending.length) { + await clearTransactionIOS(); +} +``` + +Returns: `Promise` + +### getStorefrontIOS() + +Returns the current App Store storefront country code (for example, "US", "GB"). + +```ts +import {getStorefrontIOS} from 'react-native-iap'; + +const storefront = await getStorefrontIOS(); +``` + +Returns: `Promise` + +### getPromotedProductIOS() + +Gets the currently promoted product, if any. Requires iOS 11+. + +```ts +import {getPromotedProductIOS} from 'react-native-iap'; + +const promoted = await getPromotedProductIOS(); +if (promoted) { + // Show your purchase UI for the promoted product +} +``` + +Returns: `Promise` + +### requestPurchaseOnPromotedProductIOS() + +Initiates the purchase flow for the currently promoted product. Requires iOS 11+. + +```ts +import {requestPurchaseOnPromotedProductIOS} from 'react-native-iap'; + +await requestPurchaseOnPromotedProductIOS(); +// Purchase result is delivered via purchase listeners/useIAP callbacks +``` + +Returns: `Promise` + +### getPendingTransactionsIOS() + +Returns all transactions that are pending completion in the StoreKit payment queue. + +```ts +import {getPendingTransactionsIOS} from 'react-native-iap'; + +const pending = await getPendingTransactionsIOS(); +``` + +Returns: `Promise` + +### isEligibleForIntroOfferIOS() + +Checks if the user is eligible for an introductory offer for a subscription group. Requires iOS 12.2+. + +```ts +import {isEligibleForIntroOfferIOS, fetchProducts} from 'react-native-iap'; + +// Example: derive group ID from a fetched subscription product +const [sub] = await fetchProducts({skus: ['your_sub_sku'], type: 'subs'}); +const groupId = sub?.subscriptionInfoIOS?.subscriptionGroupId ?? ''; +const eligible = groupId ? await isEligibleForIntroOfferIOS(groupId) : false; +``` + +Returns: `Promise` + +### subscriptionStatusIOS() + +Returns detailed subscription status information using StoreKit 2. Requires iOS 15+. + +```ts +import {subscriptionStatusIOS} from 'react-native-iap'; + +const statuses = await subscriptionStatusIOS('your_sub_sku'); +``` + +Returns: `Promise` + +### currentEntitlementIOS() + +Returns the current entitlement for a given SKU using StoreKit 2. Requires iOS 15+. + +```ts +import {currentEntitlementIOS} from 'react-native-iap'; + +const entitlement = await currentEntitlementIOS('your_sub_or_product_sku'); +``` + +Returns: `Promise` + +### latestTransactionIOS() + +Returns the most recent transaction for a given SKU using StoreKit 2. Requires iOS 15+. + +```ts +import {latestTransactionIOS} from 'react-native-iap'; + +const last = await latestTransactionIOS('your_sku'); +``` + +Returns: `Promise` + +### showManageSubscriptionsIOS() + +Opens the native subscription management interface and returns purchases for subscriptions whose auto‑renewal status changed while the sheet was open. Requires iOS 15+. + +```ts +import {showManageSubscriptionsIOS} from 'react-native-iap'; + +const changed = await showManageSubscriptionsIOS(); +if (changed.length > 0) { + // Update your UI / server using returned purchases +} +``` + +Returns: `Promise` + +### beginRefundRequestIOS() + +Presents the refund request sheet for a specific SKU. Requires iOS 15+. + +```ts +import {beginRefundRequestIOS} from 'react-native-iap'; + +const status = await beginRefundRequestIOS('your_sku'); +// status: 'success' | 'userCancelled' +``` + +Returns: `Promise<'success' | 'userCancelled'>` + +### isTransactionVerifiedIOS() + +Verifies the latest transaction for a given SKU using StoreKit 2. Requires iOS 15+. + +```ts +import {isTransactionVerifiedIOS} from 'react-native-iap'; + +const ok = await isTransactionVerifiedIOS('your_sku'); +``` + +Returns: `Promise` + +### getTransactionJwsIOS() + +Returns the JSON Web Signature (JWS) for a transaction derived from a given SKU. Use this for server‑side validation. Requires iOS 15+. + +```ts +import {getTransactionJwsIOS} from 'react-native-iap'; + +const jws = await getTransactionJwsIOS('your_sku'); +``` + +Returns: `Promise` + +### getReceiptDataIOS() + +Returns the base64‑encoded receipt data for server validation. + +```ts +import {getReceiptDataIOS} from 'react-native-iap'; + +const receipt = await getReceiptDataIOS(); +``` + +Returns: `Promise` + +### syncIOS() + +Forces a sync with StoreKit to ensure all transactions are up to date. Requires iOS 15+. + +```ts +import {syncIOS} from 'react-native-iap'; + +await syncIOS(); +``` + +Returns: `Promise` + +### presentCodeRedemptionSheetIOS() + +Presents the system sheet for redeeming App Store promo/offer codes. + +```ts +import {presentCodeRedemptionSheetIOS} from 'react-native-iap'; + +await presentCodeRedemptionSheetIOS(); +``` + +Returns: `Promise` + +### getAppTransactionIOS() + +Gets app transaction information for iOS apps (iOS 16.0+). AppTransaction represents the initial purchase that unlocked the app, useful for premium apps or apps that were previously paid. + +> Runtime: iOS 16.0+; Build: Xcode 15.0+ with iOS 16.0 SDK. Older SDKs will throw. + +```tsx +import {getAppTransactionIOS} from 'react-native-iap'; + +const fetchAppTransaction = async () => { + try { + const appTransaction = await getAppTransactionIOS(); + if (appTransaction) { + console.log('App Transaction ID:', appTransaction.appTransactionId); + console.log( + 'Original Purchase Date:', + new Date(appTransaction.originalPurchaseDate), + ); + console.log('Device Verification:', appTransaction.deviceVerification); + } + } catch (error) { + console.error('Failed to get app transaction:', error); + } +}; +``` + +**Returns:** `Promise` + +```ts +interface AppTransaction { + appTransactionId?: string; // iOS 18.4+ + originalPlatform?: string; // iOS 18.4+ + bundleId: string; + appVersion: string; + originalAppVersion: string; + originalPurchaseDate: number; // ms since epoch + deviceVerification: string; + deviceVerificationNonce: string; + environment: string; + signedDate: number; + appId?: number; + appVersionId?: number; + preorderDate?: number; +} +``` + +### Android Specific + +#### acknowledgePurchaseAndroid + +Acknowledge a non‑consumable purchase or subscription on Android. + +```ts +import {acknowledgePurchaseAndroid} from 'react-native-iap'; + +await acknowledgePurchaseAndroid({token: purchase.purchaseToken!}); +``` + +Notes: + +- finishTransaction() calls this automatically when `isConsumable` is false. You typically do not need to call it directly. + +#### consumePurchaseAndroid + +Consume a purchase (consumables only). This marks an item as consumed so it can be purchased again. + +Notes: + +- finishTransaction() calls Android consumption automatically when `isConsumable` is true. +- A direct JS helper is not exposed; consumption is handled internally via the native module. + +#### flushFailedPurchasesCachedAsPendingAndroid (Removed) + +This legacy helper from older libraries has been removed. The modern flow is: + +```ts +// On app startup (Android) +const purchases = await getAvailablePurchases(); + +for (const p of purchases) { + if (/* consumable */) { + // finishTransaction will consume on Android when isConsumable is true + await finishTransaction({ purchase: p, isConsumable: true }); + } else { + // finishTransaction will acknowledge on Android when isConsumable is false + await finishTransaction({ purchase: p, isConsumable: false }); + } +} +``` + +This ensures pending transactions are surfaced and properly resolved without a separate “flush” API. + +## Alternative Billing APIs + +Alternative billing enables developers to offer payment options outside of the platform's standard billing systems. Both platforms require special approval. + +### Android Alternative Billing + +Android supports two modes: + +- **Alternative Billing Only**: Only your payment system is available +- **User Choice Billing**: Users choose between Google Play and your payment system + +#### Initialize with Alternative Billing + +```tsx +import {initConnection} from 'react-native-iap'; + +// Alternative Billing Only mode +await initConnection({ + alternativeBillingModeAndroid: 'alternative-only', +}); + +// User Choice Billing mode +await initConnection({ + alternativeBillingModeAndroid: 'user-choice', +}); +``` + +**Parameters:** + +- `alternativeBillingModeAndroid`: `'none'` | `'alternative-only'` | `'user-choice'` + +#### checkAlternativeBillingAvailabilityAndroid() + +Check if alternative billing is available for the current user/device. + +```tsx +import {checkAlternativeBillingAvailabilityAndroid} from 'react-native-iap'; + +const isAvailable = await checkAlternativeBillingAvailabilityAndroid(); +if (isAvailable) { + // Proceed with alternative billing flow +} +``` + +**Returns:** `Promise` + +**Platform:** Android only + +#### showAlternativeBillingDialogAndroid() + +Show the alternative billing information dialog to the user. Must be called **before** processing payment in your payment system. + +```tsx +import {showAlternativeBillingDialogAndroid} from 'react-native-iap'; + +const userAccepted = await showAlternativeBillingDialogAndroid(); +if (userAccepted) { + // User accepted - process payment in your system + const success = await processCustomPayment(); + + if (success) { + // Create token for Google Play reporting + const token = await createAlternativeBillingTokenAndroid(); + } +} +``` + +**Returns:** `Promise` - `true` if user accepted, `false` if cancelled + +**Platform:** Android only + +#### createAlternativeBillingTokenAndroid() + +Create an external transaction token for Google Play reporting. Must be called **after** successful payment in your payment system. Token must be reported to Google Play backend within 24 hours. + +```tsx +import {createAlternativeBillingTokenAndroid} from 'react-native-iap'; + +// After successful payment in your system +const token = await createAlternativeBillingTokenAndroid( + 'premium_subscription', +); + +if (token) { + // Send token to your backend for Google Play reporting + await fetch('/api/report-transaction', { + method: 'POST', + body: JSON.stringify({token, productId: 'premium_subscription'}), + }); +} +``` + +**Parameters:** + +- `sku?: string` - Optional product SKU that was purchased + +**Returns:** `Promise` - Token string or null if creation failed + +**Platform:** Android only + +#### Complete Alternative Billing Flow (Android) + +```tsx +// Step 1: Check availability +const isAvailable = await checkAlternativeBillingAvailabilityAndroid(); +if (!isAvailable) { + console.log('Alternative billing not available'); + return; +} + +// Step 2: Show information dialog +const userAccepted = await showAlternativeBillingDialogAndroid(); +if (!userAccepted) { + console.log('User cancelled'); + return; +} + +// Step 3: Process payment in your payment system +const paymentSuccess = await yourPaymentSystem.processPayment({ + productId: 'premium_subscription', + amount: 9.99, +}); + +if (!paymentSuccess) { + console.log('Payment failed'); + return; +} + +// Step 4: Create token for Google Play reporting +const token = await createAlternativeBillingTokenAndroid( + 'premium_subscription', +); + +if (token) { + // Step 5: Report to Google Play backend (within 24 hours) + await yourBackend.reportToGooglePlay({ + token, + productId: 'premium_subscription', + userId: currentUser.id, + }); + + console.log('Alternative billing completed successfully'); +} +``` + +### iOS External Purchase + +iOS alternative billing works by redirecting users to an external website where they complete the purchase. + +#### canPresentExternalPurchaseNoticeIOS() + +Check if the device can present an external purchase notice sheet. + +```tsx +import {canPresentExternalPurchaseNoticeIOS} from 'react-native-iap'; + +const canPresent = await canPresentExternalPurchaseNoticeIOS(); +if (canPresent) { + // Present notice before external purchase +} +``` + +**Returns:** `Promise` + +**Platform:** iOS 18.2+ only + +**Requires:** `com.apple.developer.storekit.external-purchase` entitlement + +#### presentExternalPurchaseNoticeSheetIOS() + +Present an external purchase notice sheet to inform users about external purchases. This must be called before opening an external purchase link. + +```tsx +import {presentExternalPurchaseNoticeSheetIOS} from 'react-native-iap'; + +const result = await presentExternalPurchaseNoticeSheetIOS(); + +if (result.result === 'continue') { + // User chose to continue, open external purchase link + await presentExternalPurchaseLinkIOS('https://your-website.com/purchase'); +} else if (result.result === 'cancel') { + console.log('User cancelled'); +} +``` + +**Returns:** `Promise` + +```tsx +interface ExternalPurchaseNoticeResultIOS { + result: 'continue' | 'cancel'; + error?: string; +} +``` + +**Platform:** iOS 18.2+ only + +**Requires:** `com.apple.developer.storekit.external-purchase` entitlement + +#### presentExternalPurchaseLinkIOS() + +Present an external purchase link to redirect users to your website. + +```tsx +import {presentExternalPurchaseLinkIOS} from 'react-native-iap'; + +const result = await presentExternalPurchaseLinkIOS( + 'https://your-website.com/purchase', +); + +if (result.success) { + console.log('User was redirected to external website'); + // Complete purchase on your website + // Implement deep link to return to app +} else if (result.error) { + console.error('Error:', result.error); +} +``` + +**Parameters:** + +- `url: string` - The external purchase URL to open + +**Returns:** `Promise` + +```tsx +interface ExternalPurchaseLinkResultIOS { + success: boolean; + error?: string; +} +``` + +**Platform:** iOS 16.0+ only + +**Requires:** + +- `com.apple.developer.storekit.external-purchase` entitlement +- URL must be configured in Info.plist + +#### iOS Configuration + +For iOS alternative billing, you need to configure your app with the Expo config plugin: + +```tsx +// app.config.ts +export default { + plugins: [ + [ + 'react-native-iap', + { + iosAlternativeBilling: { + // Required: Countries where external purchases are supported + countries: ['kr', 'nl', 'de', 'fr'], // ISO 3166-1 alpha-2 + + // Optional: External purchase URLs per country (iOS 15.4+) + links: { + kr: 'https://your-site.com/kr/checkout', + nl: 'https://your-site.com/nl/checkout', + }, + + // Optional: Multiple URLs per country (iOS 17.5+, up to 5) + multiLinks: { + de: [ + 'https://your-site.com/de/checkout', + 'https://your-site.com/de/special-offer', + ], + }, + + // Optional: Custom link regions (iOS 18.1+) + customLinkRegions: ['de', 'fr', 'nl'], + + // Optional: Streaming regions for music apps (iOS 18.2+) + streamingLinkRegions: ['at', 'de', 'fr', 'nl'], + + // Enable external purchase link entitlement + enableExternalPurchaseLink: true, + + // Enable streaming entitlement (music apps only) + enableExternalPurchaseLinkStreaming: false, + }, + }, + ], + ], +}; +``` + +This automatically adds the required entitlements and Info.plist configuration. + +:::warning Requirements + +- **Approval Required**: You must obtain approval from Apple/Google to use alternative billing +- **iOS URL Format**: URLs must use HTTPS, have no query parameters, and be 1,000 characters or fewer +- **Android Reporting**: External transaction tokens must be reported to Google Play within 24 hours +- **Service Fees**: Reduced fees apply when using alternative billing (varies by platform and region) ::: + +For complete guides, see: + +- [Alternative Billing Guide](/docs/guides/alternative-billing) (coming soon) +- [OpenIAP Alternative Billing](https://www.openiap.dev/docs/alternative-billing) +- [Apple External Purchase](https://developer.apple.com/documentation/storekit/external-purchase) +- [Google Alternative Billing](https://developer.android.com/google/play/billing/alternative) + +## Removed APIs + +- `requestProducts()` — Removed in v3.0.0. Use `fetchProducts({ skus, type })` instead. diff --git a/docs/versioned_docs/version-14.4/api/methods/listeners.md b/docs/versioned_docs/version-14.4/api/methods/listeners.md new file mode 100644 index 000000000..bc7a4b73f --- /dev/null +++ b/docs/versioned_docs/version-14.4/api/methods/listeners.md @@ -0,0 +1,519 @@ +--- +title: Listeners +sidebar_label: Listeners +sidebar_position: 2 +--- + +import GreatFrontEndTopFixed from "@site/src/uis/GreatFrontEndTopFixed"; + +# Purchase Listeners + + + +react-native-iap provides event listeners to handle purchase updates and errors. These listeners are essential for handling the asynchronous nature of in-app purchases. + +## purchaseUpdatedListener() + +Listens for purchase updates from **the** store. + +```tsx +import {purchaseUpdatedListener} from 'react-native-iap'; + +const setupPurchaseListener = () => { + const subscription = purchaseUpdatedListener((purchase) => { + console.log('Purchase received:', purchase); + handlePurchaseUpdate(purchase); + }); + + // Clean up listener when component unmounts + return () => { + if (subscription) { + subscription.remove(); + } + }; +}; + +const handlePurchaseUpdate = async (purchase) => { + try { + // Validate receipt on your server + const isValid = await validateReceiptOnServer(purchase); + + if (isValid) { + // Grant purchase to user + await grantPurchaseToUser(purchase); + + // Finish the transaction + await finishTransaction({purchase}); + + console.log('Purchase completed successfully'); + } else { + console.error('Receipt validation failed'); + } + } catch (error) { + console.error('Error handling purchase:', error); + } +}; +``` + +**Parameters:** + +- `callback` (function): Function to call when a purchase update is received + - `purchase` (Purchase): The purchase object + +**Returns:** Subscription object with `remove()` method + +## purchaseErrorListener() + +Listens for purchase errors from the store. + +```tsx +import {purchaseErrorListener} from 'react-native-iap'; + +const setupErrorListener = () => { + const subscription = purchaseErrorListener((error) => { + console.error('Purchase error:', error); + handlePurchaseError(error); + }); + + // Clean up listener when component unmounts + return () => { + if (subscription) { + subscription.remove(); + } + }; +}; + +import {ErrorCode} from 'react-native-iap'; + +const handlePurchaseError = (error) => { + switch (error.code) { + case ErrorCode.UserCancelled: + // User cancelled the purchase + console.log('Purchase cancelled by user'); + break; + + case ErrorCode.NetworkError: + // Network error occurred + showErrorMessage( + 'Network error. Please check your connection and try again.', + ); + break; + + case ErrorCode.ItemUnavailable: + // Product is not available + showErrorMessage('This product is currently unavailable.'); + break; + + case ErrorCode.AlreadyOwned: + // User already owns this product + showErrorMessage('You already own this product.'); + break; + + default: + // Other errors + showErrorMessage(`Purchase failed: ${error.message}`); + break; + } +}; +``` + +**Parameters:** + +- `callback` (function): Function to call when a purchase error occurs + - `error` (PurchaseError): The error object + +**Returns:** Subscription object with `remove()` method + +## promotedProductListenerIOS() (iOS only) + +Listens for promoted product purchases initiated from the App Store. This fires when a user taps on a promoted product in the App Store. + +```tsx +import { + promotedProductListenerIOS, + getPromotedProductIOS, + requestPurchaseOnPromotedProductIOS, +} from 'react-native-iap'; + +const setupPromotedProductListener = () => { + const subscription = promotedProductListenerIOS((product) => { + console.log('Promoted product purchase initiated:', product); + handlePromotedProduct(product); + }); + + return () => { + if (subscription) { + subscription.remove(); + } + }; +}; + +const handlePromotedProduct = async (product) => { + try { + // Show your custom purchase UI with the product details + const confirmed = await showProductConfirmation(product); + + if (confirmed) { + // Complete the promoted purchase + await requestPurchaseOnPromotedProductIOS(); + } + } catch (error) { + console.error('Error handling promoted product:', error); + } +}; +``` + +**Parameters:** + +- `callback` (function): Function to call when a promoted product is selected + - `product` (Product): The promoted product object + +**Returns:** Subscription object with `remove()` method + +**Related Methods:** + +- `getPromotedProductIOS()`: Get the promoted product details +- `requestPurchaseOnPromotedProductIOS()`: Complete the promoted product purchase + +**Note:** This listener only works on iOS devices and is used for handling App Store promoted products. + +## userChoiceBillingListenerAndroid() (Android only) + +Android-only listener for User Choice Billing events. This fires when a user selects alternative billing instead of Google Play billing in the User Choice Billing dialog (only in `user-choice` mode). + +```tsx +import { + initConnection, + userChoiceBillingListenerAndroid, +} from 'react-native-iap'; +import {Platform} from 'react-native'; + +const setupUserChoiceBillingListener = async () => { + if (Platform.OS !== 'android') return; + + // Initialize with user-choice mode + await initConnection({ + alternativeBillingModeAndroid: 'user-choice', + }); + + const subscription = userChoiceBillingListenerAndroid((details) => { + console.log('User selected alternative billing'); + console.log('Token:', details.externalTransactionToken); + console.log('Products:', details.products); + + handleUserChoiceBilling(details); + }); + + // Clean up listener when component unmounts + return () => { + if (subscription) { + subscription.remove(); + } + }; +}; + +const handleUserChoiceBilling = async (details) => { + try { + // Step 1: Process payment in your payment system + const paymentResult = await processPaymentInYourSystem(details.products); + + if (!paymentResult.success) { + console.error('Payment failed'); + return; + } + + // Step 2: Report token to Google Play backend within 24 hours + await reportTokenToGooglePlay({ + token: details.externalTransactionToken, + products: details.products, + paymentResult, + }); + + console.log('Alternative billing completed successfully'); + } catch (error) { + console.error('Error handling user choice billing:', error); + } +}; +``` + +**Parameters:** + +- `callback` (function): Function to call when user selects alternative billing + - `details` (UserChoiceBillingDetails): The user choice billing details + - `externalTransactionToken` (string): Token that must be reported to Google within 24 hours + - `products` (string[]): List of product IDs selected by the user + +**Returns:** Subscription object with `remove()` method + +**Platform:** Android only (requires `user-choice` mode) + +**Important:** + +- Only fires when using `alternativeBillingModeAndroid: 'user-choice'` +- Token must be reported to Google Play backend within 24 hours +- If user selects Google Play billing instead, `purchaseUpdatedListener` will fire as normal + +### Example with React + +```tsx +import {useEffect} from 'react'; +import { + initConnection, + userChoiceBillingListenerAndroid, +} from 'react-native-iap'; +import {Platform} from 'react-native'; + +export default function AlternativeBillingComponent() { + useEffect(() => { + if (Platform.OS !== 'android') return; + + const initialize = async () => { + // Initialize with user-choice mode + await initConnection({ + alternativeBillingModeAndroid: 'user-choice', + }); + + // Set up listener + const subscription = userChoiceBillingListenerAndroid(async (details) => { + console.log('User chose alternative billing'); + + // Process payment and report to Google + await handleAlternativeBilling(details); + }); + + return () => { + subscription.remove(); + }; + }; + + const cleanup = initialize(); + + return () => { + cleanup.then((fn) => fn?.()); + }; + }, []); + + // Rest of component +} +``` + +## Using Listeners with React Hooks + +### Functional Components + +```tsx +import React, {useEffect} from 'react'; +import {purchaseUpdatedListener, purchaseErrorListener} from 'react-native-iap'; + +export default function PurchaseManager() { + useEffect(() => { + // Set up purchase listeners + const purchaseUpdateSubscription = purchaseUpdatedListener((purchase) => { + handlePurchaseUpdate(purchase); + }); + + const purchaseErrorSubscription = purchaseErrorListener((error) => { + handlePurchaseError(error); + }); + + // Cleanup function + return () => { + purchaseUpdateSubscription?.remove(); + purchaseErrorSubscription?.remove(); + }; + }, []); + + const handlePurchaseUpdate = async (purchase) => { + // Handle purchase logic + }; + + const handlePurchaseError = (error) => { + // Handle error logic + }; + + return
{/* Your component JSX */}
; +} +``` + +### Class Components + +```tsx +import React, {Component} from 'react'; +import {purchaseUpdatedListener, purchaseErrorListener} from 'react-native-iap'; + +class PurchaseManager extends Component { + purchaseUpdateSubscription = null; + purchaseErrorSubscription = null; + + componentDidMount() { + // Set up listeners + this.purchaseUpdateSubscription = purchaseUpdatedListener((purchase) => { + this.handlePurchaseUpdate(purchase); + }); + + this.purchaseErrorSubscription = purchaseErrorListener((error) => { + this.handlePurchaseError(error); + }); + } + + componentWillUnmount() { + // Clean up listeners + if (this.purchaseUpdateSubscription) { + this.purchaseUpdateSubscription.remove(); + } + + if (this.purchaseErrorSubscription) { + this.purchaseErrorSubscription.remove(); + } + } + + handlePurchaseUpdate = async (purchase) => { + // Handle purchase logic + }; + + handlePurchaseError = (error) => { + // Handle error logic + }; + + render() { + return
{/* Your component JSX */}
; + } +} +``` + +## Custom Hook for Purchase Handling + +You can create a custom hook to encapsulate purchase listener logic: + +```tsx +import {useEffect, useCallback} from 'react'; +import { + purchaseUpdatedListener, + purchaseErrorListener, + finishTransaction, +} from 'react-native-iap'; + +export const usePurchaseHandler = () => { + const handlePurchaseUpdate = useCallback(async (purchase) => { + try { + // Validate receipt + const isValid = await validateReceiptOnServer(purchase); + + if (isValid) { + // Grant purchase + await grantPurchaseToUser(purchase); + + // Finish transaction + await finishTransaction({purchase}); + + // Show success message + showSuccessMessage('Purchase completed successfully!'); + } else { + console.error('Receipt validation failed'); + showErrorMessage('Purchase validation failed. Please contact support.'); + } + } catch (error) { + console.error('Error handling purchase:', error); + showErrorMessage('An error occurred while processing your purchase.'); + } + }, []); + + const handlePurchaseError = useCallback((error) => { + console.error('Purchase error:', error); + + switch (error.code) { + case ErrorCode.UserCancelled: + // Don't show error for user cancellation + break; + default: + showErrorMessage(`Purchase failed: ${error.message}`); + break; + } + }, []); + + useEffect(() => { + // Set up listeners + const purchaseUpdateSubscription = + purchaseUpdatedListener(handlePurchaseUpdate); + const purchaseErrorSubscription = + purchaseErrorListener(handlePurchaseError); + + // Cleanup + return () => { + purchaseUpdateSubscription?.remove(); + purchaseErrorSubscription?.remove(); + }; + }, [handlePurchaseUpdate, handlePurchaseError]); +}; + +// Usage in component +export default function MyStoreComponent() { + usePurchaseHandler(); // Sets up listeners automatically + + return
{/* Your store UI */}
; +} +``` + +## Important Notes + +### Listener Lifecycle + +1. **Set up early**: Set up listeners as early as possible in your app lifecycle +2. **Clean up properly**: Always remove listeners to prevent memory leaks +3. **Handle app states**: Purchases can complete when your app is in the background + +### Error Handling + +Always handle both purchase updates and errors: + +```tsx +useEffect(() => { + const purchaseUpdateSubscription = purchaseUpdatedListener((purchase) => { + // Handle successful/pending purchases + }); + + const purchaseErrorSubscription = purchaseErrorListener((error) => { + // Handle purchase errors + }); + + return () => { + purchaseUpdateSubscription?.remove(); + purchaseErrorSubscription?.remove(); + }; +}, []); +``` + +### Purchase States + +Purchases can be in different states: + +- **Purchased**: Successfully completed +- **Pending**: Awaiting approval (e.g., parental approval) +- **Failed**: Purchase failed + +Handle each state appropriately in your purchase listener. + +## Alternative: useIAP Hook + +For simpler usage, consider using the `useIAP` hook which automatically manages listeners: + +```tsx +import {useIAP} from 'react-native-iap'; + +export default function StoreComponent() { + const { + /* other props */ + } = useIAP({ + onPurchaseSuccess: async (purchase) => { + await handlePurchaseUpdate(purchase); + }, + onPurchaseError: (error) => { + handlePurchaseError(error); + }, + }); + + // Rest of component +} +``` + +The `useIAP` hook provides a more React-friendly way to handle purchases without manually managing listeners. diff --git a/docs/versioned_docs/version-14.4/api/types.md b/docs/versioned_docs/version-14.4/api/types.md new file mode 100644 index 000000000..c6c76c92d --- /dev/null +++ b/docs/versioned_docs/version-14.4/api/types.md @@ -0,0 +1,195 @@ +import GreatFrontEndTopFixed from "@site/src/uis/GreatFrontEndTopFixed"; + +# Types + + + +The react-native-iap type surface is now generated in one place: `src/types.ts`. The file is produced by our GraphQL schema and represents the canonical source for all product, purchase, subscription, and request shapes. After updating any schema definitions, run `bun run generate:types` to refresh the file. + +Key runtime helpers that build on these types live alongside them: + +- [`src/types.ts`](https://github.com/hyochan/react-native-iap/blob/main/src/types.ts) – auto-generated enums and interfaces +- [`src/utils/errorMapping.ts`](https://github.com/hyochan/react-native-iap/blob/main/src/utils/errorMapping.ts) – typed error helpers (`createPurchaseError`, `ErrorCodeUtils`) +- [`src/helpers/subscription.ts`](https://github.com/hyochan/react-native-iap/blob/main/src/helpers/subscription.ts) – subscription utilities that re-export `ActiveSubscription` + +Below is a curated overview of the most commonly used types. Consult [`src/types.ts`](https://github.com/hyochan/react-native-iap/blob/main/src/types.ts) for the full schema. + +## Core Type Aliases + +```ts +export type IapPlatform = 'android' | 'ios'; + +export type ProductType = 'in-app' | 'subs'; + +export type PurchaseState = + | 'deferred' + | 'failed' + | 'pending' + | 'purchased' + | 'restored' + | 'unknown'; +``` + +For `ErrorCode` enum and error handling utilities, see [Error Handling](./error-handling). + +## Product Types + +All products share the generated `ProductCommon` interface. Platform extensions discriminate on the `platform` field via the `IapPlatform` string union. + +```ts +export interface ProductCommon { + id: string; + title: string; + description: string; + type: ProductType; + displayName?: string | null; + displayPrice: string; + currency: string; + price?: number | null; + platform: IapPlatform; +} + +export interface ProductAndroid extends ProductCommon { + nameAndroid: string; + oneTimePurchaseOfferDetailsAndroid?: ProductAndroidOneTimePurchaseOfferDetail | null; + subscriptionOfferDetailsAndroid?: + | ProductSubscriptionAndroidOfferDetails[] + | null; +} + +export interface ProductIOS extends ProductCommon { + displayNameIOS: string; + isFamilyShareableIOS: boolean; + jsonRepresentationIOS: string; + typeIOS: ProductTypeIOS; + subscriptionInfoIOS?: SubscriptionInfoIOS | null; +} + +export type Product = ProductAndroid | ProductIOS; +export type ProductSubscription = + | ProductSubscriptionAndroid + | ProductSubscriptionIOS; +``` + +## Purchase Types + +Purchases share the `PurchaseCommon` shape and discriminate on the same `platform` union. Both variants expose the unified `purchaseToken` field for server validation. + +```ts +export interface PurchaseCommon { + id: string; + productId: string; + platform: IapPlatform; + purchaseState: PurchaseState; + transactionDate: number; + quantity: number; + isAutoRenewing: boolean; + purchaseToken?: string | null; + ids?: string[] | null; +} + +export interface PurchaseAndroid extends PurchaseCommon { + autoRenewingAndroid?: boolean | null; + packageNameAndroid?: string | null; + signatureAndroid?: string | null; + dataAndroid?: string | null; +} + +export interface PurchaseIOS extends PurchaseCommon { + appAccountToken?: string | null; + environmentIOS?: string | null; + expirationDateIOS?: number | null; + originalTransactionIdentifierIOS?: string | null; + offerIOS?: PurchaseOfferIOS | null; +} + +export type Purchase = PurchaseAndroid | PurchaseIOS; +``` + +## Active Subscriptions + +`ActiveSubscription` is now part of the generated schema and shared across helpers. + +```ts +export interface ActiveSubscription { + productId: string; + isActive: boolean; + transactionId: string; + transactionDate: number; + purchaseToken?: string | null; + autoRenewingAndroid?: boolean | null; + environmentIOS?: string | null; + expirationDateIOS?: number | null; + daysUntilExpirationIOS?: number | null; + willExpireSoon?: boolean | null; +} +``` + +The helper `getActiveSubscriptions` in `src/helpers/subscription.ts` converts `Purchase` records into this shape and re-exports the type for convenience. + +## Request Parameters + +The request types have been harmonised to match the schema definitions. + +```ts +export interface RequestPurchasePropsByPlatforms { + /** Apple-specific purchase parameters */ + apple?: RequestPurchaseIosProps | null; + /** Google-specific purchase parameters */ + google?: RequestPurchaseAndroidProps | null; + /** @deprecated Use apple instead */ + ios?: RequestPurchaseIosProps | null; + /** @deprecated Use google instead */ + android?: RequestPurchaseAndroidProps | null; +} + +export interface RequestSubscriptionPropsByPlatforms { + /** Apple-specific subscription parameters */ + apple?: RequestSubscriptionIosProps | null; + /** Google-specific subscription parameters */ + google?: RequestSubscriptionAndroidProps | null; + /** @deprecated Use apple instead */ + ios?: RequestSubscriptionIosProps | null; + /** @deprecated Use google instead */ + android?: RequestSubscriptionAndroidProps | null; +} + +export type MutationRequestPurchaseArgs = + | { + request: RequestPurchasePropsByPlatforms; + type: 'in-app'; + } + | { + request: RequestSubscriptionPropsByPlatforms; + type: 'subs'; + }; +``` + +## Purchase Verification + +Purchase verification results are platform-specific unions: + +```ts +export type PurchaseVerificationResult = + | PurchaseVerificationResultAndroid + | PurchaseVerificationResultIos; +``` + +Use the higher-level `validateReceipt` helper exported from `src/index.ts` for a strongly typed wrapper around the native modules. + +## Where to Find Everything + +- For the exhaustive list of enums and interfaces, open [`src/types.ts`](https://github.com/hyochan/react-native-iap/blob/main/src/types.ts). +- For error handling utilities (`createPurchaseError`, `ErrorCodeUtils`), see [`src/utils/errorMapping.ts`](https://github.com/hyochan/react-native-iap/blob/main/src/utils/errorMapping.ts). +- All generated types are re-exported from the package root so consumers can import from `react-native-iap` directly: + +```ts +import type { + Product, + Purchase, + ActiveSubscription, + RequestPurchaseProps, +} from 'react-native-iap'; +``` + +If you need to regenerate types place new schema definitions under the GraphQL inputs and rerun the generator. EOF diff --git a/docs/versioned_docs/version-14.4/api/use-iap.md b/docs/versioned_docs/version-14.4/api/use-iap.md new file mode 100644 index 000000000..92cebf42a --- /dev/null +++ b/docs/versioned_docs/version-14.4/api/use-iap.md @@ -0,0 +1,639 @@ +--- +title: useIAP Hook +sidebar_label: useIAP Hook +sidebar_position: 1 +--- + +import GreatFrontEndTopFixed from "@site/src/uis/GreatFrontEndTopFixed"; + +# useIAP Hook + + + +The `useIAP` hook is the main interface for interacting with in-app purchases in React Native IAP. It provides a comprehensive API for managing purchases, subscriptions, and error handling. + +## Import + +```tsx +import {useIAP} from 'react-native-iap'; +``` + +## Important: Hook Behavior + +The `useIAP` hook follows React Hooks conventions and differs from calling functions directly from `react-native-iap` (index exports): + +- **Automatic connection**: Automatically calls `initConnection` on mount and `endConnection` on unmount. +- **Void-returning methods**: Methods like `fetchProducts`, `requestPurchase`, `getAvailablePurchases`, etc. return `Promise` in the hook. They do not resolve to data. Instead, they update internal state exposed by the hook: `products`, `subscriptions`, `availablePurchases`, etc. +- **Don't await for data**: When using the hook, do not write `const x = await fetchProducts(...)`. Call the method, then read the corresponding state from the hook. + +## Basic Usage + +```tsx +const { + connected, + products, + subscriptions, + availablePurchases, + fetchProducts, + requestPurchase, + validateReceipt, +} = useIAP({ + onPurchaseSuccess: (purchase) => { + // Validate on your backend, then finish the transaction + console.log('Purchase successful:', purchase); + }, + onPurchaseError: (error) => { + console.error('Purchase failed:', error); + }, +}); +``` + +## Configuration Options + +### useIAP(options) + +| Parameter | Type | Required | Description | +| --------- | --------------- | -------- | -------------------- | +| `options` | `UseIAPOptions` | No | Configuration object | + +#### UseIAPOptions + +```tsx +interface UseIAPOptions { + onPurchaseSuccess?: (purchase: Purchase) => void; + onPurchaseError?: (error: PurchaseError) => void; + shouldAutoSyncPurchases?: boolean; // Controls auto sync behavior inside the hook + onPromotedProductIOS?: (product: Product) => void; // iOS promoted products +} +``` + +### Configuration Properties + +#### onPurchaseSuccess + +- **Type**: `(purchase: Purchase) => void` +- **Description**: Called when a purchase completes successfully +- **Example**: + + ```tsx + onPurchaseSuccess: (purchase) => { + // Grant user access to purchased content + unlockFeature(purchase.productId); + }; + ``` + +#### onPurchaseError + +- **Type**: `(error: PurchaseError) => void` +- **Description**: Called when a purchase fails +- **Example**: + + ```tsx + onPurchaseError: (error) => { + if (error.code !== ErrorCode.UserCancelled) { + Alert.alert('Purchase Failed', error.message); + } + }; + ``` + +#### autoFinishTransactions + +- **Type**: `boolean` +- **Default**: `true` +- **Description**: Whether to automatically finish transactions after successful purchases + +## Return Values + +### State Properties + +#### connected + +- **Type**: `boolean` +- **Description**: Whether the IAP service is connected and ready +- **Example**: + + ```tsx + if (connected) { + // Safe to make IAP calls + fetchProducts({skus: ['product.id'], type: 'in-app'}); + } + ``` + +#### products + +- **Type**: `Product[]` +- **Description**: Array of available products +- **Example**: + + ```tsx + products.map((product) => ); + ``` + +#### subscriptions + +- **Type**: `ProductSubscription[]` +- **Description**: Array of available subscription products +- **Example**: + + ```tsx + subscriptions.map((subscription) => ( + + )); + ``` + +#### currentPurchaseError + +- **Type**: `PurchaseError | null` +- **Description**: Current purchase error (if any) +- **Example**: + + ```tsx + useEffect(() => { + if (currentPurchaseError) { + handlePurchaseError(currentPurchaseError); + } + }, [currentPurchaseError]); + ``` + +#### availablePurchases + +- **Type**: `Purchase[]` +- **Description**: Array of available purchases (restorable items) +- **Example**: + + ```tsx + availablePurchases.map((purchase) => ( + + )); + ``` + +#### promotedProductIOS + +- **Type**: `Product | undefined` +- **Description**: The promoted product details (iOS only) +- **Example**: + + ```tsx + useEffect(() => { + if (promotedProductIOS) { + // Handle promoted product + handlePromotedProduct(promotedProductIOS); + } + }, [promotedProductIOS]); + ``` + +### Methods + +#### fetchProducts + +- **Type**: `(params: { skus: string[]; type?: 'in-app' | 'subs' }) => Promise` +- **Description**: Fetch products or subscriptions and update `products` / `subscriptions` state. In the hook this returns `void` (no data result), by design. +- **Do not await for data**: Call it, then consume `products` / `subscriptions` state from the hook. +- **Example**: + + ```tsx + useEffect(() => { + if (!connected) return; + // In hook: returns void, updates state + fetchProducts({ + skus: ['com.app.premium', 'com.app.coins_100'], + type: 'in-app', + }); + fetchProducts({skus: ['com.app.premium_monthly'], type: 'subs'}); + }, [connected, fetchProducts]); + + // Later in render/effects + products.forEach((p) => console.log('product', p.id)); + subscriptions.forEach((s) => console.log('sub', s.id)); + ``` + +#### requestPurchase + +- **Type**: `(request: RequestPurchaseProps) => Promise` +- **Description**: Initiate a purchase request +- **Parameters**: + - `request`: Purchase request configuration +- **Example**: + + ```tsx + const buyProduct = async (productId: string) => { + try { + // In hook: returns void. Listen via callbacks. + await requestPurchase({ + request: { + ios: {sku: productId}, + android: {skus: [productId]}, + }, + }); + } catch (error) { + console.error('Purchase request failed:', error); + } + }; + ``` + +### Subscription Offers + +When purchasing subscriptions, you need to specify the pricing plan (offer) for each platform: + +#### Android Subscription Offers + +Android requires `subscriptionOffers` array containing offer tokens from `fetchProducts()`. Each offer token represents a specific pricing plan (base plan, introductory offer, etc.). + +```tsx +const buySubscription = async (subscriptionId: string) => { + // 1) Fetch subscription products first + await fetchProducts({skus: [subscriptionId], type: 'subs'}); + + // 2) Find the subscription and build offers + const subscription = subscriptions.find((s) => s.id === subscriptionId); + if (!subscription) return; + + const subscriptionOffers = ( + subscription.subscriptionOfferDetailsAndroid ?? [] + ).map((offer) => ({ + sku: subscriptionId, + offerToken: offer.offerToken, + })); + + // 3) Request purchase with offers + await requestPurchase({ + request: { + ios: {sku: subscriptionId}, + android: { + skus: [subscriptionId], + // Only include subscriptionOffers when offers are available + ...(subscriptionOffers.length > 0 && {subscriptionOffers}), + }, + }, + type: 'subs', + }); +}; +``` + +**Note**: `subscriptionOffers` should only be included when subscription offers are available from `fetchProducts()`. Without offers, Android purchases will fail. + +#### iOS Subscription Offers + +iOS uses `withOffer` for promotional discounts configured in App Store Connect. This is optional and only needed for special promotional pricing. + +```tsx +const buySubscriptionWithOffer = async ( + subscriptionId: string, + discountOffer?: DiscountOfferInputIOS, +) => { + await requestPurchase({ + request: { + ios: { + sku: subscriptionId, + // Optional: apply promotional offer + ...(discountOffer && {withOffer: discountOffer}), + }, + android: {skus: [subscriptionId]}, + }, + type: 'subs', + }); +}; +``` + +#### Subscription helpers (hook) + +- `getActiveSubscriptions(subscriptionIds?) => Promise` + - Returns active subscription info and also updates `activeSubscriptions` state. + - Exception to the hook’s void-return design: this method returns data for convenience. + - Example: + + ```tsx + const {getActiveSubscriptions, activeSubscriptions} = useIAP(); + + useEffect(() => { + if (!connected) return; + (async () => { + const subs = await getActiveSubscriptions(['premium_monthly']); + console.log('Subs from return:', subs.length); + console.log('Subs from state:', activeSubscriptions.length); + })(); + }, [connected]); + ``` + +- `hasActiveSubscriptions(subscriptionIds?) => Promise` + - Boolean convenience check to see if any active subscriptions exist (optionally filtered by IDs). + +> Removed in v2.9.0: `purchaseHistories` state and `getPurchaseHistories()` method. Use `getAvailablePurchases()` and `availablePurchases` instead. + +#### getAvailablePurchases + +- **Type**: `() => Promise` +- **Description**: Fetch available purchases (restorable items) from the store +- **Example**: + + ```tsx + const restorePurchases = async () => { + try { + // Updates `availablePurchases` state; do not expect a return value + await getAvailablePurchases(); + // Read from state afterwards + console.log('Available purchases count:', availablePurchases.length); + } catch (error) { + console.error('Failed to fetch available purchases:', error); + } + }; + ``` + +#### validateReceipt + +- **Type**: `(productId: string, params?: ValidationParams) => Promise` +- **Description**: Validate a purchase receipt +- **Parameters**: + - `productId`: ID of the product to validate + - `params`: **Required for Android**, optional for iOS: + - `packageName` (string, Android): Package name of your app + - `productToken` (string, Android): Purchase token from the purchase + - `accessToken` (string, Android): Optional access token for server validation + - `isSub` (boolean, Android): Whether this is a subscription +- **Returns**: Promise resolving to validation result + +**Important Platform Differences:** + +- **iOS**: Only requires the product ID +- **Android**: Requires additional parameters (packageName, productToken) + +- **Example**: + + ```tsx + const validatePurchase = async (productId: string, purchase: any) => { + try { + if (Platform.OS === 'ios') { + // iOS: Simple validation with just product ID + const result = await validateReceipt(productId); + return result; + } else if (Platform.OS === 'android') { + // Android: Requires additional parameters + const purchaseToken = purchase.purchaseToken; + const packageName = purchase.packageNameAndroid; + + if (!purchaseToken || !packageName) { + throw new Error( + 'Android validation requires packageName and productToken', + ); + } + + const result = await validateReceipt(productId, { + packageName, + productToken: purchaseToken, + isSub: false, // Set to true for subscriptions + }); + return result; + } + } catch (error) { + console.error('Validation failed:', error); + throw error; + } + }; + ``` + +#### getPromotedProductIOS + +- **Type**: `() => Promise` +- **Description**: Get the promoted product details (iOS only) +- **Example**: + + ```tsx + const handlePromotedProduct = async () => { + const promotedProduct = await getPromotedProductIOS(); + if (promotedProduct) { + console.log('Promoted product:', promotedProduct); + // Show custom purchase UI + } + }; + ``` + +#### requestPurchaseOnPromotedProductIOS + +- **Type**: `() => Promise` +- **Description**: Complete the purchase of a promoted product (iOS only) + > Removed in v2.9.0: `buyPromotedProductIOS`. Use `requestPurchaseOnPromotedProductIOS` instead. +- **Example**: + + ```tsx + const completePurchase = async () => { + try { + await requestPurchaseOnPromotedProductIOS(); + console.log('Promoted product purchase completed'); + } catch (error) { + console.error('Failed to purchase promoted product:', error); + } + }; + ``` + +## Platform-Specific Usage + +### iOS Example + +```tsx +const IOSPurchaseExample = () => { + const {connected, products, requestPurchase, validateReceipt} = useIAP({ + onPurchaseSuccess: async (purchase) => { + // Validate receipt on iOS + const validation = await validateReceipt(purchase.productId); + if (validation.isValid) { + unlockContent(purchase.productId); + } + }, + }); + + const buyProduct = (product: Product) => { + requestPurchase({ + request: { + ios: {sku: product.id}, + android: {skus: [product.id]}, + }, + }); + }; + + return ( + + {products + .filter((p) => p.platform === 'ios') + .map((product) => ( +