Add Products at runtime
This guide was written for PlayFab Economy V1 and is now outdated. If you wish to have an updated version for PlayFab Economy V2, please send me a request.
Adding products at runtime, either using a product list downloaded from your own server, or using a cloud service provider like PlayFab, allows for server-side flexibility in defining your store, without the need to update your app. However, please note that for most situations, adding new products also requires adding some new code, so often it does not make much sense to do a server-side product update only. For example, a sword product needs a sword model, a bonus level product the level scene and code to select it, a no-ads product the code to check it before showing ads and so on. If you're thinking about Asset Bundles, keep in mind that on mobile App Stores it is not allowed to include executable code in them.
With that out of the way, the following procedure is using PlayFab for downloading a product catalog and adding missing products locally. You could use a similar technique with your own server, for example serving a JSON file with all products.
Download from PlayFab
This sample requires simple setup and one script attached below. For demonstration purposes, I am starting off with an empty scene including only necessary components. You would want to incorporate this into your game flow and UI instead.
Since the PlayFab login scene included in UniPay handles the login callback and catalog download automatically, we need a way to intercept this logic. In theory, we would want to login the user (for being able to do PlayFab calls), request the product catalog, add it locally, initialize the IAPManager and switch scenes. For this, our custom login scene contains the following components:
- Demo script (see below): for this demo, enter your PlayFab user login credentials in the inspector
- IAPManager: Auto Initialize disabled, Storage Type = Memory (basic PlayFab setup)
- PlayFabManager
Assuming that you have added a product to PlayFab but not in SIS, after logging in you should see a message in the Console, stating that a product has been added by the script. Note that the icon is not set, because I did not assign it or have provided a URL. Also, all products are added to the same "IAP" category. You will want to change the category ID in the script to a different or dynamic value.
Demo Script
using PlayFab;
using PlayFab.ClientModels;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
using SIS;
/// <summary>
/// DEMO SCRIPT! Please modify according to your needs.
/// </summary>
public class DemoProductDownload : MonoBehaviour
{
//this should be incorporated in your login UI instead
public string userEmail;
public string userPassword;
//store login result to continue processing it later
private LoginResult loginResult;
/// <summary>
/// Login to PlayFab manually
/// Not using the PlayFabManager classes since it does the OnLoggedIn callback automatically
/// </summary>
private void Start()
{
GetPlayerCombinedInfoRequestParams accountParams = new GetPlayerCombinedInfoRequestParams()
{
GetUserInventory = true,
GetUserVirtualCurrency = true,
GetUserData = true,
UserDataKeys = new List<string>() { DBManager.playerKey, DBManager.selectedKey }
};
LoginWithEmailAddressRequest request = new LoginWithEmailAddressRequest()
{
TitleId = PlayFabSettings.TitleId,
Email = userEmail,
Password = userPassword,
InfoRequestParameters = accountParams
};
PlayFabClientAPI.LoginWithEmailAddress(request, OnLoggedIn, OnLoginError);
}
/// <summary>
/// Logged In, get Product Catalog from PlayFab
/// </summary>
void OnLoggedIn(LoginResult result)
{
loginResult = result;
PlayFabClientAPI.GetCatalogItems(new GetCatalogItemsRequest(), OnCatalogRetrieved, null);
}
/// <summary>
/// In case of Login errors for debugging
/// </summary>
void OnLoginError(PlayFabError error)
{
string errorText = error.ErrorMessage;
if (error.ErrorDetails != null && error.ErrorDetails.Count > 0)
{
foreach (string key in error.ErrorDetails.Keys)
{
errorText += "\n" + error.ErrorDetails[key][0];
}
}
Debug.LogError(errorText);
}
/// <summary>
/// Process the retrieved Product Catalog
/// </summary>
void OnCatalogRetrieved(GetCatalogItemsResult result)
{
List<CatalogItem> catalog = result.Catalog;
for (int i = 0; i < catalog.Count; i++)
{
CatalogItem catalogItem = catalog[i];
//product does not exist locally, add it!
if (IAPManager.GetIAPProduct(catalogItem.ItemId) == null)
{
Debug.Log("Not found locally, added from remote: " + catalogItem.ItemId);
IAPManager.GetInstance().asset.productList.Add(CatalogItemToProduct(catalogItem));
}
}
//we're logged in and done with adding new products, let PlayFabManager continue initializing
PlayFabManager.OnLoggedIn(loginResult);
//all done, leave the login and continue to the shop scene!
UnityEngine.SceneManagement.SceneManager.LoadScene("Vertical2D");
}
/// <summary>
/// Convert CatalogItem from PlayFab to IAPProduct
/// </summary>
IAPProduct CatalogItemToProduct(CatalogItem item)
{
//create new product
IAPProduct product = new IAPProduct();
product.referenceID = System.Guid.NewGuid().ToString("D");
//create or find category
//you might want to change the category here or deliver it in the API call from your server
//PlayFab does not have a category field, but you could use customData for it too
IAPCategory category = IAPManager.GetInstance().asset.categoryList.Find(x => x.ID == "IAP");
if (category == null)
{
category = new IAPCategory();
category.referenceID = System.Guid.NewGuid().ToString("D");
category.ID = "IAP";
IAPManager.GetInstance().asset.categoryList.Add(category);
}
//meta data
product.category = category;
product.ID = item.ItemId;
product.title = item.DisplayName;
product.description = item.Description;
product.type = item.IsStackable ? ProductType.Consumable : ProductType.NonConsumable;
product.fetch = true;
//real money price
if (item.VirtualCurrencyPrices.ContainsKey("RM"))
{
product.priceList.Add(new IAPExchangeObject()
{
type = IAPExchangeObject.ExchangeType.RealMoney,
realPrice = "$" + (item.VirtualCurrencyPrices["RM"] / 100f).ToString().Replace(',', '.')
});
}
//virtual currency price
else
{
foreach (string key in item.VirtualCurrencyPrices.Keys)
{
IAPCurrency cur = IAPManager.GetInstance().asset.currencyList.Find(x => x.ID.StartsWith(key, System.StringComparison.OrdinalIgnoreCase));
if (cur != null)
{
product.priceList.Add(new IAPExchangeObject()
{
type = IAPExchangeObject.ExchangeType.VirtualCurrency,
amount = (int)item.VirtualCurrencyPrices[key],
currency = cur
});
}
}
}
//rewards
if (item.Bundle != null)
{
//product rewards
if (item.Bundle.BundledItems != null)
{
foreach (string key in item.Bundle.BundledItems)
{
product.rewardList.Add(new IAPExchangeObject()
{
type = IAPExchangeObject.ExchangeType.VirtualProduct,
amount = 1,
product = IAPManager.GetIAPProduct(key)
});
}
}
//virtual currency rewards
if (item.Bundle.BundledVirtualCurrencies != null)
{
foreach (string key in item.Bundle.BundledVirtualCurrencies.Keys)
{
IAPCurrency cur = IAPManager.GetInstance().asset.currencyList.Find(x => x.ID.StartsWith(key, System.StringComparison.OrdinalIgnoreCase));
if (cur != null)
{
product.rewardList.Add(new IAPExchangeObject()
{
type = IAPExchangeObject.ExchangeType.VirtualCurrency,
amount = (int)item.Bundle.BundledVirtualCurrencies[key],
currency = cur
});
}
}
}
}
//make sure to add a reward for itself on non-consumables
if (product.type != ProductType.Consumable && !product.rewardList.Exists(x => x.product != null && x.product.ID == product.ID))
{
product.rewardList.Add(new IAPExchangeObject()
{
type = IAPExchangeObject.ExchangeType.VirtualProduct,
amount = 1,
product = product
});
}
return product;
}
}