IAP Badger: a unified approach to in-app purchases

Hi Simon,

I have the same problem.

Everything worked well few days ago…

I just hope this is not present in our live apps  :frowning:

***UPDATE***

I commented out iap badger build.settings section, and started using .lua from Github.

Everything works fine.

I will go with Github library!

Thanks.

Ivan

I’ve emailed the guy at Corona who helped set up the repo for the plug in.  Unfortunately I haven’t had a response yet, and can’t progress without help at the moment.

I’ll post any developments here.

Simon

Whilst I was out last night, enjoying a (rare) sunny evening with wine and cocktails, I got an email back from Corona saying there had been an issue with their build server causing the errors discussed above.

The good news is:

  • the wine and cocktails were excellent.
  • Corona have fixed the error on the build server.
  • I rebuilt one of my own apps this morning, installed it on an iOS and Android device, ran some tests, and the plug in appears to working normally again.

The only issue I encountered was, when I first launched the simulator to rebuild my app, it complained the plug in was missing.  Killing and restarting the simulator rectified the problem.

Hope this helps,

Simon


PS - I can’t remember whether I mentioned this, but I’ve also updated the plug in version to make sure the Apple store library is loading correctly.

So at this point is the CL Plugin updated to what you have posted in github?  

Yes, they’re the same.

Great thanks hm.  This is an awesome plugin.  Just curious, when needing to make purchases from different scenes in an app, is it best to initialize a new instance in each scene, along with the purchase/failed listeners?  Or is there a modular approach that would work better?

I know it’s terrible practice, but I set up a global instance of the library*.

The best practice, modular way would be to create a single instance, then pass that instance as a parameter in the composer.gotoScene function.

So, in your main.lua, you’d have something like:

--Create instance of library (for the once and only time) local iap=require("plugin.iap\_badger") --Call the first scene in your game, passing the library as a parameter composer.gotoScene("scene name", { effect="fade", time=300, params={ iapLibrary = iap}  } )

Then, in the scene:create listener, you keep a copy of that parameter in a local variable.  If you call a new scene from there, you’d simply keep passing the reference as a parameter to composer.gotoScene.

Simon

* It’s the only time I ever do this.  Honest.

hi bro i have a problem. i have separate scene for store where code for in-app purchasing resides. i published my game in alpha testing mode and i made an in-app product purchase for the first time, no problem with purchasing but after the purchase finished my game crashes and i does not even called the code in purchaseListener which shows an alert box. Still I can play the game but could not open the store scene and if i click it, the game crashes. Can you please help me…

Could you post a copy of the code that relates to IAP Badger from your scene? (Setting up catalogue, iap.init, any code that calls iap.purchase etc.)

Simon

This is store scene file

