macOS: Setting Up IAPs

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. Please note GMS2 only supports IAPs from the App Store in macOS games - if you're not distributing via the App Store, then you will need to provide your own IAP implementation and much of this guide may not be relevant to you.

Before continuing, you should have already setup and tested the Mac 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 An App ID

Before you can add any IAP code and test it, you first have to set up an app listing on your iTunes Connect Console for the game. This will involve you signing in to your developer console and then going to Certificates, Identifiers and Profiles > macOS > App IDs and creating a new ID for the project:

IMPORTANT! If you have already setup the App ID for the project, then you can skip down to the section about "Setting Up iTunes Connect".

IAP_MacAppID.png

When filling in the app information, make sure to select Explicit App 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:

IAP_MacExplicitID.png

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 iTunes Connect.

 

Setting Up iTunes Connect

Once you have your App ID created, you need to go to iTunes 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:

IAP_MacCreateNewApp.png

In the next section you should fill out the details and ensure that you select the correct bundle ID from the list:

IAP_MacNewApp.png

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.

IAP_MacNewIAP.png

When you add the new IAP it can only be Consumable or Non-Consumable, as subscriptions are not currently supported.

In this case we probably want to start with the consumable IAP, so select that and 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 name it appropriately (for this article we'll call it mac_test_consumable):

IAP_MacProductID.png

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 1280x800px 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:

IAP_MacIAPsConsole.png  

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  iTunes Connect > Users and RolesSandbox Testers and then click the + button to add a new user:

IAP_MacAddTester.png

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 iTunes 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):

IAP_MacGeneralOptions.png

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:

IAP_MacPackagingOptions.png

Save those settings now by clicking OK and you are ready to code, build, and then test purchases.

 

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 explain exactly how the IAP system works.

The way that GameMaker Studio deals with IAP is event driven, and most of it will be handled by the asynchronous IAP Event. The general workflow is as follows:

  1. You check for a secure stored purchase map and load it if found, otherwise you create a new one and save it.

  2. You then activate all purchases, which will trigger an IAP Event where you can query the status of the available products.

  3. In your game you have objects that connect to the store to request a product, which also triggers an IAP event where you can deal with the purchase (or failure thereof).

  4. The IAP event is parsed to deal with the purchase, taking all data from a special ds_map iap_data.

  5. If successful, you write the purchase to your purchase map and secure save it. Then activate the product that the user has bought (you might not do this immediately after the purchase when dealing with consumable products, but this depends entirely on what your IAP requires).

  6. If the purchase is consumable, you can "consume" it at any point in the game, which frees the product up to be purchased again. This will trigger another IAP event informing you of the consumption, in which case you would update the purchase map and secure save again.

 

Initialise Your IAPs

The following code is an example of how you would typically initiate in-app purchases and this would normally only be done once at the start of a game, either in a dedicated startup script or object, or in the Game Start event of the first object in your game. How you do it will depend on the project you are adding IAPs to.

The first thing required from you is to create a purchase map for tracking purchases between runs of the game. GameMaker Studio 2 does not perpetuate purchase information automatically, so you will have to do this yourself. Creating this map has the added benefit of permitting you to check it directly for purchases even when the target store is offline (as you will see in the code below), although it is not strictly necessary. 

In our example code, the consumable will simply be "mac_test_consumable", with each purchase adding 1000 to a value saved in the save map, and our non-consumable will be called "mac_test_nonconsumable" and be stored in another map value. We'll also store these strings in variables so that if we decide to change them or use the code in other games, then we only need to change them in one place.

So, our save map will hold one product key and one product value, where the non-consumable product key will have an initial value of false and the consumed product value will be set to 0. The code should look something like this (note we have no need to check for consumable purchases as they are consumed the moment they are bought): 

