Posted on by

iap-featIn the mobile development world, studios have been looking for more models to monetize their apps. This doesn’t just impact games, as business app developers are finding it more challenging to monetize as well.

There are several ways to make money with your app, including:

  1. Charge for it.
  2. Offer a limited free version and a paid full-featured version; you can use the “lite” app to drive customers to the paid one.
  3. Offer it for free and use ads to monetize.
  4. Offer it for free but charge for features.

Each version has its upsides and downsides:

  • Paid: This option has two pitfalls: first, people are now spoiled on “free,” and second, paid apps will typically reside below free apps in any ranking service, limiting your app’s visibility.
  • Free-to-play: This model worked well for many years, but it tends to clutter the app stores and some vendors have been discouraging this approach as there are better ways to accomplish the same goal.
  • Ad-funded: Using advertising is an appealing option, but you need a large install base and an app that people will use frequently for the ads to pay off. For many indie titles, the competition is stiff. Also, if you’re a game developer, keep in mind that games have a fairly short shelf life.
  • “Freemium”: You can offer the app for free and then charge for additional features using In-App Purchases (IAP). In this case, the app is free, but has some features that the customer needs to pay for before they can use them. This can be a way to accomplish option #2 — a free (but limited) game – and once you pay for it, you get the entire game. Another use of freemium is a full-blown economic system where your app has some in-game currency and players can earn this currency through play. If they need more, they can pay real-world money to get more in-game currency. Finally, you could use freemium to have a donation system: if the player enjoys the app, they may consider supporting the development for some amount of money.

In-App Purchase Options

Corona SDK currently offers IAP through three vendors:

  1. Apple’s App Store
  2. Google Play (V2)
  3. Google Play (V3)
  4. Fortumo (NOOK)
  5. Amazon In-App Purchase
  6. OUYA

For Apple, Google and Amazon, accessing IAP is done through the Corona Store API. Apple and Google are built into Corona’s core, and Amazon will be a third party plugin. Fortumo is also a third party plugin, but it does not use the store API.

Because each vendor’s “eco-system” varies from the others, there are some things that simply cannot be 100% cross-platform compatible. Part of working with IAP is learning the differences between the vendors and adapting your code to those differences.

Purchase Types

There are generally three classes of things users can purchase:

  1. One-time purchases that last the life of the app, like unlocking the full version of the app, or downloading new content once.
  2. One-time purchases that get “consumed.” Once they’re used, the app user needs to buy more. This is most commonly used for in-app currency.
  3. Subscriptions where the app user pays a monthly fee to receive some ongoing service. Subscriptions can be auto-renewing or non-renewing.

Each vendor uses different terminology for these purchases. Please view this chart to help translate each vendor’s language:

Apple Google Amazon Fortumo OUYA
One Time, Lasts Forever Non-Consumable Managed Product Entitlement Single Item Entitlement
One Time; Gets Used Consumable Unmanaged Product Consumable Virtual Credits Consumable
Subscription 1. Auto-Renewable Subscription
2. Non-Renewable Subscription
Subscription Subscription n/a n/a

Setup

Before you can use IAP, you must set up products in each vendor’s developer portal. This topic is too complex to detail in this tutorial, but in general terms, you have to:

  • Create each product you want to sell, giving it a unique ID*.
  • Submit the items for approval to the appropriate store. Both Apple and Amazon also require screenshots of your for sale screens.
  • For Apple and Google, set up unique test accounts. Amazon has a different method of testing.

* For all of these vendors, you can specify a unique ID. Google and Apple calls it Product ID. Amazon calls it a SKU. In all three cases, it’s recommended that you use the “reverse domain name” system that is used in your Bundle ID/App Identifiers. If you maintain consistent identifiers, it will make cross-platform implementation easier.

An example of this format is:

com.yourcompany.yourapp.yourproduct1

Which may translate to products like these:

com.acme.superrunner.coin10pack
com.acme.superrunner.upgrade

Google will require you to upload your .apk with the proper permissions installed before you can add products to their store portal (although you can upload an Alpha build in which you do not need to make the app live).

These are the required permissions for Android:

android =
{
   usesPermissions =
   {
      "android.permission.INTERNET",
      "com.android.vending.BILLING",
      "com.android.vending.CHECK_LICENSE",
   },
},

Plugins

Several vendors are implemented using Corona SDK’s Plugin system.  The following require you to include extra code in your build.settings to work:

