Basic store operations
An in-game store typically will involve three basic operations:
This document will illustrate sample code involved with each operation. This is derived from the InGameStore sample which is continuously updated to reflect best practices.
Preparing to call XStore APIs
All XStore
APIs operate over an XStoreContextHandle created using XStoreCreateContext.
This context allows you to perform store operations in the context of the specified user on console and the default user available on PCs. On consoles, the context is invalided after a Suspend or Quick Resume event. To safely handle these conditions we recommend closing a XStoreContextHandle and recreating it whenever the game resumes from a suspended state.
1. Determining what can be purchased
What a game usually offers for purchase is their add-ons. The following code demonstrates the basic XStoreQueryAssociatedProductsAsync API call needed for the game to know what products are available.
Only purchasable add-ons associated with the game are automatically returned in this query. Unrelated products associated with the same publisher (i.e. configured with the same Partner Center account) can also be made to return in this call so long as the game is set with a "can sell" relationship to this product in the Product relationship setup section in Partner Center.
bool CALLBACK ProductEnumerationCallback(const XStoreProduct* product, void* context)
{
// Handle adding the product to the game
printf("%s %s %u\n", product->title, product->storeId, product->productKind);
return true;
}
void QueryCatalog()
{
auto async = new XAsyncBlock{};
async->queue = m_asyncQueue;
async->callback = [](XAsyncBlock* async)
{
XStoreProductQueryHandle queryHandle = nullptr;
HRESULT hr = XStoreQueryAssociatedProductsResult(async, &queryHandle);
if (SUCCEEDED(hr))
{
hr = XStoreEnumerateProductsQuery(queryHandle, async->context, ProductEnumerationCallback);
if (SUCCEEDED(hr))
{
printf("Enumeration complete\n");
}
XStoreCloseProductsQueryHandle(queryHandle);
delete async;
}
};
XStoreProductKind typeFilter =
XStoreProductKind::Consumable |
XStoreProductKind::Durable |
XStoreProductKind::Game;
HRESULT hr = XStoreQueryAssociatedProductsAsync(
m_xStoreContext,
typeFilter,
UINT8_MAX, // placeholder maximum, see Paging
async)
if (FAILED(hr))
{
delete async;
}
}
Things to note
- Only purchasable products are returned with
XStoreQueryAssociatedProductsAsync
; products that are only granted in bundles or otherwise not set to be independently purchasable will not return. For these useXStoreQueryProductsAsync
described below. - There is no upfront knowledge of the number of products that will be returned, therefore the count must be accumulated.
Paging
Handling paging is now optional. Select a maximum value to pass that is guaranteed to be larger than the lifetime expected size of your catalog. For large catalogs or desiring to segment the result handling to show progress, detecting and handling paging can be implemented. See XStoreQueryAssociatedProductsAsync for more details.
Other options
XStoreQueryProductsAsync can be used to query specific products, if the store IDs are known or if other actionFilters
are desired.
"Actions" are usage scenarios that apply to a product, which include verbs like Purchase, License, Gift, and Redeem.
XStoreQueryAssociatedProductsForStoreIdAsync can be used to query associated products for other games. This can be useful, for example, to cross sell a different title's add-ons.
XStoreQueryProductForCurrentGameAsync is to query just the product for the currently running game.
XStoreShowAssociatedProductsUIAsync will transition the user to the Microsoft Store app to a view of associated products, which can be filtered by product kind. This can be an alternative to having to enumerate available products to present in an in-game interface.
2. Evaluating what products are owned
This will involve essentially the same code as the above with these replacements:
- XStoreQueryAssociatedProductsAsync → XStoreQueryEntitledProductsAsync
- XStoreQueryAssociatedProductsResult → XStoreQueryEntitledProductsResult
With these API, the results will comprise the products that are specifically entitled by the calling user's account. Entitled can mean directly owned, but also can mean satisfied by means of owning a parent bundle or subscription.
Note that this can also be determined by the results of XStoreQueryAssociatedProductsAsync (and related functions) as XStoreProduct contains an isInUserCollection
field that is set to true when it is entitled.
Consumable ownership
Consumable quantity is noted in XStoreProduct.skus[i].collectionData.quantity
.
Typically there is only one SKU for a consumable product.
Quantity can also be inquired independently by using XStoreQueryConsumableBalanceRemainingAsync, but this is discouraged for a large number of consumables individually as each will incur a service call.
Also, to maintain integrity of consumable-based ecosystems, it's highly recommended to utiltize service validation and redemption of consumables. For more information, see Consumable-based ecosystems.
Durable ownership
It is not enough to check if the account owns the product to determine that they should be entitled to use the product in-game. Durables must adhere to the content sharing policy that is described in Product sharing model for games.
XStoreAcquireLicenseForPackageAsync is used for durables with a package to determine if it is licensable according to the provisions of content sharing.
XStoreAcquireLicenseForDurablesAsync is used for durables without a package to do the same.
XStoreQueryAddOnLicensesAsync can be used for a digitally licensed game to return the list of licensable durables (without package).
More information can be found in Manage and license downloadable content and How to use a durable without a package.
3. Purchasing eligible products
Showing the purchase flow for a purchasable product is as simple as passing in the Store ID into the XStoreShowPurchaseUIAsync API:
void MakePurchase(const char* storeId)
{
auto async = new XAsyncBlock{};
async->context = &storeId;
async->queue = m_asyncQueue;
async->callback = [](XAsyncBlock *async)
{
const char* = reinterpret_cast<const char*>(async->context);
HRESULT hr = XStoreShowPurchaseUIResult(async);
if (SUCCEEDED(hr))
{
printf("Purchase succeeded (%s)\n", storeId);
// Refresh ownership and update game
}
else
{
printf("Purchase failed (%s) 0x%x\n", storeId, hr);
if (hr == E_GAMESTORE_ALREADY_PURCHASED)
{
printf("Already own this\n");
}
}
delete async;
};
HRESULT hr = XStoreShowPurchaseUIAsync(
m_xStoreContext,
storeId,
nullptr, // Can be used to override the title bar text
nullptr, // Can be used to provide extra details to purchase
async);
if (FAILED(hr))
{
delete async;
printf("Error calling XStoreShowPurchaseUIAsync : 0x%x\n", hr);
return;
}
}
In addition to handling the results of a potential purchase in the async callback, purchases can also be made outside of the game by explicitly switching to the Microsoft Store. They can also be made on Xbox.com, PC, mobile apps, or other outlets. It is recommended for there to be a place in the game that reliably refreshes product ownership on demand, such as a transition to the in-game store or somewhere in the settings, and not just solely as part of the initial sign-in flow.