IAP Badger: a unified approach to in-app purchases

Hi happymongoose.

Can you think of any reason why I am not getting an inventory file showing up in my sandbox?
Do we need to specify a baseDirectory or is it’s default  system.DocumentsDirectory?
Should it be a txt file or a json file?
Will the plugin create the file if it doesn’t exist?

Basically it seems to be working fine in terms of initialising and making purchases ( so far on simulator in debug mode ). The function I set for onPurchase in the catalogue table is getting called, I know this from debug console printing, but the part within this where I call iap.setInventoryValue( ‘isPremium’, true ) is not working. Firstly there is no file in the sandbox and secondly when I later call iap.printInventory(), it just prints out an empty json array string.

I’m clearly doing something wrong but what could that be?

Cheers
Jules

Hi Jules,

You don’t need to create the inventory file - IAP Badger will do that for you if it doesn’t exist.  However, you will need to call the iap.saveInventory() function to write out the inventory to disk / update it.  When you do this is down to your preference - you could save every time a change is made, or when the application is suspended or closed.

There’s no real need to specify a base directory - by default the file will be placed in system.DocumentsDirectory.  The file will be written out as JSON with a hash prepended to prevent users tampering with the conents.  I wouldn’t recommend manually altering the contents of the file - leave all that the IAP Badger (although you can check it and open it in a text editor).

Simon

Hi Simon. Thanks!

That’s what I was missing I guess, I didn’t realise iap.setInventoryValue() didn’t write out the file straight away, I must have missed this in your tutorial. If I call iap.setInventoryValue( ‘isPremium’, true ) in the function set for onPurchase  in the catalogue table (as per your tutorial), where is the best place to call iap.saveInventory()? Should I call it there and then?

On a separate note, I’m totally new to in-app-purchases and was wondering how you go about doing live testing without having to publish the app first? It seems Google need you to upload an apk and itunes needs a submission too, is there a best practice you use?

Thanks again
Jules

It doesn’t really matter where you call iap.saveInventory - it’s just a matter of preference.   I call it in the listener that I explicitly pass to iap.purchase.

In terms of testing, the process I go through is: 

  • I get everything working on the simulator with IAP Badger.
  • I make sure everything works with the device in debug mode through IAP Badger.
  • I know at this point everything on the device side is okay…
  • …so I start live testing on one ecosystem.

At this point, I know that any problems I have are 99.9% likely to do with the way the IAP is set up on the store console side, rather than with my code.

I wish I could say the next part was painless, but…

I usually start with getting iOS to work.  You won’t need to actually publish your app to test IAP, but you will need to fill in loads of information about your app - and some of it needs to be done in a certain order - and you cannot leave *anything* out.  I would set aside a good slab of time to slowly and methodically work through a tutorial.  It’s definitely something you can’t rush and each ecosystem has its quirks (for instance, on iOS, you have to upload a photo of what happens during the IAP process before you can submit the in-app product).

To get things working on iOS and Google Play, you will also need to set up test users and log onto your device using those users.  You can’t test IAP using your developer account.  You will also need to appreciate that any change you make to the console needs to percolate through Apple’s and Google’s servers around the world.  A change on the consoles can take hours to be recognised on your device - so you’ll also need a lot of patience.

So - my advice would be to grab a good tutorial, allocate three times as long as you think you’ll need, and work slowly and patiently.

And yes - on Google, you will need to upload an APK and publish to alpha to test.

This isn’t a big deal, though - once the APK has been uploaded, as long as you don’t change the version number, you can just keep recompiling your code and dropping copies of it straight to your device via USB, which avoids repeatedly uploading your app to the Google Play console.

(Obviously, you’ll need to update the console APK with your production code at a later date).

Hi Simon.

Thanks for all your advice and thanks for making this plugin, so far it seems excellent. Do you have any recommendations where to _’_grab a good tutorial’  for the processes of getting everything working well on both the store setups? ( For now I’m just doing apple and google play not amazon ).
 

For Google Play, this page will explain how to set up your products and this explains how to test them.

For iTunes Connect, there is a lot of information here and here (obviously, you can ignore any mentions to Xcode, because Corona takes care of native IAP integration).

Happy reading - I’d recommend regular injections of caffeine to reach the end of them both  :slight_smile:

Thanks again Simon.

These tutorial pointers will be really useful when I start setting up everything on the store side this week.

