This guide discusses how to implement
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.
There are four basic product types supported for
Each store names these differently. Consider this chart:
Product Type | Apple | 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 |
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
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.
Each marketplace — the Apple App Store, Google Play, and the Amazon Appstore — has different requirements and setup procedures for configuring
The following steps pertain to
Start by configuring your banking and tax information within iTunes Connect.
In the Apple Developer portal, create a new App ID that is unique and fully qualified
Back in iTunes Connect, create a new app with the same Bundle ID as your app in the Apple Developer portal.
Finally, configure your
The following steps pertain to in-app purchasing on Android.
Start by setting up your Google merchant account and link it to the Google Play Developer Console.
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
Configure your
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" }, }, }
license
table to the project's config.lua
file. Inside this table, the key
value should be set to the corresponding application = { license = { google = { key = "YOUR_KEY", }, }, }
.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.
If you haven't already, register for an Amazon Developer account.
If you're new to
Configure your
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" }, }, }
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
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
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
).
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:
productIdentifiers
— A Lua table (array) where each element is a string representing the product identifier of the item. Product identifiers must match those you entered within iTunes Connect, the Google Play Developer Console, and/or the Amazon Developer portal. For example:local productIdentifiers = { "com.domainname.testProduct1", "com.domainname.testProduct2", "com.domainname.testProduct3", }
productListener
— The listener function which will be invoked when the store finishes retrieving the product information. For instance: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" )
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
.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:
store.purchase()
.store.purchase()
was called.store.restore()
.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.isError
— Boolean 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.
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.
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 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
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
iOS devices have a setting which can disable
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
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
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.
Amazon lets you test