24

I am using in-app purchase for an iPhone app. I have a class that acts as SKProductsRequestDelegate and SKPaymentTransactionObserver, and it's all working fine in the currently released version available on iTunes.

However, after recently adding a new non-consumable product and testing it within the Sandbox environment, I'm now encountering a strange problem. Every time I launch the app, the purchase I made yesterday reappears in the transactions list passed to me by paymentQueue:updatedTransactions:, despite the fact that I had called [[SKPaymentQueue defaultQueue] finishTransaction:transaction] already (several times). It's undead!

In my paymentQueue:updatedTransactions: implementation, I have:

for (SKPaymentTransaction* transaction in transactions) 
    switch (transaction.transactionState)
    {
        case SKPaymentTransactionStatePurchased:
        case SKPaymentTransactionStateRestored:
        {
            ....
                DDLog(@"Transaction for %@ occurred originally on %@.", transaction.payment.productIdentifier, transaction.originalTransaction.transactionDate);
                ....

I then process the purchase, download the user content and finally, in another method, do this:

for (SKPaymentTransaction* transaction in [[SKPaymentQueue defaultQueue] transactions])         
            if (([transaction.payment.productIdentifier isEqualToString:theParser.currentProductID]) &&
                 ((transaction.transactionState==SKPaymentTransactionStatePurchased) || (transaction.transactionState==SKPaymentTransactionStateRestored))
               )
            {
                DDLog(@"[[ Transaction will finish: product ID = %@; date = %@ ]]", transaction.payment.productIdentifier, transaction.transactionDate);
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            }

As you may have noticed, I'm not holding on to the original transaction object for the sake of simplicity, and it's relatively easy to find it later from the call to [[SKPaymentQueue defaultQueue] transactions]. Regardless, I do indeed see the expected output; that the transaction is completed and that it precisely matches the product ID and date of the original transaction. However, next time I run the app the whole things starts over! It's like the iTunes Store was never notified that the transaction completed, or refuses to acknowledge it.

Craig McMahon
  • 1,550
  • 1
  • 14
  • 36

10 Answers10

20

This issue was also raised in the developer forums, and the general conclusion was that it was down to a difference in the handling of transactions in iPhone OS 4.0. The problem only seems to occur when there is a significant delay between receiving a notification of the finished transaction and calling finishTransaction on the payment queue. In the end we didn't find an ideal solution, but what we did was this:

  1. As soon as the transaction arrives, process it and record a value in the user preferences if processing was successful.

  2. The next time that transaction appears in the queue, which may not be until the next launch of the app, immediately call finishTransaction on it.

Our products are "non-consumable" so it's enough to check that the product paid for is valid and error-free to safely ignore any 'undead' repeated transactions from iTunes. For consumable products one would need to save more information about the purchase, such as the original payment date, to make sure that future transaction notifications can be matched to purchases that were already processed.

Craig McMahon
  • 1,550
  • 1
  • 14
  • 36
9

That problem happened to me as well, I found the a solution. That may help you in similar cases.

I was calling finishTransaction immediately but next time when I try to buy something, the previous product was coming as well! So at first time, I was buying one product. But at second time, I was buying the second product and the first product too.

I found out that I'm adding SKPaymentTransactionObserver multiple times! That was causing the problem, making multiple buys.

When the process ends, I mean when you call finishTransaction, right after that, call: [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];

That will clear out transactions and remove the observer. So next time, you won't make multiple buys.

Vaibhav Saran
  • 12,848
  • 3
  • 65
  • 75
alper_k
  • 512
  • 1
  • 6
  • 18
5

I have not delved deeply into this, but I was seeing my calls to finishTransaction reliably failing in iOS 4. On a hunch I put the call to finishTransaction in a dispatch_async call and the problem went away.

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
  [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
});

Perhaps a salient point is I was calling finishTransaction from within a block which ultimately ran after a network call to my server.

Dav Yaginuma
  • 691
  • 7
  • 14
3

I had the same issue but I solved it.

Here's my code:

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction * transaction in transactions)
    {
        switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchased:
                [self completeTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
                [self failedTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored:
                [self restoreTransaction:transaction];
                break;
            default:
                break;
        }
    }
}

- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
    NSLog(@"completeTransaction... %@", [[transaction class] description]);

    [self provideContentForProductIdentifier:transaction];
}

- (void)restoreTransaction:(SKPaymentTransaction *)transaction
{
    NSLog(@"restoreTransaction...");

    [self provideContentForProductIdentifier:transaction.originalTransaction];
}

I was then calling finishTransaction: method inside provideContentForProductIdentifier: method. And in case of restore transaction I was calling finishTransaction: to the originalTransaction object not the transaction itself.

I solved my issue by this code (method restoreTransaction:)