// Store the product names in variables for easy editing
product_consumable = "mac_test_consumable";
product_durable = "mac_test_nonconsumable";
var map_create = true; // This is to tell us if we need to create the save map
if (file_exists("iap_data.dat") // Check for the file)
{
    // The file exists so load the save map
    global.savemap= ds_map_secure_load("iap_data.dat");
    // Check the map has been loaded correctly
    if (ds_exists(global.savemap, ds_type_map))
    {
        // Check to see if the non-consumable IAP has been used
        if (ds_map_exists(global.savemap, product_durable))
        {
            map_create = false; 
            if (ds_map_find_value(global.savemap, product_durable) == false)
            {
                // The non-consumable IAP has not been used so do something
                // like enable ads, or disable extra content, etc...
            }
        }
    }
}

// The save map doesn't exist, so create it and initialise the IAPs to false
if (map_create)
{
    global.savemap= ds_map_create();
    ds_map_add(global.savemap, product_durable, false);
    ds_map_add(global.savemap, "consumed", 0);
    ds_map_secure_save(global.savemap, "iap_data.dat");
}

As you can see, we first check to see if there is a file with saved purchase data, and if there is we parse it for the non-consumable "mac_test_nonconsumable" product and then if that has not been bought we do something. We then check to see if we have any consumables pending, and if we do we call the iap_consume function. This will trigger an Asynchronous IAP event where you can then deal with it (this is important, as before another consumable can be purchased, the first must be cleared, so this check ensures that any leftover consumables from a previous run are used and the player can purchase more). This is explained further in the section below on "Acquiring A Product".

Notice that in the above code, if the file is not found, or the file data is corrupted in some way, then we create a new purchase map, initialise the products as not being bought, and then secure save that.

With that done, the next thing to do is to set up the purchase data itself and connect the game to the target store. All this is done using the iap_activate function, as shown below:

var durable_map = ds_map_create();
var productList = ds_list_create();
// Create non-consumable iap
ds_map_add(durable_map , "id", product_durable);
ds_map_add(durable_map , "title", "Test Durable");
ds_map_add(durable_map , "type", "Durable");
ds_list_add(productList, durable_map);
// Create consumable IAP
var consumable_map = ds_map_create();
ds_map_add(consumable_map, "id", product_consumable);
ds_map_add(consumable_map, "title", "Test Consumable");
ds_map_add(consumable_map, "type", "Consumable");
ds_list_add(productList, consumable_map);
// Activate IAP
iap_activate(productList);
// Clean up
ds_map_destroy(durable_map);
ds_map_destroy(consumable_map);
ds_list_destroy(productList);

With that, our products will be activated and each one will trigger its own Asynchronous IAP Event of the type "iap_ev_product", where (if you wish), you can get the full details of the purchase as they are pulled from the store. To get this information you'd have something like this:

// Get the event type
var _id = iap_data[? "type"];
// Perform different code depending on the type
switch (_id)
{
  case iap_ev_storeload: // Check to see if the store has loaded or not
    if (iap_data[? "status"] == iap_storeload_ok)
    {
        show_debug_message("STORE LOADED");
    }
    else show_debug_message("STORE NOT LOADED");
    break;

  case iap_ev_product: // Get product details
    var _product = iap_data[? "index"];
    var _map = ds_map_create();
    iap_product_details(_product, _map);
    show_debug_message("PRODCT ACTIVATED - " + string(_product));
    show_debug_message("ID - " + string(_map[? "id"]));
    show_debug_message("Title - " + string(_map[? "title"]));
    show_debug_message("Description - " + string(_map[? "description"]));
    show_debug_message("Price - " + string(_map[? "price"]));
    show_debug_message("Type - " + string(_map[? "type"]));
    show_debug_message("Verified - " + string(_map[? "verified"]));
    ds_map_destroy(_map);
    break;
}

Note that we have an IAP event type for the Store being loaded. This is not triggered by any function, but will instead be triggered automatically at the start of your game and can be used to check the initial state of the App Store. If this returns iap_storeload_failed for example, then you can disable IAPs for that run of the game (you could then use the function iap_status() to poll the condition of the store during the game and re-enable your IAPs).  

Also note that in the above example we simply output the returned data to the console, but in your projects you can use this to display information to the user, disable/enable buttons, etc...

IMPORTANT! Activating a product may also trigger the purchase IAP event, with the iap_data ID "iap_ev_purchase". This will happen if a product has been purchased previously. See the next section for more details.

 

Aquiring A Product

You'll now want to add a button instance into your game (or something similar) for the user to click to purchase a product. This is where you would call the function iap_acquire(), which will send off the purchase request to the target store. The actual code to do this is pretty simple and will look something like the following (this code, for example, could go in a Global Mouse Pressed event): 

if (iap_status() == iap_status_available)
{
    // Check consumable button press
    if (point_in_rectangle(mouse_x, mouse_y, x - 264, y - 32, x - 136, y + 32))
    {
        // Try to acquire the IAP
        iap_acquire(product_consumable, "");
    }
    // Check durable button press
    if (point_in_rectangle(mouse_x, mouse_y, x + 136, y - 32, x + 264, y + 32))
    {
        if (global.saveMap[? product_durable] == false)
        {
            iap_acquire(product_durable, "");
        }
    }
}
else { show_message_async("Store is not available."); }


As you can see, we first check to see that the store is available, then we check our previously created purchase map to see if the product has been bought or not already. The code will trigger an IAP Event of the type "iap_ev_purchase", which will contain details of the purchase being made as well as whether it has succeeded or not.

NOTE: This event type will be triggered on game start if there is a purchase that has not been consumed, so you can use it to check non-consumable purchases.

You already have added this async event and the switch statement as part of the last section of this FAQ, so you'd simply now expand your code to include the following case:


    case iap_ev_purchase:
        var _product = iap_data[? "index"];
        var _map = ds_map_create();
        iap_purchase_details(_product, _map);
        show_debug_message("PRODCT PURCHASED - " + string(_product));
        show_debug_message("Product - " + string(_map[? "product"]));
        show_debug_message("Order - " + string(_map[? "order"]));
        show_debug_message("Token - " + string(_map[? "token"]));
        show_debug_message("Payload - " + string(_map[? "payload"]));
        show_debug_message("Receipt - " + string(_map[? "receipt"]));
        show_debug_message("Response - " + string(_map[? "response"]));
        switch (_map[? "status"])
        {
            case iap_available:
                show_debug_message("iap_available");
                break;
            case iap_failed:
                show_debug_message("iap_failed");
                break;
            case iap_purchased:
                show_debug_message("iap_purchased");
                if _map[? "product"] == product_consumable
                {
                    show_debug_message("Consumable IAP Purchased");
                    iap_consume(product_consumable);
                }
                if _map[? "product"] == product_durable
                {
                    show_debug_message("Durable IAP Purchased");
                    global.savemap[? product_durable] = true;
                    ds_map_secure_save(global.savemap, "iap_data.dat");
                }
                break;
            case iap_canceled:
                show_debug_message("iap_canceled");
                break;
            case iap_refunded:
                show_debug_message("iap_refunded");
                break;
        }
        ds_map_destroy(_map);
        break;
    }

