Now that we have set up our Product ID for our In-App Purchase in iTunes Connect, we can implement it in our app to purchase the product we're going to sell. A sample menu app of Breakout was created to demonstrate how to purchase levels within an application. The app contains two levels in the level select screen. The first is available by default. The second is locked and can only be unlocked by purchasing it for $0.99. We're going create a level select screen so that it acts in that manner:
Chapter 11
folder, copy the Breakout In-App Purchase Demo
project folder to your desktop. You can download the project files accompanying this book from the Packt Publishing website. You will notice that the configuration, libraries, assets, and .lua
files needed are included.levelselect.lua
file and save it to the project folder.local store = require("store")
, which calls the store module for In-App Purchases:local composer = require( "composer" ) local scene = composer.newScene() local ui = require("ui") local movieclip = require( "movieclip" ) local store = require("store") --------------------------------------------------------------------------------- -- BEGINNING OF YOUR IMPLEMENTATION --------------------------------------------------------------------------------- local menuTimer -- AUDIO local tapSound = audio.loadSound( "tapsound.wav" ) --*************************************************** -- saveValue() --> used for saving high score, etc. --*************************************************** local saveValue = function( strFilename, strValue ) -- will save specified value to specified file local theFile = strFilename local theValue = strValue local path = system.pathForFile( theFile, system.DocumentsDirectory ) -- io.open opens a file at path. returns nil if no file found local file = io.open( path, "w+" ) if file then -- write game score to the text file file:write( theValue ) io.close( file ) end end --*************************************************** -- loadValue() --> load saved value from file (returns loaded value as string) --*************************************************** local loadValue = function( strFilename ) -- will load specified file, or create new file if it doesn't exist local theFile = strFilename local path = system.pathForFile( theFile, system.DocumentsDirectory ) -- io.open opens a file at path. returns nil if no file found local file = io.open( path, "r" ) if file then -- read all contents of file into a string local contents = file:read( "*a" ) io.close( file ) return contents else -- create file b/c it doesn't exist yet file = io.open( path, "w" ) file:write( "0" ) io.close( file ) return "0" end end -- DATA SAVING local level2Unlocked = 1 local level2Filename = "level2.data" local loadedLevel2Unlocked = loadValue( level2Filename )
create()
event and remove the "mainmenu"
, "level1"
, and "level2"
scenes:-- Called when the scene's view does not exist: function scene:create( event ) local sceneGroup = self.view -- completely remove maingame and options composer.removeScene( "mainmenu" ) composer.removeScene( "level1" ) composer.removeScene( "level2" ) print( "\nlevelselect: create event" ) end
show()
event and an array that contains a string of Product ID set as an In-App Purchase in iTunes Connect:function scene:show( event ) local sceneGroup = self.view print( "levelselect: show event" ) local listOfProducts = { -- These Product IDs must already be set up in your store -- Replace Product ID with a valid one from iTunes Connect "com.companyname.appname.NonConsumable", -- Non Consumable In-App Purchase }
validProducts
and invalidProducts
. Create a local function called unpackValidProducts()
that checks valid and invalid Product IDs:local validProducts = {} local invalidProducts = {} local unpackValidProducts = function() print ("Loading product list") if not validProducts then native.showAlert( "In-App features not available", "initStore() failed", { "OK" } ) else print( "Found " .. #validProducts .. " valid items ") for i=1, #invalidProducts do -- Debug: display the product info native.showAlert( "Item " .. invalidProducts[i] .. " is invalid.",{ "OK" } ) print("Item " .. invalidProducts[i] .. " is invalid.") end end end
loadProductsCallback()
with an event
parameter. Set up the handler to receive product information with print statements:local loadProductsCallback = function( event ) -- Debug info for testing print("loadProductsCallback()") print("event, event.name", event, event.name) print(event.products) print("#event.products", #event.products) validProducts = event.products invalidProducts = event.invalidProducts unpackValidProducts () end
transactionCallback()
with an event
parameter. Add several cases of results that are supposed to occur for every transaction.state
event. When the store is done with the transaction, call store.finishTransaction(event.transaction)
before the end of the function. Set up a another local function called setUpStore()
with an event
parameter to call store.loadProducts(listOfProducts, loadProductsCallback)
:local transactionCallback = function( event ) if event.transaction.state == "purchased" then print("Transaction successful!") saveValue( level2Filename, tostring(level2Unlocked) elseif event.transcation.state == "restored" then print("productIdentifier", event.transaction.productIdentifier) print("receipt", event.transaction.receipt) print("transactionIdentifier", event.transaction.transactionIdentifier) print("date", event.transaction.date) print("originalReceipt", event.transaction.originalReceipt) elseif event.transaction.state == "cancelled" then print("Transaction cancelled by user.") elseif event.transaction.state == "failed" then print("Transaction failed, type: ", event.transaction.errorType, event.transaction.errorString) local alert = native.showAlert("Failed ", infoString,{ "OK" }) else print("Unknown event") local alert = native.showAlert("Unknown ", infoString,{ "OK" }) end -- Tell the store we are done with the transaction. store.finishTransaction( event.transaction ) end local setupMyStore = function(event) store.loadProducts( listOfProducts, loadProductsCallback) print ("After store.loadProducts(), waiting for callback") end
local backgroundImage = display.newImageRect( "levelSelectScreen.png", 480, 320 ) backgroundImage.x = 240; backgroundImage.y = 160 sceneGroup:insert( backgroundImage ) local level1Btn = movieclip.newAnim({"level1btn.png"}, 200, 60) level1Btn.x = 240; level1Btn.y = 100 sceneGroup:insert( level1Btn ) local function level1touch( event ) if event.phase == "ended" then audio.play( tapSound ) composer.gotoScene( "loadlevel1", "fade", 300 ) end end level1Btn:addEventListener( "touch", level1touch ) level1Btn:stopAtFrame(1)
-- LEVEL 2 local level2Btn = movieclip.newAnim({"levelLocked.png","level2btn.png"}, 200, 60) level2Btn.x = 240; level2Btn.y = 180 sceneGroup:insert( level2Btn )
onBuyLevel2Touch(event)
function and create an if
statement to check event.phase == ended and level2Unlocked ~= tonumber(loadedLevel2Unlocked)
so that the scene changes to mainmenu.lua
:local onBuyLevel2Touch = function( event ) if event.phase == "ended" and level2Unlocked ~= tonumber(loadedLevel2Unlocked) then audio.play( tapSound ) composer.gotoScene( "mainmenu", "fade", 300 )
if
statement, create a local function called buyLevel2()
with a product
parameter to call the store.purchase()
function:local buyLevel2 = function ( product ) print ("Congrats! Purchasing " ..product) -- Purchase the item if store.canMakePurchases then store.purchase( {validProducts[1]} ) else native.showAlert("Store purchases are not available, please try again later", { "OK" } ) – Will occur only due to phone setting/account restrictions end end -- Enter your product ID here -- Replace Product ID with a valid one from iTunes Connect buyLevel2("com.companyname.appname.NonConsumable")
elseif
statement to check when level 2 has been purchased and unlocked, once the transaction has been completed:elseif event.phase == "ended" and level2Unlocked == tonumber(loadedLevel2Unlocked) then audio.play( tapSound ) composer.gotoScene( "loadlevel2", "fade", 300 ) end end level2Btn:addEventListener( "touch", onBuyLevel2Touch ) if level2Unlocked == tonumber(loadedLevel2Unlocked) then level2Btn:stopAtFrame(2) end
store.init()
and call transactionCallback()
as the parameter. Also call setupMyStore()
with a timer set at 500 milliseconds:store.init( "apple", transactionCallback) timer.performWithDelay (500, setupMyStore)
onCloseTouch()
with an event parameter. Have the function transition scenes to loadmainmenu.lua
upon release of the Close button. Close the enterScene()
event with end
:local closeBtn local onCloseTouch = function( event ) if event.phase == "release" then audio.play( tapSound ) composer.gotoScene( "loadmainmenu", "fade", 300 ) end end closeBtn = ui.newButton{ defaultSrc = "closebtn.png", defaultX = 100, defaultY = 30, overSrc = "closebtn.png", overX = 105, overY = 35, onEvent = onCloseTouch, id = "CloseButton", text = "", font = "Helvetica", textColor = { 255, 255, 255, 255 }, size = 16, emboss = false } closeBtn.x = 80; closeBtn.y = 280 closeBtn.isVisible = false sceneGroup:insert( closeBtn ) menuTimer = timer.performWithDelay( 200, function() closeBtn.isVisible = true; end, 1 ) end
hide()
and destroy()
events. Within the hide()
event, cancel the menuTimer
timer. Add all the event listeners for the scene events and return scene
:-- Called when scene is about to move offscreen: function scene:hide() if menuTimer then timer.cancel( menuTimer ); end print( "levelselect: hide event" ) end -- Called prior to the removal of scene's "view" (display group) function scene:destroy( event ) print( "destroying levelselect's view" ) end -- "create" event is dispatched if scene's view does not exist scene:addEventListener( "create", scene ) -- "show" event is dispatched whenever scene transition has finished scene:addEventListener( "show", scene ) -- "hide" event is dispatched before next scene's transition begins scene:addEventListener( "hide", scene ) -- "destroy" event is dispatched before view is unloaded, which can be scene:addEventListener( "destroy", scene ) return scene
In this example, we used the saveValue()
and loadValue()
functions from BeebeGames Class to implement how our locked level will go from locked to unlocked mode using movie clips as buttons. The array in local listOfProducts
displays Product ID in a string format. The Product ID in this example needs to be a nonconsumable In-App Purchase type and has to be an existing one in iTunes Connect.
The unpackValidProducts()
function checks how many valid and invalid items are in the In-App Purchase. The loadProductsCallback()
function receives the product information in the store. The transactionCallback(event)
function checks every state: "purchased"
, "restored"
, "cancelled"
, and "failed"
. When a "purchased"
state is achieved within the In-App Purchase, the saveValue()
function is called to change the value of level2.data
. When the transaction is completed, store.finishTransaction(event.transaction)
needs to be called to tell the store that you are done with your purchase.
The setupMyStore(event)
function calls store.loadProducts(listOfProducts, loadProductsCallback)
and checks the available Product ID (or IDs) in the application. The event is handled once store.init(transactionCallback)
is initialized and setupMyStore()
is called.
The onBuyLevel2Touch(event)
function allows us to check when an In-App Purchase has been made for the locked level. When the user is able to purchase and when they accept the In-App Purchase, the transaction is processed and the value of level2Unlocked
will match that of tonumber(loadedLevel2Unlocked)
. The buyLevel2(product)
function validates the purchased item with store.purchase()
once the Product ID returns valid.
After the In-App Purchase, the screen transitions to the main menu to allow the Locked button to change to the level 2 button. Once the button has changed to frame 2, level 2 is accessible.
Now that you know how to create an In-App Purchase for one product, try adding more than one product to the same application. The scenarios are open ended.
You can add the following: