This article is now obsolete, for latest information regarding IAP please refer to the "Guides" section on the extension's Wiki.
In this article we'll be looking at how you create and test In App Purchases (IAP) in your macOS apps for the App Store.
The methods shown in this guide require GMS 2.2.4 or newer, plus the "Apple IAPs" extension from the Marketplace - if you're using 2.2.3 or older you will need to update and ensure you have the correct extension in your project before you can follow this guide.
Before continuing, you should have already setup and tested the macOS export and have a test project or finished game that you want to add IAPs into.
You can find out how to set up GameMaker Studio 2 for the macOS platform here:
Set Up The App ID
Before you can add any IAP code and test it, you first have to set up an app listing on your App Store Connect Console for the game. Before that however, you will need to sign in to your Apple Developer console and then go to Certificates, Identifiers and Profiles to create a new ID for the project you want to set up a listing for:
IMPORTANT! If you have already setup the App ID for the project, then you can skip down to the section about "Setting Up App Store Connect".
When filling in the app information, make sure to select Explicit Bundle ID and supply a reverse url format App ID, for example "com.yoyogames.maciaptest". Wildcard IDs will not be valid for creating and testing IAPs:
The app ID will be created and by default already have in-app purchases enabled. You can now move on to setting up the app listing and IAP details through App Store Connect.
Setting Up App Store Connect
Once you have your App ID created, you need to go to App Store Connect and set up a basic store listing and include the information that is required for the in-app purchases you need. To start with, go to the apps listing and click the + button to add a new app:
In the next section you should fill out the details and ensure that you select the correct bundle ID from the list:
You then need to go to the Features tab, and add your first IAP. You can only add one for now, as Apple require you to upload a binary which includes IAPs before you can create others, but the process for adding them later is the same as we outline here.
When you add the new IAP it can be any one of the following types:
- Consumable - A product that is used once, after which it becomes depleted and must be purchased again.
- Non-Consumable - A product that is purchased once and does not expire or decrease with use.
- Auto-Renewable Subscription - A product that allows users to purchase dynamic content for a set period. This type of subscription renews automatically unless cancelled by the user.
- Non-Renewing Subscription - A product that allows users to purchase a service with a limited duration. The content of this in-app purchase can be static. This type of subscription does not renew automatically.
In this case we probably want to start with the consumable IAP, so select that then fill in the IAP details. Note that the Product ID will be used to identify the IAP in GameMaker Studio 2 so be sure to make it appropriate (for this article we'll call it mac_test_consumable
):
IMPORTANT! When targeting both iOS and macOS, Apple requires that you use different, unique product identifiers and doesn't distinguish between the two platforms, which is why in the examples on this page we use a "mac" prefix on the product IDs.
You can then go ahead and fill in the rest of the IAP details (price, descriptions, etc) - and be sure to supply a 640x920px screenshot in the review information, otherwise you'll get a "missing metadata" error. Once that's all done, click on the save button.
Later, after sending that first binary which includes IAP support, you will go through this process again and create an IAP listing for each purchase option that you want to include in your game. For the purposes of this tutorial we have made a second IAP called mac_test_nonconsumable
, so our panel looks like this:
Once you have all the IAPs set up that you'd like to include you can then continue on to setting up test accounts.
Setting Up A Sandbox Tester Account
Now that you've set up the initial IAP product you need to set up at least one Sandbox Tester Account. This account will be used to test the IAPs and any purchases will not be charged when using this account (note that you cannot use the developer email when setting up test accounts).
To set up this test account you need to go to App Store Connect > Users and Access > Sandbox Testers and then click the "+" button to add a new user:
Fill in the details required then press Save. With that done, you are ready to set up the game in GameMaker Studio 2 and test the IAP process.
Setting Up Your Game
Now we have our initial IAP setup in the App Store Connect console, we need to prepare our game. For that you'll need to open the project in GameMaker Studio 2 and then go to Game Options > macOS. Here you should supply the game name and App ID (Bundle ID) that you defined for the project (see the section Setup App ID, above):
Once that's done you will also need to ensure that the project is built for the Mac App Store. You can do this from the Packaging section of the Game Options:
Save those settings now by clicking OK and you are almost ready to code, build, and then test purchases.
If you are not going to use your own server to validate purchases (something that Apple recommends), there is a way to validate locally - that is almost as secure - using the extension function mac_iap_ValidateReceipt(). Using this function, however, requires you to supply your Apple Inc. Root Certificate and include it with their project in the Included Files.
Download this file and add it to your Included Files for the project and then you can use the local validation function. For more information on this file, please see the following links:
- https://www.apple.com/certificateauthority/
- https://www.apple.com/appleca/AppleIncRootCertificate.cer
Coding IAPs Overview
We need to now get down to coding our IAPs in GameMaker Studio 2. But before we get to the details, let's take a moment to give an overview of how the IAP system should work for macOS:
- At the start of the game, check the user is authorised to buy in-app products
- If they are not, then disable the possibility for purchases in your game UI and code
- If purchases are permitted, add the different products to the internal products list, and - if required - query product details
- After adding the products but before accepting purchases, query existing purchases and if there are any unfinished transactions then deal with them and enable any features based on durable or subscription transactions
- Permit the game to run as normal and let the user purchase/consume products as required, verifying each purchase, then querying them, and then finalising them
- Store non-consumable and subscription purchases on your server so they can be checked when the game starts (or store them securely locally, but a server is recommended)
- Ensure that the game has a "Restore Purchases" button, in case of a change of device or anything of that nature
Initialising Your IAPs
When dealing with IAPs we recommend that you have a dedicated, persistent, controller instance that deals with all the initialisation as well as the callback Asynchronous IAP Events that the different functions generate. This keeps it all in one place and you only need to add purchase functions to buttons and things for the player to interact with. This article builds on this premise, however you don't have to do it this way if that's not appropriate to your project.
NOTE: This article will not detail all the different returns or async callbacks in detail, but will instead concentrate on the approximate workflow and general code required to set up IAPs on macOS. For more complete information about what each function does, please see the PDF manual included with the extension.
To start with, you'll need to initialise the IAPs that you want to be available in your game, and this should be done right at the start of the game in the Create Event of the controller object. You want to accompany this with a check to see if the device is enabled to permit purchases too, as it is possible that the device has had this disabled (for children or whatever):
global.IAP_Enabled = mac_iap_IsAuthorisedForPayment();
global.ProductID[0, 0] = “mac_consumable”;
global.ProductID[1, 0] = “mac_durable”;
global.ProductID[2, 0] = “mac_subscription”;
if global IAP_Enabled
{
mac_iap_AddProduct(global.ProductID[0, 0]);
mac_iap_AddProduct(global.ProductID[1, 0]);
mac_iap_AddProduct(global.ProductID[2, 0]);
mac_iap_QueryProducts();
}
You'll notice that we first set a global variable to check for the availability of purchasing, and if that returns true we go ahead and add our products to the internal list and then fire of a product query. If it returns false, then you can disable IAPs in the game, as the user won't be able to purchase anything (and, indeed, Apple insist that you do this).
NOTE: We have used an array to hold our product IDs here as we will later want to add information from a product query to each one, however this is not required and you can store your product IDs as you wish, using macros or global variables, for example.
Product Queries
Querying your products is not essential, however doing so means that you can then display up-to-date and localised information about them in your game, rather than hard-coding them. When you send off a product query request it will trigger an Asynchronous IAP Event where the DS map async_load will have an "id" key with the key constant mac_product_update. This would be dealt with in the Async Event something like this:
var _eventId = async_load[? "id"];
switch (_eventId)
{
case mac_product_update:
// Decode the returned JSON
var _map = json_decode(async_load[? “response_json”]);
var _plist = _map[? “valid”];
var _sz = ds_list_size(_plist);
// Loop through all valid products and store any data that you require
for (var i = 0; i < _sz; ++i;)
{
var _pmap = _plist[| i];
switch(_pmap[? “productId”])
{
case “ios_consumable”:
global.ProductID[0, 1] = _pmap[? “price”];
global.ProductID[0, 2] = _pmap[? “localizedDescription”];
global.ProductID[0, 3] = _pmap[? “localizedTitle”];
break;
case “ios_durable”:
global.ProductID[1, 1] = _pmap[? “price”];
global.ProductID[1, 2] = _pmap[? “localizedDescription”];
global.ProductID[1, 3] = _pmap[? “localizedTitle”];
break;
case “ios_subscription”:
global.ProductID[2, 1] = _pmap[? “price”];
global.ProductID[2, 2] = _pmap[? “localizedDescription”];
global.ProductID[2, 3] = _pmap[? “localizedTitle”];
break;
}
}
// Parse any invalid responses here if required
ds_map_destroy(_map);
// Check previous purchases with your server or from
// a local secure file and enable features as required
// and then query on-going purchases here
break;
}
You'll notice that at the end of this switch case we have a comment about querying purchases. This should ALWAYS be done at the start of the game, and if you aren't querying products first, then you'd do it straight after initialising the IAPs in the Create Event. The next section details how to deal with that.
Purchase Queries
It may be that the game was closed before a purchase could be completed, or something went wrong or even that the user has changed devices while a purchase was in progress. To deal with those - and other - potential issues, you must query ongoing purchases at the start of your game too. This is done with the function mac_iap_QueryPurchases(), and should be done after initialising the IAPs in the Create Event, or after querying product details in the Async IAP Event.
The purchase query function will not generate an async event callback, but will instead immediately return the outstanding purchase requests which can be dealt with something like this:
var _json = mac_iap_QueryPurchases();
if _json != “”
{
// There is purchase data to be dealt with so decode it
var _map = json_decode(_json);
// Get the list of outstanding purchases
var _plist = _map[? “purchases”];
var _sz = ds_list_size(_plist);
// Loop through the purchase list
for (var i = 0; i < _sz; ++i;)
{
var _pmap = _plist[| i];
// Check that the purchase succeeded
if _pmap[? “purchaseState”] != mac_purchase_failed
{
var _receipt = mac_iap_GetReceipt();
// Now check the purchase is valid
// (ideally this would be done through your own server)
if mac_iap_ValidateReceipt() == true
{
// Award the purchase to the user
switch (_pmap[? “productId”]);
{
case global.ProductID[0, 0]: global.Gold += 100; break;
case global.ProductID[1, 0]: global.NoAds = true; break;
case global.ProductID[2, 0]: global.Subs = true; break;
}
}
else
{
// The receipt could not be validated so do NOT
// award anything to the user and call this function
mac_iap_exit(mac_invalid_receipt_exit_code);
exit;
}
// Finalise the purchase
var _ptoken = _pmap[? “purchaseToken”];
mac_iap_FinishTransaction(_ptoken);
}
}
// Here you would then save out to a file what subscription
// or non-consumable purchases have been made for future
// runs of the app, or - preferably - store this information
// on your server for future runs.
ds_map_destroy(_map);
}
Note that before awarding anything to the user we attempt to validate the purchases. This can be done through a server (recommended) or though local validation (shown above). If validation fails, you should NOT continue to check further purchases and instead break the loop and call the macOS specific function mac_iap_exit() using the constant mac_invalid_receipt_exit_code. This will close the app and the OS will attempt to obtain a valid receipt and may prompt the user for their iTunes credentials. If the system successfully obtains a valid receipt, it will relaunch the application, otherwise, it will display an error message to the user, explaining the problem.
Also note that ALL purchase queries must be finalised, whether they are awarded or not, or whether the purchase succeeded or not (but NOT when the validation has failed). Again, we discuss finalising purchases in more detail further on.
Restoring Purchases
Apple rules state that you must have a button in your game to restore purchases, and to do this you would call the function mac_iap_RestorePurchases(). This will trigger an Asynchronous IAP Event where the async_load map has the "id" constant mac_payment_queue_update. You can then check this in the Async IAP Event something like this:
var _eventId = async_load[? "id"];
switch (_eventId)
{
case ios_payment_queue_update:
// Decode the returned JSON
var _json = async_load[? “response_json”];
if _json != “”
{
var _map = json_decode(_json);
var _plist = _map[? “purchases”];
var _sz = ds_list_size(_plist);
// loop through purchases
for (var i = 0; i < _sz; ++i;)
{
var _pmap = _plist[| i];
// Check purchases
if _pmap[? “purchaseState”] != mac_purchase_failed
{
var _receipt = mac_iap_GetReceipt();
// CALL SERVER CHECK WITH RECEIPT HERE
// or validate, award the product, and finalise
if mac_iap_ValidateReceipt() == true
{
switch (_pmap[? “productId”]);
{
case global.ProductID[0, 0]: global.Gold += 100; break;
case global.ProductID[1, 0]: global.NoAds = true; break;
case global.ProductID[2, 0]: global.Subs = true; break;
}
mac_iap_FinishTransaction(_ptoken);
// Securely save the details to a file
// or save them to your server
}
else
{
// Validation failed, so deal with it here
mac_iap_exit(mac_invalid_receipt_exit_code);
exit;
}
}
else
{
var _ptoken = _pmap[? “purchaseToken”];
// Purchase failed, so finalise it
mac_iap_FinishTransaction(_ptoken);
}
ds_map_destroy(_pmap);
}
}
break;
}
Making A Purchase
To make a purchase of a product, you must call the function mac_iap_PurchaseProduct(), eg:
if global.IAP_Enabled
{
mac_iap_PurchaseProduct(global.ProductID[0, 0]);
}
This function will generate an Async IAP Event where the "id" key will be the constant mac_payment_queue_update. This can then be processed in the exact same way as outlined above in the section on Restoring Purchases, as the async callback is identical.
Validating
Before awarding and finalising any purchases, they must first be validated. Apple recommends that you do this with a private server but there is also a function to validate locally, but this is slightly less secure and requires you to include a Root Certificate with your game (see the section on Setting Up You Game for more details). The general workflow for validating using the two methods would be:
- Server: When a purchase or restore event is triggered, you would get the purchase receipt (using the function mac_iap_GetReceipt()) and then send that off to your server using one of the http_*() functions. This would then validate the purchase with Apple and send a response back. This response would then be dealt with in the Asynchronous HTTP Event, where you would then award the user the product they've bought or enable any features it unlocked. You would also store these details on your server so the game can check on restart any purchases or subscriptions.
- Local: To validate locally, you must first call the function mac_iap_GetReceipt() to retrieve the receipt file, and then call the function mac_iap_ValidateReceipt(). If that returns true then you can award products and unlock features as required. This would then be securely stored to a file so that on game restart it can be checked and all features or items be unlocked correctly.
As mentioned previously, if validation fails, you should NOT continue to check further purchases and instead break the loop and call the macOS specific function mac_iap_exit() using the constant mac_invalid_receipt_exit_code. This will close the app and the OS will attempt to obtain a valid receipt and may prompt the user for their iTunes credentials. If the system successfully obtains a valid receipt, it will relaunch the application, otherwise, it will display an error message to the user, explaining the problem.
Note that failing validation is a rare occurrence and is very indicative that there is something funny going on with the request. As such, you may want to consider locking down and preventing any further purchases – or at least not granting the products that were being validated – should validation fail 2 or more times. Any outstanding purchases should still be finalised at this time.
Finalising Purchase Requests
After making any purchase, it must be validated and finalised using the function mac_iap_FinishTransaction(). Finalising a purchase removes it from the purchase queue and tells Apple that the transaction has been completed in one way or another, and this must be done regardless of whether the purchase was a success or a failure. When we talk about success or failure, we are referring to the purchase status as returned as part of the response data from a purchase query, a restore request, or a purchase request, and not to validation failure or anything else.
Note that if you do not finalise a purchase then the user will not be able to buy that product again.