Plugins are added to your build.settings file.  Each vendor has a slightly different requirement, but you can generally copy and paste the required code from the documentation page linked above.  See our build.settings tutorial for more information on adding plugins.

The store.* API

Apple, Google and Amazon are accessed trough Corona SDK’s built-in IAP library, the store module.

NOTE:Because Fortumo and OUYA have their own API, they will not be covered in this tutorial.

For Apple and Google Play V2, Include it in your app using a require() statement as such:

local store = require( "store" )

Since Google Play V3 and Amazon are implemented via a plugin you have to require them slightly differently:

local store = require("store")
if store.target == "google" then
    store = require( "plugin.google.iap.v3" )
end
if store.target == "amazon" then
    store = require( "plugin.amazon.iap" )
end
NOTE:Google Play V3 also requires licensing code be added to your config.lua as well. Please see the documentation for that information.

Of course you need to do this in each module/scene where you’ll be accessing the store. From there, three main API calls will be used (there are few others which we’ll cover later):

  • store.init()
  • store.purchase()
  • store.restore()

The store.init() call lets you define which store you wish to work with, while providing an event listener function to handle the results from interactions with the store.

To purchase things, use store.purchase() — simply pass to the function what you want to buy, and then when the transaction with the app’s user is done, the function you defined in store.init() will be called with the results of the transaction. Your app would then respond accordingly (unlock features, add to your “coin” count, etc.).

In the event that the app user deletes your app or wipes their device and re-installs your app, they are entitled to re-obtain their purchases that were non-consumable. It’s your responsibility to restore these purchases, which Corona allows via the store.restore() call.

NOTE: Consumable items like coins, gems, etc. are not restored. None of the vendors will respond to requests to restore these.

The other functions in the store.* API includes:

  • store.availableStores — used to see if a store is valid for your device.
  • store.canLoadProducts — check to see if loading products is allowed, which it’s not for Google Play V2.
  • store.canMakePurchases — parents can turn off purchasing for their kids.
  • store.finishTransaction() — very important call, used in your event listener function.
  • store.isActive — a way to confirm the store was initialized properly.
  • store.loadProducts() — a function to return information about products, including localized descriptions, names, and prices. This is not supported on Google Play V2 but is with Google Play V3.
  • store.consumePurchase() — a Google Play V3 function to enable re-purchasing consumable items.
  • store.target — returns the value of the store selected in the build screen.

Sample Store Code

Let’s look at a sample API store project. Before we do, you’ll need a few things to support your app. First, you will need the ability to load and save settings data so you can track if the person has paid for their unlock, the current coins count, etc. Let’s include that in a utility.lua file that we’ll include with our code. This is a lot of code to digest, but it’s the minimal structure.

utility.lua

--we need a few support functions
local M = {}
local json = require("json")

function M.saveTable(t, filename)
   local path = system.pathForFile( filename, system.DocumentsDirectory)
   local file = io.open(path, "w")
   if file then
      local contents = json.encode(t)
      file:write( contents )
      io.close( file )
      return true
   else
      return false
   end
end

--simple load table function
function M.loadTable(filename)
   local path = system.pathForFile( filename, system.DocumentsDirectory)
   local contents = ""
   local myTable = {}
   local file = io.open( path, "r" )
   if file then
      local contents = file:read( "*a" )
      myTable = json.decode(contents);
      io.close( file )
      return myTable
   end
   print(filename, "file not found")
   return nil
end

function M.makeTimeStamp(dateString, mode)
    local pattern = "(%d+)%-(%d+)%-(%d+)%a(%d+)%:(%d+)%:([%d%.]+)([Z%p])(%d*)%:?(%d*)";
    local xyear, xmonth, xday, xhour, xminute, xseconds, xoffset, xoffsethour, xoffsetmin
    local monthLookup = {Jan = 1, Feb = 2, Mar = 3, Apr = 4, May = 5, Jun = 6, Jul = 7, Aug = 8, Sep = 9, Oct = 10, Nov = 11, Dec = 12}
    local convertedTimestamp
    local offset = 0
    if mode and mode == "ctime" then
        pattern = "%w+%s+(%w+)%s+(%d+)%s+(%d+)%:(%d+)%:(%d+)%s+(%w+)%s+(%d+)"
        local monthName, TZName
        monthName, xday, xhour, xminute, xseconds, TZName, xyear = string.match(dateString,pattern)
        xmonth = monthLookup[monthName]
        convertedTimestamp = os.time({year = xyear, month = xmonth,
        day = xday, hour = xhour, min = xminute, sec = xseconds})
    else
        xyear, xmonth, xday, xhour, xminute, xseconds, xoffset, xoffsethour, xoffsetmin = string.match(dateString,pattern)
        convertedTimestamp = os.time({year = xyear, month = xmonth,
        day = xday, hour = xhour, min = xminute, sec = xseconds})
        if xoffsetHour then
            offset = xoffsethour * 60 + xoffsetmin
            if xoffset == "-" then
                offset = offset * -1
            end
        end
    end
    return convertedTimestamp + offset