local composer = require( "composer" ) local iap = require("iap\_badger") local myData = require("myData") local analytics = require("analytics") local scene = composer.newScene() local widget = require("widget") local storeBg local centerX = display.contentCenterX local centerY = display.contentCenterY local width = display.contentWidth local height = display.contentHeight local spinner=nil local catalogue = { --Information about the product on the app stores products = { buy20Tokens = { productNames = {apple = "buy20tokens",google = "buy20tokens" }, productType = "consumable", onPurchase = function ()iap.addToInventory("buy20token",20) end, onRefund = function() iap.removeFromInventory("buy20token",20) end, }, buy50Tokens = { productNames = {apple = "buy50tokens",google = "buy50tokens" }, productType = "consumable", onPurchase = function ()iap.addToInventory("buy50token",50) end, onRefund = function() iap.removeFromInventory("buy50token",50) end, }, levelUnlock = { productNames = {apple="MixlevelUnlock",google = "mixlevelunlock"}, productType = "non-consumable", onPurchase = function() iap.setInventoryValue("unlocklevel",true) end, onRefund = function() iap.removeFromInventory("unlocklevel",true) end, } }, --Information about how to handle the inventory item inventoryItems = { buy20token = {productType = "consumable", reportMissingAsZero=true}, buy50token = {productType = "consumable", reportMissingAsZero=true}, unlocklevel = {productType = "non-consumable"}, } } local function failedListener() if (spinner) then spinner:removeSelf() spinner=nil end native.showAlert( "Info", "Sorry, Transaction failed." , {"Okay"} ) end --This table contains all of the options we need to specify in this example program. local iapOptions = { --The catalogue generated above catalogue=catalogue, --The filename in which to save the inventory filename="inventory.txt", --Salt for the hashing algorithm salt = "something tr1cky to gue55!", --Listeners for failed and cancelled transactions will just remove the spinner from the screen failedListener=failedListener, cancelledListener=failedListener, --Once the product has been purchased, it will remain in the inventory. Uncomment the following line --to test the purchase functions again in future. It's also useful for testing restore purchases. --doNotLoadInventory=true, --debugMode=true, } --Initialise IAP badger iap.init(iapOptions) iap.loadProducts() local productsDetail = iap.getLoadProductsCatalogue() local price1,price2 if(productsDetail)then price1 = productsDetail.buy20Tokens.localizedPrice price2 = productsDetail.buy50Tokens.localizedPrice end --Called when the relevant app store has completed the purchase local function purchaseListener(product ) spinner:removeSelf() spinner=nil myData.saveFile("levelstatus.txt",tostring(iap.getInventoryValue("unlocklevel"))) --Save the inventory change iap.saveInventory() --Give the user a message saying the purchase was successful native.showAlert("Info", "Your purchase was successful", {"Okay"}) end local function afterPurchase(product ) if(product=="buy20Tokens")then myData.saveFile("skipTokens.txt", tonumber(myData.loadFile("skipTokens.txt"))+20) elseif(product=="buy50Tokens")then myData.saveFile("skipTokens.txt", tonumber(myData.loadFile("skipTokens.txt"))+50) end iap.saveInventory() --Give the user a message saying the purchase was successful native.showAlert("Info", "Token purchase was successful", {"Okay"}) end buyUnlock=function(event) if (event.phase=="ended") then if (event.target.id == "levelunlock") then print("levelunlock pressed") analytics.logEvent("LevelUnlockEvent") local spinnerBackground = display.newRect(display.contentCenterX,240,360,600) spinnerBackground:setFillColor(0,157/255,207/255,0.75) spinnerBackground.alpha = 50 --Spinner consumes all taps so the user cannot tap the purchase button twice spinnerBackground:addEventListener("touch", function() return true end) local spinnerText = display.newText("Contacting " .. iap.getStoreName() .. "...", display.contentCenterX,180, native.systemFont, 18) spinnerText:setFillColor(0,0,0) --Add a little spinning rectangle local spinnerRect = display.newRect(display.contentCenterX,display.contentCenterY,35,35) spinnerRect:setFillColor(0.25,0.25,0.25) transition.to(spinnerRect, { time=4000, rotation=360, iterations=999999, transition=easing.inOutQuad}) --Create a group and add all these objects to it spinner=display.newGroup() spinner:insert(spinnerBackground) spinner:insert(spinnerText) spinner:insert(spinnerRect) iap.purchase("levelUnlock", purchaseListener) end end end consumeToken = function(event) if ( event.phase =="ended")then if (event.target.id == "buy20") then analytics.logEvent("20 skipTokens") iap.purchase("buy20Tokens",afterPurchase) elseif(event.target.id =="buy50")then analytics.logEvent("50 skipTokens") iap.purchase("buy50Tokens", afterPurchase) end end end local function restoreListener(productName, event) --If this is the first product to be restored, remove the spinner --(Not really necessary in a one-product app, but I'll leave this as template --code for those of you writing apps with multi-products). if (event.firstRestoreCallback) then --myData.saveFile("levelstatus.txt",tostring(iap.getInventoryValue("unlocklevel"))) --Save the inventory change spinner:removeSelf() spinner=nil native.showAlert("Restore", "Your items are being restored", {"Okay"}) restoreBtn:setEnabled( false ) restoreBtn.alpha = 0.5 levelunlockbtn:setEnabled( false ) levelunlockbtn.alpha = 0.5 end --Save any inventory changes iap.saveInventory() end local function restorePurchases(event) --Tell IAP to initiate a purchase --Use the failedListener from onPurchase, which just clears away the spinner from the screen. --You could have a separate function that tells the user "Unable to contact the app store" or --similar on a timeout. --On the simulator, or in debug mode, this function attempts to restore all of the non-consumable --items in the catalogue. if (event.phase=="ended")then analytics.logEvent("RestoredEvent") local spinnerBackground = display.newRect(display.contentCenterX,240,360,600) spinnerBackground:setFillColor(1,1,1,0.75) --Spinner consumes all taps so the user cannot tap the purchase button twice spinnerBackground:addEventListener("tap", function() return true end) local spinnerText = display.newText("Contacting " .. iap.getStoreName() .. "...", display.contentCenterX,180, native.systemFont, 18) spinnerText:setFillColor(0,0,0) --Add a little spinning rectangle local spinnerRect = display.newRect(display.contentCenterX,display.contentCenterY,35,35) spinnerRect:setFillColor(0.25,0.25,0.25) transition.to(spinnerRect, { time=4000, rotation=360, iterations=999999, transition=easing.inOutQuad}) --Create a group and add all these objects to it spinner=display.newGroup() spinner:insert(spinnerBackground) spinner:insert(spinnerText) spinner:insert(spinnerRect) iap.restore(false, restoreListener, failedListener) end end function scene:create( event ) local sceneGroup = self.view print( "store create scene" ) end -- "scene:show()" function scene:show( event ) local sceneGroup = self.view local phase = event.phase print( "store show scene" ) if ( phase == "will" ) then storeBg = display.newImage( sceneGroup, "storeBg.png") storeBg.x = centerX storeBg.y = centerY storeBg.width = width storeBg.height = height buy20 = widget.newButton{ width = display.contentWidth\*0.20, height = display.contentHeight\*0.06, defaultFile = "buybtn.png", overFile = "buybtnover.png", label = "0.45$", id = "buy20", onEvent = consumeToken } buy20.x = display.contentWidth\*0.58 buy20.y = display.contentHeight\* 0.50 sceneGroup:insert( buy20 ) buy50 = widget.newButton{ width = display.contentWidth\*0.20, height = display.contentHeight\*0.06, defaultFile = "buybtn.png", overFile = "buybtnover.png", label = "0.75$", id = "buy50", onEvent = consumeToken } buy50.x = display.contentWidth\*0.82 buy50.y = display.contentHeight\* 0.50 sceneGroup:insert( buy50 ) if (iap.getInventoryValue("unlock")==true) then buy20:setEnabled( false ) buy20.alpha = 0 print( "Game unlocked" ) end leveltext = display.newImage( sceneGroup, "leveltext.png" ) leveltext.x = display.contentCenterX leveltext.y = display.contentHeight\*0.65 leveltext.width = display.contentWidth\*0.60 leveltext.height = display.contentHeight\*0.06 levelunlockbtn = widget.newButton{ defaultFile = "levelbtnover.png", overFile = "levelbtn.png", id = "levelunlock", onEvent = buyUnlock, } levelunlockbtn.x= display.contentWidth\*0.30 levelunlockbtn.y= display.contentHeight\*0.75 sceneGroup:insert( levelunlockbtn ) restoreBtn = widget.newButton{ width = display.contentWidth\*0.40, defaultFile = "restorebtnover.png", overFile = "restorebtn.png", onEvent = restorePurchases, } restoreBtn.x = display.contentWidth\*0.70 restoreBtn.y = display.contentHeight\*0.75 sceneGroup:insert( restoreBtn ) if (iap.getInventoryValue("unlocklevel") == true) then levelunlockbtn:setEnabled( false ) levelunlockbtn.alpha = 0.5 restoreBtn:setEnabled( false ) restoreBtn.alpha = 0.5 end elseif ( phase == "did" ) then end end -- "scene:hide()" function scene:hide( event ) local sceneGroup = self.view local phase = event.phase if ( phase == "will" ) then elseif ( phase == "did" ) then end end -- "scene:destroy()" function scene:destroy( event ) local sceneGroup = self.view end -- ------------------------------------------------------------------------------- -- Listener setup scene:addEventListener( "create", scene ) scene:addEventListener( "show", scene ) scene:addEventListener( "hide", scene ) scene:addEventListener( "destroy", scene ) -- ------------------------------------------------------------------------------- return scene

