With Unity iOS Prime31 StoreKit plugin (3/14/14), sandbox, restoreCompletedTransactions() loops over stuck transactions n^2 times

Our app is developed in Unity and built to iOS and Android. For iOS in-app purchases, we are using the Prime31 StoreKit plugin. We now use the 5/24/14 version of the plugin, but the issue was first noticed when we upgraded to the 3/14/14 version.

The behavior occurs when performing certain actions such as restoreCompletedTransactions(). It will first say “paymentQueueRestoreCompletedTransactionsFinished” and display information about the purchase including its transactionState.

paymentQueueRestoreCompletedTransactionsFinished
transactionUpdatedEvent: <StoreKitTransaction> ID: test_monthly, quantity: 1, transactionIdentifier: 1000000113168263, transactionState: Restored

productPurchaseAwaitingConfirmationEvent: <StoreKitTransaction> ID: test_monthly, quantity: 1, transactionIdentifier: 1000000113168263, transactionState: Restored

Then it attempts to finish all pending transactions. If there are any stuck transactions (iphone - Transaction comes back after finishTransaction: has been called on it - Stack Overflow), the console will report that it is telling the transactions to finalize, but will say that 0 have updated, except the first time, which will say all of them have updated:

transactions that have been updated by the payment queue: 0
transactions that are currently in the payment queue (and possibly stuck there): 59
finishing transaction: 1000000113168314 : test_monthly
StoreKit: transaction completed: <SKPaymentTransaction: 0x190d49f0>
finalizing and asking the payment queue to finish transaction: <SKPaymentTransaction: 0x190d49f0>
finishing transaction: 1000000113168318 : test_monthly
....

For each of the 59 transactions, it will display those three lines (“finishing transaction”, “transaction completed”, “finalizing…”), and once it has gone through all 59 of them, it will repeat the entire block, from “productPurchaseAwaitingConfirmationEvent” (with a different transaction id) down to the last “finalizing” of the 59th transaction, for 59 times total. As a result, if there are n transactions, it will create n^2 log messages. When I pressed other buttons in that scene, such as “Get Saved Transactions” and “Finish All Pending Transactions”, it told me that there are 0 transactions to be saved or finished.

Afterwards, if it loops over all of the transactions enough times, it updates all of the transactions as restored, printing these lines out n^2 times:

purchaseSuccessfulEvent: <StoreKitTransaction> ID: test_monthly, quantity: 1, transactionIdentifier: 1000000113168959, transactionState: Restored
....

Finally, it will remove each transaction from the paymentQueue, printing these lines only n times (but will re-add them the next time you restore):

paymentQueue:removedTransaction: <SKPaymentTransaction: 0x1961d070>
...

Here are the problems with this behavior:

  1. It takes a long time to cycle
    through these transactions, and the
    app is hung while this is happening.
  2. If n is sufficiently large (roughly
    more than 25 transactions), the app
    runs out of memory and crashes once
    it loops through enough times. It
    crashes during the section when it
    is looping over the transactions
    stuck in the payment queue.
  3. Each test account has a finite
    lifespan now. Numerous transactions
    may get stuck each time the account
    is used, and eventually the account
    may become unusable because store
    actions that loop through the stuck
    transactions will always crash if n
    is sufficiently large.

The behavior happens within the Prime31 plugin code, and is reproducible in the demo scene provided with the plugin.

  1. Create a new project and import the
    Prime31 StoreKit plugin package.
  2. Put the names of your products under
    the “Request Product Data” button
    section in StoreKitGUIManager.cs.
  3. Create a sandbox store and fill it
    with products (we have a
    non-consumable and a monthly
    subscription with a one-week free
    trial).
  4. Set the bundle id of the project to
    match the bundle id for your
    sandbox.
  5. Set the version number of the
    project to a high number (In our
    case, it’s about 1200 or higher. I
    am not sure why, but I noticed that
    it loops over many more transactions
    if the version number is set at 1200
    or above (dangerous, may crash) and
    loops for a much smaller number for
    versions below 1200 (safer). I think
    it coincides with the version in
    which we started to use free
    trials.)
  6. Build to iOS.
  7. Create a test user and have it
    perform many transactions with your
    store, which should accumulate some
    stuck transactions in the payment
    queue.
  8. Click on the restore button and
    enter the login and password of that
    test account and watch the console.

Here are my questions:

  1. Does this behavior only occur in
    sandbox, or can it occur in live
    production as well? Either way, how
    can we verify that it won’t happen
    in live production or that any fix
    we may do will not break live
    production?
  2. How can we fix this problem or
    create a workaround that will allow
    us to restore transactions or other
    functions properly without
    experiencing the issue?
  3. If we can do (2), can we use the app
    and restore functionality with old
    accounts, or would we no longer be
    able to use them and must create new
    accounts that have not had any
    transactions before?

It’s a bug in 2.16, should already fixed in 2.17 (and you should keep the latest version anyway)

  • bug fix: avoid restoring the same transactions multiple times even if Apple sends them over and over again