end

return M

main.lua

local store = require( "store" )
local utility = require( "utility" )

local mySettings
local restoring

mySettings = utility.loadTable("settings.json")

if mySettings == nil then
   mySettings = {}
   mySettings.isPaid = false
   utility.saveTable(mySettings, "settings.json")
end

local function transactionCallback( event )

   print("In transactionCallback", event.transaction.state)
   local transaction = event.transaction
   local tstate = event.transaction.state
   --
   --Google does not return a "restored" state when you call store.restore()
   --You're only going to get "purchased" with Google. This is a work around
   --to the problem.
   --
   --The assumption here is that any real purchase should happen reasonably
   --quick while restores will have a transaction date sometime in the past.
   --5 minutes seems sufficient to separate a purchase from a restore.
   --
   if store.availableStores.google and tstate == "purchased" then
      local timeStamp = utility.makeTimeStamp(transaction.date,"ctime")
      if timeStamp + 360 < os.time() then  -- if the time stamp is older than 5 minutes, we will assume a restore.
         print("map this purchase to a restore")
         tstate = "restored"
         print("I think tstate is ", tstate)
         restoring = false
      end
   end

   if tstate == "purchased" then
      print("Transaction succuessful!")
      mySettings.isPaid = true
      utility.saveTable(mySettings, "settings.json")
      native.showAlert("Thank you!", "Your support is greatly appreciated!", {"Okay"})
      store.finishTransaction( transaction )
   elseif  tstate == "restored" then
      print("Transaction restored (from previous session)")
      mySettings.isPaid = true
      utility.saveTable(mySettings, "settings.json")
      store.finishTransaction( transaction )
   elseif tstate == "refunded" then
      print("User requested a refund -- locking app back")
      mySettings.isPaid = false
      utility.saveTable(mySettings, "settings.json")
      store.finishTransaction( transaction )
   elseif tstate == "revoked" then -- Amazon feature
      print ("The user who has a revoked purchase is", transaction.userId)
      --Revoke this SKU here:
      mySettings.isPaid = false
      utility.saveTable(mySettings, "settings.json")
   elseif tstate == "cancelled" then
      print("User cancelled transaction")
      store.finishTransaction( transaction )
   elseif tstate == "failed" then
      print("Transaction failed, type:", transaction.errorType, transaction.errorString)
      store.finishTransaction( transaction )
   else
      print("unknown event")
      store.finishTransaction( transaction )
   end

   print("done with store business for now")
end

local function loadProductsListener( event )
   print("In loadProductsListener")
   local products = event.products
   for i=1, #event.products do
      print(event.products[i].title)
      print(event.products[i].description)
      print(event.products[i].localizedPrice)
      print(event.products[i].productIdentifier)
   end
   for i=1, #event.invalidProducts do
      print(event.invalidProducts[i])
   end
end

if system.getInfo("targetAppStore") == "amazon" then
   store = require "plugin.amazon.iap"
   store.init( transactionCallback )
   print("The currently logged in user is: ", store.getUserId())
   store.loadProducts({"com.yourcompany.yourapp"}, loadProductsListener)
   store.restore()
else
   if store.availableStores.apple and not mySettings.isPaid then
      timer.performWithDelay(1000, function() store.init( "apple", transactionCallback); end)
   end
   if store.availableStores.google and not mySettings.isPaid then
      timer.performWithDelay( 1000,
      function()
         store.init( "google", transactionCallback );
         restoring = true;
         store.restore();
      end )
   end
end

Workflow and Notes

