Redeemable coupons

The goal-oriented nature of games means players usually expect to earn rewards for completing tasks. For example, players might expect to:

  • Earn items and experience after defeating a boss
  • Receive currency for completing a side quest
  • Receive an upgrade to their weapons when crafting

Additionally, games give players in-game rewards. You might want to offer your players in-game rewards as part of a promotional or seasonal event, as a reimbursement for an in-game issue, or as a form of recognition for reporting bugs and beta testing.

Cloud Code enables you to build a feature-rich gifting system by writing scripts to validate coupon codes and offer reward items in other services. You can alter coupon logic even when the game is live, without releasing a game client update. Simply publishing a new version of a Cloud Code script with the changes is enough to enforce the new redeeming rules.

The examples here do not prevent all malicious behavior. Cloud Code cannot prevent players from altering the code of their game clients which can enable them to interact with other Unity services directly. The writing of fully-fledged server-authoritative code is a planned feature for Cloud Code.

Basic example with Cloud Save

A coupon system has two main requirements: validating a coupon code and keeping track of which player has redeemed it. You can meet these requirements with a script that stores the valid coupons locally, then using Cloud Save to check if a coupon has been redeemed. You can also include an expiration date in the coupon metadata and check if a coupon has expired using the Date library. The output of the script can describe what the reward is:

JavaScript

/*
 * Verify that a coupon passed as a script parameter "couponId" is valid, has not expired and has not been redeemed by using Cloud Save.
 * Return the reward and make the coupon invalid for that user.
 */

const { DataApi } = require('@unity-services/cloud-save-1.4');
const _ = require('lodash-4.17');

const CLOUD_SAVE_COUPONS_KEY = 'REDEEMED_COUPONS';
const VALID_COUPONS = [
    // Gift the player 10 coins
    {
        id: 'FREECOINS10',
        reward: {
            id: 'coins',
            amount: 10,
        },
        expiresAt: new Date(2021, 09, 29),
    },
    // Gift the player a rare armor
    {
        id: 'RAREARMOR1',
        reward: {
            id: 'rare-armor',
            amount: 1,
        },
        expiresAt: new Date(2021, 09, 29),
    },
];

module.exports = async ({ params, context }) => {
    // Initialize a Cloud Save API client using the player credentials
    const { projectId, playerId } = context;
    const cloudSaveApi = new DataApi(context);

    // Validate that the script parameter "couponId" is one of the valid coupons and select the corresponding coupon configuration
    const inputCouponId = _.toUpper(params.couponId);
    const coupon = _.find(VALID_COUPONS, function (c) {
        return c.id === inputCouponId;
    });

    if (!coupon) {
        throw Error(`Invalid coupon "${params.couponId}"`);
    }

    //Check that the coupon has not expired
    if (Date.now() > coupon.expiresAt.getTime()) {
        throw Error(`The coupon "${coupon.id}" has expired`);
    }

    // Get the redeemed coupons from Cloud Save
    const cloudSaveGetResponse = await cloudSaveApi.getItems(projectId, playerId, [
        CLOUD_SAVE_COUPONS_KEY,
    ]);

    const redeemedCouponsData = _.find(cloudSaveGetResponse?.data?.results, function (r) {
        return r.key === CLOUD_SAVE_COUPONS_KEY;
    });

    const redeemedCoupons = redeemedCouponsData?.value ?? [];

    // Check if the coupon has been redeemed
    if (redeemedCoupons && _.indexOf(redeemedCoupons, coupon.id) >= 0) {
        throw Error(`The coupon "${coupon.id}" has already been redeemed`);
    }

    // Add the coupon id to the array of redeemed coupons and save it back into Cloud Save
    redeemedCoupons.push(coupon.id);
    await cloudSaveApi.setItem(projectId, playerId, {
        key: CLOUD_SAVE_COUPONS_KEY,
        value: redeemedCoupons,
    });

    // Return the reward for the coupon
    return coupon.reward;
};

// Uncomment the code below to enable the inline parameter definition
// - Requires Cloud Code JS dev environment setup with NodeJS (https://docs.unity3d.com/Packages/com.unity.services.cloudcode@2.5/manual/Authoring/javascript_project.html)
//
// module.exports.params = {
//   couponId: { type: "String", required: true },
// };

Note: If you are using Unity Editor to manage your scripts, you can uncomment the code at the bottom of the script to declare the required couponId parameters in-script. To learn more about how to manage in-script parameters, refer to Modify script parameters within the Unity Editor. Publish the script.

Using Economy

You can take this example further by using the Economy service. With the Economy service, you can configure Inventory Items and Currencies to use throughout your game. When your Economy configuration is published, you can start gifting items and currencies directly from the script after validating the coupon:

