In-App Purchases (IAP)

This guide discusses how to use the Corona store APIs for in-app purchases (IAP).

Overview

In-app purchases (IAP) allow users to purchase additional content from within an app. However, purchasable content cannot be delivered through a marketplace as if it were physical inventory — you must either bundle this content with your app when you build it, anticipating that it will be unlocked upon purchase, or you must download additional content into the app using the network APIs.

iOS Setup

To use in-app purchases on iOS, you must first configure your iOS certificates, App IDs, and provisioning profiles. Please review our Provisioning and Building guide thoroughly, as this is an essential task and you must complete each step correctly.

You must also configure in-app purchases in iTunes Connect, a task which is beyond the scope of this guide. If you need assistance, please review the following:

Notes
  • Start by submitting you banking and tax information to Apple. In-app purchases will not work until these steps are cleared — and you won't get an error message reporting this.

  • In the iOS Dev Center, create a new App ID that is unique and fully qualified, for example com.domainname.ExampleInAppPurchase.

  • In iTunes Connect, create a new app with the same Bundle ID as your app in the Dev Center. Then add your purchasable items with their product identifiers and classifications.

Google Play Setup

To use the Google IAP v3 plugin for in-app purchases on Google Play, add an entry into the plugins table of build.settings. When added, the build server will integrate the plugin during the build phase. Note that the supportedPlatforms table limits this plugin to Android because it's not supported on iOS.

    plugins =
    {
        ["plugin.google.iap.v3"] =
        {
            publisherId = "com.coronalabs",
            supportedPlatforms = { android=true }
        },
    }

In addition, you must enable the BILLING permission in build.settings. Please see the Project Build Settings guide for more information on the build.settings file.

settings =
{
    android =
    {
        usesPermissions =
        {
            "com.android.vending.BILLING",
        },
    },
}

Next, you must build your app — even if it's still in development — to create a .apk file which can be uploaded to the Google Play Developer Console. Once it's uploaded, you must activate it, but do not publish it yet!

The final step (at this stage) is to add your in-app purchases, a task which is beyond the scope of this guide. If you need assistance, please review the following:

Store Functions

Initialization

For iOS, you must first require the Corona store library:

local store = require( "store" )

For Google Play IAP v3, you must require the plugin instead of the store library:

local store = require( "plugin.google.iap.v3" )

If you want to support both platforms, a conditional routine may be implemented:

local store
local v3 = false

if ( system.getInfo( "platformName" ) == "Android" ) then
    store = require( "plugin.google.iap.v3" )
    v3 = true
elseif ( system.getInfo( "platformName" ) == "iPhone OS" ) then
    store = require( "store" )
else
    native.showAlert( "Notice", "In-app purchases are not supported in the Corona Simulator.", { "OK" } )
end

Next, you must initialize the store using the store.init() function.

store.init( [storeName,] listener )

The first argument (optional) is the store name of either "apple" or "google". If you do not specify a name, Corona will attempt to guess the correct store. If you're developing a cross-platform app, you can detect the appropriate marketplace using our APIs — see Platform Detection below for more information.

The second argument, listener, is the function you'll use to handle transaction callbacks (details below).

After calling store.init(), you may call the store.isActive() function to confirm that the store has successfully initialized (return value of true). Currently, it will return false on a Corona app built for Amazon or Nook.

Loading Product Information

You can load product information that you've entered into the stores. Corona provides the store.canLoadProducts function which returns true if the initialized store supports the loading of products. Following this check, the store.loadProducts() function will retrieve information about items available for sale. This includes the localized price, name, and description of each item.

store.loadProducts( arrayOfProductIdentifiers, listener )
  • arrayOfProductIdentifiers — a Lua table wherein each element is a string representing the product identifier of the item. For iOS, product identifiers must match those you entered in iTunes Connect, for example "com.domainname.ExampleInAppPurchase.ExampleConsumableItem". For Google Play, use the same product identifiers that you specified in the Google Play Developer Console, for example "android.test.purchased".

  • listener — a callback function that is invoked when the store finishes retrieving the product information.

The resulting event table from the callback listener will contain the following properties:

  • event.products — a table wherein each element is another table containing product information.
  • event.invalidProducts — a table in which each element is a string representing the product identifier. This table will only be populated by products that don't exist or aren't available.

In turn, each entry in the event.products table will contain the following fields:

  • title — the localized name of the item.
  • description — the localized description of the item.
  • price — the localized price of an item, as a number.
  • productIdentifier — the product identifier.

The following example shows how to use a callback function to retrieve product information. Typically, you will use this information to create a UI interface that allows users to browse and buy items.