I can see one issue with your code in line 83.

When you call **iap.loadProducts() **there will be a delay before iap.getLoadProductsCatalogue() returns anything but an empty table (because the device has to wait for information back from the server).  When you interrogate that table without checking it has been completed yet, you will crash because you a referencing keys in the table that do not yet exist.

To rectify this, you either need to poll the contents of the table, or pass a listener to  iap.loadProducts which will tell you when the server has replied to your request for product information.

Also, I’m not sure if you’re aware, but you are saving information about purchases in your own data file  and  IAP Badger.  You won’t need to do both.

If this doesn’t help fix the code, post the (relevant) parts of the log from adb logcat and it will help narrow down the problem.

Simon

I made new apk file removing this block

iap.loadProducts() local productsDetail = iap.getLoadProductsCatalogue() local price1,price2 if(productsDetail)then price1 = productsDetail.buy20Tokens.localizedPrice price2 = productsDetail.buy50Tokens.localizedPrice end 

. Now i encounter the same problem. When I press restore button the app crashes and one more thing is i mentioned buy20tokens in-app products as consumable as you see in below code

local catalogue = { --Information about the product on the app stores products = { buy20Tokens = { productNames = {apple = "buy20tokens",google = "buy20tokens" }, productType = "consumable", onPurchase = function ()iap.addToInventory("buy20token",20) end, onRefund = function() iap.removeFromInventory("buy20token",20) end, }, buy50Tokens = { productNames = {apple = "buy50tokens",google = "buy50tokens" }, productType = "consumable", onPurchase = function ()iap.addToInventory("buy50token",50) end, onRefund = function() iap.removeFromInventory("buy50token",50) end, }, levelUnlock = { productNames = {apple="MixlevelUnlock",google = "mixlevelunlock"}, productType = "non-consumable", onPurchase = function() iap.setInventoryValue("unlocklevel",true) end, onRefund = function() iap.removeFromInventory("unlocklevel",true) end, } }, --Information about how to handle the inventory item inventoryItems = { buy20token = {productType = "consumable", reportMissingAsZero=true}, buy50token = {productType = "consumable", reportMissingAsZero=true}, unlocklevel = {productType = "non-consumable"}, } }