Just something I’ve come across that may or may not be a bug…
I use this global variable detection script from spiral code studio which I find really useful for identifying any possible causes for memory leaks. It flagged up an undeclared variable in your plugin for ‘productID’ which appears when I ‘simulate fail’ or ‘cancel’ a purchase restore in debug on the the simulator. Is this an accidental global?

Cheers Jules

Hi Jules,

I’ve just had a look at the code - there is a small bug, but not one that should generate a memory leak.  productID certainly isn’t declared as a global variable within the module itself.

In lines 1598 and 1612, however, my code makes a reference to ‘productID’ that occurs only when the user is in testing mode.  When the tester initiates a fake cancelled or failed response from the server, IAP Badger should return the product code of the item the user was attempting to purchase.  However, it’s actually referencing a variable that is never defined, called ‘productID’, so it’s returning a null value instead.  (This variable did exist in a much earlier version of the code but has since been stripped out).

I’m guessing your script is picking this reference up as a global, because it’s noticed the function refers to ‘productID’, and the variable is not defined anywhere else within the scope of the module (so it’s assuming it’s a global var, defined elsewhere, potentially leading to problems).

I’ll fix the functionality side of this when I get a chance (life is hectic at the moment, and someone has requested another slight alteration to the code as well that I intend to implement).

In the meantime, I don’t think this should cause any problems in terms of memory leaks etc., and it in terms of functionality, only affects your app when running on the simulator and in debug mode.

Simon

Hi Simon,  Great job with badger! I hope you don’t mind a few questions:

  1. Does or will IAP badger work with the new Amazon 2.0 IAP API?

  2. Can I mix products in the catalog (consumable and non consumables?)

3.  I manage my ‘coins’ myself, Can I use badger to purchase consumables buy manage them as I am now?

Thanks again, Greg

Greg,

I can answer two of those questions.

  1. Yes, you can definitely mix consumables and non-consumables in your catalog. There are lots of examples and such here: http://happymongoosegames.co.uk/iapbadger.php

  2. I don’t see why not. IAP Badger has callback events that you can leverage to synchronize your data with the IAP Badger managed data. Also, IAP Badger is such that you can use it to maintain ANY inventory data regardless of whether or not it’s tied to in-app purchases. Personally, because of it’s salting and simple “encryption”, I think IAP Badger is a great tool to use to manage all game inventory, whether it be coins, crystals, XPs, whatever.

Hi Greg,

The answers provided by jerejigga look good to me.

I’ll add that:

  1. I don’t think Corona supports Amazon IAP v2 yet - there are a couple of threads running on the Amazon forum about this.  When it does become available, I’ll add support (if IAP v2 is available and I’ve somehow missed it, can someone make a post here to alert me please!)

  2. You aren’t required to use IAP Badger for inventory management.  As jrerejigga suggests, just call your own functions for coin management in the onPurchase listener in your product catalogue (see iap.purchase).  The module will only save information about the inventory if you explicitly tell it to do so.

BTW - according to the forums, I believe Amazon are currently saying that Corona users should mention that they use Corona to build their app in the developer comments sections during the submission process and they’ll waive the need to support IAP v2 (I haven’t tried this myself though).

Thanks for the responses!   Interesting idea to use IAP Badger for more than just IAP data, its almost worth another module from Simon :slight_smile:

Does IAP Badger ‘consume’ the products according to Google / Apple so that a user can re-purchase when they are consumed?

I guess the amazon v2 issue will affect us all for a while!   btw I hjad a fun bug… I had my own save table and load table in my fileio module, when I went to save, it was calling the iap savetable which encrypted my data… I had a good laugh when I figured out what was going on…

Thanks, Greg

Hi Greg,

IAP Badger handles consuming products on your behalf - your code just needs respond to the functions / listeners as described in the IAP Badger documentation / tutorial and your IAP’s will work correctly across Apple, Google and Amazon (well, maybe Amazon…)

Simon  :slight_smile:

please ignore, I moved the FailedListener code above the initOptions and it worked.

Hi,  I have a problem with FailedListener not being called on cancel / fail purchase in debug mode

as in the example, I set:

local iapOptions = {
    catalogue=catalogue,
    filename=“in.txt”,   
    salt = “tr1cky to gue55!”,
    failedListener=failedListener2,
    cancelledListener=failedListener2,
    debugMode = true,
    debugStore = “apple”,
}
iap.init(iapOptions)

and then on cancel / fail, it should call:

function failedListener2()
    print (“WERE AT FAILED”)
–    if (spinner~= nil) then
        spinner:removeSelf()
        spinner=nil
–   end
end

but it never gets there… any ideas?

Thanks, Greg