The basic workflow for your app involves:

  1. Determine the store your app is running on and initialize the store using store.init().
  2. If you think the app has been reinstalled (i.e. there is no settings file, etc.), then (for Google and Amazon) you can call store.restore() when your app starts because it is silent. Apple will prompt your app to login to the iTunes App Store when your app starts. It’s best to delay the restore until later. Apple wants you to have a button specifically to restore purchases.
  3. For Apple, Google Play V3 and Amazon, you can use store.loadProducts() to get a list of your items using localized names and prices. Google Play V2 does not offer this with the version of IAP that Corona SDK uses. When Google shows the purchase dialog, the localized values will be shown then. Many people will just hard code their items in a custom display.
  4. Offer a purchase button or display that allows the customer a chance to purchase your items. This should lead to a call to store.purchase().
  5. Inside the event handler function, perform whatever activities you need to unlock your app or process your purchase.  For Google Play V3, this might be a good time to call store.consumePurchase() if the items is something like coins or gems that are being added to the app’s inventory immediately and can’t be tracked separately.  Items like potions in a fantasy game, or purchasable power up’s can be consumed when they are actually used.
  6. Make sure to call store.finishTransaction() at the end, regardless of the transaction state. Failing to call this function can cause your app to receive too many transactions!

In addition, each vendor’s store variances create situations that you need to adapt to. Let’s look at a couple of key situations:

  1. Consumables are never restored. You will never get a “restored” state that contains a consumable item. The reason is that these items are considered “used up” and there is no need to restore them. You will only get restored items for non-consumable items.
  2. Google does not provided a “restored” state. Even if you call store.restore(), Goggle considers these “purchased” items. Thus, you need to determine the difference between “purchased” and “restored” if that’s important for the app. To handle this, you should store the transaction’s receipt and, when you receive a purchase request, you should compare against previous purchases and see if you found a matched receipt (this accounts for apps being deleted and re-installed). Note that you can’t save your receipts locally — instead, you have to setup an online method to store and retrieve these receipts. While this is not a trivial task, comparing these digital receipts (which are complex, encrypted chunks of data) makes it difficult for someone to fake a receipt during the restore phase. This is the best practice to help mitigate piracy. The code above does not, for brevity. What it does, however, is attempt to map purchases to restores when Google is involved.
  3. If there is nothing to be restored, the event listener will not be called, so you should plan for this condition accordingly. If there is nothing to restore, there is nothing to do. Many people get snagged by this because they are looking for a “trigger” to say the process was complete. You should use a timer with a reasonable timeout to trigger your effect if there is no work to do.
  4. Amazon and Google allow refunds, but they handle it differently. Google can generate a “refunded” state. If you see this state come through, you should have your app lock whatever feature that product ID unlocked. Of course, this only happens for non-consumable items.  Products that are are consumed cannot be refunded. For Amazon, the state is “revoked”.   Apple does not support refunds.
  5. Apple triggers a store login for restores. The observant reader may have noticed that the store.init() sections above do not have a store.restore(). Google and Amazon will silently trigger restorations if the user is logged into the respective app store. Apple however, will prompt for the user’s password if they have not accessed the billing system in the last few minutes. Because of this, you don’t want a modal dialog to interfere with your users getting into the app and enjoying it. You can defer this restore until the user takes an action, until the place in your code where you actually need to determine their past purchases (or perhaps provide the user a “Restore past purchases” button in the interface). Apple will reject apps that do not call store.restore() in a proper manner.

Making Purchases

Making purchases is fairly easy. Simply pass your product identifier to the store.purchase() function:

local function removeAds( event )

   if event.phase == "began" then
      event.target:setFillColor(224)
   elseif event.phase == "ended" then
      event.target:setFillColor(255)
      if system.getInfo("targetAppStore") == "amazon" or system.getInfo("targetAppStore") == "google" then
         store.purchase( "com.acme.superrunner.upgrade" )
      else
         store.purchase( { "com.acme.superrunner.upgrade" } )
      end
   end
   return true
end

local buyBtn = display.newImageRect("images/buy_button.png", 614, 65)
group:insert( buyBtn )
buyBtn.x = display.contentCenterX - 465
buyBtn.y = 430
buyBtn:addEventListener( "touch", removeAds )

This code should be fairly straightforward and would be typically found in the scene where your store interface is set up. The one “gotcha” here is that both Apple and Google Play V2 allow you to purchase multiple items at once. Therefore, store.purchase() accepts an array of product ID strings.  Amazon and Google Play V3, however, only allows purchasing one item at a time, so you only pass a single string. This is a case that you need to code for — while most users only purchase one thing at a time, it needs to be an array of items, even if just one is being passed in.

NOTE: You cannot use both Google Play V2 and V3 at the same time.  The code above is assuming you are using V3.  If you are using V2, simply remove the extra test in the if statement to see if you are targeting the Google store.

Testing In-App Purchase

