Search code examples
androidreact-nativein-app-purchasereact-native-iap

isAcknowledgedAndroid is always false for consumable product


Description

My first time implementing in-app purchase so I might have a bit less knowledge about the whole lifecycle of the IAP. I only have in-app products to buy virtual coins that the user can buy as many times as they like hence isConsumable will be true for my case.

My implementation if IAP can be summed up to three changes:

  1. App.tsx the entry point of the app which has my navigator etc I did initConnection
  2. In the Home.tsx which is the first screen that the App.tsx loads in any case i.e. auth or non-auth users is where I added purchaseUpdatedListener and this is also where finishTransaction happens.
  3. The BuyCoins.tsx screen is where I getProducts as well as requestPurchase when a buy button is pressed.

For all the three points above here is the code:

App.tsx

import {
  clearTransactionIOS,
  endConnection,
  flushFailedPurchasesCachedAsPendingAndroid,
  initConnection,
  withIAPContext,
} from 'react-native-iap';

  useEffect(() => {
    const initIAP = async () => {
      try {
        await initConnection();
        if (Platform.OS == 'android') {
          await flushFailedPurchasesCachedAsPendingAndroid();
        } else {
          await clearTransactionIOS();
        }
      } catch (error) {
        console.log('Error initializing IAP: ', error);
      }
    };

    initIAP();

    return () => {
      endConnection();
    };
  }, []);

Home.tsx



  useEffect(() => {
    const subscriptionListener = purchaseUpdatedListener(
      async (purchase: Purchase) => {

        if (currentPurchase) {
          //TODO: Send the receipt to the server
          await finishTransaction({
            purchase: currentPurchase,
            isConsumable: true,
          });
          console.log(
            'currentPurchase @ subscriptionListener: ',
            currentPurchase.transactionReceipt,
          );
        }
      },
    );

    return () => {
      subscriptionListener.remove();
    };
  }, []);

BuyCoins.tsx

    const handleProductPurchase = async (sku: Sku) => {
    try {
      const params =
        Platform.OS === 'android'
          ? { skus: [sku] }
          : { sku, andDangerouslyFinishTransactionAutomaticallyIOS: false };

      await requestPurchase(params);
    } catch (error) {
      errorLog({ message: 'handleProductPurchase', error });
      setIsLoading(false);
    }
  };

Expected Behavior

When I purchase it goes through successfully but in finish payment, if I try to print the transactionReceipt I get:

{"orderId":"ABC123","packageName":"com.MYAPP.TEST","productId":"passtest","purchaseTime":1234,"purchaseState":0,"purchaseToken":"safkhbvkhvbkfhbvfehbv","quantity":1,"acknowledged":false}

The "acknowledged":false should be true as it's a consumable product and as soon as the user buys any product it should return true because I did call it after await finishTransaction...

Logs

Here are the results of the logs I have in the above code:

 LOG  purchase.purchaseStateAndroid:  1
 LOG  Transaction finished and consumed: {"orderId":"GPA.1234","packageName":"com.myapp","productId":"coins_pack_1","purchaseTime":1234,"purchaseState":0,"purchaseToken":"THE_PURCHASE_TOKEN","quantity":1,"acknowledged":false}
 LOG  Error finishing transaction:  [Error: An unknown or unexpected error has occurred. Please try again later.]
 LOG  Error finishing transaction:  [Error: An unknown or unexpected error has occurred. Please try again later.]
 LOG  Error finishing transaction:  [Error: An unknown or unexpected error has occurred. Please try again later.]
 LOG  Error finishing transaction:  [Error: Purchase failed with code: 8]

Environment:

  • react-native-iap: 12.16.0
  • react-native: 0.75.4
  • Platforms (iOS, Android, emulator, simulator, device): Android Emulator and Physical Device

[Optional] Additional Context

I sometimes get error in finishTransaction [Error: Purchase failed with code: 8]


Solution

  • In your purchaseUpdatedListener, you’re using currentPurchase from the useIAP hook instead of the purchase parameter passed to the listener. This can lead to stale or incorrect data, causing acknowledged: false.

    First of all, to use the purchase parameter from the listener directly instead of currentPurchase:

    useEffect(() => {
      const subscriptionListener = purchaseUpdatedListener(
        async (purchase: Purchase) => { // Use the incoming purchase parameter
          try {
            if (purchase.purchaseStateAndroid === PurchaseStateAndroid.PURCHASED) {
              await finishTransaction({
                purchase, // Use the purchase from listener
                isConsumable: true,
                purchaseToken: purchase.purchaseToken,
                productId: purchase.productId,
              });
            }
          } catch (error) {
            console.log('FinishTransaction error:', error);
          }
        }
      );
    
      return () => subscriptionListener.remove();
    }, [finishTransaction]); // Add finishTransaction as dependency
    

    Remove redundant platform checks and simplify the purchase logic:

    const handleProductPurchase = async (sku: Sku) => {
      try {
        await requestPurchase({
          sku,
          andDangerouslyFinishTransactionAutomaticallyIOS: false,
        });
      } catch (error) {
        console.log('Purchase error:', error);
      }
    };
    

    After finishing the transaction, check if the purchase is acknowledged:

    if (Platform.OS === 'android' && !purchase.acknowledged) {
      console.warn('Purchase not acknowledged!');
    }
    

    Changes made:

    • Using the listener’s purchase parameter ensures you’re working with the latest transaction data.
    • Providing purchaseToken and productId ensures Google Play acknowledges the purchase.