local function loadProductsCallback( event )

    local validProducts = event.products
    local invalidProducts = event.invalidProducts

    print( "Valid Products:", #validProducts )

    for i = 1,#validProducts do
        local currentItem = validProducts[i]
        print( currentItem.title )
        print( currentItem.description )
        print( currentItem.price )
        print( currentItem.productIdentifier )
    end

    print( "Invalid Products:", #invalidProducts )

    for i = 1,#invalidProducts do
        print( invalidProducts[i] )
    end
end

local arrayOfProductIdentifiers = 
{
    "com.domainname.ExampleInAppPurchase.ExampleConsumableItem",
    "com.domainname.ExampleInAppPurchase.ExampleNonConsumableItem",
    "com.domainname.ExampleInAppPurchase.ExampleSubscriptionItem"
}

if ( store.isActive ) then

    if ( store.canLoadProducts ) then
        store.loadProducts( arrayOfProductIdentifiers, loadProductsCallback )
    else
        --this store does not support an app fetching products
    end

end

Purchases Enabled

iOS devices have a setting which disables in-app purchase. This is commonly used to prevent children from accidentally purchasing items without permission. Corona provides the store.canMakePurchases API to check whether purchasing is enabled. You should use this preemptively to notify your users if purchasing is forbidden. This API returns true if purchases are enabled, false otherwise.

Purchasing Products

To initiate a purchase, use the store.purchase() API. When called, this API will submit purchase requests to the store. When the store finishes processing the transaction, the listener function you specified in store.init() will be invoked.

For iOS, store.purchase() expects an array (Lua table) of products to purchase. Each item within the table may be a string representing the product identifier. Alternatively, this table may be the array of product elements returned as event.products in the loadProductsCallback listener.

store.purchase( arrayOfProducts )  -- iOS

-- Example
store.purchase( { "com.domainname.ExampleInAppPurchase.ExampleConsumableItem" } )

For Google Play, store.purchase() expects a single identifier as a string.

store.purchase( "productIdentifier" )  -- Google Play

-- Example
store.purchase( "com.domainname.ExampleInAppPurchase.ExampleConsumableItem" )
Note

Currently, there is no explicit API to specify quantities for consumable items. However, you may place the product in the table multiple times and Corona will set the quantity behind the scenes.

Transaction Listener

Calling store.init() enables you to handle transaction callbacks from the store using the listener function you provide. This listener should handle all of the following circumstances:

  • An item was just purchased via store.purchase().
  • A purchase transaction was cancelled by the user after store.purchase() was called.
  • A purchase transaction failed for various reasons.
  • Purchase of an item in a previous session was interrupted and the store needs to resume/complete the transaction.
  • A restore purchased items request was initiated via store.restore() (details below).

In the resulting transaction callback function, event.transaction will contain the following properties:

  • state — a string representing the state of the transaction: purchased, restored, cancelled, or failed.
  • productIdentifier — the product identifier associated with the transaction.
  • receipt — a unique receipt from the store, returned as a JSON packet in string form.
  • signature — a digital signature string that can be used to verify the purchase. This is the inapp_signature returned by Google Play.
  • identifier — a unique transaction identifier (string) returned from the store.
  • date — the date when the transaction occurred.
  • errorType — the type of error (string) that occurred if the state is failed.
  • errorString — a (sometimes) more descriptive error message of what went wrong in the failed case.

On iOS, the following properties are also returned, relevant to a store.restore() call:

  • originalReceipt — a unique receipt from the original purchase attempt, returned as a hexadecimal string.
  • originalIdentifier — a unique transaction identifier (string) returned from the original purchase attempt.
  • originalDate — the date when the original transaction occurred.

After you receive a transaction event, you should take the appropriate action. For example, if the user successfully purchased an item, you might save this information to a file or local database and unlock the ability to use the item. This file can then be referenced in future sessions so you know that the item has been purchased.

The following is an example framework for the transaction callback function:

local function storeTransaction( event )

    local transaction = event.transaction

    if ( transaction.state == "purchased" ) then

        --handle a successful transaction here
        print( "productIdentifier", transaction.productIdentifier )
        print( "receipt", transaction.receipt )
        print( "signature:", transaction.signature )
        print( "transactionIdentifier", transaction.identifier )
        print( "date", transaction.date )

    elseif ( transaction.state == "cancelled" ) then

        --handle a cancelled transaction here

    elseif ( transaction.state == "failed" ) then

        --handle a failed transaction here
    end

    --tell the store that the transaction is complete!
    --if you're providing downloadable content, do not call this until the download has completed
    store.finishTransaction( event.transaction )

end
Important

As noted above, you must call store.finishTransaction() on the transaction object when the transaction is complete. If you don't, the store will think that the transaction was interrupted and will attempt to resume it on the next application launch. If you're offering the item as downloadable content, do not call this until the download is complete.

Consuming Consumable/Unmanaged Items

Google Play IAP v3 requires that you consume purchases to make the items available for purchase again. Essentially, once a product is purchased, it is considered "owned" and it cannot be purchased again. Thus, you must send a consumption request to revert "owned" products to "unowned" products so they become available for purchase again. Consuming products also discards their previous purchase data.

Note that some items are designed to be purchased only once and you should not consume them. For example, if a purchase unlocks a new world within a game, it should be ineligible for future consumption. In the Google IAP portal, these type of purchases are set up as managed products. Alternatively, some items can be purchased multiple times, for example energy packs and gems. These are set up as unmanaged products. However, because Google now considers these purchases as "owned" just like managed products, they must be consumed before they can be purchased again. For further information, please see Google's documentation.

To consume items, call store.consumePurchase():

store.consumePurchase( { "product1", "product2" }, listener )

In this case, listener can be a unique callback function to handle the consumption, or it can simply be your main transaction listener. In either case, the value of event.transaction.state will be consumed. There are no callbacks for invalid products.

Restoring Purchased Items

If a user wipes clean the information on a device or buys a new device, he/she needs a way to restore items previously purchased from your app (without paying for them again). The store.restore() API initiates this process. You can check for previously-purchased items in the same storeTransaction listener that you use for your successful, failed, and cancelled purchases.

Note

In iOS, the transaction state of restored items will be "restored" and, in this case, your app may use the originalReceipt, originalIdentifier, and originalDate fields of the transaction object.

In the Google Play Marketplace, there is no "restored" state for items. All purchased items will be grouped under the "purchased" state. When you do a restore, you should clear all purchases saved to file/database — except for consumable purchases — and treat the returned restored purchases as normal purchases.

local function storeTransaction( event )

    local transaction = event.transaction

    if ( transaction.state == "purchased" ) then

        --on Google Play, check and update all purchased items here

    elseif ( transaction.state == "restored" ) then

        --handle "restored" transactions here (iOS only)
        print( "productIdentifier", transaction.productIdentifier )
        print( "originalReceipt", transaction.originalReceipt )
        print( "originalTransactionIdentifier", transaction.originalIdentifier )
        print( "originalDate", transaction.originalDate )

    end

    store.finishTransaction( event.transaction )

end

Handling Refunds

The Google Play Marketplace allows for transactions to be refunded. You can check for refunded transactions in the same storeTransaction listener that you use for your successful, failed, and cancelled purchases. The transaction state will be "refunded" and, in this case, you can disable the content that was refunded, or delete it from the app locally if it was downloaded.

local function storeTransaction( event )

    local transaction = event.transaction

    if ( transaction.state == "purchased" ) then

        --handle a successful transaction here

    elseif ( transaction.state == "refunded" ) then

        --refunds are only supported by the Google Play Marketplace
        print( "productIdentifier", transaction.productIdentifier )

    end

    store.finishTransaction( event.transaction )

end

Platform Detection

To detect which marketplace your app should use, Corona offers the following functions, both of which can be read before you call store.init().

The following examples show how you can use a different product list for each platform and set currentProductList depending on the available store.

local store
local v3 = false

if ( system.getInfo( "platformName" ) == "Android" ) then
    store = require( "plugin.google.iap.v3" )
    v3 = true
elseif ( system.getInfo( "platformName" ) == "iPhone OS" ) then
    store = require( "store" )
else
    native.showAlert( "Notice", "In-app purchases are not supported in the Corona Simulator.", { "OK" } )
end

local currentProductList = nil

local appleProductList = {
    "com.domainname.ExampleInAppPurchase.ConsumableTier1",
    "com.domainname.ExampleInAppPurchase.NonConsumableTier1",
    "com.domainname.ExampleInAppPurchase.SubscriptionTier1"
}

local googleProductList = {
    --these product IDs are for testing and are supported by all Android apps (purchasing these products will not bill your account)
    "android.test.purchased",
    "android.test.canceled",
    "android.test.item_unavailable"
}

--utilize 'store.availableStores' function:
if ( store.availableStores.apple ) then

    currentProductList = appleProductList
    store.init( "apple", storeTransaction )

elseif ( v3 == true or store.availableStores.google ) then

    currentProductList = googleProductList
    store.init( "google", storeTransaction )

else
    print( "In-app purchases are not supported on this system/device." )
end

--OR utilize 'store.target' function:
if ( store.target == "apple" ) then

    currentProductList = appleProductList
    store.init( "apple", storeTransaction )

elseif ( store.target == "google" ) then

    currentProductList = googleProductList
    store.init( "google", storeTransaction )

else
    print( "In-app purchases are not supported on this system/device." )
end