Skip to content

Restore undelivered Google Play purchases

Retrieve Google Play purchases whose payment is complete but whose product delivery is not finished, and complete the remaining product delivery and transaction finishing.

First complete the setup and installation in Implement Google Play payments.

When to run

A purchase remains unfinished in the following situations.

  • The app terminated or the network disconnected right after payment
  • Receipt verification succeeded, but product delivery or transaction finishing failed
  • The user completed a deferred payment, such as a convenience store top-up, later

Consumable products that are not consumed cannot be purchased again, so run the restoration when the app launches and when the in-app store screen opens. Even if the two times overlap, do not let the restoration run twice at the same time, and do not process an order while an in-progress purchase is processing the same order. You must also run this restoration first when you receive StoreItemAlreadyOwned in the step that starts the purchase.

Overall flow

Step Category What you do
1 Recipe code Get undelivered purchases
2 App code Fill in the order information from the time of purchase
3 Recipe code Prepare purchase information
4 App code, App server, Hive Axyl Server API Verify receipts and deliver products
5 Recipe code Finish the transaction
6 Hive Axyl SDK Record delivery results

1. Get undelivered purchases

  • Recipe code  Call the recipe code from the app.

Call FindUndeliveredAsync() to get the list of purchases that have not been consumed yet.

Process the retrieved items one at a time, in order.

using Hive.Axyl.Samples.Recipes;

var recipe = new UndeliveredPurchaseRecipe(new GooglePurchaseSource());

var query = new UndeliveredQuery(
    country:     "KR",
    language:    "ko",
    serverId:    "server01",
    appVersion:  Application.version,
    accountUuid: accountUuid);

FindUndeliveredOutcome found = await recipe.FindUndeliveredAsync(query, token);

if (found.Status != FindUndeliveredStatus.Success)
{
    Debug.LogError($"{found.FailedStep}: {found.BusinessOutcome} {found.Error?.Message}");
    return;
}

foreach (PendingPurchase entry in found.Undelivered)
{
    // Process the items one at a time, in order, starting from step 2.
}

The recipe performs the following tasks internally.

Category Call Where to check
Store plugin Connect BillingClient with StartConnectionAsync() BillingClient connection
Store plugin Get the list of unconsumed purchases with QueryPurchasesAsync() Retrieve undelivered receipts again

Google Play keeps the list of undelivered purchases on the device. It returns only purchases whose payment is complete, and purchases whose payment is in progress are not in the list. They have not been charged yet, so there is no product to deliver, and Play notifies again when the payment completes.

An empty list means that there are no purchases to finish; it is not an error.

2. Fill in the order information from the time of purchase

  • App code  Implement this in the app.

The purchase records that Play keeps do not contain the payment amount, the currency, or the order information passed when the purchase started. In the items the recipe returns, Order.Price is 0, Order.Currency is empty, and Order.IapPayload is null, while ServerId and AccountUuid contain the values you put in the query conditions in step 1.

Before verification, replace these values with the order information from the time of purchase. First, find the purchase with the same StoreTransactionId among the purchase information saved in Implement Google Play payments; if there is none, use the order record kept for the same account and product ID. Deferred payments such as convenience store top-ups do not receive Pending when the purchase starts, so they remain only in the order records. pendingStore.FindAsync() and FindOrderRecordAsync() in the example code are methods that the app implements.

// Find the order information from the time of purchase in the purchase information, then in the order records.
PurchaseOrder original =
    (await pendingStore.FindAsync(entry.StoreTransactionId))?.Order
    ?? await FindOrderRecordAsync(accountUuid, entry.Order.ProductId);

if (original == null)
{
    // For items whose information from the time of purchase cannot be found, do not guess the amount; record them as errors.
    continue;
}

var pending = new PendingPurchase(
    entry.AxylReceipt,
    entry.OrderId,
    original,             // Amount, currency, and order information from the time of purchase
    entry.Market,
    entry.VerifyToken,
    entry.FinishToken,
    entry.StoreTransactionId,
    entry.StoreVerificationError);

// Save the filled-in purchase information, and then proceed to step 3.
await pendingStore.SaveAsync(pending);
Do not guess the amount at the time of purchase

Do not fill it in with the price from the current product list instead, and do not verify with the amount left empty. If the price changed after the purchase, the amount differs from what the user actually paid, and if you leave the amount empty, the Hive Axyl server compares 0 with the actual payment amount. Leave items that cannot be found in either record unfinished and record them as errors. Google automatically refunds purchases that are not consumed after the acknowledgment period passes.

3. Prepare purchase information

  • Recipe code  Call the recipe code from the app.

Call PreparePurchaseAsync() with the filled-in purchase information to record the purchase result on the Hive Axyl server again and prepare the purchase information to send to the app server. The recipe does not request receipt verification in this step, so even if you receive a success result, you must not deliver the product yet.

PreparePurchaseOutcome prepared = await recipe.PreparePurchaseAsync(pending, token);

