In-App Purchasing (IAP)

This guide discusses how to implement in-app purchasing (IAP) within Solar2D apps.

Overview

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

Product Types

There are four basic product types supported for in-app purchasing:

  1. Items which the user can only buy once, for instance paying to unlock the full game, activate a special player ability, unlock levels 20-40, etc.
  2. Items which the user can buy multiple times, for example gem/coin packs, extra lives, etc. Note that for Google IAP, these items must be consumed before they can be purchased again.
  3. Items that can be purchased on a subscription basis that auto-renews, such as a monthly service fee to play a multiplayer game.
  4. Items that can be purchased on a subscription basis that does not auto-renew, for instance an annual fee to see premium content within an informational app.

Each store names these differently. Consider this chart:

Product Type Apple Google Amazon
one-time purchase non-consumable managed entitlement
multiple-time purchase consumable managed with consumption consumable
auto-renewing subscription auto-renewable subscription subscriptions subscription
non-renewing subscription non-renewing subscription    

Product Identifiers

Products are individually identified by a unique string known as a product identifier. Each store may use a different term, but in all cases it's a string value which must be unique for your app. It's recommended that you follow the reverse-domain naming convention across all stores, for example:

com.acmegames.SuperRunner.UnlockFullGame

When configuring products within a store's portal, product identifiers should be named clearly and accurately since you'll need to use them within your Solar2D code to load products, submit purchase requests, and identify the item after a completed transaction. In addition, if you intend to support multiple stores/platforms, you should use a consistent name for each product across all of them — this will prevent the need for extra conditional code throughout your IAP implementation.

Setup / Configuration

Each marketplace — the Apple App Store, Google Play, and the Amazon Appstore — has different requirements and setup procedures for configuring in-app purchasing. Please follow the steps below for each marketplace you intend to target.

The following steps pertain to in-app purchasing on iOS.

  1. Start by configuring your banking and tax information within iTunes Connect. In-app purchases will not work until these steps are cleared, and you won't get an error message reporting so.

  2. In the Apple Developer portal, create a new App ID that is unique and fully qualified (you can not use a wildcard App ID for apps that utilize in-app purchasing). If you need guidance on this process, please see here.

  3. Back in iTunes Connect, create a new app with the same Bundle ID as your app in the Apple Developer portal.

  4. Finally, configure your in-app purchases (products) within iTunes Connect. This task is beyond the scope of this guide, so please see Apple's In-App Purchase Configuration Guide for further assistance.

The following steps pertain to in-app purchasing on Android.

  1. Start by setting up your Google merchant account and link it to the Google Play Developer Console. In-app purchases will not work until these steps are cleared, and you won't get an error message reporting so. Please see here for detailed instructions.

  2. While still in console, create a new app and fill out all of the information needed for publishing the app — note, however, that some details don't need to be final for simply testing in-app purchasing.

  3. Configure your in-app purchases (products). This task is beyond the scope of this guide, so please see Google's Administering In-App Billing guide for further assistance.

  4. On the Solar2D side, integrate the Google Billing plugin by adding an entry into the plugins table of the project's build.settings file:

settings =
{
    plugins =
    {
        ["plugin.google.iap.billing"] =
        {
            publisherId = "com.coronalabs"
        },
    },
}
  1. Optionally add a license table to the project's config.lua file. Inside this table, the key value should be set to the corresponding per-app Licensing key obtained from the Google Play Developer Console. This key is indicated in the Monetization setup section of Monetize. This key would allow the plugin to cryptographically verify purchase receipts right on the device.
application =
{
    license =
    {
        google =
        {
            key = "YOUR_KEY",
        },
    },
}
  1. When you're ready to test in-app purchasing, build your app to create a .apk file which can be uploaded to the Google Play Developer Console. Then, proceed to Google's Testing In-App Billing guide.

The following steps pertain to in-app purchasing on Amazon.

  1. If you haven't already, register for an Amazon Developer account.

  2. If you're new to Amazon in-app purchasing, read Amazon's Understanding In-App Purchasing guide.

  3. Configure your in-app purchases (products). This task is beyond the scope of this guide, so please see Amazon's Submitting IAP Items guide for further assistance.

  4. On the Solar2D side, integrate the Amazon IAP plugin by adding an entry into the plugins table of the project's build.settings file:

settings =
{
    plugins =
    {
        ["plugin.amazon.iap"] =
        {
            publisherId = "com.coronalabs"
        },
    },
}
  1. Finally, install the Amazon App Tester or publish your app in the Amazon Appstore. Details on testing can be found here.

Initializing IAP

