UNPKG

react-native-iap

Version:

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

245 lines (237 loc) 9.59 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 { 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 [promotedProductIOS, setPromotedProductIOS] = 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 getProductsInternal = useCallback(async skus => { try { const result = await fetchProducts({ skus, type: 'in-app' }); const newProducts = (result ?? []).filter(item => item.type === 'in-app'); setProducts(prevProducts => mergeWithDuplicateCheck(prevProducts, newProducts, product => product.id)); } catch (error) { console.error('Error fetching products:', error); } }, [mergeWithDuplicateCheck]); const getSubscriptionsInternal = useCallback(async skus => { try { const result = await fetchProducts({ skus, type: 'subs' }); const newSubscriptions = (result ?? []).filter(item => item.type === 'subs'); setSubscriptions(prevSubscriptions => mergeWithDuplicateCheck(prevSubscriptions, newSubscriptions, 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 requestType = params.type ?? 'in-app'; const result = await fetchProducts({ skus: params.skus, type: requestType }); const items = result ?? []; if (requestType === 'subs') { const newSubscriptions = items.filter(item => item.type === 'subs'); setSubscriptions(prevSubscriptions => mergeWithDuplicateCheck(prevSubscriptions, newSubscriptions, subscription => subscription.id)); return; } if (requestType === 'all') { const newProducts = items.filter(item => item.type === 'in-app'); const newSubscriptions = items.filter(item => item.type === 'subs'); setProducts(prevProducts => mergeWithDuplicateCheck(prevProducts, newProducts, product => product.id)); setSubscriptions(prevSubscriptions => mergeWithDuplicateCheck(prevSubscriptions, newSubscriptions, subscription => subscription.id)); return; } const newProducts = items.filter(item => item.type === 'in-app'); setProducts(prevProducts => mergeWithDuplicateCheck(prevProducts, newProducts, 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 args => { try { await finishTransactionInternal(args); } catch (err) { throw err; } }, []); const requestPurchase = useCallback(requestObj => requestPurchaseInternal(requestObj), []); // 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 => { // 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; } 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, promotedProductIOS, activeSubscriptions, getAvailablePurchases: getAvailablePurchasesInternal, fetchProducts: fetchProductsInternal, requestPurchase, validateReceipt, restorePurchases: async () => { try { await restorePurchasesTopLevel(); await getAvailablePurchasesInternal(); } catch (e) { console.warn('Failed to restore purchases:', e); } }, getProducts: getProductsInternal, getSubscriptions: getSubscriptionsInternal, getPromotedProductIOS, requestPurchaseOnPromotedProductIOS, getActiveSubscriptions: getActiveSubscriptionsInternal, hasActiveSubscriptions: hasActiveSubscriptionsInternal }; } //# sourceMappingURL=useIAP.js.map