if (prepared.Pending != null)
{
    // Replace the saved purchase information with the value the prepare step returned.
    await pendingStore.SaveAsync(prepared.Pending);
}

if (prepared.Status != PreparePurchaseStatus.Success)
{
    // Keep the saved purchase information and move on to the next item.
    continue;
}

The payment confirmation request sent when finishing the transaction works on the basis of this record, so the restoration also sends the request to save the purchase result again. Even if it is rejected because the purchase is already saved, the recipe treats it as a success, and the app server decides whether to deliver in step 4. The recipe performs the following tasks internally.

Category Call Where to check
Hive Axyl SDK Save the purchase result with RecordStorePurchaseAsync() Save payment result data

4. Verify receipts and deliver products

  • App code  Implement this in the app.

    App server  Implement this on the app server.

    Hive Axyl Server API  Call the Hive Axyl Server API from the app server.


    Detailed procedure: Verify receipts with the Hive Axyl Server API

For restored purchases, as for new purchases, the app server verifies the receipt and delivers the product. Send prepared.Pending from step 3 to the app server to request verification and delivery as a purchase restoration, and proceed to step 5 only when the app server responds with delivery completed.

4.1. Request verification and delivery from the app server

VerifyAndGrantOnceAsync() in the example code is the app's method implemented in Implement Google Play payments. For restoration, pass 2, which indicates a purchase restoration, as the second argument requestType. The usage example passes PurchaseVerificationKind.Recovery as this argument, and the app server integration code converts it to 2.

// Request receipt verification and product delivery from the app server as a purchase restoration.
PurchaseDeliveryConfirmation delivery = await VerifyAndGrantOnceAsync(
    prepared.Pending,
    requestType: 2,   // 2: Purchase restoration. PurchaseVerificationKind.Recovery in the usage example
    token);

if (delivery == null)
{
    // The app server did not confirm delivery.
    // Keep the saved purchase information and move on to the next item.
    continue;
}

4.2. Verify receipts and deliver products on the app server

Have the app server verify the receipt and deliver the product in the same way as in Implement Google Play payments. The differences from a new purchase are as follows.

  1. Put 2, which indicates a purchase restoration, in requestType of the receipt verification request.
  2. Check the same delivery records as for new purchases to see whether the purchase has already been delivered. requestType is not the basis for preventing duplicate delivery.
  3. If the purchase has already been delivered, do not deliver it again; respond with delivery completed. The app finishes the transaction in step 5 only if you respond with delivery completed.

If you filled in the order information from the time of purchase in step 2, Order.IapPayload contains the iapPayload passed when the purchase started. If you put this value in the verification request, it comes back as is in hiveAxylPayload of the response, so use this value to decide which delivery target or server to deliver to.

5. Finish the transaction

  • Recipe code  Call the recipe code from the app.

When the app server responds with delivery completed, call ClosePurchaseAsync() to finish the purchase. If you do not finish it, you cannot sell the same product again, and it is retrieved again in the next restoration.

ClosePurchaseOutcome closed = await recipe.ClosePurchaseAsync(
    prepared.Pending, delivery, token);

if (closed.Status == ClosePurchaseStatus.Success)
{
    // The purchase is complete, so delete the saved purchase information.
    await pendingStore.DeleteAsync(prepared.Pending);
}

The recipe performs the following tasks internally.

Category Call Where to check
Hive Axyl SDK Confirm the payment with RequestPurchaseAsync() Request payment confirmation
Store plugin Consume the purchase in Play with ConsumeAsync() Finish consumable product transactions

6. Record delivery results

Record the delivery result on the Hive Axyl server. This is the same as when you process a new purchase.

In AxylTransactionId, put the hiveAxylTransactionId that VerifyAndGrantOnceAsync() in 4.1 kept separately from the app server response. This value is not contained in PurchaseDeliveryConfirmation.

using Hive.Axyl.Payments;

var payments = HiveCore.Resolve<IPaymentsService>();

await payments.ItemResultAsync(new ItemResultBody
{
    AxylTransactionId = axylTransactionId,   // hiveAxylTransactionId received in the app server response in 4.1 and kept separately
    Status = 1,   // 1: Delivery succeeded
});

Failure handling

Restoration iterates over multiple items, so even if one item fails, keep processing the rest. What the app does in each situation is as follows.

  • Empty query result: A normal state with no purchases to finish
  • Query failure: A state you must distinguish from an empty list. Record the error and query again at the next run
  • Preparation failure: Keep the saved purchase information and proceed to the next item
  • Delivery not confirmed by the app server: Keep the saved purchase information and proceed to the next item
  • Finishing failure: Verification and delivery are already done. It is retrieved again in the next restoration, so you can leave it as is

For how to handle each BusinessOutcome value, see Rejection reasons in payment recipes.

Next steps

For the procedure to implement new purchases, see Implement Google Play payments.