- (void)restoreTransaction:(SKPaymentTransaction *)transaction
    {
        NSLog(@"restoreTransaction...");

        //Pass the main transaction object.
        [self provideContentForProductIdentifier:transaction];
    }
Ahmad Baraka
  • 1,093
  • 9
  • 12
3

Check that you're not simply adding the observer multiple times. I had the same problem with multiple updatedTransactions, but then I noticed I was adding a new observer each time in didBecomeActive. And it was called once each time I for example restored purchases in sandbox.

jscs
  • 63,694
  • 13
  • 151
  • 195
JCD
  • 31
  • 1
2

I had this problem too. The bug turned out to be on my side. The problem was that there was a past transaction lurking around that had been executed (the content provided) but not cleaned up using finishTransaction. Unfortunately, on asking at several places including an Apple TSI, I discovered that there was no way to poll such 'undead' transactions - you just had to register for notifications and wait for the corresponding paymentQueue:updatedTransactions:. This complicated my code, but not by much.

What I do now, which has been working fine:

  • When the store is about to be invoked (you're flashing some marketing slides, a terms of use maybe), fetch your product list and register for notifications as an observer via [[SKPaymentQueue defaultQueue] addTransactionObserver:self]
  • Keep a state variable that gets updated to YES when you push a payment in the queue using [[SKPaymentQueue defaultQueue] addPayment:payment]
  • When you get notified of a successful purchase via paymentQueue:updatedTransactions: check the state variable. If it has not been set, it means you have received a notification for a past payment. In that case, honor that payment rather than pushing a new one.

This method naturally assumes that you have the time to wait for old transactions to show up before starting a new transaction.

er0
  • 1,746
  • 2
  • 16
  • 31
  • 1
    it seems like you are really onto something here but I am a bit confused as to exactly what you are doing. If you are getting notified of a successful purchase (SKPaymentTransactionStatePurchased) via paymentQueue:updatedTransactions:, you say that if the flag is not set you honor the payment rather than pushing a new one. But if you have gotten to this point, regardless of flag or not - do you ever push a new payment? Is there ever a case when you get this notification that you do push a new payment? – SAHM Jul 21 '12 at 20:47
1

I was having the EXACT same issue.

Based on the answers, I did some experiments and found that if I hold a reference to the Queue, the problem went away.

For example:

// myStoreManagerClass.h
...
SKPaymentQueue *_myQueue;
...

//myStoreManagerClass.m
...
if(_myQueue == nil) {
_myQueue = [[SKPaymentQueue defaultQueue];
}
...

I then made sure that all my methods used my instance variable reference. Since doing that, the issue has cleared up.

Anthony
  • 11
  • 1
0

It's possibly caused by multiple observers. Don't think you're safe by just removing observer in dealloc.

You called [[SKPaymentQueue defaultQueue] addTransactionObserver:self] in viewDidLoad and [[SKPaymentQueue defaultQueue] removeTransactionObserver:self] in dealloc. You think you're safe like I did but you're not.

Actually, if you purchase something, finish transaction, remove it from payment queue, pop out this view controller from navigation view controller, then you enter the view controller again, you're probably have multiple transaction observers.

Every time you push this view controller you're adding the view controller itself as transactionObserver but every time you pop this view controller doesn't guarantee that you're removing the view controller itself from transactionObservers.

Somehow a view controller's dealloc not get called even if you pop the view controller. So the view controller is still observing in the dark.

I think the best solution for this situation is detecting if this view controller is visible when you process the transaction.

Simply add this before you process the transaction, it works for me:

if ( !self.view.window) {
    return;
}

visible detection is referenced from here.


ps. Maybe put/remove transactionObserver in viewWillAppear/viewWillDisappear is another way to solve that, but you have to carefully handle the keyboard show/hide events in case the user needs type password.

Community
  • 1
  • 1
Alston
  • 1,166
  • 2
  • 13
  • 26
0

I'm wondering, is the defaultQueue guaranteed to be the same queue passed in paymentQueue:updatedTransactions:? If not, then perhaps the issue is with calling finishTransaction on a different SKPaymentQueue than the one the transaction originated from.

TomSwift
  • 39,369
  • 12
  • 121
  • 149
  • In my little test, they seems the same queue. In `paymentQueue:updatedTransactions:`, `queue==[SKPaymentQueue defaultQueue]` return `YES` – Alston Feb 10 '15 at 04:44
-3

I using this code and it working for me

if ([[SKPaymentQueue defaultQueue].transactions count] > 0) {
    for (SKPaymentTransaction *transaction in [SKPaymentQueue defaultQueue].transactions) {
        @try {
            [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
        } @catch (NSException *exception) {
            NSLog([NSString stringWithFormat:@"%@", exception.reason]);
        }
    }
}