In App Purchase with cordova-plugin-purchase

Hey everyone,

I’m trying to set up In App Purchase in my app. I’m focusing on iOS first. I have added the plugin cordova-plugin-purchase, from what I understand since v13, it is better to use it standalone, which I’m doing with the following code :

const initializeIAP = () => {
  const { store, ProductType, Platform, logLevel } = CdvPurchase;
  const idProduct = "MyPRODUCTID";
  store.verbosity = 4;

  store.initialize([Platform.APPLE_APPSTORE]);
  store.register([
    {
      id: idProduct,
      type: ProductType.PAID_SUBSCRIPTION,
      platform: Platform.APPLE_APPSTORE,
    },
  ]);

  const myProduct = store.get("MMmonthly", Platform.APPLE_APPSTORE);
  console.log(myProduct);
};

document.addEventListener("deviceready", initializeIAP);

I have the IAP product (paid subscription) set in AppleStore Connect.
I’m logged on a iOS simulator with a sandbox Apple account.

The value of console.log(myProduct); is always undefined.

What am I missing? Any idea?

Thanks for your help in advance

Last time I knew, you cannot test IAP in an emulator using products setup in App Store Connect. You have to set them up locally in XCode. See Testing in-app purchases in Xcode | Apple Developer Documentation.

You can, however, run/build your app as a Mac app in XCode and test with real products set up. I believe this is only an option on M1 and above Macs. You’ve gotta create a dev cert and profile for this to work in your Developer account along with adding your device.

1 Like

In addition to what @twestrick says, you can also test App Store Connect products directly on a physical iOS device instead of the emulator.

Thanks for your answers :slight_smile: I set up the products in Xcode, thanks for the helpful link. No luck in the simulator. I hooked my iPhone and installed the app on it via xCode, no luck. I also tried to install the app as Mac app (on M1) and had no luck.

I keep on getting the error: Product not found in AppStore. #400

I’m not sure where the problem is … It seems everything initializes properly, but my product is never found. I have added the products in xCode through the interface to add a store configuration.

How are you supposed to code as well if you can’t test your code directly. DO you have to build the app and send it on a phone every time you make a change to the code?

If I look at this video https://www.youtube.com/watch?v=uqbF_IbYh8E&t=1114s
The guy can test on a simulator and did not do any extra config it seems.

Thanks for the help

You can use console.log() in the app to dump debug info. You can do this even when running on the device; console.log() output gets sent to the device console in XCode.

I’m using Capacitor with React; I wrapped the plugin in a class and added a debug info method. Here are the relevant methods from the class:

  public initialize(
    state: SubscriptionStoreState,
    dispatch: React.Dispatch<StoreStateReducerAction>,
  ) {
    // Register all products for use in the app.
    this.store.register(classicProducts);

    // Configure the validator.
    this.store.validator = iaptic.validator;

    // Set debug message verbosity.
    if (isEnvEdoDebugOn()) {
      this.store.verbosity = CdvPurchase.LogLevel.DEBUG;
    } else {
      // Default is quiet.
      this.store.verbosity = CdvPurchase.LogLevel.QUIET;
    }

    // Track all store errors.
    this.store.error((error) => {
      if (error.code === CdvPurchase.ErrorCode.PAYMENT_CANCELLED) {
        debugLog('The user cancelled the purchase flow.');
        return;
      }
      debugLog('Store Error', JSON.stringify(error));
      dispatch({
        type: StoreState.HasError,
        payload: {
          error: error.message,
        },
      });
    });

    // Declare store event listeners.
    this.store
      .when()
      .receiptsVerified(() => {
        this.dumpDebugInfo('RECEIPTS VERIFIED!');
      })
      .receiptsReady(() => {
        this.dumpDebugInfo('RECEIPTS READY!');
      })
      .productUpdated((product) => {
        debugLog('Product updated', product);
      })
      .approved((transaction) => {
        debugLog('Transaction approved', transaction);
        // Upon approval, verify the receipt.
        // If we are getting ready, do not change the state.
        if (this.store.isReady && !state.isInitializing()) {
          dispatch({ type: StoreState.Verifying });
        }
        // setIsVerifying(true);
        transaction
          .verify()
          .catch((reason: unknown) =>
            catchPromiseError(reason, 'Failed to verify transaction!'),
          );
      })
      // Upon receipt validation, mark the subscription as owned.
      .verified((receipt) => {
        debugLog('Receipt verified', receipt);
        // Once the receipt is verified, we may have an active sub.
        invalidateQuery(this.queryClient, queryKeyIapActiveSub).catch(
          (reason: unknown) =>
            catchPromiseError(reason, 'Failed to invalidate ASQ!'),
        );
        receipt
          .finish()
          .catch((reason: unknown) =>
            catchPromiseError(reason, 'Failed to finish receipt!'),
          );
      })
      .unverified((receipt) => {
        debugLog('Receipt not verified', receipt);
        if (state.isMakingPurchase()) {
          dispatch({ type: StoreState.Idle });
        }
      })
      // Upon the subscription becoming owned.
      .finished((processedTransaction) => {
        debugLog('Transaction complete', processedTransaction);
        invalidateQuery(this.queryClient, queryKeyUseUser)
          // .then(() => setShowSuccessAlert(true))
          .catch((reason: unknown) =>
            catchPromiseError(reason, 'Failed to invalidate! sWF'),
          );
      });

  /**
   * Prints debug info to the console to try to figure out what the heck is going on.
   */
  private dumpDebugInfo = (state: string): void => {
    if (isEnvEdoDebugOn()) {
      const { products } = this.store;
      // eslint-disable-next-line no-console
      console.log(
        state,
        this.store,
        'vP',
        this.store.verifiedPurchases,
        'vR',
        this.store.verifiedReceipts,
        'iR',
        this.store.isReady,
        'lR',
        this.store.localReceipts,
        'lT',
        this.store.localTransactions,
        'products',
        products,
        'expired sub',
        this.getFirstExpiredSubscription(),
        'IAP SUB INFO',
        this.getIAPSubscriptionInfo(),
      );
    }
  };

It should work, there must be a configuration issue somewhere. As ptmkenny said, if you do have a physical iOS device, you can run your app via XCode on it and get real products from your App Store Connect configuration.

Not an answer to your question, but I started with this Cordova plugin and eventually switched to RevenueCat. They don’t charge until you reach $2.5k MTR and they have a 1st party Capacitor plugin. I found it much easier to set up. They also have a ton of other features.

Hey @ptmkenny , thanks for the detailed answer and the code it’s helpful :slight_smile:
@twestrick I actually switched to RevenueCat this morning and by setting up everything, I discovered their great step-by-step guide to test in iOS (on device or locally) and finally managed to get something working. I can communicate with the products and get the Purchase from iOS to show up. So I’m confident I should be able to finish from now.

Here’s the guide for those who are struggling (I’m sure it would be useful even using the cordoba purchase plugin): Apple App Store & TestFlight | In-App Subscriptions Made Easy – RevenueCat

I really appreciate the help and the time you took to answer!

1 Like