Documentation

Introduction to the purchasing controller

Learn how one controller connects to every store your application sells through, and how it sends each call to the right store.
Read time 8 minutesLast updated 14 hours ago

PurchasingController
is a single object that controls every store your application sells through, such as the Apple App Store or Google Play, plus Direct-to-Consumer (D2C) payment providers. It connects to each store, fetches products and purchases from each store, and reports every store's events through one set of event handlers.
This page explains when to use
PurchasingController
, how it routes calls to each store, how it provides platform extensions, and how to migrate an existing project from
StoreController
.
Note
PurchasingController
is available in Unity In-App Purchasing (IAP) 5.5 and later. In IAP 5.4, use
StoreController
. The setup and D2C payment provider pages show the code for both versions in tabs. For an example, refer to Write the startup script.

When to use the purchasing controller

PurchasingController
is the recommended controller for all new projects, including projects that sell through a single store. A project that starts with one store can add another later with a small code change, because the rest of its purchasing code already handles more than one store.
For example, a project that sells only through the platform-native store creates its controller with one store name:
m_Controller = new PurchasingController(UnityIAPServices.GetDefaultStore());
Adding D2C payment providers to that project later takes two code changes. The first adds
PaymentProvider.Name
to the constructor:
m_Controller = new PurchasingController(UnityIAPServices.GetDefaultStore(), PaymentProvider.Name);
The second starts purchases through the Purchase Options UI, which lets the player choose how to pay:
var purchaseOptions = m_Controller.PaymentProvidersExtendedService?.GetPaymentOptionProviderUGUI();if (purchaseOptions != null){ await purchaseOptions.ShowPurchaseOption(catalogListingId);}
The event handlers, the purchase confirmation, and a product fetch that loops over
StoreNames
stay as they are. D2C payment providers also need a Remote Catalog, a signed-in player, and a configured provider. For the full list of requirements, refer to Integrate D2C payment providers.
Important
You can create the controller with
PaymentProvider.Name
at any time, but call
Connect
only after Unity Gaming Services initializes, and fetch products only after the player signs in. If you call them earlier, they fail as follows:
  • Connect
    fails for
    PaymentProvider.Name
    , and
    OnStoreDisconnected
    reports
    Unity Services uninitialized. Make sure you use 'UnityServices.InitializeAsync()' before connecting.
  • The product fetch fails, and
    OnProductsFetchFailed
    reports
    Unknown error: Player is not authorized.
If your project connects its controller before Unity Gaming Services initializes, move the
Connect
call to after initialization when you add
PaymentProvider.Name
.
StoreController
remains supported. A project that uses
StoreController
keeps working without changes. For a complete startup script that uses
PurchasingController
, refer to Set up and initialize In-App Purchasing.
Important
Create one
PurchasingController
for the lifetime of your application, and reuse it in every scene. Don't create a second
PurchasingController
, and don't use
StoreController
in the same project. Controllers that include the same store share that store's services, so each controller receives the same events and your handlers run twice, which can grant an item twice. A product that you fetch with one and purchase with the other can also produce an order that
PurchasingController
doesn't confirm.

How the purchasing controller routes calls

You create a
PurchasingController
with the names of the stores it controls. For example,
new PurchasingController(UnityIAPServices.GetDefaultStore(), PaymentProvider.Name)
controls the platform-native store and D2C payment providers. The
StoreNames
property lists the stores in the order you passed them.
The constructor throws an
ArgumentException
if you pass no store name, an empty name, or the same name twice, and a
StoreException
if a name isn't a registered store.
The controller sends each call to a store according to the following rules.

Background calls apply to every store

Calls that start work in the background take an optional
storeName
parameter. When you omit it, the call applies to every store on the controller. When you pass a store name, the call applies to that store only.

Method

Behavior with no store name

Connect
Connects every store. The returned
Task
completes when every store has connected or failed.
FetchPurchases
Fetches purchases from every store. Each store reports its result separately.
RestoreTransactions
Restores the transactions of every store. The callback runs once for each store, with its name.
ProcessPendingOrdersOnPurchasesFetched
Applies the setting to every store.
SetStoreReconnectionRetryPolicyOnDisconnection
Applies the same retry policy instance to every store. A policy that keeps its own state shares it across stores.

Product fetches name their store

FetchProducts
and
FetchProductsWithNoRetries
require a store name and fetch products from that one store. A product definition holds the product ID of a single store, so each store needs its own definitions.
One catalog provider serves every store. For each store, get that store's definitions from the provider with
GetProducts
, and pass them to the controller with the same store name:
foreach (var storeName in m_Controller.StoreNames){ m_Controller.FetchProductsWithNoRetries(m_Catalog.GetProducts(storeName), storeName);}
When two stores fetch the same catalog listing, each store returns its own
Product
, with its own price.

Purchases follow the product or the order

Calls that take a product, a cart, or an order go to the store that object belongs to. Each
Product
and each
Order
holds the name of its store in a read-only
StoreName
property.

Method

Store that receives the call

