In app purchase 2 not working on production

Hello everyone,

I tried to implement in app purchases in Android. I did it successfully with IAP1 a few years ago, but I found the implementation sort of dumb. So in my new app I wanted to switch to IAP2, but there isn’t much documentation about it.
I followed the steps provided by ThielCole, found here
After that, I took the example app from git and rewrote it to work with subscriptions, and moved everything to a single provider. By doing that, I removed a lot duplicate code and code naming differences between the buy and the restore page. I tested everything with non-production build, and everything worked fine. Now my app being on the Play store (In beta) it doesn’t work. Clicking on the button doesn’t do anything (Or, calling the purchase function doesn’t do anything)

I’ve decided to post the full service here, and once we get the problem fixed, this service is imo an great subscription based example that everybody can use :wink:
I don’t know what I’m doing wrong and I might have stripped to many code, I might need something extra to make it work in production but since all the debugging code is stripped I don’t know where to start (Typing this, i suddenly think to remember it’s possible to build an release without production flag, would that help?)

Anyway, here’s the provider code:

import { Injectable } from '@angular/core';
import { Platform} from "ionic-angular";
import { IAPProduct, InAppPurchase2 } from "@ionic-native/in-app-purchase-2";
import { AdmobProvider} from "../admob.provider";
import { AlertController } from "ionic-angular";

@Injectable()
export class PurchaseProvider {

  public item;

  public productInfo: any = {
    name: 'Ad free',
    googleProductId: 'adfreenew'
  };

  constructor(private platform: Platform, private iap2: InAppPurchase2, private admobProvider: AdmobProvider, private alertCtrl:AlertController) {
    this.setup();
    this.item = this.iap2.get(this.productInfo.googleProductId);
  }

  setup() {
    if (!this.platform.is('cordova')) {
      return;
    }
    let productId;
    try {
      if (this.platform.is('android')) {
        productId = this.productInfo.googleProductId;
      }

      this.iap2.register({
        id: productId,
        alias: productId,
        type: this.iap2.PAID_SUBSCRIPTION
      });

      this.registerHandlers(productId);

      this.iap2.ready(function () {
        console.log(JSON.stringify(this.store.get(productId)));
        console.log('Store is Ready: ' + JSON.stringify(status));
        console.log('Products: ' + JSON.stringify(this.store.products));
      });

      // Errors On The Specific Product
      this.iap2.when(productId).error((error) => {
        alert('An Error Occured' + JSON.stringify(error));
      });
      // Refresh Always
      console.log('Refresh Store');
      this.iap2.refresh();
    } catch (err) {
      console.log('Error On Store Issues' + JSON.stringify(err));
    }
  }

  registerHandlers(productId) {
        // Handlers
        this.iap2.when(productId).approved( (product: IAPProduct) => {
          // Purchase was approved
          product.finish();
        });

    this.iap2.when(productId).owned((product: IAPProduct) => {

      this.admobProvider.disableAds();
      product.finish();
    });

    this.iap2.when(productId).verified((product: IAPProduct) => {
      product.verify();
      this.admobProvider.disableAds();
      let alert = this.alertCtrl.create({
        title: 'Purchase succeeded',
        message: 'Thanks you so much for your support, you rock',
      });
      alert.present();
    });

    this.iap2.when(productId).updated((product: IAPProduct) => {
      console.log('Loaded' + JSON.stringify(product));

    });

    this.iap2.when(productId).cancelled((product) => {
       let alert = this.alertCtrl.create({
        title: 'Purchase failed',
        message: 'Thanks for trying the adfree purchase but you where unlucky and it failed',
        buttons: [
          {
            text: 'Try Again',
            role: 'retry',
            handler: () => {
              this.purchase();
            }
          },
          {
            text: 'Ok',
          }
        ]
      });
      alert.present();
    });

    // Overall Store Error
    this.iap2.error((err) => {
      //alert('Store Error ' + JSON.stringify(err));
    });
  }