This tutorial can’t go in-depth about this topic because it’s too complex and too “vendor specific,” but here are a few tips to help you out:

Apple

  • You need specific test accounts which you can create in iTunes Connect. They are standard Apple IDs, so you will need to have a unique email address for each account. If you have your own web hosting service, you can set up aliases like “iap01@yourdomain.com” and “iap02@yourdmain.com” that send emails to your primary email address as a “catch all.” Note that Apple will make you validate the email by clicking on a link within it, so you must be able to receive it.
  • You must log out from iTunes on your device before you run the test app. This test app can be provisioned with either a Development or AdHoc profile to access the IAP sandbox. When prompted, log in with your test account.
  • You get “one shot” to buy your item because Apple remembers that your account has purchased that item. If you need to test again, you need a different account. There is no way to reset these purchases.
  • If you see “you need to submit your app and reject the binary,” you can disregard that advice.  Currently, you just need to submit your items for review — but it does take a little time for your items to propagate through Apple’s servers.

Google

  • Google requires specific test accounts as well. They are regular email addresses that you provide in the portal. The trick is getting your device to no longer be “you” but rather to be one of your test accounts. Google provides some product IDs that you can use instead of your own IDs. You can test a product that “always succeeds” and one that “always fails” and do this as many times as you need to. However, once you start using your actual Product IDs, you get one shot, just like Apple, and there’s no way to reset them other than using a different account.
  • Google’s system is sensitive to developers. For instance, if you request too many restore() requests in too short of a time, they will block your account for several weeks, assuming there is suspicious activity tied to it.

Amazon

Amazon’s IAP plugin is still in beta and is subject to change.  We recommend you visit the Amazon plugin forum for up-to date information on this plugin’s use.

  • Besides the differences listed above, their testing process involves downloading their IAP App Development Kit. This contains a .apk file that you can side-load onto your device which intercepts your app’s purchases during testing. You can then use your existing account (or other accounts) to test.
  • You must also download a JSON file from their developer portal which contains your products and side-load this onto the device as well. Their IAP test app uses the JSON file instead of actually talking back to Amazon’s servers.
  • From the IAP test app, you can reject purchases, which gives you a good chance to test “revoked” purchase states.

In Summary…

As you can see, IAP is a key part of the monetization puzzle. While some people prefer to utilize a paid app approach, using IAP with a “freemium” model can be a great way to monetize your apps and games.


Posted by . Thanks for reading...

