UNPKG

react-native-iap

Version:

React Native In-App Purchases module for iOS and Android using Nitro

266 lines (258 loc) 10 kB
"use strict"; // External dependencies import { useCallback, useEffect, useState, useRef } from 'react'; import { Platform } from 'react-native'; // Internal modules import { initConnection, purchaseErrorListener, purchaseUpdatedListener, promotedProductListenerIOS, getAvailablePurchases, finishTransaction as finishTransactionInternal, requestPurchase as requestPurchaseInternal, fetchProducts, validateReceipt as validateReceiptInternal, getActiveSubscriptions, hasActiveSubscriptions, restorePurchases as restorePurchasesTopLevel, getPromotedProductIOS, requestPurchaseOnPromotedProductIOS } from "../index.js"; // Types import { ProductQueryType, ErrorCode } from "../types.js"; import { normalizeErrorCodeFromNative } from "../utils/errorMapping.js"; // Types for event subscriptions /** * React Hook for managing In-App Purchases. * See documentation at https://react-native-iap.hyo.dev/docs/hooks/useIAP */ export function useIAP(options) { const [connected, setConnected] = useState(false); const [products, setProducts] = useState([]); const [promotedProductsIOS] = useState([]); const [subscriptions, setSubscriptions] = useState([]); const [availablePurchases, setAvailablePurchases] = useState([]); const [currentPurchase, setCurrentPurchase] = useState(); const [promotedProductIOS, setPromotedProductIOS] = useState(); const [currentPurchaseError, setCurrentPurchaseError] = useState(); const [promotedProductIdIOS] = useState(); const [activeSubscriptions, setActiveSubscriptions] = useState([]); const optionsRef = useRef(options); const connectedRef = useRef(false); // Helper function to merge arrays with duplicate checking const mergeWithDuplicateCheck = useCallback((existingItems, newItems, getKey) => { const merged = [...existingItems]; newItems.forEach(newItem => { const isDuplicate = merged.some(existingItem => getKey(existingItem) === getKey(newItem)); if (!isDuplicate) { merged.push(newItem); } }); return merged; }, []); useEffect(() => { optionsRef.current = options; }, [options]); useEffect(() => { connectedRef.current = connected; }, [connected]); const subscriptionsRef = useRef({}); const subscriptionsRefState = useRef([]); useEffect(() => { subscriptionsRefState.current = subscriptions; }, [subscriptions]); const clearCurrentPurchase = useCallback(() => { setCurrentPurchase(undefined); }, []); const clearCurrentPurchaseError = useCallback(() => { setCurrentPurchaseError(undefined); }, []); const getProductsInternal = useCallback(async skus => { try { const result = await fetchProducts({ skus, type: ProductQueryType.InApp }); setProducts(prevProducts => mergeWithDuplicateCheck(prevProducts, result, product => product.id)); } catch (error) { console.error('Error fetching products:', error); } }, [mergeWithDuplicateCheck]); const getSubscriptionsInternal = useCallback(async skus => { try { const result = await fetchProducts({ skus, type: ProductQueryType.Subs }); setSubscriptions(prevSubscriptions => mergeWithDuplicateCheck(prevSubscriptions, result, subscription => subscription.id)); } catch (error) { console.error('Error fetching subscriptions:', error); } }, [mergeWithDuplicateCheck]); const fetchProductsInternal = useCallback(async params => { if (!connectedRef.current) { console.warn('[useIAP] fetchProducts called before connection; skipping'); return; } try { const result = await fetchProducts(params); if (params.type === ProductQueryType.Subs) { setSubscriptions(prevSubscriptions => mergeWithDuplicateCheck(prevSubscriptions, result, subscription => subscription.id)); } else { setProducts(prevProducts => mergeWithDuplicateCheck(prevProducts, result, product => product.id)); } } catch (error) { console.error('Error fetching products:', error); } }, [mergeWithDuplicateCheck]); const getAvailablePurchasesInternal = useCallback(async _skus => { try { const result = await getAvailablePurchases({ alsoPublishToEventListenerIOS: false, onlyIncludeActiveItemsIOS: true }); setAvailablePurchases(result); } catch (error) { console.error('Error fetching available purchases:', error); } }, []); const getActiveSubscriptionsInternal = useCallback(async subscriptionIds => { try { const result = await getActiveSubscriptions(subscriptionIds); setActiveSubscriptions(result); return result; } catch (error) { console.error('Error getting active subscriptions:', error); // Don't clear existing activeSubscriptions on error - preserve current state // This prevents the UI from showing empty state when there are temporary network issues return []; } }, []); const hasActiveSubscriptionsInternal = useCallback(async subscriptionIds => { try { return await hasActiveSubscriptions(subscriptionIds); } catch (error) { console.error('Error checking active subscriptions:', error); return false; } }, []); const finishTransaction = useCallback(async ({ purchase, isConsumable }) => { try { return await finishTransactionInternal({ purchase, isConsumable }); } catch (err) { throw err; } finally { if (purchase.id === currentPurchase?.id) { clearCurrentPurchase(); } if (purchase.id === currentPurchaseError?.productId) { clearCurrentPurchaseError(); } } }, [currentPurchase?.id, currentPurchaseError?.productId, clearCurrentPurchase, clearCurrentPurchaseError]); const requestPurchaseWithReset = useCallback(async requestObj => { clearCurrentPurchase(); clearCurrentPurchaseError(); try { return await requestPurchaseInternal(requestObj); } catch (error) { throw error; } }, [clearCurrentPurchase, clearCurrentPurchaseError]); // No local restorePurchases; use the top-level helper via returned API const validateReceipt = useCallback(async (sku, androidOptions) => { return validateReceiptInternal(sku, androidOptions); }, []); const initIapWithSubscriptions = useCallback(async () => { // Register listeners BEFORE initConnection to avoid race condition subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(async purchase => { setCurrentPurchaseError(undefined); setCurrentPurchase(purchase); // Always refresh subscription state after a purchase event try { await getActiveSubscriptionsInternal(); await getAvailablePurchasesInternal(); } catch (e) { console.warn('[useIAP] post-purchase refresh failed:', e); } if (optionsRef.current?.onPurchaseSuccess) { optionsRef.current.onPurchaseSuccess(purchase); } }); subscriptionsRef.current.purchaseError = purchaseErrorListener(error => { const mappedError = { code: normalizeErrorCodeFromNative(error.code), message: error.message, productId: undefined }; // Ignore init error until connected if (mappedError.code === ErrorCode.InitConnection && !connectedRef.current) { return; } setCurrentPurchase(undefined); setCurrentPurchaseError(mappedError); if (optionsRef.current?.onPurchaseError) { optionsRef.current.onPurchaseError(mappedError); } }); if (Platform.OS === 'ios') { subscriptionsRef.current.promotedProductsIOS = promotedProductListenerIOS(product => { setPromotedProductIOS(product); if (optionsRef.current?.onPromotedProductIOS) { optionsRef.current.onPromotedProductIOS(product); } }); } const result = await initConnection(); setConnected(result); if (!result) { // Clean up some listeners but leave purchaseError for potential retries subscriptionsRef.current.purchaseUpdate?.remove(); subscriptionsRef.current.promotedProductsIOS?.remove(); subscriptionsRef.current.purchaseUpdate = undefined; subscriptionsRef.current.promotedProductsIOS = undefined; return; } }, [getActiveSubscriptionsInternal, getAvailablePurchasesInternal]); useEffect(() => { initIapWithSubscriptions(); const currentSubscriptions = subscriptionsRef.current; return () => { currentSubscriptions.purchaseUpdate?.remove(); currentSubscriptions.purchaseError?.remove(); currentSubscriptions.promotedProductsIOS?.remove(); currentSubscriptions.promotedProductIOS?.remove(); // Keep connection alive across screens to avoid race conditions setConnected(false); }; }, [initIapWithSubscriptions]); return { connected, products, promotedProductsIOS, promotedProductIdIOS, subscriptions, finishTransaction, availablePurchases, currentPurchase, currentPurchaseError, promotedProductIOS, activeSubscriptions, clearCurrentPurchase, clearCurrentPurchaseError, getAvailablePurchases: getAvailablePurchasesInternal, fetchProducts: fetchProductsInternal, requestPurchase: requestPurchaseWithReset, validateReceipt, restorePurchases: async () => { try { const purchases = await restorePurchasesTopLevel({ alsoPublishToEventListenerIOS: false, onlyIncludeActiveItemsIOS: true }); setAvailablePurchases(purchases); } catch (e) { console.warn('Failed to restore purchases:', e); } }, getProducts: getProductsInternal, getSubscriptions: getSubscriptionsInternal, getPromotedProductIOS, requestPurchaseOnPromotedProductIOS, getActiveSubscriptions: getActiveSubscriptionsInternal, hasActiveSubscriptions: hasActiveSubscriptionsInternal }; } //# sourceMappingURL=useIAP.js.map