  async purchase() {

    if (!this.platform.is('cordova')) {
      return
    }

    let productId;

    if (this.platform.is('ios')) {
      productId = this.productInfo.appleProductId;
    } else if (this.platform.is('android')) {
      productId = this.productInfo.googleProductId;
    }

    console.log('Products: ' + JSON.stringify(this.iap2.products));
    console.log('Ordering From Store: ' + productId);
    try {
      let product = this.iap2.get(productId);
      console.log('Product Info: ' + JSON.stringify(product));
      let order = await this.iap2.order(productId);
    } catch (err) {
      console.log('Error Ordering ' + JSON.stringify(err));
    }
  }

  async restore() {
    if (!this.platform.is('cordova')) {
      return
    }

    let productId;

    if (this.platform.is('ios')) {
      productId = this.productInfo.appleProductId;
    } else if (this.platform.is('android')) {
      productId = this.productInfo.googleProductId;
    }

    console.log('Products: ' + JSON.stringify(this.iap2.products));
    console.log('Refreshing Store: ' + productId);
    try {
      let product = this.iap2.get(productId);
      console.log('Product Info: ' + JSON.stringify(product));
      this.iap2.refresh();
    } catch (err) {
      console.log('Error Ordering ' + JSON.stringify(err));
    }
  }
}

Any help is highly appreciated :slight_smile:

When I

run ionic cordova run android --release

without production flag, the in app purchase works. I guess something is breaking when running --prod

I resolved the issue myself.
The constructor calls the setup function which is fine in testing environments, but apparently not when minified. I don’t know if it’s a bug in Ionic/angular or just my code itself but I now call setup within the purchase function and everything is working.

Hi @nathantaal,

I am trying to utilize in App Purchase 2 in my ionic application. But its not working. The moment the code call register method, it throws an error and I get this error message "2019-03-10 07:37:28.989085-0700 lets-crack[13776:125053] Error On Store Issues{“line”:110493,“column”:113,“sourceURL”:"http://localhost:8080/build/vendor.js"}" . With this details I am not able to debug and am struggling for iAP2 implementation for the past 3 weeks. It would be great if someone helps me to resolve this issue.

  this.iap2.register({
    id: productId,
    alias: productId,
    type: this.iap2.CONSUMABLE
  });

