Time for action – using the Corona store module to create an In-App Purchase

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:

  1. In the 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.
  2. Create a new levelselect.lua file and save it to the project folder.
  3. Set up the scene with the following variables and saving/loading functions. The most important variable of all is 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 )
  4. Create the 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
  5. Next, create the 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
      }
  6. Add a local blank table for 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
  7. Create a local function called 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
  8. Create a local function called 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
  9. Set up the display objects for the background and level 1 button:
      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)
  10. Set up the level 2 button placement:
      -- LEVEL 2
      local level2Btn = movieclip.newAnim({"levelLocked.png","level2btn.png"}, 200, 60)
      level2Btn.x = 240; level2Btn.y = 180
      sceneGroup:insert( level2Btn )
  11. Use the local 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  )
  12. Within the same 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")
    
  13. Add an 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
  14. Activate the In-App Purchase with 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)
  15. Create the Close UI button and a local function called 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
  16. Create the 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
  17. Save the file and run the project in the Corona simulator. When you select the Play button, you will notice a 1 button and a Locked button on the level select screen. When you press the Locked button, it calls the store to make a transaction. You will notice a print statement in the terminal that displays what Product ID is being referred to for purchase. Full In-App Purchase features cannot be tested in the simulator. You will have to create a distribution build and upload it on an iOS device to initiate a purchase in the store.
    Time for action – using the Corona store module to create an In-App Purchase

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:

How you handle new products for your store is up to you.