Question

I have a few users who have reported that after attempting an in app purchase the app is now crashing on startup. I have asked them to delete and reinstall the app which has not worked, and attempted to ask them to go into airplane mode to stop any network communication that has not worked.

I am unable to replicate the error in anyway on my devices and my in app purchase goes through just fine in sandbox and production modes. My thought is that somehow their transaction received a nil productIdentifier which is causing the startup crash but I am not sure what transaction observer methods get called at app startup that I can fix the issue for them.

Is there someway to "clear" the transaction queue or otherwise test for nil productidentifiers on start up and allow these users to get the app at least running again? I've done several hundred in app purchases using the code below and this just recently began happening. Which of the helper methods get called on app startup?

In AppDelegate.m

[[SKPaymentQueue defaultQueue] addTransactionObserver:[MovieClockIAPHelper sharedHelper]];

In app helper code:

- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers {
    if ((self = [super init])) {

        // Store product identifiers
        _productIdentifiers = [productIdentifiers retain];

        // Check for previously purchased products

        NSMutableSet * purchasedProducts = [NSMutableSet set];
        for (NSString * productIdentifier in _productIdentifiers) {

            BOOL productPurchased = [[NSUserDefaults standardUserDefaults] boolForKey:productIdentifier];

            if (productPurchased) {
                [purchasedProducts addObject:productIdentifier];
                NSLog(@"Previously purchased: %@", productIdentifier);
            }
            else{
            NSLog(@"Not purchased: %@", productIdentifier);
            }
        }
        self.purchasedProducts = purchasedProducts;

    }
    return self;
}

- (void)requestProducts {

    self.request = [[[SKProductsRequest alloc] initWithProductIdentifiers:_productIdentifiers] autorelease];
    _request.delegate = self;
    [_request start];

}

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {

    NSLog(@"Received products results...");   
    self.products = response.products;
    self.request = nil;    

    [[NSNotificationCenter defaultCenter] postNotificationName:kProductsLoadedNotification object:_products];    
}


- (void)restoreCompletedTransactions {
    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}

- (void)provideContent:(NSString *)productIdentifier {

    NSLog(@"Toggling flag for: %@", productIdentifier);
    [[NSUserDefaults standardUserDefaults] setBool:TRUE forKey:productIdentifier];
    [[NSUserDefaults standardUserDefaults] synchronize];
    [_purchasedProducts addObject:productIdentifier];

    [[NSNotificationCenter defaultCenter] postNotificationName:kProductPurchasedNotification object:productIdentifier];

}

- (void)completeTransaction:(SKPaymentTransaction *)transaction {

    NSLog(@"completeTransaction...");

    [self recordTransaction: transaction];
    [self provideContent: transaction.payment.productIdentifier];
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];

}

- (void)restoreTransaction:(SKPaymentTransaction *)transaction {

    NSLog(@"restoreTransaction...");


    [self recordTransaction: transaction];
    [self provideContent: transaction.originalTransaction.payment.productIdentifier];
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];

}

- (void)failedTransaction:(SKPaymentTransaction *)transaction {

    if (transaction.error.code != SKErrorPaymentCancelled)
    {
        NSLog(@"Transaction error: %@", transaction.error.localizedDescription);
    }

    [[NSNotificationCenter defaultCenter] postNotificationName:kProductPurchaseFailedNotification object:transaction];

    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];

}

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    NSLog(@"in the payment queue");

    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];
            default:
                break;
        }
        }

}

-(void)buyProduct:(SKProduct *)product
{
    NSLog(@"In buyproduct Buying %@...", product.productIdentifier);

    SKPayment *payment = [SKPayment paymentWithProduct:product];

    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

- (void)dealloc
{
    [_productIdentifiers release];
    _productIdentifiers = nil;
    [_products release];
    _products = nil;
    [_purchasedProducts release];
    _purchasedProducts = nil;
    [_request release];
    _request = nil;
    [super dealloc];
}

@end
Was it helpful?

Solution

I've run into a similar issue when the transaction is in the SKPaymentTransactionStateRestored. My testing has indicated this might be an issue with IOS 7.0.3, but I have not been able to verify this.

StoreKit keeps a persistent list of transactions that your application must finish. As you've noted the transaction will be reported on every startup until it is finished.

The solution that we implemented is to check if the product identifier is nil before usage from the entry point:

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions

Even when we received a transaction with a nil product identifier we were able to successfully call finishTransaction.

I hope this helps.

OTHER TIPS

One of my users complained four days ago about the same problem and was able to send me a crash log. He uses iOS 7.0.3 on iPhone 5,2. Crash occurs after identifying a SKPaymentTransactionStateRestored while trying to build a dictionary with the productIdentifier property from originalTransaction.payment:

NSDictionary* userInfoDict = @{@"productID":transaction.payment.productIdentifier};

I think that either originalTransaction or its properties payment or productIdentifier is nil. I stored the productIdentifier from transaction.payment.productIdentifier instead from transaction.originalTransaction.payment.productIdentifier while restoring and use that as productIdentifier from then on:

case SKPaymentTransactionStateRestored:
{
  NSString *productID = transaction.originalTransaction.payment.productIdentifier;
  if (!productID) {
    productID = transaction.payment.productIdentifier;
  }
  [self handlePurchaseSuccessful:transaction.originalTransaction productIdentifier:productID];
}

Still waiting for review / feedback if that fixes the issue.

Elaborating on Shawn's answer, in - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions you probably have some code like this:

- (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];

                        default:

                                break;
        }     
    }
}

- (void) restoreTransaction: (SKPaymentTransaction *)transaction
{   
    /* Handle restore here */
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; 
}

You can handle nil productIdentifiers in restoreTransaction: by adding a check to see if the productIdentifier is nil, like this:

- (void) restoreTransaction: (SKPaymentTransaction *)transaction
{   
    if (!transaction.originalTransaction.payment.productIdentifier) {
        NSLog(@"productIdentifier is nil; Apple bug?");
        [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
        return;
    }

    /* Handle restore here */
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}

This technique fixed the problem for me in my app. The app started, logged "productIdentifier is nil; Apple bug?" and didn't crash. When I then manually re-restored transactions, Apple sent a valid transaction, and the app worked as designed.

I fixed this issue as Marimba posted above and it helped for our customers:

case SKPaymentTransactionStateRestored:
{
  NSString *productID = transaction.originalTransaction.payment.productIdentifier;
  if (productID == nil) {
    productID = transaction.payment.productIdentifier;
  }
  [self handlePurchaseSuccessful:transaction.originalTransaction productIdentifier:productID];
}

I had the same problem paymentQueue:updatedTransactions: was being called with SKPaymentTransaction instances with a state of SKPaymentTransactionStateRestored and a nil productIdentifier in the originalTransaction.

It sounds really weird and might be a 7.0.3 SDK bug.

Do you all still compile with the SDK 6 ?

I'm about to resubmit my app to the store to see if the issue gets fixed but just finishing such transactions but I hope other transactions with a valid productIdentifier are going to be processed now that the app won't crash so that I know what to restore.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top