Here is my provider code.

	import { Injectable } from '@angular/core';
	import { Platform } from "ionic-angular";
	import { IAPProduct, InAppPurchase2 } from "@ionic-native/in-app-purchase-2/ngx";
	import { AlertController } from "ionic-angular";
	const APPLE_PRODUCT_ID = '<apple.product.id>';

	@Injectable()
	export class PurchaseServiceProvider {

	  public item;

	  public productInfo: any = {
		name: 'Ad free',
		googleProductId: 'adfreenew'
	  };

	  constructor(
		private platform: Platform,
		private iap2: InAppPurchase2,
		private alertCtrl: AlertController
	  ) {

	  }

	  setup() {
		console.log('purchase-service-provider:entering setup()');
		if (!this.platform.is('cordova')) { return; }
		let productId;
		try {
		  if (this.platform.is('ios')) {
			productId = APPLE_PRODUCT_ID;
		  } else if (this.platform.is('android')) {
			productId = this.productInfo.googleProductId;
		  }
		  console.log('purchase-service-provider:setup:productId:' + productId);

		  this.iap2.register({
			id: productId,
			alias: productId,
			type: this.iap2.CONSUMABLE
		  });

		  console.log('purchase-service-provider:setup:after  calling register:');
		  this.registerHandlers(productId);
		  console.log('purchase-service-provider:setup:after calling registerHandlers:');

		  this.iap2.ready(function () {
			console.log('purchase-service-provider:setup:' + JSON.stringify(this.store.get(productId)));
			console.log('purchase-service-provider:setup:Store is Ready: ' + JSON.stringify(status));
			console.log('purchase-service-provider:setup:Products: ' + JSON.stringify(this.store.products));
		  });
		  console.log('purchase-service-provider:setup:after ready:');

		  // Errors On The Specific Product
		  this.iap2.when(productId).error((error) => {
			alert('An Error Occured' + JSON.stringify(error));
		  });
		  // Refresh Always
		  console.log('Refresh Store');
		  this.iap2.refresh();
		} catch (err) {
		  console.log('Error On Store Issues' + JSON.stringify(err));
		}
		console.log('purchase-service-provider:setup:exiting');
	  }

	  registerHandlers(productId) {
		console.log('purchase-service-provider:registerHandlers:entering');
		// Handlers
		this.iap2.when(productId).approved((product: IAPProduct) => {
		  // Purchase was approved
		  product.finish();
		});
		console.log('purchase-service-provider:registerHandlers:when approved');
		this.iap2.when(productId).owned((product: IAPProduct) => {
		  product.finish();
		});
		console.log('purchase-service-provider:registerHandlers:when owned');
		this.iap2.when(productId).verified((product: IAPProduct) => {
		  product.verify();
		  let alert = this.alertCtrl.create({
			title: 'Purchase succeeded',
			message: 'Thanks you so much for your support, you rock',
		  });
		  alert.present();
		});
		console.log('purchase-service-provider:registerHandlers:when verified');

		this.iap2.when(productId).updated((product: IAPProduct) => {
		  console.log('Loaded' + JSON.stringify(product));

		});
		console.log('purchase-service-provider:registerHandlers:when updated');

		this.iap2.when(productId).cancelled((product) => {
		  let alert = this.alertCtrl.create({
			title: 'Purchase failed',
			message: 'Thanks for trying the adfree purchase but you where unlucky and it failed',
			buttons: [
			  {
				text: 'Try Again',
				role: 'retry',
				handler: () => {
				  this.purchase();
				}
			  },
			  {
				text: 'Ok',
			  }
			]
		  });
		  alert.present();
		});
		console.log('purchase-service-provider:registerHandlers:when cancelled');

		// Overall Store Error
		this.iap2.error((err) => {
		  //alert('Store Error ' + JSON.stringify(err));
		  console.log('purchase-service-provider:registerHandlers:error:' + JSON.stringify(err));

		});
		console.log('purchase-service-provider:registerHandlers:when error');
		console.log('purchase-service-provider:registerHandlers:exiting');

	  }

	  async purchase() {
		console.log('purchase-service-provider:purchase:entering:');

		if (!this.platform.is('cordova')) { return }

		console.log('purchase-service-provider:purchase:calling setup:');

		this.setup();

		//this.item = this.iap2.get(APPLE_PRODUCT_ID);

		let productId;

		if (this.platform.is('ios')) {
		  productId = APPLE_PRODUCT_ID;
		} else if (this.platform.is('android')) {
		  productId = this.productInfo.googleProductId;
		}

		console.log('purchase-service-provider:purchase:productId:' + productId);

		console.log('purchase-service-provider:purchase:Products: ' + JSON.stringify(this.iap2.products));
		console.log('purchase-service-provider:purchase:Ordering From Store: ' + productId);
		try {
		  let product = this.iap2.get(productId);
		  console.log('purchase-service-provider:purchase:Product Info: ' + JSON.stringify(product));
		  let order = await this.iap2.order(productId);
		} catch (err) {
		  console.log('purchase-service-provider:purchase:Error Ordering ' + JSON.stringify(err));
		}
	  }

	  async restore() {
		if (!this.platform.is('cordova')) {
		  return
		}

		let productId;

		if (this.platform.is('ios')) {
		  productId = APPLE_PRODUCT_ID;
		} else if (this.platform.is('android')) {
		  productId = this.productInfo.googleProductId;
		}

		console.log('Products: ' + JSON.stringify(this.iap2.products));
		console.log('Refreshing Store: ' + productId);
		try {
		  let product = this.iap2.get(productId);
		  console.log('Product Info: ' + JSON.stringify(product));
		  this.iap2.refresh();
		} catch (err) {
		  console.log('Error Ordering ' + JSON.stringify(err));
		}
	  }
	}

Thanks,
Ganesh

can you provide me full code of inapppurchase2 i need this please.

Hi nathantaal, how do you verify if the subscription is still valid?

@ganeshpragasam @rajputsachin @lolaswift

I’ve made it cleaner over time but it’s essentially still the same. You will need to follow the steps as provided in the cordova plugin meaning uploading an signed build with billing key and permissions in it. Cordova will do this for you, but on capacitor it needs to be done manually. After the cordova stuff is done, this code is all I need:

