9

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

Will
  • 1,697
  • 17
  • 34
  • 2
    You should look at http://stackoverflow.com/questions/1757467/when-to-use-restorecompletedtransactions-for-in-app-purchases . In my case, i used it with a button "Restore previous transactions" – Michaël Azevedo Apr 18 '13 at 13:36
  • Thanks Micazeve, but from an analytical perspective it sounds like the user could just skip pressing the "restore" button and restore via trying to repurchase, thus continuing to skip where I analyze if the user did a purchase. Perhaps when they select a product, rather than immediately starting the transaction, I can do a most likely long and complicated process of calling restoreCompletedTransactions, seeing if the selected product is in that list, and if not, -then- let the user purchase it. Such a pain... – Will Apr 18 '13 at 13:46
  • You're just right, this button is not necessary since trying to repurchase the items already bought warns the user that he already bought this item. The main issue in this situation is that the user is warned AFTER he accepted to pay again : the "Restore button" is thus useful in a psychological way, because the user doesn't have to make the paiement procedure a second time. -- You can try to implement your method (which should works), but as you said, such a pain ;) – Michaël Azevedo Apr 18 '13 at 13:52

5 Answers5

9

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!

Will
  • 1,697
  • 17
  • 34
  • 1
    "and if they "re-purchase" anything, I cannot differentiate if it was an original purchase or a restoration purchase" You don't need to differentiate that. The store will differentiate it for you. You queue the purchase and if this user has already made this purchase the store will supply it without charging the user for payment a second time. – matt Apr 18 '13 at 20:53
  • Matt, I know that it won't charge the user twice, but for recording purchases (in my instance logging an event via Flurry, but this could also be used for storing the data in a database), I needed to differentiate between an original purchase and a restoration. – Will Apr 22 '13 at 13:03
  • 2
    I still don't agree. Just let the store do what the store does. Let the user go through the store's dialog ("You've already purchased this, do you want to redownload it?"). If it is a restoration, then when your transaction observer is notified, the transaction will have an `originalTransaction` value, and there's your differentiation (and all the original data). – matt Apr 22 '13 at 14:56
  • Matt, I found the variable you mentioned, and you are right that I am wanting to look for. I will update my answer to reflect the best way of doing this. Thanks! – Will Apr 23 '13 at 18:48
7

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Well, I updated my answer after your comments but if it is only this part where you inferred "it should be ok without a restore button" I don't think it is enough. – guenis Apr 18 '13 at 22:47
  • @guenis That's what I'm trying to find out. What is Apple asking for? Do they insist on an actual button? On the one hand you'd think this wouldn't be necessary, but on the other hand they do say "you must include an interface that allows users to restore these purchases". So it sounds to me like you *do* need a button. So my point here is, okay, but if the user ignores the restore button and just tries to make the purchase again, you don't need to worry: it will work, and the user won't be charged twice. – matt Apr 18 '13 at 23:07
  • Modified my answer to explain how you can tell when a "purchase" is actually a free re-download (when the user "purchases" something he already owns). – matt Apr 22 '13 at 15:12
2

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;
        }
    };
}
Community
  • 1
  • 1
guenis
  • 2,520
  • 2
  • 25
  • 37
  • Is that really true? The docs imply that you could skip the button entirely. You could do a restore on launch, for example. And even if there is no restore, the user can do the purchase again and won't be charged a second time, so why should the store care if there is no restore button? – matt Apr 18 '13 at 20:55
  • i totally agree with you. what I wrote was a sum up of things I read here and there, I am yet to submit an app with in-app support. Restore on launch seems the most plausible solution, and frankly one which I didn't think of while writing the answer. I will update my answer based on what you said, thanks. – guenis Apr 18 '13 at 22:35
  • 2
    you cant do a restore on launch without annoying the user with a password request every time he launches. no practical way around the button – ngb Jul 19 '13 at 11:37
0

The Apple IOS SDK documentation is actually misleading on this issue as it says that:

@property(nonatomic, readonly) SKPaymentTransaction *originalTransaction
The contents of this property are undefined except when transactionState is set to SKPaymentTransactionStateRestored.

The problem is that when the user clicks your buy button, go through the purchase process, and finally get a message that he has already paid and that he can redownload for free, your observer does not send you a SKPaymentTransactionStateRestored. It actually sends you a SKPaymentTransactionStatePurchased.

The good news is that despite the documentation, you will actually get the property originalTransaction even when the state of your transaction is SKPaymentTransactionStatePurchased. Just test to see if this property is nil or not and you will know if the transaction was a new purchase or an old purchased restored for free.

Erwan
  • 3,733
  • 30
  • 25
0

As far as i have observed, there are two possible cases where user may have to call restore purchase,

  1. If you are not maintaining purchase information in your backend server: you may loose purchase info if user uninstall the app or any such cases where app data is lost. In this case user may call restore and Storekit will give receipt of purchase again through which you can decide whether user had purchased earlier and whether that purchase is active now.

  2. If you are storing purchase information at your backed(backed api decides whether user is privileged of subscription feature) : there might be an case where you were able to purchase from Apple but somehow failed to update to your server, in this case user can restore purchase which willl update purchase info at your backend server, thereafter user can use app across multiple devices/platform/reinstall but still be entitled to have privileged access.

Therefore it is always recommend to show restore option in the same page as purchase. If user unintentionally tries to purchase while having active subscription without opting to restore, 1.if he had selected same plan as of currently active onr, he will be informed about the existing and subscription 2.if he selects other plan apart from current subscription, he/she will be informed about current subscription as well as option to upgrade/downgrade as per latest selection of plan.

nca
  • 397
  • 3
  • 12