Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS StoreKit - When to call - (void)restoreCompletedTransactions?

I have lots of 1 time purchase IAPs inside of my application. Users can purchase them just fine.

My problem is that I am integrating with Flurry to track real purchases versus just a restoration of a purchase, but my SKPaymentTransaction's transactionState always comes back as SKPaymentTransactionStatePurchased rather than SKPaymentTransactionStateRestored.

Apparently SKPaymentTransactionStateRestored is only called when - (void)restoreCompletedTransactions, but when do I call this method?

My thought process is that a purchase should go like this: 1) User selects product, 2) User is asked if they would like to purchase product for X amount. 3) Server checks if the user has purchased before, and if they have restore it by setting SKPaymentTransactionStateRestored. Otherwise, process transaction and set SKPaymentTransactionStatePurchased. Apparently this is wrong and I am suppose to call - (void)restoreCompletedTransactions somewhere in between???

Thanks,

Will

like image 467
Will Avatar asked Apr 18 '13 13:04

Will


3 Answers

EDIT:

Originally I had posted a very long, unneeded method to get what I needed done, but as you will see below, Matt helped me figure out the variable I was looking for.

For an example, let's imagine a user previously purchased my app, bought all of the non-consumable IAP's available, then deleted the app. When the user reinstalls the app, I want to be able to determine when they go to "purchase" the products again, will it be an original (first time) purchase, or a restoration purchase?

I have implemented a "Restore all purchases" button, but let's say the user ignores/does not see it, and tries to select a product they have purchased before.

As with a normal purchase, I do the following:

if ([SKPaymentQueue canMakePayments])
{
      self.productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:productID]];

      self.productRequest.delegate = self;
      [self.productRequest start];
}
else
{
     //error message here
}

After the user has logged into their iTunes account, the App will let them know they have already purchased this and it will now be restored. The following delegate method will be called:

 -(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{   
    for (SKPaymentTransaction *transaction in transactions)
    {
        switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchased:
            {
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                if(transaction.originalTransaction)
                {
                    NSLog(@"Just restoring the transaction");
                }
                else
                {
                    NSLog(@"First time transaction");
                }

                break;
            }
            default:
                break;
        }
    }
}

No matter if the transaction was a restoration or a first time purchase, transaction.transactionState is going to be equal to SKPaymentTransactionStatePurchased.

Now from this point how do we determine if that purchase is an original or restoration purchase?

Simple: As seen above, just see if transaction.originalTransaction is initialized. Per Apple's note: // Only valid if state is SKPaymentTransactionStateRestored.

If the SKPaymentTransaction's originalTransaction is initialized, that means that there was a previous transaction. Otherwise, this transaction is an original one!

Once again, thanks to Matt for pointing me in the right direction, and for making the logic a lot cleaner!

like image 72
Will Avatar answered Nov 13 '22 11:11

Will


The canonical expectation seems to be that you provide a restore button that calls restoreCompletedTransactions. As for what happens if the user ignores this and just proceeds to try to pass through your purchase interface to bring back features he's already purchased, you may be worrying yourself needlessly; the docs say:

If the user attempts to purchase a restorable product (instead of using the restore interface you implemented), the application receives a regular transaction for that item, not a restore transaction. However, the user is not charged again for that product. Your application should treat these transactions identically to those of the original transaction.

The store will put up dialogs interacting with the user ("You've already purchased this, do you want to download it again for free?"), and notification of the free re-purchase will take place in your paymentQueue:updatedTransactions: as usual, and you can use its originalTransaction.transactionIdentifier to identify it with the original purchase.

like image 29
matt Avatar answered Nov 13 '22 12:11

matt


If you are implementing in-app purchases, you need to put a restore button somewhere in your app otherwise it is a rejection reason. You may look at Get list of purchased products, inApp Purchase iPhone for one incidence.

EDIT 2: There might be other ways than putting a button to avoid rejection. One way matt suggested in comments is to restore in app launch, which seems enough to me to conform to apple's guidelines.

Also have a look at last part of this tutorial it mentions the same issue, and shows one simple way to implement it: http://www.raywenderlich.com/21081/introduction-to-in-app-purchases-in-ios-6-tutorial

EDIT:

//Draft implementation of transaction observer delegate

-(void) paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction * transaction in transactions) {
        switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchased:                
                ...
            case SKPaymentTransactionStateFailed:
                ...
            case SKPaymentTransactionStateRestored:
                ...
            default:
                break;
        }
    };
}
like image 2
guenis Avatar answered Nov 13 '22 12:11

guenis