JavaScript

/*
 * Verify that a coupon passed as a script parameter "couponId" is valid, has not expired and has not been redeemed by using Cloud Save.
 * Gift the appropriate reward in Economy.
 * Note: the Economy configuration needs to be published before it is available from Cloud Code scripts
 *
 */

const { DataApi } = require('@unity-services/cloud-save-1.4');
const { CurrenciesApi, InventoryApi } = require('@unity-services/economy-2.4');
const _ = require('lodash-4.17');

const CLOUD_SAVE_COUPONS_KEY = 'REDEEMED_COUPONS';
const VALID_COUPONS = [
    // Gift the player 10 coins
    {
        id: 'FREECOINS10',
        reward: {
            id: 'COINS',
            type: 'currency',
            amount: 10,
        },
        expiresAt: new Date(2021, 09, 29),
    },
    // Gift the player a rare armor
    {
        id: 'RAREARMOR1',
        reward: {
            id: 'RARE_ARMOR',
            type: 'inventoryItem',
            amount: 1,
        },
        expiresAt: new Date(2021, 10, 21),
    },
];

module.exports = async ({ params, context }) => {
    // Initialize a Cloud Save API client using the player credentials
    const { projectId, playerId } = context;
    const cloudSaveApi = new DataApi(context);

    // Validate that the script parameter "couponId" is one of the valid coupons and select the corresponding coupon configuration
    const inputCouponId = _.toUpper(params.couponId);
    const coupon = _.find(VALID_COUPONS, function (c) {
        return c.id === inputCouponId;
    });

    if (!coupon) {
        throw Error(`Invalid coupon "${params.couponId}"`);
    }

    //Check that the coupon has not expired
    if (Date.now() > coupon.expiresAt.getTime()) {
        throw Error(`The coupon "${coupon.id}" has expired`);
    }

    // Get the redeemed coupons from Cloud Save
    const cloudSaveGetResponse = await cloudSaveApi.getItems(projectId, playerId, [
        CLOUD_SAVE_COUPONS_KEY,
    ]);

    const redeemedCouponsData = _.find(cloudSaveGetResponse?.data?.results, function (r) {
        return r.key === CLOUD_SAVE_COUPONS_KEY;
    });

    const redeemedCoupons = redeemedCouponsData?.value ?? [];

    // Check if the coupon has been redeemed
    if (redeemedCoupons && _.indexOf(redeemedCoupons, coupon.id) >= 0) {
        throw Error(`The coupon "${coupon.id}" has already been redeemed`);
    }

    // Gift the coupon reward to the player
    await redeemCouponReward(coupon.reward, projectId, playerId, context);

    // Add the coupon id to the array of redeemed coupons and save it back into Cloud Save
    redeemedCoupons.push(coupon.id);
    await cloudSaveApi.setItem(projectId, playerId, {
        key: CLOUD_SAVE_COUPONS_KEY,
        value: redeemedCoupons,
    });

    // Return the reward for the coupon
    return {
        id: coupon.reward.id,
        amount: coupon.reward.amount,
    };
};

// Gift the reward to a player based on the reward.type property
async function redeemCouponReward(reward, projectId, playerId, context) {
    // Initialize a Economy API clients using the player credentials
    const currenciesApi = new CurrenciesApi(context);
    const inventoryApi = new InventoryApi(context);

    // Gift the reward
    switch (reward.type) {
        case 'currency':
            await currenciesApi.incrementPlayerCurrencyBalance(projectId, playerId, reward.id, {
                amount: reward.amount,
            });
            break;
        case 'inventoryItem':
            await inventoryApi.addInventoryItem(projectId, playerId, addInventoryRequest: {
                inventoryItemId: reward.id,
            });
            break;
        default:
            throw Error('Invalid reward type');
    }
}

// Uncomment the code below to enable the inline parameter definition
// - Requires Cloud Code JS dev environment setup with NodeJS (https://docs.unity3d.com/Packages/com.unity.services.cloudcode@2.5/manual/Authoring/javascript_project.html)
//
// module.exports.params = {
//   couponId: { type: "String", required: true },
// };

Note: If you are using Unity Editor to manage your scripts, you can uncomment the code at the bottom of the script to declare the required couponId parameters in-script. To learn more about how to manage in-script parameters, refer to Modify script parameters within the Unity Editor. Publish the script.

Integrating Remote Config

One shortcoming of this example is the configurability of coupons. Making a new coupon or invalidating existing ones requires code changes, which does not provide enough flexibility. You can define coupons in Remote Config as custom JSON values:

You must save Remote Config values before using them in Cloud Code scripts. This configuration removes the barrier for people outside of engineering to managing coupons, as they would no longer need to alter live code (which also reduces risk). Integrating with Remote Config enables you to manage coupon codes and rewards, and define special coupons based on Remote Config Campaigns.

JavaScript

/*
 * Retrieve coupon details from Remote Config.
 * Verify that a coupon passed as a script parameter "couponId" is valid, has not expired and has not been redeemed by using Cloud Save.
 * Gift the appropriate reward in Economy.
 * Note: the Economy configuration needs to be published and Remote Config values need to be saved before they are available in Cloud Code scripts.
 *
 */

const { DataApi } = require('@unity-services/cloud-save-1.3');
const { CurrenciesApi, InventoryApi } = require('@unity-services/economy-2.4');
const { SettingsApi } = require('@unity-services/remote-config-1.1');
const _ = require('lodash-4.17');

const CLOUD_SAVE_COUPONS_KEY = 'REDEEMED_COUPONS';
const REMOTE_CONFIG_COUPONS_KEY = 'COUPONS';

module.exports = async ({ params, context }) => {
    // Initialize the Cloud Save and Remote Config API clients using the player credentials
    const { projectId, environmentId, playerId } = context;
    const cloudSaveApi = new DataApi(context);
    const remoteConfig = new SettingsApi(context);

    // Fetch the available coupons from Remote Config
    const remoteConfigResponse = await remoteConfig.assignSettingsGet(
        projectId,
        environmentId,
        'settings',
        [REMOTE_CONFIG_COUPONS_KEY]
    );
    const availableCoupons =
        remoteConfigResponse?.data?.configs?.settings?.[REMOTE_CONFIG_COUPONS_KEY];

    // Validate that the script parameter "couponId" is one of the valid coupons and select the corresponding coupon configuration
    const inputCouponId = _.toUpper(params.couponId);
    const coupon = _.find(availableCoupons, function (c) {
        return c.id === inputCouponId;
    });

    if (!coupon) {
        throw Error(`Invalid coupon "${params.couponId}"`);
    }

    //Check that the coupon has not expired
    if (Date.now() > coupon.expiresAt) {
        throw Error(`The coupon "${coupon.id}" has expired`);
    }

    // Get the redeemed coupons from Cloud Save
    const cloudSaveGetResponse = await cloudSaveApi.getItems(projectId, playerId, [
        CLOUD_SAVE_COUPONS_KEY,
    ]);

    const redeemedCouponsData = _.find(cloudSaveGetResponse?.data?.results, function (r) {
        return r.key === CLOUD_SAVE_COUPONS_KEY;
    });

    const redeemedCoupons = redeemedCouponsData?.value ?? [];

    // Check if the coupon has been redeemed
    if (redeemedCoupons && _.indexOf(redeemedCoupons, coupon.id) >= 0) {
        throw Error(`The coupon "${coupon.id}" has already been redeemed`);
    }

    // Gift the coupon reward to the player
    await redeemCouponReward(coupon.reward, projectId, playerId, context);

    // Add the coupon id to the array of redeemed coupons and save it back into Cloud Save
    redeemedCoupons.push(coupon.id);
    await cloudSaveApi.setItem(projectId, playerId, {
        key: CLOUD_SAVE_COUPONS_KEY,
        value: redeemedCoupons,
    });

    // Return the reward for the coupon
    return {
        id: coupon.reward.id,
        amount: coupon.reward.amount,
    };
};

// Gift the reward to a player based on the reward.type property
async function redeemCouponReward(reward, projectId, playerId, context) {
    // Initialize a Economy API clients using the player credentials
    const currenciesApi = new CurrenciesApi(context);
    const inventoryApi = new InventoryApi(context);

    // Gift the reward
    switch (reward.type) {
        case 'currency':
            await currenciesApi.incrementPlayerCurrencyBalance(projectId, playerId, reward.id, {
                amount: reward.amount,
            });
            break;
        case 'inventoryItem':
            await inventoryApi.addInventoryItem(projectId, playerId, addInventoryRequest: {
                inventoryItemId: reward.id,
            });
            break;
        default:
            throw Error('Invalid reward type');
    }
}

// Uncomment the code below to enable the inline parameter definition
// - Requires Cloud Code JS dev environment setup with NodeJS (https://docs.unity3d.com/Packages/com.unity.services.cloudcode@2.5/manual/Authoring/javascript_project.html)
//
// module.exports.params = {
//   couponId: { type: "String", required: true },
// };

Note: If you are using Unity Editor to manage your scripts, you can uncomment the code at the bottom of the script to declare the required couponId parameters in-script. To learn more about how to manage in-script parameters, refer to Modify script parameters within the Unity Editor. Publish the script.