Restore undelivered Apple App Store purchases
Retrieve Apple App Store purchases whose payment is complete but whose product delivery has not been wrapped up, and complete the remaining product delivery and transaction finishing.
Complete the setup and installation in Implement Apple App Store payments first.
When to run
Purchases remain unfinished in the following situations.
- The app was closed or the network was disconnected right after payment
- Receipt verification succeeded, but product delivery or finishing the transaction failed
- A payment that was awaiting parental approval was approved later
StoreKit delivers unfinished transactions again every time the app launches. Run restoration when the app launches and when the in-app store screen opens. Even if the two times overlap, make sure restoration does not run twice at the same time, and do not process an order while an in-progress purchase is processing the same order.
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 receive the list of unfinished transactions.
Process the retrieved items one at a time in order, and skip transactions of subscription products. The list of unfinished transactions also includes transactions of subscription payments that stopped before being confirmed, but this restoration handles only consumable products. subscriptionProductIds in the example code is the ProductSubscriptions list in the response of Get the product ID list. Finish subscription transactions by following Implement Apple App Store subscription payments.
using Hive.Axyl.Samples.Recipes;
var recipe = new UndeliveredPurchaseRecipe(new ApplePurchaseSource());
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)
{
if (subscriptionProductIds.Contains(entry.Order.ProductId))
{
// This restoration does not handle transactions of subscription products.
continue;
}
// 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 | Get unfinished transactions with GetUnfinishedTransactionsAsync() | Get unfinished transactions |
The Apple App Store keeps the list of undelivered purchases on the device. If the user changed devices or reinstalled the app, you must first run StoreKit purchase sync for the list to be populated. For how to do this, see Sync purchases.
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.
StoreKit transactions 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, and 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 Apple App Store payments, and if there is none, use the order record left with the same account and product ID. A payment that was awaiting parental approval does not receive Pending when the purchase starts, so it remains 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 first, and 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 purchase-time information cannot be found, do not guess the amount; record an error.
continue;
}
var pending = new PendingPurchase(
entry.AxylReceipt,
entry.OrderId,
original, // Amount, currency, and order information at 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 an error. StoreKit delivers unfinished transactions again on the next launch.
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 you finish the transaction works based on this record, so 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 in step 4 whether to deliver the product. 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 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 that you implemented in Implement Apple App Store payments. For restoration, pass 2, which indicates 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 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
On the app server, verify receipts and deliver products in the same way as in Implement Apple App Store payments. The differences from a new purchase are as follows.
- Put
2, which indicates purchase restoration, inrequestTypeof the receipt verification request. - Check whether the purchase has already been delivered in the same delivery records as for new purchases.
requestTypeis not the basis for preventing duplicate delivery. - 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 when 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 determine 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 transaction. If you do not finish it, it is retrieved again in the next restoration.
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 | Finish the StoreKit transaction with FinishTransactionAsync() | Finish the transaction |
6. Record delivery results
-
Hive Axyl SDK Call the Hive Axyl SDK from the app.
Detailed procedure: Save product delivery results
Record the delivery result on the Hive Axyl server. This is the same as when you handle a new purchase.
In AxylTransactionId, put the hiveAxylTransactionId that VerifyAndGrantOnceAsync() in 4.1 kept separately from the app server response. This value is not included in PurchaseDeliveryConfirmation.
Failure handling
Restoration iterates over multiple items, so keep processing the rest even if one item fails. 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 that you must distinguish from an empty list. Record the error and query again on the next launch
- 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 complete. The item is retrieved again in the next restoration, so you can leave it as is
For how to respond to each BusinessOutcome value, see Rejection reasons in payment recipes.
StoreKit signature verification errors
The recipe also includes in the query results items whose transaction signature StoreKit could not verify, and passes the error that StoreKit left in entry.StoreVerificationError. The final decision is made by the Hive Axyl server's receipt verification that the app server requests, and this verification rejects receipts that cannot be verified. To make a separate decision based on your app's policy, check this value before delivering the product.
Next steps
For the procedure to implement new purchases, see Implement Apple App Store payments.