Module Inclusion

Because each IAP provider utilizes a different module/plugin on the Solar2D side, you must load the proper one.

If you are only supporting one store, you can simply require() the proper module as follows:

local store = require( "store" )  -- iOS
local store = require( "plugin.google.iap.billing" )  -- Android
local store = require( "plugin.amazon.iap" )  -- Amazon

Alternatively, if you want to support multiple platforms, a conditional routine can be implemented. In the following example, the system.getInfo() call is used to detect which store the built app will target, and that property is used to load the proper module.

local store

local targetAppStore = system.getInfo( "targetAppStore" )

if ( "apple" == targetAppStore ) then  -- iOS
    store = require( "store" )
elseif ( "google" == targetAppStore ) then  -- Android
    store = require( "plugin.google.iap.billing" )
elseif ( "amazon" == targetAppStore ) then  -- Amazon
    store = require( "plugin.amazon.iap" )
else
    print( "In-app purchases are not available for this platform." )
end

When building your app for Android-based devices, including Amazon's Kindle Fire devices, remember to select the correct Target App Store from the Solar2D build dialog window (guide).

Initialization

Once the proper module is loaded, you must initialize it using the store.init() call:

store.init( transactionListener )

The only required argument, transactionListener, is the function you intend to use to handle store transaction requests, including purchases and potential refunds. For example:

local function transactionListener( event )

    local transaction = event.transaction

    if ( transaction.isError ) then
        print( transaction.errorType )
        print( transaction.errorString )
    else
        -- No errors; proceed
    end
end

-- Initialize store
store.init( transactionListener )

The store.init() call is required and must be executed before you attempt to call any other store-related functions.

For Google IAP, this same transaction listener function also handles initialization (init) events. Thus, if you're using Google IAP, you should differentiate store transaction events from initialization events by conditionally checking the value of event.name. Please see the Google IAP store.init() documentation for an example of doing so.

Sometime after calling store.init(), you may check the store.isActive property to confirm that the store has successfully initialized (value of true).

Loading Products

In your Solar2D app, you can use the store.loadProducts() function to load product information which you've entered into the respective stores:

store.loadProducts( productIdentifiers, productListener )

This function requires the following two arguments:

local productIdentifiers = {
    "com.domainname.testProduct1",
    "com.domainname.testProduct2",
    "com.domainname.testProduct3",
}
local productIdentifiers = {
    "com.domainname.testProduct1",
    "com.domainname.testProduct2",
    "com.domainname.testProduct3",
}

local function productListener( event )

end

-- Load store products; store must be properly initialized by this point!
store.loadProducts( productIdentifiers, productListener )

If you kept your product identifiers consistent for all platforms, you can use one product array, but if your identifiers vary between stores, you'll need to use separate arrays and conditional logic with system.getInfo( "targetAppStore" ) to load the appropriate products for the respective store.

Product Properties

When products are loaded, the product listener function (productListener argument for store.loadProducts()) will receive an event table as its sole argument. The properties/keys associated with this table will differ slightly because of core variances in store functionality, but at the very minimum, these two properties will be available:

  • event.products — A table in which each element is another table containing detailed product information. This is primarily the table you'll need to inspect to get detailed information about the loaded products.

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

Typically, you should iterate (loop) through event.products to determine which products are available. For example:

local productIdentifiers = {
    "com.domainname.testProduct1",
    "com.domainname.testProduct2",
    "com.domainname.testProduct3",
}

local function productListener( event )

    for i = 1,#event.products do
        print( event.products[i].productIdentifier )
    end
end

-- Load store products; store must be properly initialized by this point!
store.loadProducts( productIdentifiers, productListener )

For each instance within event.products, various properties will be available and these properties can be used to create an interface (store scene) or display available products in some other manner. Because of core variances in store functionality, these properties will differ for each store, but the following common properties will be available for products in all stores:

  • productIdentifier — A string representing the product identifier.
  • title — A string representing the product title.
  • description — A string representing the product description.
  • localizedPrice — The product price as a localized currency string, for example $0.99.

Together, the above properties should be sufficient to display an informative product listing to the user/player, but you may want to consult the documentation for iOS, Android, and Amazon to inspect additional properties.

Handling Transactions

When a user/player chooses to buy a product within your app, a connection will be made with the store's servers to initiate a transaction. The store will then process the transaction and send data back to your app with the results. As outlined above, this event is handled by the transaction listener function (transactionListener argument within store.init()).

Your transaction listener function should handle all of the following circumstances:

Transaction Properties