PurchaseProduct(Product)
The store the product came from,
Product.StoreName
.
Purchase(ICart)
The store that all the products in the cart come from.
CheckEntitlement(Product)
The store the product came from,
Product.StoreName
.
ConfirmPurchase(PendingOrder)
The store the order belongs to,
Order.StoreName
.
PurchaseProduct(string catalogListingId, string storeName)
The store you name, because a catalog listing ID doesn't belong to one store.
A purchase uses one store. A cart that mixes products from different stores fails.

Lookups name their store

Methods that return a store's state require a store name, so that you always know which store answered:
GetProducts
,
GetPurchases
,
GetProductById
,
GetProductByCatalogListingId
, and
GetConnectionState
.

Events report their store

PurchasingController
has the same events as
StoreController
, such as
OnPurchasePending
and
OnProductsFetched
. Each event passes the name of the store it comes from as its first parameter. For example, an
OnPurchasePending
handler takes
(string storeName, PendingOrder order)
.
One subscription covers every store on the controller. To handle one store differently, check the
storeName
parameter in your handler:
m_Controller.OnPurchasePending += (storeName, order) =>{ if (storeName == PaymentProvider.Name) { // Handle D2C orders differently here, if you need to. } // Grant the content for the order, then confirm it with its own store. m_Controller.ConfirmPurchase(order);};
OnAuthAccountChanged
fires once for each store when the signed-in player changes. Each store clears its product and purchase caches before the event fires, so fetch your catalog again and then fetch products and purchases from the store the event names. For more information, refer to Handle an account change.

Methods report errors instead of throwing

After you create the controller, its methods don't throw exceptions. When the controller can't route a call, for example, because it names a store that isn't on the controller, the controller logs an error and reports the failure through the event or callback that the call normally uses, such as
OnPurchaseFailed
or
OnProductsFetchFailed
. A
ConfirmPurchase
call that can't be routed reports a
FailedOrder
through
OnPurchaseConfirmed
. Lookups return an empty collection,
null
, or
ConnectionState.Unavailable
.
For a call with no store to report under, such as a
null
order or a product with no store name, the controller only logs an error. Settings calls that name a store that isn't on the controller,
SetStoreReconnectionRetryPolicyOnDisconnection
and
ProcessPendingOrdersOnPurchasesFetched
, also only log an error.

Platform extensions

PurchasingController
has the same platform extension properties as
StoreController
, such as
AppleStoreExtendedService
,
GooglePlayStoreExtendedPurchaseService
, and
PaymentProvidersExtendedService
. Each property returns the extension of the store on the controller that provides it, or
null
if no store does.
To use the Purchase Options UI, get it from
PaymentProvidersExtendedService
with
GetPaymentOptionProviderUGUI
or
GetPaymentOptionProviderUITK
. Purchases that the player makes through it arrive on the controller's events, as long as the controller includes both the platform's default store,
UnityIAPServices.GetDefaultStore()
, and
PaymentProvider.Name
.

Migrate an existing project

To move a project from
StoreController
to
PurchasingController
, follow these steps:
  1. Replace every
    StoreController
    in the project with one
    PurchasingController
    that lists every store those controllers use. For example, replace
    UnityIAPServices.StoreController()
    and
    UnityIAPServices.StoreController(PaymentProvider.Name)
    with
    new PurchasingController(UnityIAPServices.GetDefaultStore(), PaymentProvider.Name)
    . If one of the controllers uses
    PaymentProvider.Name
    , call
    Connect
    after Unity Gaming Services initializes, and fetch products after the player signs in.
  2. Replace the event subscriptions of each
    StoreController
    with one subscription to each
    PurchasingController
    event, and add the store name as the first parameter of each handler. For example,
    OnPurchasePending(PendingOrder order)
    becomes
    OnPurchasePending(string storeName, PendingOrder order)
    .
  3. Pass a store name to every product fetch and every lookup. For example,
    GetProducts()
    becomes
    GetProducts(storeName)
    , and
    FetchProducts(definitions)
    becomes
    FetchProducts(definitions, storeName)
    , called once for each store.
  4. Replace catalog provider calls that pass a controller method directly, such as
    catalog.FetchProducts(m_StoreController.FetchProductsWithNoRetries)
    , with a call on the controller for each store. For each store, call
    m_Controller.FetchProductsWithNoRetries(catalog.GetProducts(storeName), storeName)
    , as shown in Product fetches name their store.
  5. Update the callback of
    RestoreTransactions
    to take the store name as its first parameter. The callback then takes
    (storeName, success, error)
    .
  6. Remove every remaining use of
    StoreController
    , so that the project doesn't use both.
Calls that take a product or an order, such as
PurchaseProduct(Product)
and
ConfirmPurchase(PendingOrder)
, don't change. They go to the store of the object you pass.
PurchasingController
doesn't implement
IStoreService
,
IProductService
, or
IPurchaseService
. If code in your project, such as a third-party plug-in, requires those interfaces, keep the project on
StoreController
.

Additional resources