Here, if a non-consumable purchase is detected then we set the save map to true and save the file out for all subsequent runs. If a consumable is purchased we immediately call the function iap_consume() to use the purchase and free it up so it can be bought again.

When you call the consume function, a further callback will be generated in the async IAP event. This time it will have the ID "iap_ev_consume" and you would parse it in the same switch as before, like this:

    case iap_ev_consume:
        if iap_data[? "product"] == product_consumable
            {
            global.savemap[? "consumed"] += 1000;
            ds_map_secure_save(global.savemap, "iap_data.dat");
            }
        break;

 

Restoring Purchases

The code we've shown so far already covers restoring purchases, as un-consumed purchases will trigger an IAP async event type "iap_ev_purchase" when the game starts, but Apple require that all macOS and iOS apps have a dedicated "restore" button. In this button you'd call the function iap_restore_all()

This function will trigger two different IAP events:

  • iap_ev_restore: The iap_data map will have an additional "result" key which will be either true or false, depending on whether the restore was successful or not.

  • iap_ev_purchase: If the restore event was a success, the Async IAP event will be triggered once for each available pending purchase with this event type so you can check the purchase details.

 

Testing Purchases

Once you have that all set up, you can go ahead test the purchases using the Sandbox test account you've just set up. You cannot test purchases by simply running the game from GameMaker Studio 2 and instead you have to make a macOS package using the Create Executable option.

Once the package has been built, browse to where it was saved by GMS2 and run it, then click on any one of the IAP purchase buttons. You should be prompted the first time to select a user account to sign in with, and it's here that you should supply the Sandbox tester email and  password:

IAP_MacSignInSandbox.png

After filling in these details, clicking the Buy button should show you a purchase confirmation dialogue, and this dialogue should show "Environment: Sandbox":

IAP_MacPurchaseConfirmation.png

If the confirmation dialogue does NOT show this, then you will be making the purchase using real money and so you should cancel and then go back and ensure you have set up a sandbox tester ID and not a regular tester ID.

Clicking Buy here should now take you back to the game and after a few moments the purchase should be completed.

 

Summary

Setting up IAPs is a lot simpler than it may first appear, and basically comes down to the following points:

  1. Set up a basic store presence for the game
  2. Set up IAPs for the game on the store app console
  3. Set up a Sandbox Testers Account
  4. Code your IAPs, with most of the processing being done in the dedicated IAP Async Event
  5. Create a final game package and upload it. IMPORTANT! Due to a change in the App Store submission requirements, you will not be able to upload your package "as-is" without getting an error about unsupported architectures. Please see the comment beneath this article for details of how to fix.
  6. Test!

It's also important to note that the code shown in this article is suitable for use cross-platform on all supported stores apart from Amazon on Android. You would simply change the product names as required.

 

 

Have more questions? Submit a request

1 Comments

  • Avatar
    Mark Alexander

    Apple have changed the App Store requirements and they are not allowing i386 architecture at all for projects. This means that currently you will need to do a bit of editing to the final built project on the Mac before uploading it to the store to remove the i386 packages. This will be fixed in a coming update.

    To fix this issue, you will need to first open a Terminal prompt on the Mac. Then find the location of the game project in XCode, and then in the project find the location of the libYoYoIAP.dylib (it should be in the Supporting Files directory). In terminal you'd then use the commands:

    lipo -remove i386 /<path to the file>/libYoYoIAP.dylib -output /<path to the file>/libYoYoIAP.new.dylib
    rm /<path to the file>/libYoYoIAP.dylib
    mv /<path to the file>/libYoYoIAP.new.dylib /path/to/the/file/libYoYoIAP.dylib


    Be sure to replace the <path to the file> with the actual path to the file! Those lines in Terminal should create a new libYoYoIAP.dylib file that only has the x86_64 architecture in it, and you can then build as before and prepare the application for upload to the Mac App Store. The new package should now work without any error messages.

    Edited by Mark Alexander
Article is closed for comments.