Skip to content
This repository was archived by the owner on Apr 26, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 49 additions & 5 deletions src/__tests__/hooks/useIAP.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('hooks/useIAP (renderer)', () => {
let mockGetAvailablePurchases: jest.SpyInstance;
let mockGetActiveSubscriptions: jest.SpyInstance;
let mockHasActiveSubscriptions: jest.SpyInstance;
let mockRestorePurchases: jest.SpyInstance;
let mockSyncIOS: jest.SpyInstance;

beforeEach(() => {
jest.spyOn(IAP, 'initConnection').mockResolvedValue(true as any);
Expand All @@ -61,8 +61,8 @@ describe('hooks/useIAP (renderer)', () => {
mockFetchProducts = jest
.spyOn(IAP, 'fetchProducts')
.mockResolvedValue([] as any);
mockRestorePurchases = jest
.spyOn(IAP, 'restorePurchases')
mockSyncIOS = jest
.spyOn(IAP, 'syncIOS')
.mockResolvedValue(undefined as any);
jest.spyOn(IAP, 'purchaseUpdatedListener').mockImplementation((cb: any) => {
capturedPurchaseListener = cb;
Expand Down Expand Up @@ -240,9 +240,9 @@ describe('hooks/useIAP (renderer)', () => {
expect(onError).toHaveBeenCalledWith(hasSubsError);
});

it('calls onError when restorePurchases fails', async () => {
it('calls onError when restorePurchases fails (syncIOS error on iOS)', async () => {
const restoreError = new Error('Failed to restore');
mockRestorePurchases.mockRejectedValueOnce(restoreError);
mockSyncIOS.mockRejectedValueOnce(restoreError);

let api: any;
const onError = jest.fn();
Expand All @@ -260,9 +260,53 @@ describe('hooks/useIAP (renderer)', () => {
await api.restorePurchases();
});

expect(mockSyncIOS).toHaveBeenCalled();
expect(onError).toHaveBeenCalledWith(restoreError);
});

it('calls onError when restorePurchases fails (getAvailablePurchases error)', async () => {
const purchaseError = new Error('Failed to get purchases after restore');
mockGetAvailablePurchases.mockRejectedValueOnce(purchaseError);

let api: any;
const onError = jest.fn();
const Harness = () => {
api = useIAP({onError});
return null;
};

await act(async () => {
TestRenderer.create(React.createElement(Harness));
});
await act(async () => {});

await act(async () => {
await api.restorePurchases();
});

expect(onError).toHaveBeenCalledWith(purchaseError);
});

it('restorePurchases calls syncIOS then getAvailablePurchases on iOS', async () => {
let api: any;
const Harness = () => {
api = useIAP();
return null;
};

await act(async () => {
TestRenderer.create(React.createElement(Harness));
});
await act(async () => {});

await act(async () => {
await api.restorePurchases();
});

expect(mockSyncIOS).toHaveBeenCalled();
expect(mockGetAvailablePurchases).toHaveBeenCalled();
});

it('does not call onError when operations succeed', async () => {
let api: any;
const onError = jest.fn();
Expand Down
25 changes: 14 additions & 11 deletions src/hooks/useIAP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
verifyPurchaseWithProvider as verifyPurchaseWithProviderTopLevel,
getActiveSubscriptions,
hasActiveSubscriptions,
restorePurchases as restorePurchasesTopLevel,
syncIOS,
getPromotedProductIOS,
requestPurchaseOnPromotedProductIOS,
checkAlternativeBillingAvailabilityAndroid,
Expand Down Expand Up @@ -333,7 +333,18 @@ export function useIAP(options?: UseIapOptions): UseIap {
[],
);

// No local restorePurchases; use the top-level helper via returned API
const restorePurchases = useCallback(async (): Promise<void> => {
try {
if (Platform.OS === 'ios') {
await syncIOS();
}

await getAvailablePurchasesInternal();
} catch (error) {
RnIapConsole.warn('Failed to restore purchases:', error);
invokeOnError(error);
}
}, [getAvailablePurchasesInternal, invokeOnError]);
Comment thread
hyochan marked this conversation as resolved.

const validateReceipt = useCallback(
async (options: VerifyPurchaseProps): Promise<VerifyPurchaseResult> =>
Expand Down Expand Up @@ -506,15 +517,7 @@ export function useIAP(options?: UseIapOptions): UseIap {
validateReceipt,
verifyPurchase,
verifyPurchaseWithProvider,
restorePurchases: async () => {
try {
await restorePurchasesTopLevel();
await getAvailablePurchasesInternal();
} catch (error) {
RnIapConsole.warn('Failed to restore purchases:', error);
invokeOnError(error);
}
},
restorePurchases,
getPromotedProductIOS,
requestPurchaseOnPromotedProductIOS,
getActiveSubscriptions: getActiveSubscriptionsInternal,
Expand Down
Loading