react-native-iap icon indicating copy to clipboard operation
react-native-iap copied to clipboard

getAvailablePurchases does not returns unfinished transactions

Open dharmeshgigs opened this issue 3 months ago • 4 comments

Hi,

Scenario For Android

  • Initiate request purchase
  • Click on buy button
  • Payment successful
  • Immediately user interrupted by another app or app killed
  • App restarts, getAvailablePurchases returns Empty list
  • When Next time, Try to initiate the request purchase returns "You already own this item"

How can I fix or handle this case?

Below is my code fetch unfinished transactions

for (const purchase of purchases) { if (Platform.OS === 'android') { const androidPurchase = purchase as PurchaseAndroid; const body = { paymentProvider: IN_APP_PURCHASE_PROVIDER.GOOGLE_PLAY, purchaseToken: androidPurchase.purchaseToken, productId: androidPurchase.productId, receipt: '', transactionId: '', exTransactionId: androidPurchase.obfuscatedAccountIdAndroid, }; retryExclusiveContent(body); } else if (Platform.OS === 'ios') { const iosPurchase = purchase as PurchaseIOS; const body = { paymentProvider: IN_APP_PURCHASE_PROVIDER.APPLE, purchaseToken: iosPurchase.purchaseToken, productId: iosPurchase.productId, receipt: '', transactionId: iosPurchase.transactionId, exTransactionId: iosPurchase.appAccountToken, }; retryExclusiveContent(body); } finishTransactionPurchase(purchase); }

dharmeshgigs avatar Nov 29 '25 05:11 dharmeshgigs

On Android, getAvailablePurchases() internally wraps:

BillingClient.queryPurchasesAsync(INAPP)

BillingClient.queryPurchasesAsync(SUBS)

So if the returned array is empty, it simply means:

Google Play returned no purchases from its query API.

Android Billing sometimes returns empty results due to:

The app being killed immediately after purchase (before onPurchasesUpdated).

Temporary caching or sync delays inside Google Play Billing.

Multiple Google accounts logged into the device.

A purchase being in a transitional state not yet reflected in the Billing client cache.

These situations are well-known and have been referenced across Google Billing discussions, even though many issue tracker links are internal and not publicly accessible.


  1. Why "You already own this item" appears

If Google considers the item purchased but the app has not acknowledged/consumed it, calling requestPurchase again triggers:

ITEM_ALREADY_OWNED "You already own this item"

This is expected behavior according to the Android Billing documentation:

A previously completed purchase must be acknowledged/consumed before another purchase can be initiated.

Official docs: https://developer.android.com/google/play/billing/errors#item-already-owned


  1. Recommended recovery strategy

A. Catch "already owned" and trigger restore

try { await requestPurchase({ sku: productId }); } catch (e) { if (isAlreadyOwnedError(e)) { // Allow Google Play some time to sync await delay(1000);

const purchases = await getAvailablePurchases();
await handleRestore(purchases); // validate + grant entitlement + finishTransaction
return;

}

throw e; }

B. Retry getAvailablePurchases()

Because Google sometimes returns an empty list temporarily, 1–2 retries with a short delay (500–1000 ms) can resolve it.

C. Provide a “Restore Purchases” button

This helps users recover entitlements when Google’s cache is delayed.


  1. What this means for react-native-iap

getAvailablePurchases() is the official, correct recovery method on Android.

If Google Play returns an empty list, the library cannot override or bypass that—it must rely on BillingClient’s state.

Apps should implement:

Error handling for ITEM_ALREADY_OWNED

Retry logic for restore

Manual restore option

finishTransaction only after successful server validation


  1. One-sentence conclusion

On Android, interrupted purchases may not immediately appear in getAvailablePurchases() due to Google Play Billing’s cache/update delays. The correct approach is to catch "You already own this item", retry restore, validate entitlements, and then finish the transaction.

hyochan avatar Nov 29 '25 06:11 hyochan

Will check this and let you know.

dharmeshgigs avatar Nov 29 '25 08:11 dharmeshgigs

@hyochan Thanks, working for Android.

Scenario For iOS

  • Initiate request purchase
  • Click on buy button
  • Payment successful
  • Immediately user interrupted by another app or app killed
  • App restarts, getAvailablePurchases returns Empty list
  • When Next time, Try to initiate the request purchase show no dialog. like android "Already own item popup"

How to handle same case for iOS?

dharmeshgigs avatar Dec 01 '25 10:12 dharmeshgigs

@dharmeshgigs On iOS, interrupted/unfinished transactions are stored separately. Use getPendingTransactionsIOS to retrieve them:

import {
  getPendingTransactionsIOS,
  finishTransaction,
  getAvailablePurchases
} from 'react-native-iap';

const checkAndRecoverPurchases = async () => {
  if (Platform.OS === 'ios') {
    // 1. Check for pending (unfinished) transactions first
    const pendingTransactions = await getPendingTransactionsIOS();

    if (pendingTransactions.length > 0) {
      console.log('Found pending transactions:', pendingTransactions);

      for (const purchase of pendingTransactions) {
        // 2. Verify with your server
        const isValid = await verifyPurchaseOnServer(purchase);

        if (isValid) {
          // 3. Grant entitlement to user
          await grantEntitlement(purchase.productId);

          // 4. Finish the transaction
          await finishTransaction({
            purchase,
            isConsumable: false, // true for consumables
          });
        }
      }
    }
  }
};

hyochan avatar Dec 01 '25 14:12 hyochan