33 Responses to “Tutorial: Understanding In-App Purchases”

  1. Christopher

    I wouldn’t recommend saving the IAP settings into a json file. This is easily edited even without jail breaking your device. Any popular game will have this publicized in short time.

    Some form of obfuscation would be smart, the more the better.

    Reply
  2. Nick

    Rob, awsome post.
    But i have one question, didn’t google change policies about in-app? And now you can only buy them via google? So Fortumo wont be supported anymore?

    In-app purchases:
    Developers offering virtual goods or currencies within a game downloaded from Google Play must use Google Play’s in-app billing service as the method of payment.
    Developers offering additional content, services or functionality within another category of app downloaded from Google Play must use Google Play’s in-app billing service as the method of payment, except:
    where payment is primarily for physical goods or services (e.g. buying movie tickets, or buying a publication where the price also includes a hard copy subscription); or
    where payment is for digital content or goods that may be consumed outside of the application itself (e.g. buying songs that can be played on other music players).
    Developers must not mislead users about the applications they are selling nor about any in-app services, goods, content or functionality they are selling.

    Reply
  3. Kazuhira

    Nice post Rob! I got it working now, but I have one last problem left to fix, I have this error: “field ‘day’ missing in date table” I know it is somewhere within the utility.lua file, but I can’t pinpoint where it is. I am kinda new to corona so I am having a hard time adapting to all of the codes. Thank you in advance.

    Reply
  4. Scott

    Have a quick question…. great tutuorial btw.. When you code if available.stores.google and…….if available.stores.apple and…….

    —-”not storyboard.settings.isPaid”—–

    wouldn’t you have to in this case write “not mySettings.isPaid” instead or does either way work?

    Reply
  5. Steve

    Hi Rob, thanks for the great overview.

    I have a question, though. At the beginning of the Main.lua file you have the isPaid boolean saved to the utilities module in a json file. However at the end you have it checking storyboard.settings for a the isPaid boolean.

    Which one is the better method? Or am I missing something…

    Reply
  6. Rob Miracle

    Scott, Steve… good catch. I pulled this code from one of my apps that was using storyboard as a way to pass values between modules. We now recommend using a module dedicated to your shared data and I guess I missed scrubbing that reference when cleaning it up for the blog post, so let me go fix it. You can do either.

    Reply
  7. Olivier Romanetti

    Thanks for this tutorial Rob.
    In the tutorial, after making a purchase you check the tstate value on your transactionCallback function. Ok.
    If tstate is equal to “purchased” value you put mySettings.isPaid = true and you save it. Ok
    In this case, there is no “real” data exchange in the transaction. Only a flag which determine if I can unlock a functionality which is already on my app.
    What about if in my in-App purchase I have for example 10 mp3 files which are NOT already on my app?
    How Can I manage real data from the transaction, my mp3 files?
    Where should I save these real data and how can I do that?
    Thanks for your help
    Best regards
    Olivier

    Reply
  8. Rob Miracle

    When you get the tstate == “purchased” value, in that if block, you can then start a network.download() of the content and set a flag when it’s been successfully downloaded.

    Rob

    Reply
  9. Olivier Romanetti

    Thanks Rob.
    I create an In-App Purchase Item,
    I choose “Hosting Content with Apple”
    I did a package .pkg with xCode, containing my sound files, and I uploaded it with application loader to Apple.
    I can see it in the “in-app Purchase details” of my item on iTunes Connect.
    I would like to make a network.download() call but I don’t know what is the HTTP request URL for my package…
    Where Apple provides the URL for an in-app Purchase item?
    Thanks for your help
    Best
    Olivier

    Reply
    • Rob Miracle

      I have no experience with Apple hosting of IAP products. You would have to ask on their forums or talk to their Tech Support to find the URL. I suspect though that they may have a Native API Call that we don’t support so that you don’t need to know the URL. In which case, it’s very likely you won’t be able to use Apple’s hosting with our product.

      Rob

      Reply
  10. Olivier Romanetti

    Hi Rob
    I have another question :)
    I use your code above to test In-App Purchase.
    I don’t understand why but the line just below in my code :

    timer.performWithDelay(1000, function() store.init( “apple”, transactionCallback); end)

    causes a call to transactionCallback function with a tstate equal to “purchased”
    As if a call to store.purchase was made, but I don’t do it.
    Have I missed something?
    Do you have an idea ?
    Thanks for your help
    Best
    Olivier

    Reply
    • Rob Miracle

      That line just says to initialize the store for Apple after 1 second.You shouldn’t get any activity in your call back for a purchase until after you call store.purchase().

      Rob

      Reply
  11. Mark Steelman

    How secure is this settings.json file that you are storing the info in? I don’t see the file in iTunes but I don’t have a jail broken device.

    I have an in app paid for currency and would prefer to make my game playable off line. However, I may need to make it validate online every time if players can simply edit this file and give themselves all the game currency they please.

    Reply
    • Rob Miracle

      I think in the case for this tutorial, I didn’t include any encryption on the file. Most mortals won’t be able to see the file. Any hacker worth their weight will be able to see it. Just because you don’t jail break, a cheater won’t hesitate to jailbreak their phone.

      You can do things to obscure your data. Don’t name the variable “coins”. Give it some random key and add in a bunch of other random keys and change their values. You could also use encryption to encrypt it. We support the crypt.* API calls as well as the SSL plugin for Pro and Enterprise subscribers.

      Another technique would be to calculate an MD5 hash of your json string before you write it out, save it, and then save the MD5 hash. When you read the file back in, convert the inbound string to an MD5 hash and compare it to your saved MD5 hash. If it changed, you’ve been hacked.

      I’m not a security expert, so your mileage may vary.

      Reply
  12. Douglas Starnes

    I am having trouble figuring out what edition of the SDK I need to do IAP through iTunes and Google Play. Since they are built into the Corona SDK ‘core’ does that mean they are included in the Starter edition? I realize the IAP plugins for Amazon and others are Basic edition only but can IAP be done with iTunes and Google Play using the Starter edition?

    Reply
    • Rob Miracle

      Hi Douglas. If you want to use IAP you must be either a Basic, Pro or Enterprise subscriber. Starter accounts do not have access to the IAP plugins or core IAP features (store.*).

      Reply
  13. Jimmy

    Hi, I have a question about store plugin:
    Can I use the store plugin in xcode simulatore for my test without basic corona licensing?

    Thanks

    Reply

Leave a Reply

  • (Will Not Be Published)