react-native-iap
Version:
React Native In-App Purchases module for iOS and Android using Nitro
1,146 lines (1,083 loc) • 36 kB
JavaScript
"use strict";
// External dependencies
import { Platform } from 'react-native';
// Side-effect import ensures Nitro installs its dispatcher before IAP is used (no-op in tests)
import 'react-native-nitro-modules';
import { NitroModules } from 'react-native-nitro-modules';
// Internal modules
import { ProductQueryType } from "./types.js";
import { convertNitroProductToProduct, convertNitroPurchaseToPurchase, validateNitroProduct, validateNitroPurchase, convertNitroSubscriptionStatusToSubscriptionStatusIOS } from "./utils/type-bridge.js";
import { parseErrorStringToJsonObj } from "./utils/error.js";
import { normalizeErrorCodeFromNative } from "./utils/errorMapping.js";
// Export all types
export * from "./types.js";
export * from "./utils/error.js";
// Internal constants/helpers for bridging legacy Nitro expectations
const NITRO_PRODUCT_TYPE_INAPP = 'inapp';
const NITRO_PRODUCT_TYPE_SUBS = 'subs';
function toNitroProductType(type) {
return type === ProductQueryType.Subs ? NITRO_PRODUCT_TYPE_SUBS : NITRO_PRODUCT_TYPE_INAPP;
}
function isSubscriptionQuery(type) {
return type === ProductQueryType.Subs;
}
function normalizeProductQueryType(type) {
if (type === ProductQueryType.All || type === 'all') {
return ProductQueryType.All;
}
if (type === ProductQueryType.Subs || type === 'subs') {
return ProductQueryType.Subs;
}
if (type === ProductQueryType.InApp || type === 'inapp') {
return ProductQueryType.InApp;
}
return ProductQueryType.InApp;
}
// ActiveSubscription and PurchaseError types are already exported via 'export * from ./types'
// Export hooks
export { useIAP } from "./hooks/useIAP.js";
// iOS promoted product aliases for API parity
export const getPromotedProductIOS = async () => requestPromotedProductIOS();
export const requestPurchaseOnPromotedProductIOS = async () => buyPromotedProductIOS();
// Restore completed transactions (cross-platform)
export const restorePurchases = async (options = {
alsoPublishToEventListenerIOS: false,
onlyIncludeActiveItemsIOS: true
}) => {
if (Platform.OS === 'ios') {
await syncIOS();
}
return getAvailablePurchases(options);
};
// Development utilities removed - use type bridge functions directly if needed
// Create the RnIap HybridObject instance lazily to avoid early JSI crashes
let iapRef = null;
const IAP = {
get instance() {
if (iapRef) return iapRef;
// Attempt to create the HybridObject and map common Nitro/JSI readiness errors
try {
iapRef = NitroModules.createHybridObject('RnIap');
} catch (e) {
const msg = String(e?.message ?? e ?? '');
if (msg.includes('Nitro') || msg.includes('JSI') || msg.includes('dispatcher') || msg.includes('HybridObject')) {
throw new Error('Nitro runtime not installed yet. Ensure react-native-nitro-modules is initialized before calling IAP.');
}
throw e;
}
return iapRef;
}
};
/**
* Initialize connection to the store
*/
export const initConnection = async () => {
try {
return await IAP.instance.initConnection();
} catch (error) {
console.error('Failed to initialize IAP connection:', error);
throw error;
}
};
/**
* End connection to the store
*/
export const endConnection = async () => {
try {
// If never initialized, treat as ended
if (!iapRef) return true;
return await IAP.instance.endConnection();
} catch (error) {
console.error('Failed to end IAP connection:', error);
throw error;
}
};
/**
* Fetch products from the store
* @param params - Product request configuration
* @param params.skus - Array of product SKUs to fetch
* @param params.type - Optional filter: 'inapp' (default) for products, 'subs' for subscriptions, or 'all' for both.
* @returns Promise<Product[]> - Array of products from the store
*
* @example
* ```typescript
* // Regular products
* const products = await fetchProducts({ skus: ['product1', 'product2'] });
*
* // Subscriptions
* const subscriptions = await fetchProducts({ skus: ['sub1', 'sub2'], type: 'subs' });
* ```
*/
export const fetchProducts = async ({
skus,
type = ProductQueryType.InApp
}) => {
try {
if (!skus || skus.length === 0) {
throw new Error('No SKUs provided');
}
const normalizedType = normalizeProductQueryType(type);
if (normalizedType === ProductQueryType.All) {
const [inappNitro, subsNitro] = await Promise.all([IAP.instance.fetchProducts(skus, NITRO_PRODUCT_TYPE_INAPP), IAP.instance.fetchProducts(skus, NITRO_PRODUCT_TYPE_SUBS)]);
const allNitro = [...inappNitro, ...subsNitro];
const validAll = allNitro.filter(validateNitroProduct);
if (validAll.length !== allNitro.length) {
console.warn(`[fetchProducts] Some products failed validation: ${allNitro.length - validAll.length} invalid`);
}
return validAll.map(convertNitroProductToProduct);
}
const nitroProducts = await IAP.instance.fetchProducts(skus, toNitroProductType(normalizedType));
// Validate and convert NitroProducts to TypeScript Products
const validProducts = nitroProducts.filter(validateNitroProduct);
if (validProducts.length !== nitroProducts.length) {
console.warn(`[fetchProducts] Some products failed validation: ${nitroProducts.length - validProducts.length} invalid`);
}
const typedProducts = validProducts.map(convertNitroProductToProduct);
return typedProducts;
} catch (error) {
console.error('[fetchProducts] Failed:', error);
throw error;
}
};
/**
* Request a purchase for products or subscriptions
* @param params - Purchase request configuration
* @param params.request - Platform-specific purchase parameters
* @param params.type - Type of purchase: 'inapp' for products (default) or 'subs' for subscriptions
*
* @example
* ```typescript
* // Product purchase
* await requestPurchase({
* request: {
* ios: { sku: productId },
* android: { skus: [productId] }
* },
* type: 'inapp'
* });
*
* // Subscription purchase
* await requestPurchase({
* request: {
* ios: { sku: subscriptionId },
* android: {
* skus: [subscriptionId],
* subscriptionOffers: [{ sku: subscriptionId, offerToken: 'token' }]
* }
* },
* type: 'subs'
* });
* ```
*/
/**
* Request a purchase for products or subscriptions
* ⚠️ Important: This is an event-based operation, not promise-based.
* Listen for events through purchaseUpdatedListener or purchaseErrorListener.
* @param params - Purchase request configuration
* @param params.requestPurchase - Platform-specific purchase parameters (in-app)
* @param params.requestSubscription - Platform-specific subscription parameters (subs)
* @param params.type - Type of purchase (defaults to in-app)
*/
export const requestPurchase = async params => {
try {
const {
requestPurchase: purchaseRequest,
requestSubscription
} = params;
const normalizedPurchaseRequest = purchaseRequest ?? undefined;
const normalizedSubscriptionRequest = requestSubscription ?? undefined;
const effectiveType = normalizeProductQueryType(params.type);
const isSubs = isSubscriptionQuery(effectiveType);
let request;
if (isSubs) {
if (__DEV__ && normalizedPurchaseRequest && !normalizedSubscriptionRequest) {
console.warn('[react-native-iap] `requestPurchase` was provided for a subscription request. Did you mean to use `requestSubscription`?');
}
request = normalizedSubscriptionRequest ?? normalizedPurchaseRequest;
} else {
if (__DEV__ && normalizedSubscriptionRequest && !normalizedPurchaseRequest) {
console.warn('[react-native-iap] `requestSubscription` was provided for an in-app purchase request. Did you mean to use `requestPurchase`?');
}
request = normalizedPurchaseRequest ?? normalizedSubscriptionRequest;
}
if (!request) {
throw new Error('Missing purchase request configuration');
}
// Validate platform-specific requests
if (Platform.OS === 'ios') {
const iosRequest = request.ios;
if (!iosRequest?.sku) {
throw new Error('Invalid request for iOS. The `sku` property is required.');
}
} else if (Platform.OS === 'android') {
const androidRequest = request.android;
if (!androidRequest?.skus?.length) {
throw new Error('Invalid request for Android. The `skus` property is required and must be a non-empty array.');
}
} else {
throw new Error('Unsupported platform');
}
// Transform the request for the unified interface
const unifiedRequest = {};
if (Platform.OS === 'ios' && request.ios) {
const iosReq = request.ios;
const autoFinishSubs = isSubs && iosReq.andDangerouslyFinishTransactionAutomatically == null;
unifiedRequest.ios = {
...iosReq,
// Align with native SwiftUI flow: auto-finish subscriptions by default
...(autoFinishSubs ? {
andDangerouslyFinishTransactionAutomatically: true
} : {})
};
}
if (Platform.OS === 'android' && request.android) {
if (isSubs) {
const subsRequest = request.android;
unifiedRequest.android = {
...subsRequest,
subscriptionOffers: subsRequest.subscriptionOffers || []
};
} else {
unifiedRequest.android = request.android;
}
}
// Call unified method - returns void, listen for events instead
await IAP.instance.requestPurchase(unifiedRequest);
} catch (error) {
console.error('Failed to request purchase:', error);
throw error;
}
};
/**
* Get available purchases (purchased items not yet consumed/finished)
* @param params - Options for getting available purchases
* @param params.alsoPublishToEventListener - Whether to also publish to event listener
* @param params.onlyIncludeActiveItems - Whether to only include active items
*
* @example
* ```typescript
* const purchases = await getAvailablePurchases({
* onlyIncludeActiveItemsIOS: true
* });
* ```
*/
export const getAvailablePurchases = async ({
alsoPublishToEventListenerIOS = false,
onlyIncludeActiveItemsIOS = true
} = {}) => {
try {
// Create unified options
const options = {};
if (Platform.OS === 'ios') {
// Provide both new and deprecated keys for compatibility
options.ios = {
alsoPublishToEventListenerIOS,
onlyIncludeActiveItemsIOS,
alsoPublishToEventListener: alsoPublishToEventListenerIOS,
onlyIncludeActiveItems: onlyIncludeActiveItemsIOS
};
} else if (Platform.OS === 'android') {
// For Android, we need to call twice for inapp and subs
const inappNitroPurchases = await IAP.instance.getAvailablePurchases({
android: {
type: 'inapp'
}
});
const subsNitroPurchases = await IAP.instance.getAvailablePurchases({
android: {
type: 'subs'
}
});
// Validate and convert both sets of purchases
const allNitroPurchases = [...inappNitroPurchases, ...subsNitroPurchases];
const validPurchases = allNitroPurchases.filter(validateNitroPurchase);
if (validPurchases.length !== allNitroPurchases.length) {
console.warn(`[getAvailablePurchases] Some Android purchases failed validation: ${allNitroPurchases.length - validPurchases.length} invalid`);
}
return validPurchases.map(convertNitroPurchaseToPurchase);
} else {
throw new Error('Unsupported platform');
}
const nitroPurchases = await IAP.instance.getAvailablePurchases(options);
// Validate and convert NitroPurchases to TypeScript Purchases
const validPurchases = nitroPurchases.filter(validateNitroPurchase);
if (validPurchases.length !== nitroPurchases.length) {
console.warn(`[getAvailablePurchases] Some purchases failed validation: ${nitroPurchases.length - validPurchases.length} invalid`);
}
return validPurchases.map(convertNitroPurchaseToPurchase);
} catch (error) {
console.error('Failed to get available purchases:', error);
throw error;
}
};
/**
* Finish a transaction (consume or acknowledge)
* @param params - Transaction finish parameters
* @param params.purchase - The purchase to finish
* @param params.isConsumable - Whether this is a consumable product (Android only)
*
* @example
* ```typescript
* await finishTransaction({
* purchase: myPurchase,
* isConsumable: true
* });
* ```
*/
export const finishTransaction = async ({
purchase,
isConsumable = false
}) => {
try {
// Create unified params
const params = {};
if (Platform.OS === 'ios') {
if (!purchase.id) {
throw new Error('purchase.id required to finish iOS transaction');
}
params.ios = {
transactionId: purchase.id
};
} else if (Platform.OS === 'android') {
const androidPurchase = purchase;
const token = androidPurchase.purchaseToken;
if (!token) {
throw new Error('purchaseToken required to finish Android transaction');
}
params.android = {
purchaseToken: token,
isConsumable
};
} else {
throw new Error('Unsupported platform');
}
const result = await IAP.instance.finishTransaction(params);
// Handle variant return type
if (typeof result === 'boolean') {
return result;
}
// It's a PurchaseResult
return result;
} catch (error) {
// If iOS transaction has already been auto-finished natively, treat as success
if (Platform.OS === 'ios') {
const err = parseErrorStringToJsonObj(error);
const msg = (err?.message || '').toString();
const code = (err?.code || '').toString();
if (msg.includes('Transaction not found') || code === 'E_ITEM_UNAVAILABLE') {
// Consider already finished
return true;
}
}
console.error('Failed to finish transaction:', error);
throw error;
}
};
/**
* Acknowledge a purchase (Android only)
* @param purchaseToken - The purchase token to acknowledge
*
* @example
* ```typescript
* await acknowledgePurchaseAndroid('purchase_token_here');
* ```
*/
export const acknowledgePurchaseAndroid = async purchaseToken => {
try {
if (Platform.OS !== 'android') {
throw new Error('acknowledgePurchaseAndroid is only available on Android');
}
const result = await IAP.instance.finishTransaction({
android: {
purchaseToken,
isConsumable: false
}
});
// Result is a variant, extract PurchaseResult
if (typeof result === 'boolean') {
// This shouldn't happen for Android, but handle it
return {
responseCode: 0,
code: '0',
message: 'Success',
purchaseToken
};
}
return result;
} catch (error) {
console.error('Failed to acknowledge purchase Android:', error);
throw error;
}
};
/**
* Consume a purchase (Android only)
* @param purchaseToken - The purchase token to consume
*
* @example
* ```typescript
* await consumePurchaseAndroid('purchase_token_here');
* ```
*/
export const consumePurchaseAndroid = async purchaseToken => {
try {
if (Platform.OS !== 'android') {
throw new Error('consumePurchaseAndroid is only available on Android');
}
const result = await IAP.instance.finishTransaction({
android: {
purchaseToken,
isConsumable: true
}
});
// Result is a variant, extract PurchaseResult
if (typeof result === 'boolean') {
// This shouldn't happen for Android, but handle it
return {
responseCode: 0,
code: '0',
message: 'Success',
purchaseToken
};
}
return result;
} catch (error) {
console.error('Failed to consume purchase Android:', error);
throw error;
}
};
// ============================================================================
// EVENT LISTENERS
// ============================================================================
// Store wrapped listeners for proper removal
const listenerMap = new WeakMap();
/**
* Purchase updated event listener
* Fired when a purchase is successful or when a pending purchase is completed.
*
* @param listener - Function to call when a purchase is updated
* @returns EventSubscription object with remove method
*
* @example
* ```typescript
* const subscription = purchaseUpdatedListener((purchase) => {
* console.log('Purchase successful:', purchase);
* // 1. Validate receipt with backend
* // 2. Deliver content to user
* // 3. Call finishTransaction to acknowledge
* });
*
* // Later, clean up
* subscription.remove();
* ```
*/
export const purchaseUpdatedListener = listener => {
// Wrap the listener to convert NitroPurchase to Purchase
const wrappedListener = nitroPurchase => {
if (validateNitroPurchase(nitroPurchase)) {
const convertedPurchase = convertNitroPurchaseToPurchase(nitroPurchase);
listener(convertedPurchase);
} else {
console.error('Invalid purchase data received from native:', nitroPurchase);
}
};
// Store the wrapped listener for removal
listenerMap.set(listener, wrappedListener);
let attached = false;
try {
IAP.instance.addPurchaseUpdatedListener(wrappedListener);
attached = true;
} catch (e) {
const msg = String(e ?? '');
if (msg.includes('Nitro runtime not installed')) {
console.warn('[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()');
} else {
throw e;
}
}
return {
remove: () => {
const wrapped = listenerMap.get(listener);
if (wrapped) {
if (attached) {
try {
IAP.instance.removePurchaseUpdatedListener(wrapped);
} catch {}
}
listenerMap.delete(listener);
}
}
};
};
/**
* Purchase error event listener
* Fired when a purchase fails or is cancelled by the user.
*
* @param listener - Function to call when a purchase error occurs
* @returns EventSubscription object with remove method
*
* @example
* ```typescript
* const subscription = purchaseErrorListener((error) => {
* switch (error.code) {
* case 'E_USER_CANCELLED':
* // User cancelled - no action needed
* break;
* case 'E_ITEM_UNAVAILABLE':
* // Product not available
* break;
* case 'E_NETWORK_ERROR':
* // Retry with backoff
* break;
* }
* });
*
* // Later, clean up
* subscription.remove();
* ```
*/
export const purchaseErrorListener = listener => {
const wrapped = error => {
listener({
code: normalizeErrorCodeFromNative(error.code),
message: error.message,
productId: undefined
});
};
listenerMap.set(listener, wrapped);
let attached = false;
try {
IAP.instance.addPurchaseErrorListener(wrapped);
attached = true;
} catch (e) {
const msg = String(e ?? '');
if (msg.includes('Nitro runtime not installed')) {
console.warn('[purchaseErrorListener] Nitro not ready yet; listener inert until initConnection()');
} else {
throw e;
}
}
return {
remove: () => {
const stored = listenerMap.get(listener);
if (stored) {
if (attached) {
try {
IAP.instance.removePurchaseErrorListener(stored);
} catch {}
}
listenerMap.delete(listener);
}
}
};
};
/**
* iOS-only listener for App Store promoted product events.
* Fired when a user clicks on a promoted in-app purchase in the App Store.
*
* @param listener - Callback function that receives the promoted product
* @returns EventSubscription object with remove method
*
* @example
* ```typescript
* const subscription = promotedProductListenerIOS((product) => {
* console.log('Promoted product:', product);
* // Trigger purchase flow for the promoted product
* });
*
* // Later, clean up
* subscription.remove();
* ```
*
* @platform iOS
*/
export const promotedProductListenerIOS = listener => {
if (Platform.OS !== 'ios') {
console.warn('promotedProductListenerIOS: This listener is only available on iOS');
return {
remove: () => {}
};
}
// Wrap the listener to convert NitroProduct to Product
const wrappedListener = nitroProduct => {
if (validateNitroProduct(nitroProduct)) {
const convertedProduct = convertNitroProductToProduct(nitroProduct);
listener(convertedProduct);
} else {
console.error('Invalid promoted product data received from native:', nitroProduct);
}
};
// Store the wrapped listener for removal
listenerMap.set(listener, wrappedListener);
let attached = false;
try {
IAP.instance.addPromotedProductListenerIOS(wrappedListener);
attached = true;
} catch (e) {
const msg = String(e ?? '');
if (msg.includes('Nitro runtime not installed')) {
console.warn('[promotedProductListenerIOS] Nitro not ready yet; listener inert until initConnection()');
} else {
throw e;
}
}
return {
remove: () => {
const wrapped = listenerMap.get(listener);
if (wrapped) {
if (attached) {
try {
IAP.instance.removePromotedProductListenerIOS(wrapped);
} catch {}
}
listenerMap.delete(listener);
}
}
};
};
// ============================================================================
// iOS-SPECIFIC FUNCTIONS
// ============================================================================
/**
* Validate receipt on both iOS and Android platforms
* @param sku - Product SKU
* @param androidOptions - Android-specific validation options (required for Android)
* @returns Promise<ReceiptValidationResultIOS | ReceiptValidationResultAndroid> - Platform-specific receipt validation result
*/
export const validateReceipt = async (sku, androidOptions) => {
try {
const params = {
sku,
androidOptions
};
const nitroResult = await IAP.instance.validateReceipt(params);
// Convert Nitro result to public API result
if (Platform.OS === 'ios') {
const iosResult = nitroResult;
const result = {
isValid: iosResult.isValid,
receiptData: iosResult.receiptData,
jwsRepresentation: iosResult.jwsRepresentation,
latestTransaction: iosResult.latestTransaction ? convertNitroPurchaseToPurchase(iosResult.latestTransaction) : undefined
};
return result;
} else {
// Android
const androidResult = nitroResult;
const result = {
autoRenewing: androidResult.autoRenewing,
betaProduct: androidResult.betaProduct,
cancelDate: androidResult.cancelDate,
cancelReason: androidResult.cancelReason,
deferredDate: androidResult.deferredDate,
deferredSku: androidResult.deferredSku?.toString() ?? null,
freeTrialEndDate: androidResult.freeTrialEndDate,
gracePeriodEndDate: androidResult.gracePeriodEndDate,
parentProductId: androidResult.parentProductId,
productId: androidResult.productId,
productType: androidResult.productType === 'subs' ? 'subs' : 'inapp',
purchaseDate: androidResult.purchaseDate,
quantity: androidResult.quantity,
receiptId: androidResult.receiptId,
renewalDate: androidResult.renewalDate,
term: androidResult.term,
termSku: androidResult.termSku,
testTransaction: androidResult.testTransaction
};
return result;
}
} catch (error) {
console.error('[validateReceipt] Failed:', error);
const errorJson = parseErrorStringToJsonObj(error);
throw new Error(errorJson.message);
}
};
/**
* Sync iOS purchases with App Store (iOS only)
* @returns Promise<boolean>
* @platform iOS
*/
export const syncIOS = async () => {
if (Platform.OS !== 'ios') {
throw new Error('syncIOS is only available on iOS');
}
try {
return await IAP.instance.syncIOS();
} catch (error) {
console.error('[syncIOS] Failed:', error);
const errorJson = parseErrorStringToJsonObj(error);
throw new Error(errorJson.message);
}
};
/**
* Request the promoted product from the App Store (iOS only)
* @returns Promise<Product | null> - The promoted product or null if none available
* @platform iOS
*/
export const requestPromotedProductIOS = async () => {
if (Platform.OS !== 'ios') {
return null;
}
try {
const nitroProduct = await IAP.instance.requestPromotedProductIOS();
if (nitroProduct) {
return convertNitroProductToProduct(nitroProduct);
}
return null;
} catch (error) {
console.error('[getPromotedProductIOS] Failed:', error);
const errorJson = parseErrorStringToJsonObj(error);
throw new Error(errorJson.message);
}
};
/**
* Present the code redemption sheet for offer codes (iOS only)
* @returns Promise<boolean> - True if the sheet was presented successfully
* @platform iOS
*/
export const presentCodeRedemptionSheetIOS = async () => {
if (Platform.OS !== 'ios') {
return false;
}
try {
return await IAP.instance.presentCodeRedemptionSheetIOS();
} catch (error) {
console.error('[presentCodeRedemptionSheetIOS] Failed:', error);
const errorJson = parseErrorStringToJsonObj(error);
throw new Error(errorJson.message);
}
};
/**
* Buy promoted product on iOS
* @returns Promise<void>
* @platform iOS
*/
export const buyPromotedProductIOS = async () => {
if (Platform.OS !== 'ios') {
throw new Error('buyPromotedProductIOS is only available on iOS');
}
try {
await IAP.instance.buyPromotedProductIOS();
} catch (error) {
console.error('[buyPromotedProductIOS] Failed:', error);
const errorJson = parseErrorStringToJsonObj(error);
throw new Error(errorJson.message);
}
};
/**
* Clear unfinished transactions on iOS
* @returns Promise<void>
* @platform iOS
*/
export const clearTransactionIOS = async () => {
if (Platform.OS !== 'ios') {
return;
}
try {
await IAP.instance.clearTransactionIOS();
} catch (error) {
console.error('[clearTransactionIOS] Failed:', error);
const errorJson = parseErrorStringToJsonObj(error);
throw new Error(errorJson.message);
}
};
/**
* Begin a refund request for a product on iOS 15+
* @param sku - The product SKU to refund
* @returns Promise<string | null> - The refund status or null if not available
* @platform iOS
*/
export const beginRefundRequestIOS = async sku => {
if (Platform.OS !== 'ios') {
return null;
}
try {
return await IAP.instance.beginRefundRequestIOS(sku);
} catch (error) {
console.error('[beginRefundRequestIOS] Failed:', error);
const errorJson = parseErrorStringToJsonObj(error);
throw new Error(errorJson.message);
}
};
/**
* Get subscription status for a product (iOS only)
* @param sku - The product SKU
* @returns Promise<SubscriptionStatusIOS[]> - Array of subscription status objects
* @throws Error when called on non-iOS platforms or when IAP is not initialized
* @platform iOS
*/
export const subscriptionStatusIOS = async sku => {
if (Platform.OS !== 'ios') {
throw new Error('subscriptionStatusIOS is only available on iOS');
}
try {
const statuses = await IAP.instance.subscriptionStatusIOS(sku);
if (!statuses || !Array.isArray(statuses)) return [];
return statuses.map(s => convertNitroSubscriptionStatusToSubscriptionStatusIOS(s));
} catch (error) {
console.error('[subscriptionStatusIOS] Failed:', error);
const errorJson = parseErrorStringToJsonObj(error);
throw new Error(errorJson.message);
}
};
/**
* Get current entitlement for a product (iOS only)
* @param sku - The product SKU
* @returns Promise<Purchase | null> - Current entitlement or null
* @platform iOS
*/
export const currentEntitlementIOS = async sku => {
if (Platform.OS !== 'ios') {
return null;
}
try {
const nitroPurchase = await IAP.instance.currentEntitlementIOS(sku);
if (nitroPurchase) {
return convertNitroPurchaseToPurchase(nitroPurchase);
}
return null;
} catch (error) {
console.error('[currentEntitlementIOS] Failed:', error);
const errorJson = parseErrorStringToJsonObj(error);
throw new Error(errorJson.message);
}
};
/**
* Get latest transaction for a product (iOS only)
* @param sku - The product SKU
* @returns Promise<Purchase | null> - Latest transaction or null
* @platform iOS
*/
export const latestTransactionIOS = async sku => {
if (Platform.OS !== 'ios') {
return null;
}
try {
const nitroPurchase = await IAP.instance.latestTransactionIOS(sku);
if (nitroPurchase) {
return convertNitroPurchaseToPurchase(nitroPurchase);
}
return null;
} catch (error) {
console.error('[latestTransactionIOS] Failed:', error);
const errorJson = parseErrorStringToJsonObj(error);
throw new Error(errorJson.message);
}
};
/**
* Get pending transactions (iOS only)
* @returns Promise<Purchase[]> - Array of pending transactions
* @platform iOS
*/
export const getPendingTransactionsIOS = async () => {
if (Platform.OS !== 'ios') {
return [];
}
try {
const nitroPurchases = await IAP.instance.getPendingTransactionsIOS();
return nitroPurchases.map(convertNitroPurchaseToPurchase);
} catch (error) {
console.error('[getPendingTransactionsIOS] Failed:', error);
const errorJson = parseErrorStringToJsonObj(error);
throw new Error(errorJson.message);
}
};
/**
* Show manage subscriptions screen (iOS only)
* @returns Promise<Purchase[]> - Subscriptions where auto-renewal status changed
* @platform iOS
*/
export const showManageSubscriptionsIOS = async () => {
if (Platform.OS !== 'ios') {
return [];
}
try {
const nitroPurchases = await IAP.instance.showManageSubscriptionsIOS();
return nitroPurchases.map(convertNitroPurchaseToPurchase);
} catch (error) {
console.error('[showManageSubscriptionsIOS] Failed:', error);
const errorJson = parseErrorStringToJsonObj(error);
throw new Error(errorJson.message);
}
};
/**
* Check if user is eligible for intro offer (iOS only)
* @param groupID - The subscription group ID
* @returns Promise<boolean> - Eligibility status
* @platform iOS
*/
export const isEligibleForIntroOfferIOS = async groupID => {
if (Platform.OS !== 'ios') {
return false;
}
try {
return await IAP.instance.isEligibleForIntroOfferIOS(groupID);
} catch (error) {
console.error('[isEligibleForIntroOfferIOS] Failed:', error);
const errorJson = parseErrorStringToJsonObj(error);
throw new Error(errorJson.message);
}
};
/**
* Get receipt data (iOS only)
* @returns Promise<string> - Base64 encoded receipt data
* @platform iOS
*/
export const getReceiptDataIOS = async () => {
if (Platform.OS !== 'ios') {
throw new Error('getReceiptDataIOS is only available on iOS');
}
try {
return await IAP.instance.getReceiptDataIOS();
} catch (error) {
console.error('[getReceiptDataIOS] Failed:', error);
const errorJson = parseErrorStringToJsonObj(error);
throw new Error(errorJson.message);
}
};
/**
* Check if transaction is verified (iOS only)
* @param sku - The product SKU
* @returns Promise<boolean> - Verification status
* @platform iOS
*/
export const isTransactionVerifiedIOS = async sku => {
if (Platform.OS !== 'ios') {
return false;
}
try {
return await IAP.instance.isTransactionVerifiedIOS(sku);
} catch (error) {
console.error('[isTransactionVerifiedIOS] Failed:', error);
const errorJson = parseErrorStringToJsonObj(error);
throw new Error(errorJson.message);
}
};
/**
* Get transaction JWS representation (iOS only)
* @param sku - The product SKU
* @returns Promise<string | null> - JWS representation or null
* @platform iOS
*/
export const getTransactionJwsIOS = async sku => {
if (Platform.OS !== 'ios') {
return null;
}
try {
return await IAP.instance.getTransactionJwsIOS(sku);
} catch (error) {
console.error('[getTransactionJwsIOS] Failed:', error);
const errorJson = parseErrorStringToJsonObj(error);
throw new Error(errorJson.message);
}
};
/**
* Get the storefront identifier for the user's App Store account (iOS only)
* @returns Promise<string> - The storefront identifier (e.g., 'USA' for United States)
* @platform iOS
*
* @example
* ```typescript
* const storefront = await getStorefrontIOS();
* console.log('User storefront:', storefront); // e.g., 'USA', 'GBR', 'KOR'
* ```
*/
export const getStorefrontIOS = async () => {
if (Platform.OS !== 'ios') {
throw new Error('getStorefrontIOS is only available on iOS');
}
try {
// Call the native method to get storefront
const storefront = await IAP.instance.getStorefrontIOS();
return storefront;
} catch (error) {
console.error('Failed to get storefront:', error);
throw error;
}
};
/**
* Gets the storefront country code from the underlying native store.
* Returns a two-letter country code such as 'US', 'KR', or empty string on failure.
*
* Cross-platform alias aligning with expo-iap.
*/
export const getStorefront = async () => {
if (Platform.OS === 'android') {
try {
// Optional since older builds may not have the method
const result = await IAP.instance.getStorefrontAndroid?.();
return result ?? '';
} catch {
return '';
}
}
return getStorefrontIOS();
};
/**
* Deeplinks to native interface that allows users to manage their subscriptions
* Cross-platform alias aligning with expo-iap
*/
export const deepLinkToSubscriptions = async (options = {}) => {
if (Platform.OS === 'android') {
await IAP.instance.deepLinkToSubscriptionsAndroid?.({
skuAndroid: options.skuAndroid,
packageNameAndroid: options.packageNameAndroid
});
return;
}
// iOS: Use manage subscriptions sheet (ignore returned purchases for deeplink parity)
if (Platform.OS === 'ios') {
try {
await IAP.instance.showManageSubscriptionsIOS();
} catch {
// no-op
}
return;
}
return;
};
/**
* iOS only - Gets the original app transaction ID if the app was purchased from the App Store
* @platform iOS
* @description
* This function retrieves the original app transaction information if the app was purchased
* from the App Store. Returns null if the app was not purchased (e.g., free app or TestFlight).
*
* @returns {Promise<string | null>} The original app transaction ID or null
*
* @example
* ```typescript
* const appTransaction = await getAppTransactionIOS();
* if (appTransaction) {
* console.log('App was purchased, transaction ID:', appTransaction);
* } else {
* console.log('App was not purchased from App Store');
* }
* ```
*/
export const getAppTransactionIOS = async () => {
if (Platform.OS !== 'ios') {
throw new Error('getAppTransactionIOS is only available on iOS');
}
try {
// Call the native method to get app transaction
const appTransaction = await IAP.instance.getAppTransactionIOS();
return appTransaction;
} catch (error) {
console.error('Failed to get app transaction:', error);
throw error;
}
};
// Export subscription helpers
export { getActiveSubscriptions, hasActiveSubscriptions } from "./helpers/subscription.js";
// Type conversion utilities
export { convertNitroProductToProduct, convertNitroPurchaseToPurchase, convertProductToProductSubscription, validateNitroProduct, validateNitroPurchase, checkTypeSynchronization } from "./utils/type-bridge.js";
// Deprecated exports for backward compatibility
/**
* @deprecated Use acknowledgePurchaseAndroid instead
*/
export const acknowledgePurchase = acknowledgePurchaseAndroid;
/**
* @deprecated Use consumePurchaseAndroid instead
*/
export const consumePurchase = consumePurchaseAndroid;
//# sourceMappingURL=index.js.map