I purchased it once and when i again purchase it i got transaction failed event and in logact it says product already owned.

Here is the logcat file generated when i press restore button

W/FlurryAgent( 3633): Flurry session ended D/AndroidRuntime( 3633): procName from cmdline: com.vigames.mathiemind E/AndroidRuntime( 3633): in writeCrashedAppName, pkgName :com.vigames.mathiemind D/AndroidRuntime( 3633): file written successfully with content: com.vigames.mathiemind S tringBuffer : ;com.vigames.mathiemind D/BstCommandProcessor-Application( 1846): Application crash has been observed. W/BstCommandProcessor-Application( 1846): in sendHttpRequest, requestType is of CRASH\_APP type but one of the requiredInfo is NULL, crashedApp = com.bluestacks.BstCommandProcesso r.BstCrashedAppInfo@4e7abeac D/BstCommandProcessor-Application( 1846): in sendHttpRequest, request to send to (fqdn): http://10.0.2.2:2861/AppCrashedInfo D/BstCommandProcessor-Application( 1846): data: {"packageName":"com.vigames.mathiemind"," shortPackageName":"com.vigames.mathiemind","versionCode":3,"versionName":"3.0.0"} I/Process ( 3633): Sending signal. PID: 3633 SIG: 9 E/AndroidRuntime( 3633): FATAL EXCEPTION: Thread-202 E/AndroidRuntime( 3633): Process: com.vigames.mathiemind, PID: 3633 E/AndroidRuntime( 3633): java.lang.IllegalArgumentException: java.security.spec.InvalidKe ySpecException: java.lang.RuntimeException: error:0D07207B:asn1 encoding routines:ASN1\_ge t\_object:header too long E/AndroidRuntime( 3633): at plugin.google.iap.v3.util.Security.generatePublicKey(S ecurity.java:109) E/AndroidRuntime( 3633): at plugin.google.iap.v3.util.Security.verifyPurchase(Secu rity.java:89) E/AndroidRuntime( 3633): at plugin.google.iap.v3.util.IabHelper.queryPurchases(Iab Helper.java:879) E/AndroidRuntime( 3633): at plugin.google.iap.v3.util.IabHelper.queryInventory(Iab Helper.java:567) E/AndroidRuntime( 3633): at plugin.google.iap.v3.util.IabHelper$2.run(IabHelper.ja va:640) E/AndroidRuntime( 3633): at java.lang.Thread.run(Thread.java:841) E/AndroidRuntime( 3633): Caused by: java.security.spec.InvalidKeySpecException: java.lang .RuntimeException: error:0D07207B:asn1 encoding routines:ASN1\_get\_object:header too long E/AndroidRuntime( 3633): at com.android.org.conscrypt.OpenSSLKey.getPublicKey(Open SSLKey.java:101) E/AndroidRuntime( 3633): at com.android.org.conscrypt.OpenSSLRSAKeyFactory.engineG eneratePublic(OpenSSLRSAKeyFactory.java:47) E/AndroidRuntime( 3633): at java.security.KeyFactory.generatePublic(KeyFactory.jav a:171) E/AndroidRuntime( 3633): at plugin.google.iap.v3.util.Security.generatePublicKey(S ecurity.java:104) E/AndroidRuntime( 3633): ... 5 more E/AndroidRuntime( 3633): Caused by: java.lang.RuntimeException: error:0D07207B:asn1 encod ing routines:ASN1\_get\_object:header too long E/AndroidRuntime( 3633): at com.android.org.conscrypt.NativeCrypto.d2i\_PUBKEY(Nati ve Method) E/AndroidRuntime( 3633): at com.android.org.conscrypt.OpenSSLKey.getPublicKey(Open SSLKey.java:99) E/AndroidRuntime( 3633): ... 8 more W/InputDispatcher( 1556): channel '4e9fc308 com.vigames.mathiemind/com.ansca.corona.Coron aActivity (server)' ~ Consumer closed input channel or an error occurred. events=0x9 E/InputDispatcher( 1556): channel '4e9fc308 com.vigames.mathiemind/com.ansca.corona.Coron aActivity (server)' ~ Channel is unrecoverably broken and will be disposed! W/InputDispatcher( 1556): Attempted to unregister already unregistered input channel '4e9 fc308 com.vigames.mathiemind/com.ansca.corona.CoronaActivity (server)' E/SeNsOr-acc( 1556): Host sensor port is 11003 D/SeNsOr-megn( 1556): activate enabled: false I/ActivityManager( 1556): Process com.vigames.mathiemind (pid 3633) has died. I/WindowState( 1556): WIN DEATH: Window{4e9fc308 u0 com.vigames.mathiemind/com.ansca.coro na.CoronaActivity} W/WindowManager( 1556): Force-removing child win Window{4e9ac130 u0 SurfaceView} from con tainer Window{4e9fc308 u0 com.vigames.mathiemind/com.ansca.corona.CoronaActivity} W/ActivityManager( 1556): Force removing ActivityRecord{4e98ca2c u0 com.vigames.mathiemin d/com.ansca.corona.CoronaActivity t4}: app died, no saved state W/WindowManager( 1556): Failed looking up window W/WindowManager( 1556): java.lang.IllegalArgumentException: Requested window android.os.B inderProxy@4ea3378c does not exist W/WindowManager( 1556): at com.android.server.wm.WindowManagerService.windowForCl ientLocked(WindowManagerService.java:8107) W/WindowManager( 1556): at com.android.server.wm.WindowManagerService.windowForCl ientLocked(WindowManagerService.java:8098) W/WindowManager( 1556): at com.android.server.wm.WindowState$DeathRecipient.binde rDied(WindowState.java:1047) W/WindowManager( 1556): at android.os.BinderProxy.sendDeathNotice(Binder.java:493 ) W/WindowManager( 1556): at dalvik.system.NativeStart.run(Native Method) I/WindowState( 1556): WIN DEATH: null D/ActivityManager( 1556): TopActivityInfo, pkgName: com.bluestacks.gamepophome activityNa me: com.bluestacks.gamepophome/tv.gamepop.home.GamePopMain\_ callingPackage: bstSpecialA ppKeyboardHandlingEnabled = false D/ActivityManager( 1556): Showing guidance for pkgName: com.bluestacks.gamepophome D/ActivityManager( 1556): Sending intent for guidance ad screen W/ContextImpl( 1556): Implicit intents with startService are not safe: Intent { act=tv.ga mepop.home.utils.GuidanceScreenForAd (has extras) } com.android.server.am.ActivityStackSu pervisor.bstShowAdGuidance:2895 com.android.server.am.ActivityStackSupervisor.bstSendTopA ctivityInfo:2779 com.android.server.am.ActivityStack.resumeTopActivityLocked:1520 D/GuidanceScreen( 1860): event === app\_launch