Hi again,

Are you declaring your failedListener2 function after your iapOptions table?  If so, your failedListener and cancelledListener will be set to nil at this point,because failedListener2 is a forward declaration - which means the function will never be called.

The solution is to define failedListener2 earlier in your code than your iapOptions table, or set up a forward reference near the start of your module.

Regards,

Simon

Hi Simon, moving iapOptions up worked.   I just updated to 2830, and am getting a unable to publish… restricted library (store) from within the simulator on windows.  Can I just ignore this message?

Thanks, Greg

Hi Simon.
I’m finally getting round to testing iap and need to just check something…
should productNames for the products table in iapCatalogue refer to the reverse domain ProductID I set in iTunes connect or the Reference Name? So either ‘Premium’ or ‘com.mydomain.myappname.premium’ ?

And then… when I update my app’s purchase scene and access the information like this…
 

local productsCatalogue = iap.getLoadProductsCatalogue() local title = productsCatalogue.premium.title local description = productsCatalogue.premium.description local localizedPrice = productsCatalogue.premium.localizedPrice

…Will title come through as what is called ‘Reference Name’ in iTunes Connect? I want to check this so I have consistent naming across the app stores.
 

@juliusbangert The values in that product table (title, description, etc.) come from the callback for store.loadProducts. I have included the link to the docs for that function below. This document does not answer your question unfortunately. I just wanted to point out that this is not an IAP Badger specific question, but a general Corona IAP question. Also, below is a snippet of the applicable IAP Badger code. I hope this points you in the right direction.

https://docs.coronalabs.com/api/library/store/loadProducts.html

--Callback function local function loadProductsCallback(event) --If an error was reported (so the product catalogue couldn't be loaded), leave now if (event.isError) then return end --Create an empty catalogue loadProductsCatalogue={} --Copy in all the valid products into the product catalogue for i=1, #event.products do --Grab a copy of the event data (only need to perform a shallow copy) local eventData={} for key, value in pairs(event.products[i]) do eventData[key]=value end --Convert the product identifier (app store specific) into a catalogue product name local catalogueKey=nil for key, value in pairs(catalogue.products) do if (value.productNames[targetStore]==eventData.productIdentifier) then catalogueKey = key break end end if (not catalogueKey) then print("Unable to find a catalogue product name for the product with identifier " .. eventData.productIdentifier) end --Store copy loadProductsCatalogue[catalogueKey]=eventData -- loadProductsCatalogue[eventData.productIdentifier]=eventData end --If a user specified callback function was specified, call it if (loadProductsUserCallback~=nil) then loadProductsUserCallback(event) end end --If possible, this function will download a product table from either Google Play, the App Store or Amazon and call the --specified callback function when complete. The function itself will return true, indicating that a request was made. --If no product table is available, or the store cannot process the request, the function will return false. --If running on the simulator, the user's callback function will be passed a fake array containing fake data based on the product --catalogue specification. -- --Assuming the function is successful, a table containing valid products will be placed in loadProductsCatalgoue, which can be --access by the getLoadProductsCatalogue function - so strictly speaking it is not always necessary to pass a callback and simply --interrogate the loadProductsCatalogue instead. The table will contain false if loadProducts failed, or nil if loadProducts has never --been called. -- --The loadProducts table will be in the form of: --productName -- { -- product data -- } --productName -- { -- product data -- } --... -- -- --callback (optional): the function to call after loadProducts is complete. The original loadProducts callback event data from -- Corona will be passed. local function loadProducts(callback) --If running on the simulator, fake the product array if (targetStore=="simulator") or (debugMode) then fakeLoadProducts(callback) return true end --Return a nil value if products cannot be loaded from the real store if (store.canLoadProducts~=true) then --Record that the loadProductsCatalgue failed loadProductsCatalogue=false --Return that this function failed return false end --Generate a list of products local listOfProducts={} for key, value in pairs(catalogue.products) do listOfProducts[#listOfProducts+1]=value.productNames[targetStore] end --Save the user callback function loadProductsUserCallback=callback --Load products store.loadProducts(listOfProducts, loadProductsCallback) end public.loadProducts = loadProducts

@juliusbangert I just did a quick Google search and found this additional info regarding the product catalog values. Perhaps this will be more helpful than my previous post.

From https://coronalabs.com/blog/2013/09/03/tutorial-understanding-in-app-purchases/

  • store.loadProducts() — a function to return information about products, including localized descriptions, names, and prices. This is not supported on Google Play V2 but is with Google Play V3.