import {InAppPurchase2, IAPProduct} from '@ionic-native/in-app-purchase-2/ngx';
import {AdmobService} from './admob.service';

@Injectable({
    providedIn: 'root'
})
export class PremiumService {
    product;

    constructor(public iap2: InAppPurchase2, private admob: AdmobService) {
    }

    setup() {
        this.iap2.register({
            id: 'adfreenew',
            alias: 'Ad free',
            type: this.iap2.PAID_SUBSCRIPTION
        });
        this.product = this.iap2.get('adfreenew');
        this.registerHandlersForPurchase('adfreenew');

        // restore purchase
        this.iap2.refresh();
    }

    getProduct() {
        return this.product;
    }

    subscribe() {
        this.registerHandlersForPurchase('adfreenew');
        this.iap2.order('adfreenew');
        this.iap2.refresh();
    }



    registerHandlersForPurchase(productId) {
        this.iap2.when(productId).owned((product: IAPProduct) => {
            // alert(` owned ${product.owned}`);
            product.finish();
            this.admob.disableAds();
        });

        this.iap2.when(productId).approved((product: IAPProduct) => {
            // alert('approved');
            this.admob.disableAds();
        });

        this.iap2.when(productId).refunded((product: IAPProduct) => {
            // alert('refunded');
        });

        this.iap2.when(productId).expired((product: IAPProduct) => {
            // alert('expired');
        });
    }
}

By running this (function setup) at app startup, I make sure the subscription is still active (It will only fire .owned or .approved if that’s the case), to answer @lolaswift’s answer.
So a day after first upload to Google Play, if everything is configured correctly, this will work but only on a signed apk (preferly by downloading the derived apk from play console, or released version from play store)

thankx for this solution.

@nathantaal
and what about your subscribe() function? where did u call it? In HTML, when you click the button?

My listed product does not appear with price information. Does this only appear with the app release?

My listed product does not appear with price information. Does this only appear with the app release?

Indeed my solution was also to remove the ‘–prod’ flag. (ex. from package.json , where you have your scripts), before the changes you see below, my actual behavior was. In production if i call noAdsClicked(), nothing would happen

change this

ionic cordova build android --prod --release

to this

ionic cordova build android --release

The downsides of removing the --prod flag, is that your app size will double (mine went from 9mb to 17mb)

Also the account that I have on developer console is the same with the one that I’m logged in Google Play, so in production I still see the test mode ( but I asked some friends to send me a screenshot for different accounts), see the below screenshots
Mine:

Their:

If someone needs my code (I’m pretty sure is not the best implementation, but Hey it works. So if you have suggestions I’m glad to see them)

app.component.ts

 initializeApp() {
    this.platform.ready().then(() => {
      this.adsService.initAdsConfig();
        this.store.register({
          id: environment.NO_ADS_ID, 
          type: this.store.NON_CONSUMABLE,
        });
}}

in-the-component-where-i-use-it.ts

import { InAppPurchase2 } from '@ionic-native/in-app-purchase-2/ngx';

...

constructor(private store: InAppPurchase2){

this.store
        .when(environment.NO_ADS_ID)
        .approved((p) => {
           
          p.verify();
        })
        .verified((p) => {
          // code here after the payment is OK
          p.finish();
        });
      this.store.refresh();

}

  noAdsClicked() {
    this.store
      .order(environment.NO_ADS_ID, { applicationUsername: this.user.username })
      .then(
        (p) => {
          this.store.get(environment.NO_ADS_ID);
          console.log(p);
        },
        (err) => {}
      );
  }

in-the-component-where-i-use-it.html

  <button (click)="noAdsClicked()">
          FARA RECLAME (9.99RON)
  </button>

I keep getting this error from Google play when I test this on a real device with a test user account?

This test purchase was cancelled because it was not acknowledged. You should ensure all purchases are acknowledged so they are not subject to refunds. For more information see https://developer.android.com/google/play/billing/integrate#process.

How exactly do we acknowledge a purchase?

1 Like