Making a Purchase
Reading the products
GetProducts() returns every product on sale. GetProduct() returns one product by its full store identifier, or null when it is not in the catalogue:
var inAppPurchases = playOpsSDK.InAppPurchases();
foreach (var product in inAppPurchases.GetProducts()){ Debug.Log($"{product.Identifier}: {product.Title} — {product.FormattedPrice}");}
var coins = inAppPurchases.GetProduct("com.example.game.iap.coins_100");GetProducts() is empty until the store has been started from the configuration.
Only show products the store has priced
A product is listed as soon as it is in the configuration, but it is only on sale once the store has described it. Until then, Title and FormattedPrice fall back to the values in your configuration. Check StoreInfo and RejectedByStore before you show a buy button:
var product = inAppPurchases.GetProduct(productId);if (product == null){ // Not in the catalogue yet: the store has not been started. return;}
if (product.RejectedByStore){ // The store does not know this identifier: it is not configured for sale for this app. return;}
if (product.StoreInfo == null){ // Waiting for the store to price this product. return;}
ShowBuyButton(product.Title, product.FormattedPrice);Buying a product
Purchase() charges the player for one product and returns a Task that completes once the store has answered:
var economyChangeId = "shop-" + Guid.NewGuid().ToString("N");PurchaseOperationResult result = await inAppPurchases.Purchase(product, economyChangeId);
if (result.Outcome == PurchaseOutcome.Succeeded){ GrantProduct(result.Transaction.ProductId); inAppPurchases.FinishTransaction(result.Transaction);}You do not have to check that the store is ready first. If the store is reconnecting, Purchase() waits for it for up to 15 seconds before it answers StoreUnavailable.
The result
PurchaseOperationResult describes the purchase whatever happened:
| Property | Description |
|---|---|
Outcome | What happened. See the table below. |
Transaction | The purchase to deliver and finish. Set only when Outcome is Succeeded. |
SessionId | An id the SDK creates for every call to Purchase(). All the purchase analytics events about this one purchase carry it, so they can be grouped together. |
FailureCode | Why a Failed purchase failed. |
StatusCode | Outcome and FailureCode folded into one value to switch on. |
Error | The store’s own message, for diagnostics. |
Outcomes
| Outcome | What happened | What to do |
|---|---|---|
Succeeded | The store confirmed the payment. | Grant the product, then call FinishTransaction(result.Transaction). |
Cancelled | The player cancelled. | Nothing is owed. |
Failed | The store refused the purchase. FailureCode says why: NetworkError, PaymentDeclined, ItemUnavailable, StoreUnavailable, AlreadyOwned or Unknown. | Nothing is owed. |
AnotherPurchaseInProgress | A purchase was already running. Purchases are not queued. | Try again once the first one has answered. |
StoreUnavailable | The store could not be reached in time, or it was never started because the configuration has no products. | Tell the player to try again later. |
A purchase without an internet connection answers Failed with the failure code NetworkError, without asking the store.
If you prefer a single value to switch on, use StatusCode. It has one value per outcome and one per failure cause:
switch (result.StatusCode){ case PurchaseStatusCode.Success: GrantProduct(result.Transaction.ProductId); inAppPurchases.FinishTransaction(result.Transaction); break; case PurchaseStatusCode.Cancelled: case PurchaseStatusCode.StoreUnavailable: // No goods owed, nothing to finish. break;}Payments that need approval
Some payments are held by the store before they are charged: Ask to Buy on iOS, or a pending payment method on Google Play. While the store holds the payment, Purchase() keeps waiting. If the store has still not taken the payment when the SDK stops waiting, the purchase answers Cancelled and nothing is owed yet. If the payment goes through later, the purchase arrives through PurchaseRecovered, even after a restart.
PurchaseOutcome and PurchaseStatusCode also have a Deferred value, but the Google Play and App Store integrations never return it. A held payment ends as Cancelled.
The economy change id
A purchase pays for a change in what the player owns in your game, for example 100 coins added to their balance. economyChangeId is an id your game creates for that change. The SDK never reads or changes it. It keeps it with the purchase and reports it on the purchase analytics events purchaseStarted, purchaseSucceeded, purchaseFailed, purchaseCancelled and unvalidatedTransaction. It is also on the Transaction you deliver.
Pass it whenever your game records the change in its own analytics events. If those events carry the same id, you can follow one purchase from your game’s side, such as the offer the player tapped and the coins you added, through to the store’s answer. The SDK does not check that the id is unique. The demo creates a new one for every purchase:
var economyChangeId = "playops-demo-" + Guid.NewGuid().ToString("N");var result = await inAppPurchases.Purchase(product, economyChangeId);It is optional. If you leave it out, the purchase works the same way, but the events and the Transaction carry an empty economy change id. The purchase still gets its own SessionId, so its events can still be grouped together. They just cannot be matched to your game’s own events:
var result = await inAppPurchases.Purchase(product);Reacting to every outcome
PurchaseResolved is raised once for every purchase your game asked for, whatever the store answered. Use it for reporting or UI that is not tied to the purchase call:
inAppPurchases.PurchaseResolved += report =>{ Debug.Log($"{report.ProductId}: {report.Status}");};A PurchaseReport cannot be delivered or finished. Delivery always goes through the result of Purchase() or through PurchaseRecovered.