When a transaction occurs, various properties will be available within the event.transaction table which is dispatched to the transaction listener function. Because of core variances in store functionality, these properties will differ for each store, but the following common properties will be available for transactions in all stores:

  • state — A string indicating the state of the transaction, for example "purchased", "cancelled", or "failed".
  • identifier — The unique string identifier for the transaction.
  • productIdentifier — A string representing the product identifier associated with the transaction.
  • receipt — A JSON-formatted string representation of the transaction receipt.
  • date — A string representing the date when the transaction occurred.
  • isErrorBoolean value indicating whether an error occurred. If this is true, errorType and errorString will be strings stating the reason.
  • errorType — A string representing the type of error that occurred if isError is true.
  • errorString — A more descriptive error message (string) if isError is true.

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 listener function:

local function transactionListener( event )

    local transaction = event.transaction

    if ( transaction.isError ) then
        print( transaction.errorType )
        print( transaction.errorString )
    else
        -- No errors; proceed
        if ( transaction.state == "purchased" or transaction.state == "restored" ) then
            -- Handle a normal purchase or restored purchase here
            print( transaction.state )
            print( transaction.productIdentifier )
            print( 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( transaction )
    end
end

-- Initialize store
store.init( transactionListener )

For Google IAP, this same transaction listener function also handles initialization (init) events. Thus, if you're using Google IAP, you should differentiate store transaction events from initialization events by conditionally checking the value of event.name. Please see the Google IAP store.init() documentation for an example of doing so.

As indicated, 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.

        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( transaction )
    end
end

As noted above, the properties and values returned will vary slightly because of core variances in store functionality. For example, Apple will return "restored" for the state property of restored purchases, but Google Play and Amazon will group normal purchases and restored purchases collectively under the "purchased" state. In addition, each store returns an assortment of unique properties which you may need to inspect, so please consult the documentation for iOS, Android, and Amazon respectively.

Purchasing Products

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

store.purchase( productIdentifier )

As noted in Transaction Properties above, event.transaction within the transaction listener function will receive a state property of "purchased" for a successfully processed purchase.

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() function initiates this process:

store.restore()

When called, a restore process will be initiated and, during this process, the transaction listener function may be called multiple times (once for each item). Using each product identifier (event.transaction.productIdentifier), you can then reset those products to "owned" or "unlocked" using whatever integration is suitable for your app.

As noted above, Apple will return "restored" for the state property of restored purchases, but Google Play and Amazon will group normal purchases and restored purchases collectively under the "purchased" state. Typically, you can simply handle both conditions using an or operator as in the example shown under Transaction Properties above.

local transaction = event.transaction

if ( transaction.state == "purchased" or transaction.state == "restored" ) then
    -- Handle a normal purchase or restored purchase here

Store-Specific Functionality

Each marketplace offers some unique and potentially critical functionality which you must be aware of. The following list summarizes these, but you should always consult the documentation for iOS, Android, and Amazon to inspect platform-specific functionality in detail.

Purchasing Disabled (Apple)

iOS devices have a setting which can disable in-app purchasing entirely. This is commonly used to prevent children from accidentally purchasing items without permission. For Apple IAP, Solar2D provides the store.canMakePurchases property to check whether purchasing is enabled or disabled. You should use this to check in advance if purchasing is allowed and notify the user if it's forbidden.

Consuming Items (Google)

Google IAP requires that you consume purchases to make item(s) available for purchase again. Essentially, once a product is purchased, it is considered "owned" and it cannot be purchased again. However, since you'll almost certainly want to encourage players to buy certain items again — gem/coin packs, extra lives, etc. — you must send a consumption request to revert "owned" products to "unowned" products so that they become available for purchase again. Consuming products also discards their previous purchase data.

To consume items, call store.consumePurchase() with the associated product identifier:

store.consumePurchase( productIdentifier )

When invoked, and upon receipt of a successful consumption event, the value of event.transaction.state within the transaction listener function will be "consumed". At this point, you may re-enable the product for purchase within your user interface or store scene.

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 or grants a permanent power-up to a character, it should be ineligible for future purchase.

Handling Refunds (Google)

Google IAP allows for transactions to be refunded (instructions). Upon receipt of a successful refund event, the value of event.transaction.state within the transaction listener function will be "refunded" and, in this case, you can disable the content that was refunded and/or delete it from the app locally if it was downloaded.

Sandbox Mode (Amazon)

Amazon lets you test in-app purchasing via a "sandbox" mode in which no real purchases are made. If you want to implement some form of debugging in your Solar2D app, you can use the store.isSandboxMode() API to check if the app is currently in testing mode.