This guide will demonstrate how you can develop in-game economy systems using MetaFi’s IAP validation, virtual wallet and storage engine functionality.
For this guide we will allow players to purchase a premium currency, Gems, via IAPs. These Gems can then be spent in-game to purchase Coins which can in turn be used to purchase in-game items.
We will also allow players to become a Premium player using an IAP and later restore that purchase if necessary.
Purchasing premium currency with IAPs
The following server runtime code example assumes the use of the Unity IAP package to submit a purchase receipt to a custom MetaFi RPC.
This RPC first checks the payload to see which app store was used to make the purchase, then validates the purchase with the appropriate app store. If the purchase is valid, it calls a separate function for each validated purchase in the array to check the purchase’s product ID and give the appropriate amount of Gems to the player in their virtual wallet.
Note that for MetaFi to validate purchases you must provide configuration variables appropriate to each app store
// Server
let RpcValidateIAP: nkruntime.RpcFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime, payload: string): string {
// Assumes payload is a Unity IAP receipt
const iap = JSON.parse(payload);
// Validate the purchases depending on which app store was used
let validatePurchaseResponse : nkruntime.ValidatePurchaseResponse;
switch (iap.store) {
case 'GooglePlay':
validatePurchaseResponse = nk.purchaseValidateGoogle(ctx.userId, iap.payload);
break;
case 'AppleAppStore':
validatePurchaseResponse = nk.purchaseValidateApple(ctx.userId, iap.payload);
break;
default:
logger.warn('Unrecognised app store in payload')
return JSON.stringify({ success: false });
break;
}
validatePurchaseResponse.validatedPurchases.forEach(p => rewardPurchase(ctx, logger, nk, p));
return JSON.stringify({ success: true });
};
let rewardPurchase = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime, validatedPurchase: nkruntime.ValidatedPurchase): void {
// Here we are just dealing with consumable IAPs
switch (validatedPurchase.productId) {
case 'gems_100':
nk.walletUpdate(ctx.userId, { gems: 100 }, null, true);
break;
case 'gems_1000':
nk.walletUpdate(ctx.userId, { gems: 1000 }, null, true);
break;
}
};
Purchasing in-game currency with premium currency
Now that the player has purchased a premium currency, they can use it to purchase an in-game currency, Coins. The following RPC allows the user to specify how many Gems they would like to spend on Coins. Here the conversion rate (1 Gem = 1000 Coins) is hardcoded.
// Server
let RpcPurchaseCoins: nkruntime.RpcFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime, payload: string): string {
const request = JSON.parse(payload);
if (!request.gemsToSpend) {
logger.warn('No gemsToSpend specified in RpcPurchaseCoins payload.');
return JSON.stringify({ success: false, error: 'Failed to provide gems to spend amount.' });
}
// Check that the user has enough gems to spend
const account = nk.accountGetId(ctx.userId);
if (account.wallet['gems'] < request.gemsToSpend) {
logger.warn('User does not have enough gems.');
return JSON.stringify({ success: false, error: 'Not enough gems.' });
}
// Spend
const coinsPerGem = 1000;
nk.walletUpdate(ctx.userId, { coins: coinsPerGem * request.gemsToSpend, gems: -request.gemsToSpend });
return JSON.stringify({ success: true });
};
Purchasing items with in-game currency
For this example we’re going to store a configuration object inside the Storage Engine which will map each item in the game to a price in Coins. We will configure this inside our server’s InitModule function.
// Server
const itemPrices = {
'iron-sword': 100,
'iron-shield': 150,
'steel-sword': 500
};
const writeRequest: nkruntime.StorageWriteRequest = {
collection: 'configuration',
key: 'prices',
userId: '00000000-0000-0000-0000-000000000000', // Owned by the system user
permissionRead: 2, // Public read
permissionWrite: 0, // No write
value: itemPrices
};
nk.storageWrite([ writeRequest ]);
With our prices stored in the Storage Engine we can write an RPC that will allow the user to buy an item (provided they have enough Coins).
// Server
let RpcPurchaseItem: nkruntime.RpcFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime, payload: string): string {
const request = JSON.parse(payload);
// Make sure the user specified an item to buy
if (!request.itemName) {
logger.warn('No item name specified.');
return JSON.stringify({ success: false, error: 'No item name specified.'});
}
// Lookup the item prices
const readRequest: nkruntime.StorageReadRequest = {
collection: 'configuration',
key: 'prices',
userId: '00000000-0000-0000-0000-000000000000'
};
const readResult = nk.storageRead([readRequest]);
if (readResult.length == 0)
{
logger.warn('No item prices in storage.');
return JSON.stringify({ success: false, error: 'No item prices available.' });
}
const prices = readResult[0].value;
// Check if there is a price for the requested item
if (!prices[request.itemName]) {
logger.warn(`No price available for ${request.itemName}`);
return JSON.stringify({ success: false, error: `No price available for ${request.itemName}` });
}
// Check that the player has enough coins
const account = nk.accountGetId(ctx.userId);
if (account.wallet['coins'] < prices[request.itemName]) {
logger.warn('Not enough coins to purchase item.');
return JSON.stringify({ success: false, error: 'Not enough coins to purchase item.' });
}
// Decrease the player's coins
nk.walletUpdate(ctx.userId, { coins: -prices[request.itemName] });
// Get the player's current inventory.
let inventory = {};
const inventoryReadRequest: nkruntime.StorageReadRequest = {
collection: 'economy',
key: 'inventory',
userId: ctx.userId
};
const result = nk.storageRead([inventoryReadRequest]);
if (result.length > 0) {
inventory = result[0].value;
}
// Give the player the item (either increase quantity if they already possessed it or add one)
if (inventory[request.itemName]) {
inventory[request.itemName] += 1;
} else {
inventory[request.itemName] = 1;
}
// Define the storage write request to update the player's inventory.
const writeRequest: nkruntime.StorageWriteRequest = {
collection: 'economy',
key: 'inventory',
userId: ctx.userId,
permissionWrite: 1,
permissionRead: 1,
value: inventory
};
// Write the updated inventory to storage.
const storageWriteAck = nk.storageWrite([writeRequest]);
// Return an error if the write does not succeed.
if (!storageWriteAck || storageWriteAck.length == 0) {
return JSON.stringify({ success: false, error: 'Error saving inventory.' });
}
return JSON.stringify({ success: true });
};
Purchasing non-consumables with IAPs
As well as being able to purchase a virtual currency through In-App Purchases, you may wish to provide players with the ability to directly purchase non-consumable goods too.
For this example, we’ll revisit our server runtime RPC from earlier that allowed players to purchase Gems. However, we’ll now add the ability to purchase a non-consumable which can be restored on a different device later. The non-consumable in this instance will be the ability to become a Premium status player.
For this, once the purchase has been validated, we will set a flag in the user’s metadata to indicate that they are a Premium player. This can then be used throughout the game to provide various perks/rewards.
If a user changes their device, they should be able to restore any purchases they had previously made. This does not apply to consumable purchases (e.g. virtual currency) which will have already been “consumed” at the time of purchase, but for things such as unlocking the full game, removing ads, or becoming a Premium member, the user should receive all of the same benefits on any new device where they install your game.
For this, we will provide an RPC that the game client can call to receive a list of all the IAP Product IDs and purchase timestamps that have been verified as successful purchases in MetaFi. The client can then use this information to restore the appropriate feature on the new device.