From 244fd95dcca97f1421665fa9bc16617d08e3153b Mon Sep 17 00:00:00 2001 From: hyochan Date: Wed, 25 Mar 2026 16:02:35 +0900 Subject: [PATCH 1/2] fix(useIAP): forward PurchaseOptions in getAvailablePurchases and restorePurchases The hook's getAvailablePurchases and restorePurchases methods were hardcoding PurchaseOptions (alsoPublishToEventListenerIOS, onlyIncludeActiveItemsIOS), making it impossible for hook consumers to customize these values. This change adds an optional PurchaseOptions parameter to both methods, defaulting to the previous behavior for backward compatibility. Related to hyochan/expo-iap#329 --- src/hooks/useIAP.ts | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/hooks/useIAP.ts b/src/hooks/useIAP.ts index faff89651..f083da4fe 100644 --- a/src/hooks/useIAP.ts +++ b/src/hooks/useIAP.ts @@ -40,6 +40,7 @@ import type { VerifyPurchaseResult, VerifyPurchaseWithProviderProps, VerifyPurchaseWithProviderResult, + PurchaseOptions, } from '../types'; import type { ActiveSubscription, @@ -63,7 +64,7 @@ type UseIap = { promotedProductIOS?: Product; activeSubscriptions: ActiveSubscription[]; finishTransaction: (args: MutationFinishTransactionArgs) => Promise; - getAvailablePurchases: (skus?: string[]) => Promise; + getAvailablePurchases: (options?: PurchaseOptions) => Promise; fetchProducts: (params: { skus: string[]; type?: ProductQueryType | null; @@ -83,7 +84,7 @@ type UseIap = { verifyPurchaseWithProvider: ( options: VerifyPurchaseWithProviderProps, ) => Promise; - restorePurchases: () => Promise; + restorePurchases: (options?: PurchaseOptions) => Promise; getPromotedProductIOS: () => Promise; requestPurchaseOnPromotedProductIOS: () => Promise; getActiveSubscriptions: ( @@ -271,11 +272,13 @@ export function useIAP(options?: UseIapOptions): UseIap { ); const getAvailablePurchasesInternal = useCallback( - async (_skus?: string[]): Promise => { + async (options?: PurchaseOptions): Promise => { try { const result = await getAvailablePurchases({ - alsoPublishToEventListenerIOS: false, - onlyIncludeActiveItemsIOS: true, + alsoPublishToEventListenerIOS: + options?.alsoPublishToEventListenerIOS ?? false, + onlyIncludeActiveItemsIOS: options?.onlyIncludeActiveItemsIOS ?? true, + includeSuspendedAndroid: options?.includeSuspendedAndroid ?? false, }); setAvailablePurchases(result); } catch (error) { @@ -333,18 +336,21 @@ export function useIAP(options?: UseIapOptions): UseIap { [], ); - const restorePurchases = useCallback(async (): Promise => { - try { - if (Platform.OS === 'ios') { - await syncIOS(); - } + const restorePurchases = useCallback( + async (options?: PurchaseOptions): Promise => { + try { + if (Platform.OS === 'ios') { + await syncIOS(); + } - await getAvailablePurchasesInternal(); - } catch (error) { - RnIapConsole.warn('Failed to restore purchases:', error); - invokeOnError(error); - } - }, [getAvailablePurchasesInternal, invokeOnError]); + await getAvailablePurchasesInternal(options); + } catch (error) { + RnIapConsole.warn('Failed to restore purchases:', error); + invokeOnError(error); + } + }, + [getAvailablePurchasesInternal, invokeOnError], + ); const validateReceipt = useCallback( async (options: VerifyPurchaseProps): Promise => From 4a392874bf47cdcd5639e64147bbb03123bcf55c Mon Sep 17 00:00:00 2001 From: hyochan Date: Wed, 25 Mar 2026 16:48:25 +0900 Subject: [PATCH 2/2] fix: address PR review comments - Replace restorePurchasesTopLevel with direct syncIOS call to eliminate redundant getAvailablePurchases network request - Forward includeSuspendedAndroid to Android Nitro calls in getAvailablePurchases - Update tests for new syncIOS mock and Android includeSuspended param Co-Authored-By: Claude Opus 4.6 (1M context) --- src/__tests__/index.test.ts | 4 ++-- src/index.ts | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 3c938bd8c..8a2b6814d 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -812,10 +812,10 @@ describe('Public API (src/index.ts)', () => { .mockResolvedValueOnce([nitro('s1')]); const res = await IAP.getAvailablePurchases(); expect(mockIap.getAvailablePurchases).toHaveBeenNthCalledWith(1, { - android: {type: 'inapp'}, + android: {type: 'inapp', includeSuspended: false}, }); expect(mockIap.getAvailablePurchases).toHaveBeenNthCalledWith(2, { - android: {type: 'subs'}, + android: {type: 'subs', includeSuspended: false}, }); expect(res.map((p: any) => p.productId).sort()).toEqual(['p1', 's1']); }); diff --git a/src/index.ts b/src/index.ts index 907412e5c..c65fac80e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -737,11 +737,14 @@ export const getAvailablePurchases: QueryField< return validPurchases.map(convertNitroPurchaseToPurchase); } else if (Platform.OS === 'android') { // For Android, we need to call twice for inapp and subs + const includeSuspended = Boolean( + options?.includeSuspendedAndroid ?? false, + ); const inappNitroPurchases = await IAP.instance.getAvailablePurchases({ - android: {type: 'inapp'}, + android: {type: 'inapp', includeSuspended}, }); const subsNitroPurchases = await IAP.instance.getAvailablePurchases({ - android: {type: 'subs'}, + android: {type: 'subs', includeSuspended}, }); // Validate and convert both sets of purchases