Hello can anybody help me in this issue

Hi,

Sorry - work is very busy at the moment which is limiting my time.  Could you please download one of the example code zip files from here and substitute your Google Play identifiers with your own and sign with your android key.  Now run the program and see what happens - as the code on this page is tested, it will help identify whether this is an IAP Badger problem, or to do with how your code is set up on the console.

Simon

Thanks for your time. I downloaded example 1 and replaced google product id with the one I created in developer console. I published the app in alpha mode now when i click the remove ads button it is just loading and nothing happens . I logcat i got this

W/Corona ( 3645): Please call init before trying to purchase products. W/ContextImpl( 2261): Implicit intents with startService are not safe: Intent { act=com.p op.store.AppPlayerSubsciptionService.BIND } android.content.ContextWrapper.bindService:51 7 com.bluestacks.s2p.SubscriptionHelper.\<init\>:27 com.bluestacks.s2p.PingService.onHandle Intent:61 I/ActivityManager( 1533): Start proc com.pop.store for service com.pop.store/.iap.AppPlay erSubscriptionService\_: pid=4018 uid=10060 gids={50060, 3003, 1028, 1015, 1023} D/BluestacksAccountCheckingService( 2261): in onStartCommand W/ContextImpl( 2261): Implicit intents with startService are not safe: Intent { act=com.b luestacks.START\_SILENT\_INSTALL\_MESSENGER } android.content.ContextWrapper.bindService:517 com.bluestacks.s2p.SilentServiceHelper.\<init\>:32 com.bluestacks.s2p.PingService.onHandle Intent:64 I/S2P. ( 2261): Checking Internet connection before showing pay or install pop up. W/ContextImpl( 2261): Implicit intents with startService are not safe: Intent { act=com.b luestacks.AccountService.BIND } android.content.ContextWrapper.bindService:517 com.bluest acks.s2p.BluestacksAccountCheckingService.onStartCommand:81 android.app.ActivityThread.ha ndleServiceArgs:2702 D/SubsriptionHelper( 2261): Service Connected D/BluestacksAccountCheckingService( 2261): S2PHelperService Connected D/BluestacksAccountCheckingService( 2261): email = vimaleee94@gmail.com D/BluestacksAccountCheckingService( 2261): AppSync configured I/S2P. ( 2261): Internet is available. ResponseCode : 200

The line in your log that says, “Please call init before trying to purchase products” indicates that the store libraries might not being included properly.  Could you print out your build.settings file as it stands now?

-- Supported values for orientation: -- portrait, portraitUpsideDown, landscapeLeft, landscapeRight settings = { orientation = { default = "portrait", supported = { "portrait", } }, iphone = { plist = { UIStatusBarHidden = false, UIPrerenderedIcon = true, -- set to false for "shine" overlay --UIApplicationExitsOnSuspend = true, -- uncomment to quit app on suspend --[[-- iOS app URL schemes: CFBundleURLTypes = { { CFBundleURLSchemes = { "fbXXXXXXXXXXXXXX", -- example scheme for facebook "coronasdkapp", -- example second scheme } } } --]] } }, plugins = { --Google in app billing v3 ["plugin.google.iap.v3"] = { -- required publisherId = "com.coronalabs", supportedPlatforms = { android = true }, }, }, -- Android permissions androidPermissions = { "android.permission.INTERNET", "android.permission.ACCESS\_NETWORK\_STATE", "com.android.vending.BILLING", }, }

I didn’t change anything from your example file

Did you add your Google license key for your app in config.lua?

No

Check out the bottom of this page: https://docs.coronalabs.com/plugin/google-iap-v3/